mirror of
				https://github.com/yangjian102621/geekai.git
				synced 2025-11-04 16:23:42 +08:00 
			
		
		
		
	luma page, upload image and remove image function is ready
This commit is contained in:
		@@ -26,7 +26,6 @@ import (
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"runtime/debug"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
@@ -228,6 +227,7 @@ func needLogin(c *gin.Context) bool {
 | 
			
		||||
		c.Request.URL.Path == "/api/suno/client" ||
 | 
			
		||||
		c.Request.URL.Path == "/api/suno/detail" ||
 | 
			
		||||
		c.Request.URL.Path == "/api/suno/play" ||
 | 
			
		||||
		c.Request.URL.Path == "/api/download" ||
 | 
			
		||||
		strings.HasPrefix(c.Request.URL.Path, "/api/test") ||
 | 
			
		||||
		strings.HasPrefix(c.Request.URL.Path, "/api/user/clogin") ||
 | 
			
		||||
		strings.HasPrefix(c.Request.URL.Path, "/api/config/") ||
 | 
			
		||||
@@ -316,64 +316,58 @@ func staticResourceMiddleware() gin.HandlerFunc {
 | 
			
		||||
 | 
			
		||||
		url := c.Request.URL.String()
 | 
			
		||||
		// 拦截生成缩略图请求
 | 
			
		||||
		if strings.HasPrefix(url, "/static/") {
 | 
			
		||||
			if strings.Contains(url, "?imageView2") {
 | 
			
		||||
				r := strings.SplitAfter(url, "imageView2")
 | 
			
		||||
				size := strings.Split(r[1], "/")
 | 
			
		||||
				if len(size) != 8 {
 | 
			
		||||
					c.String(http.StatusNotFound, "invalid thumb args")
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
				with := utils.IntValue(size[3], 0)
 | 
			
		||||
				height := utils.IntValue(size[5], 0)
 | 
			
		||||
				quality := utils.IntValue(size[7], 75)
 | 
			
		||||
 | 
			
		||||
				// 打开图片文件
 | 
			
		||||
				filePath := strings.TrimLeft(c.Request.URL.Path, "/")
 | 
			
		||||
				file, err := os.Open(filePath)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					c.String(http.StatusNotFound, "Image not found")
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
				defer file.Close()
 | 
			
		||||
 | 
			
		||||
				// 解码图片
 | 
			
		||||
				img, _, err := image.Decode(file)
 | 
			
		||||
				// for .webp image
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					img, err = webp.Decode(file)
 | 
			
		||||
				}
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					c.String(http.StatusInternalServerError, "Error decoding image")
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				var newImg image.Image
 | 
			
		||||
				if height == 0 || with == 0 {
 | 
			
		||||
					// 固定宽度,高度自适应
 | 
			
		||||
					newImg = resize.Resize(uint(with), uint(height), img, resize.Lanczos3)
 | 
			
		||||
				} else {
 | 
			
		||||
					// 生成缩略图
 | 
			
		||||
					newImg = resize.Thumbnail(uint(with), uint(height), img, resize.Lanczos3)
 | 
			
		||||
				}
 | 
			
		||||
				var buffer bytes.Buffer
 | 
			
		||||
				err = jpeg.Encode(&buffer, newImg, &jpeg.Options{Quality: quality})
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					logger.Error(err)
 | 
			
		||||
					c.String(http.StatusInternalServerError, err.Error())
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				// 设置图片缓存有效期为一年 (365天)
 | 
			
		||||
				c.Header("Cache-Control", "max-age=31536000, public")
 | 
			
		||||
				// 直接输出图像数据流
 | 
			
		||||
				c.Data(http.StatusOK, "image/jpeg", buffer.Bytes())
 | 
			
		||||
				c.Abort() // 中断请求
 | 
			
		||||
			} else if strings.Contains(url, "?download=true") {
 | 
			
		||||
				filename := filepath.Base(url)
 | 
			
		||||
				c.Header("Content-Disposition", "attachment; filename="+filename)
 | 
			
		||||
				c.Header("Content-Type", "application/octet-stream")
 | 
			
		||||
		if strings.HasPrefix(url, "/static/") && strings.Contains(url, "?imageView2") {
 | 
			
		||||
			r := strings.SplitAfter(url, "imageView2")
 | 
			
		||||
			size := strings.Split(r[1], "/")
 | 
			
		||||
			if len(size) != 8 {
 | 
			
		||||
				c.String(http.StatusNotFound, "invalid thumb args")
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			with := utils.IntValue(size[3], 0)
 | 
			
		||||
			height := utils.IntValue(size[5], 0)
 | 
			
		||||
			quality := utils.IntValue(size[7], 75)
 | 
			
		||||
 | 
			
		||||
			// 打开图片文件
 | 
			
		||||
			filePath := strings.TrimLeft(c.Request.URL.Path, "/")
 | 
			
		||||
			file, err := os.Open(filePath)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				c.String(http.StatusNotFound, "Image not found")
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			defer file.Close()
 | 
			
		||||
 | 
			
		||||
			// 解码图片
 | 
			
		||||
			img, _, err := image.Decode(file)
 | 
			
		||||
			// for .webp image
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				img, err = webp.Decode(file)
 | 
			
		||||
			}
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				c.String(http.StatusInternalServerError, "Error decoding image")
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			var newImg image.Image
 | 
			
		||||
			if height == 0 || with == 0 {
 | 
			
		||||
				// 固定宽度,高度自适应
 | 
			
		||||
				newImg = resize.Resize(uint(with), uint(height), img, resize.Lanczos3)
 | 
			
		||||
			} else {
 | 
			
		||||
				// 生成缩略图
 | 
			
		||||
				newImg = resize.Thumbnail(uint(with), uint(height), img, resize.Lanczos3)
 | 
			
		||||
			}
 | 
			
		||||
			var buffer bytes.Buffer
 | 
			
		||||
			err = jpeg.Encode(&buffer, newImg, &jpeg.Options{Quality: quality})
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				logger.Error(err)
 | 
			
		||||
				c.String(http.StatusInternalServerError, err.Error())
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// 设置图片缓存有效期为一年 (365天)
 | 
			
		||||
			c.Header("Cache-Control", "max-age=31536000, public")
 | 
			
		||||
			// 直接输出图像数据流
 | 
			
		||||
			c.Data(http.StatusOK, "image/jpeg", buffer.Bytes())
 | 
			
		||||
			c.Abort() // 中断请求
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
		c.Next()
 | 
			
		||||
 
 | 
			
		||||
@@ -17,19 +17,21 @@ import (
 | 
			
		||||
	"geekai/utils/resp"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
	"io"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type UploadHandler struct {
 | 
			
		||||
type NetHandler struct {
 | 
			
		||||
	BaseHandler
 | 
			
		||||
	uploaderManager *oss.UploaderManager
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewUploadHandler(app *core.AppServer, db *gorm.DB, manager *oss.UploaderManager) *UploadHandler {
 | 
			
		||||
	return &UploadHandler{BaseHandler: BaseHandler{App: app, DB: db}, uploaderManager: manager}
 | 
			
		||||
func NewNetHandler(app *core.AppServer, db *gorm.DB, manager *oss.UploaderManager) *NetHandler {
 | 
			
		||||
	return &NetHandler{BaseHandler: BaseHandler{App: app, DB: db}, uploaderManager: manager}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *UploadHandler) Upload(c *gin.Context) {
 | 
			
		||||
func (h *NetHandler) Upload(c *gin.Context) {
 | 
			
		||||
	file, err := h.uploaderManager.GetUploadHandler().PutFile(c, "file")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
@@ -60,7 +62,7 @@ func (h *UploadHandler) Upload(c *gin.Context) {
 | 
			
		||||
	resp.SUCCESS(c, file)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *UploadHandler) List(c *gin.Context) {
 | 
			
		||||
func (h *NetHandler) List(c *gin.Context) {
 | 
			
		||||
	var data struct {
 | 
			
		||||
		Urls []string `json:"urls,omitempty"`
 | 
			
		||||
	}
 | 
			
		||||
@@ -95,7 +97,7 @@ func (h *UploadHandler) List(c *gin.Context) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Remove remove files
 | 
			
		||||
func (h *UploadHandler) Remove(c *gin.Context) {
 | 
			
		||||
func (h *NetHandler) Remove(c *gin.Context) {
 | 
			
		||||
	userId := h.GetLoginUserId(c)
 | 
			
		||||
	id := h.GetInt(c, "id", 0)
 | 
			
		||||
	var file model.File
 | 
			
		||||
@@ -119,3 +121,28 @@ func (h *UploadHandler) Remove(c *gin.Context) {
 | 
			
		||||
	_ = h.uploaderManager.GetUploadHandler().Delete(objectKey)
 | 
			
		||||
	resp.SUCCESS(c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (h *NetHandler) Download(c *gin.Context) {
 | 
			
		||||
	fileUrl := c.Query("url")
 | 
			
		||||
	// 使用http工具下载文件
 | 
			
		||||
	if fileUrl == "" {
 | 
			
		||||
		resp.ERROR(c, types.InvalidArgs)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	// 使用http.Get下载文件
 | 
			
		||||
	r, err := http.Get(fileUrl)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		resp.ERROR(c, err.Error())
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	defer r.Body.Close()
 | 
			
		||||
 | 
			
		||||
	if r.StatusCode != http.StatusOK {
 | 
			
		||||
		resp.ERROR(c, "error status:"+r.Status)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 将下载的文件内容写入响应
 | 
			
		||||
	c.Status(http.StatusOK)
 | 
			
		||||
	_, _ = io.Copy(c.Writer, r.Body)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -128,7 +128,7 @@ func main() {
 | 
			
		||||
		fx.Provide(handler.NewChatRoleHandler),
 | 
			
		||||
		fx.Provide(handler.NewUserHandler),
 | 
			
		||||
		fx.Provide(chatimpl.NewChatHandler),
 | 
			
		||||
		fx.Provide(handler.NewUploadHandler),
 | 
			
		||||
		fx.Provide(handler.NewNetHandler),
 | 
			
		||||
		fx.Provide(handler.NewSmsHandler),
 | 
			
		||||
		fx.Provide(handler.NewRedeemHandler),
 | 
			
		||||
		fx.Provide(handler.NewCaptchaHandler),
 | 
			
		||||
@@ -249,10 +249,11 @@ func main() {
 | 
			
		||||
			group.POST("tokens", h.Tokens)
 | 
			
		||||
			group.GET("stop", h.StopGenerate)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.UploadHandler) {
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.NetHandler) {
 | 
			
		||||
			s.Engine.POST("/api/upload", h.Upload)
 | 
			
		||||
			s.Engine.POST("/api/upload/list", h.List)
 | 
			
		||||
			s.Engine.GET("/api/upload/remove", h.Remove)
 | 
			
		||||
			s.Engine.GET("/api/download", h.Download)
 | 
			
		||||
		}),
 | 
			
		||||
		fx.Invoke(func(s *core.AppServer, h *handler.SmsHandler) {
 | 
			
		||||
			group := s.Engine.Group("/api/sms/")
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								web/public/images/loading.gif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								web/public/images/loading.gif
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 16 KiB  | 
@@ -91,6 +91,22 @@
 | 
			
		||||
          position relative
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      .params {
 | 
			
		||||
        display flex
 | 
			
		||||
        justify-content right
 | 
			
		||||
        color #e1e1e1
 | 
			
		||||
        font-size 14px
 | 
			
		||||
        padding 10px 30px
 | 
			
		||||
 | 
			
		||||
        .item-group {
 | 
			
		||||
          margin-left 20px
 | 
			
		||||
          .label {
 | 
			
		||||
            margin-right 5px
 | 
			
		||||
            position relative
 | 
			
		||||
            top 1px
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
@@ -150,7 +166,16 @@
 | 
			
		||||
            font-size 14px
 | 
			
		||||
 | 
			
		||||
            .iconfont {
 | 
			
		||||
              font-size 12px
 | 
			
		||||
              font-size 11px
 | 
			
		||||
              position relative
 | 
			
		||||
              margin-right 5px
 | 
			
		||||
              top -2px
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            .el-image {
 | 
			
		||||
              width 14px
 | 
			
		||||
              height 14px
 | 
			
		||||
              margin-right 5px
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            &:hover {
 | 
			
		||||
 
 | 
			
		||||
@@ -69,3 +69,17 @@ export function httpPost(url, data = {}, options = {}) {
 | 
			
		||||
        })
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function httpDownload(url) {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
        axios({
 | 
			
		||||
            method: 'GET',
 | 
			
		||||
            url: url,
 | 
			
		||||
            responseType: 'blob' // 将响应类型设置为 `blob`
 | 
			
		||||
        }).then(response => {
 | 
			
		||||
            resolve(response)
 | 
			
		||||
        }).catch(err => {
 | 
			
		||||
            reject(err)
 | 
			
		||||
        })
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
@@ -2,15 +2,24 @@
 | 
			
		||||
  <div class="page-luma">
 | 
			
		||||
    <div class="prompt-box">
 | 
			
		||||
      <div class="images">
 | 
			
		||||
        <div v-for="img in images" class="item">
 | 
			
		||||
        <div v-for="img in images" :key="img" class="item">
 | 
			
		||||
          <el-image :src="img" fit="cover"/>
 | 
			
		||||
          <el-icon><CircleCloseFilled /></el-icon>
 | 
			
		||||
          <el-icon @click="remove(img)"><CircleCloseFilled /></el-icon>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="prompt-container">
 | 
			
		||||
        <div class="input-container">
 | 
			
		||||
          <div class="upload-icon">
 | 
			
		||||
            <i class="iconfont icon-image"></i>
 | 
			
		||||
 | 
			
		||||
            <el-upload
 | 
			
		||||
                class="avatar-uploader"
 | 
			
		||||
                :auto-upload="true"
 | 
			
		||||
                :show-file-list="false"
 | 
			
		||||
                :http-request="upload"
 | 
			
		||||
                accept=".jpg,.png,.jpeg"
 | 
			
		||||
            >
 | 
			
		||||
              <i class="iconfont icon-image"></i>
 | 
			
		||||
            </el-upload>
 | 
			
		||||
          </div>
 | 
			
		||||
          <textarea
 | 
			
		||||
              class="prompt-input"
 | 
			
		||||
@@ -24,6 +33,16 @@
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="params">
 | 
			
		||||
          <div class="item-group">
 | 
			
		||||
            <span class="label">循环</span>
 | 
			
		||||
            <el-switch  v-model="loop" size="small" style="--el-switch-on-color:#BF78BF;" />
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="item-group">
 | 
			
		||||
            <span class="label">提示词优化</span>
 | 
			
		||||
            <el-switch  v-model="promptExtend" size="small" style="--el-switch-on-color:#BF78BF;" />
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
@@ -40,12 +59,11 @@
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="video-name">{{item.name}}</div>
 | 
			
		||||
          <div class="opts">
 | 
			
		||||
            <a :href="item.url+'?download=true'" download="video.mp4">
 | 
			
		||||
              <button class="btn">
 | 
			
		||||
                <i class="iconfont icon-download"></i>
 | 
			
		||||
                <span>下载</span>
 | 
			
		||||
              </button>
 | 
			
		||||
            </a>
 | 
			
		||||
            <button class="btn" @click="download(item)" :disabled="item.downloading">
 | 
			
		||||
              <i class="iconfont icon-download" v-if="!item.downloading"></i>
 | 
			
		||||
              <el-image src="/images/loading.gif" fit="cover" v-else />
 | 
			
		||||
              <span>下载</span>
 | 
			
		||||
            </button>
 | 
			
		||||
          </div>
 | 
			
		||||
        </el-col>
 | 
			
		||||
      </el-row>
 | 
			
		||||
@@ -56,19 +74,21 @@
 | 
			
		||||
<script setup>
 | 
			
		||||
import {ref} from "vue";
 | 
			
		||||
import {CircleCloseFilled} from "@element-plus/icons-vue";
 | 
			
		||||
import {httpDownload, httpPost} from "@/utils/http";
 | 
			
		||||
import {showMessageError} from "@/utils/dialog";
 | 
			
		||||
import {ElMessage} from "element-plus";
 | 
			
		||||
 | 
			
		||||
const row = ref(1)
 | 
			
		||||
const prompt = ref('')
 | 
			
		||||
const images = ref([
 | 
			
		||||
    "http://nk.img.r9it.com/chatgpt-plus/1719371605709871.jpg",
 | 
			
		||||
    "http://nk.img.r9it.com/chatgpt-plus/1719371605709871.jpg"
 | 
			
		||||
])
 | 
			
		||||
const loop = ref(false)
 | 
			
		||||
const promptExtend = ref(false)
 | 
			
		||||
const images = ref([])
 | 
			
		||||
 | 
			
		||||
const videos = ref([
 | 
			
		||||
  {
 | 
			
		||||
    id: 1,
 | 
			
		||||
    name: 'a dancing girl',
 | 
			
		||||
    url: 'http://localhost:5678/static/upload/2024/8/1724574661747320.mp4',
 | 
			
		||||
    url: 'https://storage.cdn-luma.com/dream_machine/d133794f-3124-4059-a9f2-e5fed79f0d5b/watermarked_video01944f69966f14d33b6c4486a8cfb8dde.mp4',
 | 
			
		||||
    cover: 'https://storage.cdn-luma.com/dream_machine/d133794f-3124-4059-a9f2-e5fed79f0d5b/video_0_thumb.jpg',
 | 
			
		||||
    playing: false
 | 
			
		||||
  },
 | 
			
		||||
@@ -102,6 +122,44 @@ const videos = ref([
 | 
			
		||||
  },
 | 
			
		||||
])
 | 
			
		||||
 | 
			
		||||
const download = (item) => {
 | 
			
		||||
  const downloadURL = `${process.env.VUE_APP_API_HOST}/api/download?url=${item.url}`
 | 
			
		||||
  // parse filename
 | 
			
		||||
  const urlObj = new URL(item.url);
 | 
			
		||||
  const fileName = urlObj.pathname.split('/').pop();
 | 
			
		||||
  item.downloading = true
 | 
			
		||||
  httpDownload(downloadURL).then(response  => {
 | 
			
		||||
    const blob = new Blob([response.data]);
 | 
			
		||||
    const link = document.createElement('a');
 | 
			
		||||
    link.href = URL.createObjectURL(blob);
 | 
			
		||||
    link.download = fileName;
 | 
			
		||||
    document.body.appendChild(link);
 | 
			
		||||
    link.click();
 | 
			
		||||
    document.body.removeChild(link);
 | 
			
		||||
    URL.revokeObjectURL(link.href);
 | 
			
		||||
    item.downloading = false
 | 
			
		||||
  }).catch(() => {
 | 
			
		||||
    showMessageError("下载失败")
 | 
			
		||||
    item.downloading = false
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const upload = (file) => {
 | 
			
		||||
  const formData = new FormData();
 | 
			
		||||
  formData.append('file', file.file, file.name);
 | 
			
		||||
  // 执行上传操作
 | 
			
		||||
  httpPost('/api/upload', formData).then((res) => {
 | 
			
		||||
    images.value.push(res.data.url)
 | 
			
		||||
    ElMessage.success({message: "上传成功", duration: 500})
 | 
			
		||||
  }).catch((e) => {
 | 
			
		||||
    ElMessage.error('图片上传失败:' + e.message)
 | 
			
		||||
  })
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const remove = (img) => {
 | 
			
		||||
  images.value = images.value.filter(item => item !== img)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user