Skip to content

Commit 9b3d0b3

Browse files
committed
feat: 优化图片展示和上传
1 parent ec6f6a5 commit 9b3d0b3

File tree

10 files changed

+229
-136
lines changed

10 files changed

+229
-136
lines changed

backend/db/memo.go

+23-20
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,32 @@ package db
22

33
import (
44
"time"
5+
6+
"github.com/kingwrcy/moments/vo"
57
)
68

79
type Memo struct {
8-
Id int32 `gorm:"column:id;primary_key;NOT NULL" json:"id,omitempty"`
9-
Content string `gorm:"column:content" json:"content,omitempty"`
10-
Imgs string `gorm:"column:imgs" json:"imgs,omitempty"`
11-
FavCount int32 `gorm:"column:favCount;default:0;NOT NULL" json:"favCount,omitempty"`
12-
CommentCount int32 `gorm:"column:commentCount;default:0;NOT NULL" json:"commentCount,omitempty"`
13-
UserId int32 `gorm:"column:userId;NOT NULL" json:"userId,omitempty"`
14-
CreatedAt *time.Time `gorm:"column:createdAt;default:CURRENT_TIMESTAMP;NOT NULL" json:"createdAt,omitempty"`
15-
UpdatedAt *time.Time `gorm:"column:updatedAt;NOT NULL" json:"updatedAt,omitempty"`
16-
Music163Url string `gorm:"column:music163Url" json:"music163Url,omitempty"`
17-
BilibiliUrl string `gorm:"column:bilibiliUrl" json:"bilibiliUrl,omitempty"`
18-
Location string `gorm:"column:location" json:"location,omitempty"`
19-
ExternalUrl string `gorm:"column:externalUrl" json:"externalUrl,omitempty"`
20-
ExternalTitle string `gorm:"column:externalTitle" json:"externalTitle,omitempty"`
21-
ExternalFavicon string `gorm:"column:externalFavicon;default:/favicon.png;NOT NULL" json:"externalFavicon,omitempty"`
22-
Pinned *bool `gorm:"column:pinned;default:false;NOT NULL" json:"pinned,omitempty"`
23-
Ext string `gorm:"column:ext;default:{};NOT NULL" json:"ext,omitempty"`
24-
ShowType *int32 `gorm:"column:showType;default:1;NOT NULL" json:"showType,omitempty"`
25-
User *User `json:"user,omitempty"`
26-
Comments []Comment `json:"comments,omitempty"`
27-
Tags *string `json:"tags,omitempty"`
10+
Id int32 `gorm:"column:id;primary_key;NOT NULL" json:"id,omitempty"`
11+
Content string `gorm:"column:content" json:"content,omitempty"`
12+
Imgs string `gorm:"column:imgs" json:"imgs,omitempty"`
13+
FavCount int32 `gorm:"column:favCount;default:0;NOT NULL" json:"favCount,omitempty"`
14+
CommentCount int32 `gorm:"column:commentCount;default:0;NOT NULL" json:"commentCount,omitempty"`
15+
UserId int32 `gorm:"column:userId;NOT NULL" json:"userId,omitempty"`
16+
CreatedAt *time.Time `gorm:"column:createdAt;default:CURRENT_TIMESTAMP;NOT NULL" json:"createdAt,omitempty"`
17+
UpdatedAt *time.Time `gorm:"column:updatedAt;NOT NULL" json:"updatedAt,omitempty"`
18+
Music163Url string `gorm:"column:music163Url" json:"music163Url,omitempty"`
19+
BilibiliUrl string `gorm:"column:bilibiliUrl" json:"bilibiliUrl,omitempty"`
20+
Location string `gorm:"column:location" json:"location,omitempty"`
21+
ExternalUrl string `gorm:"column:externalUrl" json:"externalUrl,omitempty"`
22+
ExternalTitle string `gorm:"column:externalTitle" json:"externalTitle,omitempty"`
23+
ExternalFavicon string `gorm:"column:externalFavicon;default:/favicon.png;NOT NULL" json:"externalFavicon,omitempty"`
24+
Pinned *bool `gorm:"column:pinned;default:false;NOT NULL" json:"pinned,omitempty"`
25+
Ext string `gorm:"column:ext;default:{};NOT NULL" json:"ext,omitempty"`
26+
ShowType *int32 `gorm:"column:showType;default:1;NOT NULL" json:"showType,omitempty"`
27+
User *User `json:"user,omitempty"`
28+
Comments []Comment `json:"comments,omitempty"`
29+
Tags *string `json:"tags,omitempty"`
30+
ImgConfigs *[]*vo.ImgConfig `gorm:"-" json:"imgConfigs,omitempty"`
2831
}
2932

