From 46c682cb78f57679e03b72481d55e1236bb5be43 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Wed, 4 Nov 2026 19:28:40 +0800 Subject: [PATCH] =?UTF-8?q?feat(points):=20=E6=94=AF=E6=8C=81=E5=90=8E?= =?UTF-8?q?=E5=8F=B0=E4=B8=8A=E4=BC=A0=E7=A7=AF=E5=88=86=E5=95=86=E5=93=81?= =?UTF-8?q?=E5=9B=BE=E7=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/modules/file/service.go | 6 +- backend/internal/modules/file/storage_test.go | 10 ++ .../admin/views/AdminPointsMallView.vue | 100 +++++++++++++++++- 3 files changed, 112 insertions(+), 4 deletions(-) diff --git a/backend/internal/modules/file/service.go b/backend/internal/modules/file/service.go index 3a24eb6..4d236fd 100644 --- a/backend/internal/modules/file/service.go +++ b/backend/internal/modules/file/service.go @@ -109,7 +109,7 @@ func normalizeContentType(contentType string, data []byte) string { func fileURLForScene(scene string, key string) string { fileURL := "/api/files/object?key=" + url.QueryEscape(key) - if scene == "home-banner" || scene == "avatar" || scene == "announcement" || scene == "mohong" || scene == "crash" || scene == "aw-recycle" || scene == "cooperation-feedback" || scene == "support-qrcode" { + if scene == "home-banner" || scene == "avatar" || scene == "announcement" || scene == "mohong" || scene == "crash" || scene == "aw-recycle" || scene == "cooperation-feedback" || scene == "support-qrcode" || scene == "points-mall" { fileURL = "/api/public/files/object?key=" + url.QueryEscape(key) } return fileURL @@ -118,12 +118,14 @@ func fileURLForScene(scene string, key string) string { func normalizeScene(scene string) string { scene = strings.TrimSpace(strings.ToLower(scene)) switch scene { - case "listing", "handoff", "dispute", "realname", "avatar", "home-banner", "chat", "payment-cert", "announcement", "qrcode", "support-qrcode", "mohong", "crash", "aw-recycle", "cooperation-feedback", "manual-disbursement", "operating-expense": + case "listing", "handoff", "dispute", "realname", "avatar", "home-banner", "chat", "payment-cert", "announcement", "qrcode", "support-qrcode", "mohong", "crash", "aw-recycle", "cooperation-feedback", "manual-disbursement", "operating-expense", "points-mall": return scene case "listing_screenshot", "listing-screenshot": return "listing" case "operating_expense": return "operating-expense" + case "points_mall": + return "points-mall" default: return "misc" } diff --git a/backend/internal/modules/file/storage_test.go b/backend/internal/modules/file/storage_test.go index e1c5d12..e59fda2 100644 --- a/backend/internal/modules/file/storage_test.go +++ b/backend/internal/modules/file/storage_test.go @@ -87,6 +87,16 @@ func TestSupportQrcodeSceneUsesPublicFileURL(t *testing.T) { } } +func TestPointsMallSceneUsesPublicFileURL(t *testing.T) { + if got := normalizeScene("points_mall"); got != "points-mall" { + t.Fatalf("normalize points_mall scene = %q, want points-mall", got) + } + got := fileURLForScene("points-mall", "points-mall/2026/11/04/example.webp") + if !strings.HasPrefix(got, "/api/public/files/object?") { + t.Fatalf("points-mall file url = %q, want public file endpoint", got) + } +} + func TestNormalizeSceneListingScreenshotAndOperatingExpense(t *testing.T) { if got := normalizeScene("listing_screenshot"); got != "listing" { t.Fatalf("normalize listing_screenshot scene = %q, want listing", got) diff --git a/frontend/src/features/admin/views/AdminPointsMallView.vue b/frontend/src/features/admin/views/AdminPointsMallView.vue index f9d6b6e..b1b3d7f 100644 --- a/frontend/src/features/admin/views/AdminPointsMallView.vue +++ b/frontend/src/features/admin/views/AdminPointsMallView.vue @@ -13,6 +13,7 @@ import { type PointsMallRedemption, } from '@/features/points/api/pointsMall' import { useAdminSessionStore } from '@/stores/adminSession' +import { uploadAdminFile } from '@/shared/api/files' import { readError } from '@/shared/utils/error' const adminSession = useAdminSessionStore() @@ -23,6 +24,8 @@ const products = ref([]) const redemptions = ref([]) const editorVisible = ref(false) const editingID = ref(0) +const coverUploading = ref(false) +const coverFileInput = ref(null) const canManage = computed(() => adminSession.hasPermission('points_mall:manage')) const form = reactive(defaultProduct()) @@ -102,6 +105,27 @@ async function completeRedemption(item: PointsMallRedemption) { } } +function triggerCoverUpload() { + coverFileInput.value?.click() +} + +async function handleCoverFileChange(event: Event) { + const input = event.target as HTMLInputElement + const file = input.files?.[0] + input.value = '' + if (!file) return + coverUploading.value = true + try { + const uploaded = await uploadAdminFile(file, 'points-mall') + form.cover_url = uploaded.url + ElMessage.success('商品图片上传成功') + } catch (error) { + ElMessage.error(readError(error, '商品图片上传失败')) + } finally { + coverUploading.value = false + } +} + function defaultProduct(): PointsMallProductPayload { return { title: '', @@ -203,8 +227,30 @@ function defaultProduct(): PointsMallProductPayload { - - + +
+
+ 商品封面预览 + 暂无图片 +
+
+ + {{ coverUploading ? '上传中' : '上传本地图片' }} + + 移除图片 + + 支持 JPG、PNG、WebP,上传后自动保存到商品资料。 +
+
+
@@ -269,9 +315,59 @@ function defaultProduct(): PointsMallProductPayload { gap: 12px; } +.cover-editor { + display: grid; + grid-template-columns: 180px minmax(0, 1fr); + gap: 14px; + width: 100%; +} + +.cover-preview { + display: grid; + width: 180px; + height: 120px; + place-items: center; + overflow: hidden; + border: 1px dashed #cbd5e1; + border-radius: 10px; + background: #f8fafc; + color: #94a3b8; + font-size: 12px; +} + +.cover-preview img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.cover-actions { + display: grid; + align-content: start; + gap: 9px; +} + +.cover-actions .el-button { + justify-self: start; + margin-left: 0; +} + +.cover-actions small { + color: #94a3b8; + font-size: 11px; +} + +.hidden-cover-input { + display: none; +} + @media (max-width: 760px) { .form-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } + + .cover-editor { + grid-template-columns: 1fr; + } }