From d9f552577a9f06b2d9133e641589d88a0ff3ae31 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sat, 15 Aug 2026 16:37:30 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=90=88=E4=BD=9C=E4=B8=8E?= =?UTF-8?q?=E5=8F=8D=E9=A6=88=E5=85=A5=E5=8F=A3=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/modules/file/handler.go | 3 +- backend/internal/modules/file/service.go | 4 +- backend/internal/modules/file/storage_test.go | 11 +++ .../systemconfig/cooperation_feedback.go | 43 ++++++++++ backend/internal/modules/systemconfig/dto.go | 17 +++- .../modules/systemconfig/repository.go | 2 + .../modules/systemconfig/service_home.go | 25 +++++- .../admin/components/AwRecycleDialog.vue | 38 ++++----- .../admin/views/AdminSystemConfigsView.vue | 79 ++++++++++++++++++- .../src/features/listings/api/homeConfig.ts | 40 +++++++++- .../src/features/listings/views/HomeView.vue | 49 +++++++++++- .../listings/views/MobileHomeView.css | 5 ++ .../listings/views/MobileHomeView.vue | 50 ++++++++++++ frontend/src/shared/utils/imageUpload.ts | 2 + 14 files changed, 332 insertions(+), 36 deletions(-) create mode 100644 backend/internal/modules/systemconfig/cooperation_feedback.go diff --git a/backend/internal/modules/file/handler.go b/backend/internal/modules/file/handler.go index a90a154..a5242f5 100644 --- a/backend/internal/modules/file/handler.go +++ b/backend/internal/modules/file/handler.go @@ -72,7 +72,8 @@ func (h *Handler) writeObject(c *gin.Context, publicOnly bool) { !strings.HasPrefix(key, "announcement/") && !strings.HasPrefix(key, "mohong/") && !strings.HasPrefix(key, "crash/") && - !strings.HasPrefix(key, "aw-recycle/") { + !strings.HasPrefix(key, "aw-recycle/") && + !strings.HasPrefix(key, "cooperation-feedback/") { response.Error(c, http.StatusNotFound, "not_found", "文件不存在或暂不可访问") return } diff --git a/backend/internal/modules/file/service.go b/backend/internal/modules/file/service.go index 00842ec..b0f58e6 100644 --- a/backend/internal/modules/file/service.go +++ b/backend/internal/modules/file/service.go @@ -106,7 +106,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 == "payment-cert" || scene == "announcement" || scene == "mohong" || scene == "crash" || scene == "aw-recycle" { + if scene == "home-banner" || scene == "avatar" || scene == "payment-cert" || scene == "announcement" || scene == "mohong" || scene == "crash" || scene == "aw-recycle" || scene == "cooperation-feedback" { fileURL = "/api/public/files/object?key=" + url.QueryEscape(key) } return fileURL @@ -115,7 +115,7 @@ 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", "mohong", "crash", "aw-recycle", "manual-disbursement": + case "listing", "handoff", "dispute", "realname", "avatar", "home-banner", "chat", "payment-cert", "announcement", "qrcode", "mohong", "crash", "aw-recycle", "cooperation-feedback", "manual-disbursement": return scene default: return "misc" diff --git a/backend/internal/modules/file/storage_test.go b/backend/internal/modules/file/storage_test.go index 386f32f..9799a6b 100644 --- a/backend/internal/modules/file/storage_test.go +++ b/backend/internal/modules/file/storage_test.go @@ -40,3 +40,14 @@ func TestAWRecycleSceneUsesPublicFileURL(t *testing.T) { t.Fatalf("aw-recycle file url = %q, want public file endpoint", got) } } + +func TestCooperationFeedbackSceneUsesPublicFileURL(t *testing.T) { + if got := normalizeScene("cooperation-feedback"); got != "cooperation-feedback" { + t.Fatalf("normalize cooperation-feedback scene = %q", got) + } + key := "cooperation-feedback/2026/07/19/example.webp" + got := fileURLForScene("cooperation-feedback", key) + if !strings.HasPrefix(got, "/api/public/files/object?") { + t.Fatalf("cooperation-feedback file url = %q, want public file endpoint", got) + } +} diff --git a/backend/internal/modules/systemconfig/cooperation_feedback.go b/backend/internal/modules/systemconfig/cooperation_feedback.go new file mode 100644 index 0000000..0995015 --- /dev/null +++ b/backend/internal/modules/systemconfig/cooperation_feedback.go @@ -0,0 +1,43 @@ +package systemconfig + +import ( + "encoding/json" + "strings" +) + +const cooperationFeedbackConfigKey = "mobile.cooperation_feedback" + +func defaultCooperationFeedback() CooperationFeedbackConfig { + return CooperationFeedbackConfig{ + Title: "合作与反馈", + Subtitle: "点击查看联系方式", + ImageURL: "", + Enabled: true, + } +} + +func defaultCooperationFeedbackConfigValue() string { + raw, err := json.Marshal(defaultCooperationFeedback()) + if err != nil { + return `{"title":"合作与反馈","subtitle":"点击查看联系方式","image_url":"","enabled":true}` + } + return string(raw) +} + +func normalizeCooperationFeedback(item CooperationFeedbackConfig) CooperationFeedbackConfig { + def := defaultCooperationFeedback() + title := strings.TrimSpace(item.Title) + if title == "" { + title = def.Title + } + subtitle := strings.TrimSpace(item.Subtitle) + if subtitle == "" { + subtitle = def.Subtitle + } + return CooperationFeedbackConfig{ + Title: title, + Subtitle: subtitle, + ImageURL: strings.TrimSpace(item.ImageURL), + Enabled: item.Enabled, + } +} diff --git a/backend/internal/modules/systemconfig/dto.go b/backend/internal/modules/systemconfig/dto.go index d784fe4..1523fcc 100644 --- a/backend/internal/modules/systemconfig/dto.go +++ b/backend/internal/modules/systemconfig/dto.go @@ -22,10 +22,11 @@ type HomeAnnouncementsDTO struct { } type HomeConfigDTO struct { - Announcements []string `json:"announcements"` - Banners []HomeBannerItem `json:"banners"` - PublishOptions PublishOptionsDTO `json:"publish_options"` - AWRecycle AWRecycleConfig `json:"aw_recycle"` + Announcements []string `json:"announcements"` + Banners []HomeBannerItem `json:"banners"` + PublishOptions PublishOptionsDTO `json:"publish_options"` + AWRecycle AWRecycleConfig `json:"aw_recycle"` + CooperationFeedback CooperationFeedbackConfig `json:"cooperation_feedback"` } // AWRecycleConfig 首页「AW炫彩回收」入口:点击展示可配置图片。 @@ -36,6 +37,14 @@ type AWRecycleConfig struct { Enabled bool `json:"enabled"` } +// CooperationFeedbackConfig 首页「合作与反馈」入口:点击展示可配置图片。 +type CooperationFeedbackConfig struct { + Title string `json:"title"` + Subtitle string `json:"subtitle"` + ImageURL string `json:"image_url"` + Enabled bool `json:"enabled"` +} + type OrderAgreementsDTO struct { VirtualAssetPurchase AgreementContentDTO `json:"virtual_asset_purchase"` RenterAgreement AgreementContentDTO `json:"renter_agreement"` diff --git a/backend/internal/modules/systemconfig/repository.go b/backend/internal/modules/systemconfig/repository.go index 75e1b03..a9e29ba 100644 --- a/backend/internal/modules/systemconfig/repository.go +++ b/backend/internal/modules/systemconfig/repository.go @@ -48,6 +48,7 @@ var defaultConfigs = []defaultConfig{ {Key: homeAnnouncementsConfigKey, Value: defaultHomeAnnouncementsConfigValue(), Description: "移动端首页公告 JSON 数组"}, {Key: homeBannersConfigKey, Value: defaultHomeBannersConfigValue(), Description: "移动端首页轮播图 JSON 数组"}, {Key: awRecycleConfigKey, Value: defaultAWRecycleConfigValue(), Description: "首页 AW炫彩回收入口配置 JSON(标题/副标题/展示图片)"}, + {Key: cooperationFeedbackConfigKey, Value: defaultCooperationFeedbackConfigValue(), Description: "首页 合作与反馈入口配置 JSON(标题/副标题/展示图片)"}, {Key: publishOptionsConfigKey, Value: defaultPublishOptionsConfigValue(), Description: "发布页选项配置 JSON"}, {Key: salePriceConfigKey, Value: defaultSalePriceConfigValue(), Description: "内部出售定价规则 JSON"}, } @@ -71,6 +72,7 @@ var adminVisibleConfigKeys = []string{ "mobile.home_announcements", "mobile.home_banners", "mobile.aw_recycle", + "mobile.cooperation_feedback", "order.agreements", "order.pending_payment_timeout_minutes", "order.return_overdue_grace_minutes", diff --git a/backend/internal/modules/systemconfig/service_home.go b/backend/internal/modules/systemconfig/service_home.go index 8f4fe2b..945e582 100644 --- a/backend/internal/modules/systemconfig/service_home.go +++ b/backend/internal/modules/systemconfig/service_home.go @@ -32,15 +32,20 @@ func (s *Service) HomeConfig(ctx context.Context) (*HomeConfigDTO, error) { if err != nil { return nil, err } + cooperationFeedback, err := s.cooperationFeedback(ctx) + if err != nil { + return nil, err + } publishOptions, err := s.publishOptions(ctx) if err != nil { return nil, err } return &HomeConfigDTO{ - Announcements: announcements, - Banners: banners, - PublishOptions: publishOptions, - AWRecycle: awRecycle, + Announcements: announcements, + Banners: banners, + PublishOptions: publishOptions, + AWRecycle: awRecycle, + CooperationFeedback: cooperationFeedback, }, nil } @@ -79,3 +84,15 @@ func (s *Service) awRecycle(ctx context.Context) (AWRecycleConfig, error) { } return normalizeAWRecycle(item), nil } + +func (s *Service) cooperationFeedback(ctx context.Context) (CooperationFeedbackConfig, error) { + value, err := s.repo.FindValue(ctx, cooperationFeedbackConfigKey) + if err != nil { + return defaultCooperationFeedback(), err + } + item := defaultCooperationFeedback() + if err := json.Unmarshal([]byte(value), &item); err != nil { + item = defaultCooperationFeedback() + } + return normalizeCooperationFeedback(item), nil +} diff --git a/frontend/src/features/admin/components/AwRecycleDialog.vue b/frontend/src/features/admin/components/AwRecycleDialog.vue index cf2ecc5..184c7c3 100644 --- a/frontend/src/features/admin/components/AwRecycleDialog.vue +++ b/frontend/src/features/admin/components/AwRecycleDialog.vue @@ -4,10 +4,7 @@ import { ElMessage } from 'element-plus' import { ref, watch } from 'vue' import { uploadAdminFile } from '@/shared/api/files' -import { - defaultAWRecycleConfig, - type AWRecycleConfig, -} from '@/features/listings/api/homeConfig' +import type { AWRecycleConfig } from '@/features/listings/api/homeConfig' import { updateSystemConfig, type SystemConfig } from '@/features/admin/api/systemConfigs' import { safeParseJSON } from '@/shared/utils/json' import AuthImage from '@/shared/components/business/AuthImage.vue' @@ -15,6 +12,10 @@ import AuthImage from '@/shared/components/business/AuthImage.vue' const props = defineProps<{ modelValue: boolean config: SystemConfig + defaults: AWRecycleConfig + dialogTitle: string + uploadScene: string + imageHint: string }>() const emit = defineEmits<{ @@ -25,31 +26,31 @@ const emit = defineEmits<{ const submitting = ref(false) const uploading = ref(false) const description = ref('') -const draft = ref({ ...defaultAWRecycleConfig }) +const draft = ref({ ...props.defaults }) watch( () => props.modelValue, val => { if (!val) return - draft.value = parseAWRecycle(props.config.value) + draft.value = parseImageEntry(props.config.value) description.value = props.config.description || '' }, { immediate: true } ) -function parseAWRecycle(raw: string): AWRecycleConfig { - const parsed = safeParseJSON(raw, defaultAWRecycleConfig) +function parseImageEntry(raw: string): AWRecycleConfig { + const parsed = safeParseJSON(raw, props.defaults) return { title: typeof parsed.title === 'string' && parsed.title.trim() ? parsed.title.trim() - : defaultAWRecycleConfig.title, + : props.defaults.title, subtitle: typeof parsed.subtitle === 'string' && parsed.subtitle.trim() ? parsed.subtitle.trim() - : defaultAWRecycleConfig.subtitle, + : props.defaults.subtitle, image_url: typeof parsed.image_url === 'string' ? parsed.image_url.trim() : '', - enabled: typeof parsed.enabled === 'boolean' ? parsed.enabled : true, + enabled: typeof parsed.enabled === 'boolean' ? parsed.enabled : props.defaults.enabled, } } @@ -60,7 +61,7 @@ async function handleUpload(event: Event) { if (!file) return uploading.value = true try { - const uploaded = await uploadAdminFile(file, 'aw-recycle') + const uploaded = await uploadAdminFile(file, props.uploadScene) draft.value.image_url = uploaded.url ElMessage.success('图片已上传') } catch (error) { @@ -75,8 +76,8 @@ async function handleSave() { try { const value = JSON.stringify( { - title: draft.value.title.trim() || defaultAWRecycleConfig.title, - subtitle: draft.value.subtitle.trim() || defaultAWRecycleConfig.subtitle, + title: draft.value.title.trim() || props.defaults.title, + subtitle: draft.value.subtitle.trim() || props.defaults.subtitle, image_url: draft.value.image_url.trim(), enabled: draft.value.enabled, }, @@ -101,7 +102,7 @@ async function handleSave() { diff --git a/frontend/src/shared/utils/imageUpload.ts b/frontend/src/shared/utils/imageUpload.ts index 3389926..478ffb6 100644 --- a/frontend/src/shared/utils/imageUpload.ts +++ b/frontend/src/shared/utils/imageUpload.ts @@ -4,6 +4,7 @@ const IMAGE_UPLOAD_MAX_SIDE: Record = { announcement: 1600, 'home-banner': 1920, 'aw-recycle': 1920, + 'cooperation-feedback': 1920, listing: 1920, dispute: 1920, handoff: 1920, @@ -17,6 +18,7 @@ const IMAGE_UPLOAD_QUALITY: Record = { announcement: 0.84, 'home-banner': 0.84, 'aw-recycle': 0.84, + 'cooperation-feedback': 0.84, listing: 0.84, dispute: 0.86, handoff: 0.86,