3033
func (m *Memo) TableName() string {

backend/handler/file.go

+58-32
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"io"
99
"os"
1010
"path"
11-
"path/filepath"
1211
"strings"
1312
"time"
1413

@@ -45,45 +44,47 @@ func (f FileHandler) Upload(c echo.Context) error {
4544
var (
4645
result []string
4746
)
47+
4848
form, err := c.MultipartForm()
4949
if err != nil {
5050
f.base.log.Error().Msgf("读取上传图片异常:%s", err)
5151
return FailRespWithMsg(c, Fail, "上传图片异常")
5252
}
53-
files := form.File["files"]
5453

54+
if err := os.MkdirAll(f.base.cfg.UploadDir, 0755); err != nil {
55+
f.base.log.Error().Msgf("创建父级目录异常:%s", err)
56+
return FailRespWithMsg(c, Fail, "创建父级目录异常")
57+
}
58+
59+
files := form.File["files"]
5560
for _, file := range files {
56-
// Source
61+
// 原始图片
5762
src, err := file.Open()
5863
if err != nil {
5964
f.base.log.Error().Msgf("打开上传图片异常:%s", err)
6065
return FailRespWithMsg(c, Fail, "上传图片异常")
6166
}
6267
defer src.Close()
63-
// Destination
68+
69+
// 创建原始图片
6470
img_filename := strings.ReplaceAll(uuid.NewString(), "-", "")
6571
img_filepath := path.Join(f.base.cfg.UploadDir, img_filename)
66-
67-
thumb_filename := img_filename + "_thumb"
68-
thumb_filepath := path.Join(f.base.cfg.UploadDir, thumb_filename)
69-
70-
if err := os.MkdirAll(filepath.Dir(img_filepath), 0755); err != nil {
71-
f.base.log.Error().Msgf("创建父级目录异常:%s", err)
72-
return FailRespWithMsg(c, Fail, "创建父级目录异常")
73-
}
7472
dst, err := os.Create(img_filepath)
7573
if err != nil {
7674
f.base.log.Error().Msgf("打开目标图片异常:%s", err)
7775
return FailRespWithMsg(c, Fail, "上传图片异常")
7876
}
7977
defer dst.Close()
80-
// Copy
78+
79+
// 保存图片
8180
if _, err = io.Copy(dst, src); err != nil {
8281
f.base.log.Error().Msgf("复制图片异常:%s", err)
8382
return FailRespWithMsg(c, Fail, "上传图片异常")
8483
}
8584

86-
// compress image
85+
// 生成并保存缩略图
86+
thumb_filename := img_filename + "_thumb"
87+
thumb_filepath := path.Join(f.base.cfg.UploadDir, thumb_filename)
8788
if err := CompressImage(f, img_filepath, thumb_filepath, 30); err != nil {
8889
f.base.log.Error().Msgf("压缩图片异常:%s", err)
8990
}
@@ -113,7 +114,6 @@ type s3PresignedResp struct {
113114
// @Success 200 {object} s3PresignedResp
114115
// @Router /api/file/s3PreSigned [post]
115116
func (f FileHandler) S3PreSigned(c echo.Context) error {
116-
117117
var (
118118
req PreSignedReq
119119
sysConfig db.SysConfig
@@ -126,15 +126,30 @@ func (f FileHandler) S3PreSigned(c echo.Context) error {
126126
if err := f.base.db.First(&sysConfig).Error; errors.Is(err, gorm.ErrRecordNotFound) {
127127
return FailResp(c, Fail)
128128
}
129+
129130
if err := json.Unmarshal([]byte(sysConfig.Content), &sysConfigVo); err != nil {
130131
f.base.log.Error().Msgf("无法反序列化系统配置, %s", err)
131132
return FailRespWithMsg(c, Fail, err.Error())
132133
}
133-
cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion(sysConfigVo.S3.Region),
134-
config.WithEndpointResolver(aws.EndpointResolverFunc(func(service, region string) (aws.Endpoint, error) {
135-
return aws.Endpoint{URL: sysConfigVo.S3.Endpoint}, nil
136-
})),
137-
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(sysConfigVo.S3.AccessKey, sysConfigVo.S3.SecretKey, "")))
134+
135+
cfg, err := config.LoadDefaultConfig(
136+
context.TODO(),
137+
config.WithRegion(sysConfigVo.S3.Region),
138+
config.WithEndpointResolver(
139+
aws.EndpointResolverFunc(
140+
func(service, region string) (aws.Endpoint, error) {
141+
return aws.Endpoint{URL: sysConfigVo.S3.Endpoint}, nil
142+
},
143+
),
144+
),
145+
config.WithCredentialsProvider(
146+
credentials.NewStaticCredentialsProvider(
147+
sysConfigVo.S3.AccessKey,
148+
sysConfigVo.S3.SecretKey,
149+
"",
150+
),
151+
),
152+
)
138153
if err != nil {
139154
f.base.log.Error().Msgf("无法加载SDK配置, %s", err)
140155
return FailRespWithMsg(c, Fail, err.Error())
@@ -143,22 +158,33 @@ func (f FileHandler) S3PreSigned(c echo.Context) error {
143158
client := s3.NewFromConfig(cfg)
144159
presignedClient := s3.NewPresignClient(client)
145160

146-
key := fmt.Sprintf("moments/%s/%s", time.Now().Format("2006/01/02"), strings.ReplaceAll(uuid.NewString(), "-", ""))
147-
presignedResult, err := presignedClient.PresignPutObject(context.TODO(), &s3.PutObjectInput{
148-
Bucket: aws.String(sysConfigVo.S3.Bucket),
149-
Key: aws.String(key),
150-
ContentType: aws.String(req.ContentType),
151-
}, func(opts *s3.PresignOptions) {
152-
opts.Expires = time.Minute * 5
153-
})
161+
key := fmt.Sprintf(
162+
"moments/%s/%s",
163+
time.Now().Format("2006/01/02"),
164+
strings.ReplaceAll(uuid.NewString(), "-", ""),
165+
)
166+
presignedResult, err := presignedClient.PresignPutObject(
167+
context.TODO(),
168+
&s3.PutObjectInput{
169+
Bucket: aws.String(sysConfigVo.S3.Bucket),
170+
Key: aws.String(key),
171+
ContentType: aws.String(req.ContentType),
172+
},
173+
func(opts *s3.PresignOptions) {
174+
opts.Expires = time.Minute * 5
175+
},
176+
)
154177

155178
if err != nil {
156179
f.base.log.Error().Msgf("无法获取预签名URL, %s", err)
157180
return FailRespWithMsg(c, Fail, fmt.Sprintf("无法获取预签名URL, %s", err))
158181
}
159182

160-
return SuccessResp(c, s3PresignedResp{
161-
PreSignedUrl: presignedResult.URL,
162-
ImageUrl: fmt.Sprintf("%s/%s", sysConfigVo.S3.Domain, key),
163-
})
183+
return SuccessResp(
184+
c,
185+
s3PresignedResp{
186+
PreSignedUrl: presignedResult.URL,
187+
ImageUrl: fmt.Sprintf("%s/%s", sysConfigVo.S3.Domain, key),
188+
},
189+
)
164190
}

backend/handler/memo.go

+57-3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"net/http"
1010
"net/url"
1111
"os"
12+
"path"
1213
"path/filepath"
1314
"regexp"
1415
"strconv"
@@ -93,6 +94,47 @@ type memoListResp struct {
9394
Total int64 `json:"total,omitempty"` //总数
9495
}
9596

97+
/*
98+
这里通过 memo.Imgs 生成 ImgConfigs,ImgConfig 包含图片的原始 Url 和 ThumbUrl
99+
正常情况下这里应该是在上传图片的时候来保存 ImgConfig,而不是在获取 memo 时计算
100+
但是由于历史实现的 memo 模型结构不支持保存额外的图片信息,所以这里先临时实现一个版本,有精力再重构
101+
*/
102+
func (m MemoHandler) handleImgConfigs(sysConfigVO *vo.FullSysConfigVO, memo *db.Memo) {
103+
var imgConfigs []*vo.ImgConfig
104+
105+
imgs := strings.Split(memo.Imgs, ",")
106+
for _, img := range imgs {
107+
if img == "" {
108+
continue
109+
}
110+
111+
imgConfig := &vo.ImgConfig{
112+
Url: &img,
113+
ThumbUrl: &img,
114+
}
115+
116+
if strings.HasPrefix(img, "/upload/") {
117+
thumb_filename := img + "_thumb"
118+
thumb_filepath := path.Join(m.base.cfg.UploadDir, path.Base(thumb_filename))
119+
if fs_util.Exists(thumb_filepath) {
120+
imgConfig.ThumbUrl = &thumb_filename
121+
}
122+
} else if sysConfigVO.S3.ThumbnailSuffix != "" && strings.HasPrefix(img, sysConfigVO.S3.Domain) {
123+
thumbnailSuffix := sysConfigVO.S3.ThumbnailSuffix
124+
if !strings.HasPrefix(thumbnailSuffix, "?") {
125+
thumbnailSuffix = "?" + thumbnailSuffix
126+
}
127+
128+
newThumbUrl := img + thumbnailSuffix
129+
imgConfig.ThumbUrl = &newThumbUrl
130+
}
131+
132+
imgConfigs = append(imgConfigs, imgConfig)
133+
}
134+
135+
memo.ImgConfigs = &imgConfigs
136+
}
137+
96138
// ListMemos godoc
97139
//
98140
// @Tags Memo
@@ -196,6 +238,10 @@ func (m MemoHandler) ListMemos(c echo.Context) error {
196238
list[i].Comments = comments
197239
}
198240

241+
for i := range list {
242+
m.handleImgConfigs(&sysConfigVO, &list[i])
243+
}
244+
199245
return SuccessResp(c, memoListResp{
200246
List: list,
201247
Total: total,
@@ -237,15 +283,17 @@ func (m MemoHandler) RemoveMemo(c echo.Context) error {
237283
if memo.Imgs != "" {
238284
imgs := strings.Split(memo.Imgs, ",")
239285
for _, img := range imgs {
240-
if !strings.HasPrefix(img, "/upload/") {
241-
return SuccessResp(c, h{})
286+
if img == "" || !strings.HasPrefix(img, "/upload/") {
287+
continue
242288
}
289+
243290
img := strings.ReplaceAll(img, "/upload/", "")
244291
_ = os.Remove(filepath.Join(m.base.cfg.UploadDir, img))
245292
thumbImg := strings.ReplaceAll(img+"_thumb", "/upload/", "")
246293
_ = os.Remove(filepath.Join(m.base.cfg.UploadDir, thumbImg))
247294
}
248295
}
296+
249297
return SuccessResp(c, h{})
250298
}
251299

@@ -411,12 +459,16 @@ func (m MemoHandler) SaveMemo(c echo.Context) error {
411459
// @Router /api/memo/get [post]
412460
func (m MemoHandler) GetMemo(c echo.Context) error {
413461
var (
414-
memo db.Memo
462+
memo db.Memo
463+
sysConfig db.SysConfig
464+
sysConfigVO vo.FullSysConfigVO
415465
)
416466

417467
ctx := c.(CustomContext)
418468
currentUser := ctx.CurrentUser()
419469

470+
m.base.db.First(&sysConfig)
471+
420472
id, err := strconv.Atoi(c.QueryParam("id"))
421473
if err != nil {
422474
return FailResp(c, ParamError)
@@ -443,6 +495,8 @@ func (m MemoHandler) GetMemo(c echo.Context) error {
443495

444496
memo.Comments = comments
445497

498+
m.handleImgConfigs(&sysConfigVO, &memo)
499+
446500
return SuccessResp(c, memo)
447501
}
448502

backend/handler/rss.go

+6-1
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,14 @@ func getContentWithExt(memo db.Memo, host string) string {
150150
if memo.Imgs != "" {
151151
imgs := strings.Split(memo.Imgs, ",")
152152
for _, img := range imgs {
153-
if img[:7] == "/upload" {
153+
if img == "" {
154+
continue
155+
}
156+
157+
if strings.HasPrefix(img, "/upload/") {
154158
img = host + img
155159
}
160+
156161
content += fmt.Sprintf("\n\n![%s](%s)", img, img)
157162
}
158163
}

backend/vo/memo_vo.go

+5
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,8 @@ type DoubanBook struct {
7575
PubDate string `json:"pubDate,omitempty"` //发布日期
7676
Keywords string `json:"keywords,omitempty"` //关键字
7777
}
78+
79+
type ImgConfig struct {
80+
Url *string `json:"url,omitempty"`
81+
ThumbUrl *string `json:"thumbUrl,omitempty"`
82+
}

front/components/Memo.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
<div class="flex flex-col gap-2">
4949
<external-url-preview :favicon="item.externalFavicon" :title="item.externalTitle" :url="item.externalUrl"
5050
v-if="item.externalFavicon&&item.externalTitle&&item.externalUrl"/>
51-
<upload-image-preview :imgs="item.imgs||''" :memo-id="item.id"/>
51+
<upload-image-preview :imgs="item.imgs" :imgConfigs="item.imgConfigs" :memo-id="item.id"/>
5252

5353
<music-preview v-if="extJSON.music && extJSON.music.id" v-bind="extJSON.music"/>
5454
<douban-book-preview v-if="extJSON.doubanBook && extJSON.doubanBook.title" :book="extJSON.doubanBook"/>

front/components/MemoEdit.vue

+7-10
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ const locationLabel = computed(() => {
167167
})
168168
169169
const handleDragImage = (imgs: string[]) => {
170-
state.imgs = imgs.join(",")
170+
state.imgs = imgs.filter(Boolean).join(",")
171171
}
172172
173173
const updateMusic = (music: MusicDTO) => {
@@ -186,13 +186,10 @@ const {y: windowY} = useWindowScroll()
186186
const isOpen = ref(false)
187187
const virtualElement = ref({getBoundingClientRect: () => ({})})
188188
const handleRemoveImage = (img: string) => {
189-
const imgs = state.imgs.split(",")
190-
const index = imgs.findIndex(r => r === img)
191-
if (index < 0) {
192-
return
193-
}
194-
imgs.splice(index, 1)
195-
state.imgs = imgs.join(",")
189+
state.imgs = state.imgs
190+
.split(",")
191+
.filter(item => item && item != img)
192+
.join(",")
196193
}
197194
198195
function onContextMenu() {
@@ -278,9 +275,9 @@ const saveMemo = async () => {
278275
externalFavicon: state.externalUrl ? state.externalFavicon : "",
279276
externalTitle: state.externalTitle,
280277
externalUrl: state.externalUrl,
281-
imgs: state.imgs.split(','),
278+
imgs: state.imgs.split(",").filter(Boolean),
282279
location: state.location,
283-
tags: selectedLabel.value
280+
tags: selectedLabel.value,
284281
})
285282
toast.success("保存成功!")
286283
await navigateTo('/')

0 commit comments

Comments
 (0)