新增首页 AW炫彩回收入口,支持后台配置展示图
首页第三入口点击查看回收说明图;配置项 mobile.aw_recycle 可配标题/图片;aw-recycle 走公开文件链路,首页鉴权预览兼容旧私有地址。
This commit is contained in:
@@ -70,7 +70,9 @@ func (h *Handler) writeObject(c *gin.Context, publicOnly bool) {
|
|||||||
!strings.HasPrefix(key, "avatar/") &&
|
!strings.HasPrefix(key, "avatar/") &&
|
||||||
!strings.HasPrefix(key, "payment-cert/") &&
|
!strings.HasPrefix(key, "payment-cert/") &&
|
||||||
!strings.HasPrefix(key, "announcement/") &&
|
!strings.HasPrefix(key, "announcement/") &&
|
||||||
!strings.HasPrefix(key, "mohong/") && !strings.HasPrefix(key, "crash/") {
|
!strings.HasPrefix(key, "mohong/") &&
|
||||||
|
!strings.HasPrefix(key, "crash/") &&
|
||||||
|
!strings.HasPrefix(key, "aw-recycle/") {
|
||||||
response.Error(c, http.StatusNotFound, "not_found", "文件不存在或暂不可访问")
|
response.Error(c, http.StatusNotFound, "not_found", "文件不存在或暂不可访问")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ func normalizeContentType(contentType string, data []byte) string {
|
|||||||
|
|
||||||
func fileURLForScene(scene string, key string) string {
|
func fileURLForScene(scene string, key string) string {
|
||||||
fileURL := "/api/files/object?key=" + url.QueryEscape(key)
|
fileURL := "/api/files/object?key=" + url.QueryEscape(key)
|
||||||
if scene == "home-banner" || scene == "avatar" || scene == "payment-cert" || scene == "announcement" || scene == "mohong" || scene == "crash" {
|
if scene == "home-banner" || scene == "avatar" || scene == "payment-cert" || scene == "announcement" || scene == "mohong" || scene == "crash" || scene == "aw-recycle" {
|
||||||
fileURL = "/api/public/files/object?key=" + url.QueryEscape(key)
|
fileURL = "/api/public/files/object?key=" + url.QueryEscape(key)
|
||||||
}
|
}
|
||||||
return fileURL
|
return fileURL
|
||||||
@@ -115,7 +115,7 @@ func fileURLForScene(scene string, key string) string {
|
|||||||
func normalizeScene(scene string) string {
|
func normalizeScene(scene string) string {
|
||||||
scene = strings.TrimSpace(strings.ToLower(scene))
|
scene = strings.TrimSpace(strings.ToLower(scene))
|
||||||
switch scene {
|
switch scene {
|
||||||
case "listing", "handoff", "dispute", "realname", "avatar", "home-banner", "chat", "payment-cert", "announcement", "qrcode", "mohong", "crash":
|
case "listing", "handoff", "dispute", "realname", "avatar", "home-banner", "chat", "payment-cert", "announcement", "qrcode", "mohong", "crash", "aw-recycle":
|
||||||
return scene
|
return scene
|
||||||
default:
|
default:
|
||||||
return "misc"
|
return "misc"
|
||||||
|
|||||||
@@ -29,3 +29,14 @@ func TestNormalizeSceneAllowsAnnouncement(t *testing.T) {
|
|||||||
t.Fatalf("normalize announcement scene = %q", got)
|
t.Fatalf("normalize announcement scene = %q", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAWRecycleSceneUsesPublicFileURL(t *testing.T) {
|
||||||
|
if got := normalizeScene("aw-recycle"); got != "aw-recycle" {
|
||||||
|
t.Fatalf("normalize aw-recycle scene = %q", got)
|
||||||
|
}
|
||||||
|
key := "aw-recycle/2026/07/19/example.webp"
|
||||||
|
got := fileURLForScene("aw-recycle", key)
|
||||||
|
if !strings.HasPrefix(got, "/api/public/files/object?") {
|
||||||
|
t.Fatalf("aw-recycle file url = %q, want public file endpoint", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package systemconfig
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const awRecycleConfigKey = "mobile.aw_recycle"
|
||||||
|
|
||||||
|
func defaultAWRecycle() AWRecycleConfig {
|
||||||
|
return AWRecycleConfig{
|
||||||
|
Title: "AW炫彩回收",
|
||||||
|
Subtitle: "点击查看回收说明",
|
||||||
|
ImageURL: "",
|
||||||
|
Enabled: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultAWRecycleConfigValue() string {
|
||||||
|
raw, err := json.Marshal(defaultAWRecycle())
|
||||||
|
if err != nil {
|
||||||
|
return `{"title":"AW炫彩回收","subtitle":"点击查看回收说明","image_url":"","enabled":true}`
|
||||||
|
}
|
||||||
|
return string(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeAWRecycle(item AWRecycleConfig) AWRecycleConfig {
|
||||||
|
def := defaultAWRecycle()
|
||||||
|
title := strings.TrimSpace(item.Title)
|
||||||
|
if title == "" {
|
||||||
|
title = def.Title
|
||||||
|
}
|
||||||
|
subtitle := strings.TrimSpace(item.Subtitle)
|
||||||
|
if subtitle == "" {
|
||||||
|
subtitle = def.Subtitle
|
||||||
|
}
|
||||||
|
return AWRecycleConfig{
|
||||||
|
Title: title,
|
||||||
|
Subtitle: subtitle,
|
||||||
|
ImageURL: strings.TrimSpace(item.ImageURL),
|
||||||
|
Enabled: item.Enabled,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,15 @@ type HomeConfigDTO struct {
|
|||||||
Announcements []string `json:"announcements"`
|
Announcements []string `json:"announcements"`
|
||||||
Banners []HomeBannerItem `json:"banners"`
|
Banners []HomeBannerItem `json:"banners"`
|
||||||
PublishOptions PublishOptionsDTO `json:"publish_options"`
|
PublishOptions PublishOptionsDTO `json:"publish_options"`
|
||||||
|
AWRecycle AWRecycleConfig `json:"aw_recycle"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AWRecycleConfig 首页「AW炫彩回收」入口:点击展示可配置图片。
|
||||||
|
type AWRecycleConfig struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Subtitle string `json:"subtitle"`
|
||||||
|
ImageURL string `json:"image_url"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OrderAgreementsDTO struct {
|
type OrderAgreementsDTO struct {
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ var defaultConfigs = []defaultConfig{
|
|||||||
{Key: "integration.paddle_ocr_model", Value: "PaddleOCR-VL-1.6", Description: "PaddleOCR 识别模型"},
|
{Key: "integration.paddle_ocr_model", Value: "PaddleOCR-VL-1.6", Description: "PaddleOCR 识别模型"},
|
||||||
{Key: homeAnnouncementsConfigKey, Value: defaultHomeAnnouncementsConfigValue(), Description: "移动端首页公告 JSON 数组"},
|
{Key: homeAnnouncementsConfigKey, Value: defaultHomeAnnouncementsConfigValue(), Description: "移动端首页公告 JSON 数组"},
|
||||||
{Key: homeBannersConfigKey, Value: defaultHomeBannersConfigValue(), Description: "移动端首页轮播图 JSON 数组"},
|
{Key: homeBannersConfigKey, Value: defaultHomeBannersConfigValue(), Description: "移动端首页轮播图 JSON 数组"},
|
||||||
|
{Key: awRecycleConfigKey, Value: defaultAWRecycleConfigValue(), Description: "首页 AW炫彩回收入口配置 JSON(标题/副标题/展示图片)"},
|
||||||
{Key: publishOptionsConfigKey, Value: defaultPublishOptionsConfigValue(), Description: "发布页选项配置 JSON"},
|
{Key: publishOptionsConfigKey, Value: defaultPublishOptionsConfigValue(), Description: "发布页选项配置 JSON"},
|
||||||
{Key: salePriceConfigKey, Value: defaultSalePriceConfigValue(), Description: "内部出售定价规则 JSON"},
|
{Key: salePriceConfigKey, Value: defaultSalePriceConfigValue(), Description: "内部出售定价规则 JSON"},
|
||||||
}
|
}
|
||||||
@@ -67,6 +68,7 @@ var adminVisibleConfigKeys = []string{
|
|||||||
"listing.sale_price_config",
|
"listing.sale_price_config",
|
||||||
"mobile.home_announcements",
|
"mobile.home_announcements",
|
||||||
"mobile.home_banners",
|
"mobile.home_banners",
|
||||||
|
"mobile.aw_recycle",
|
||||||
"order.agreements",
|
"order.agreements",
|
||||||
"order.pending_payment_timeout_minutes",
|
"order.pending_payment_timeout_minutes",
|
||||||
"order.return_overdue_grace_minutes",
|
"order.return_overdue_grace_minutes",
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ func (s *Service) HomeConfig(ctx context.Context) (*HomeConfigDTO, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
awRecycle, err := s.awRecycle(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
publishOptions, err := s.publishOptions(ctx)
|
publishOptions, err := s.publishOptions(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -36,6 +40,7 @@ func (s *Service) HomeConfig(ctx context.Context) (*HomeConfigDTO, error) {
|
|||||||
Announcements: announcements,
|
Announcements: announcements,
|
||||||
Banners: banners,
|
Banners: banners,
|
||||||
PublishOptions: publishOptions,
|
PublishOptions: publishOptions,
|
||||||
|
AWRecycle: awRecycle,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,3 +67,15 @@ func (s *Service) homeBanners(ctx context.Context) ([]HomeBannerItem, error) {
|
|||||||
}
|
}
|
||||||
return normalizeHomeBanners(items), nil
|
return normalizeHomeBanners(items), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) awRecycle(ctx context.Context) (AWRecycleConfig, error) {
|
||||||
|
value, err := s.repo.FindValue(ctx, awRecycleConfigKey)
|
||||||
|
if err != nil {
|
||||||
|
return defaultAWRecycle(), err
|
||||||
|
}
|
||||||
|
item := defaultAWRecycle()
|
||||||
|
if err := json.Unmarshal([]byte(value), &item); err != nil {
|
||||||
|
item = defaultAWRecycle()
|
||||||
|
}
|
||||||
|
return normalizeAWRecycle(item), nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,231 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { readError } from '@/shared/utils/error'
|
||||||
|
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 { updateSystemConfig, type SystemConfig } from '@/features/admin/api/systemConfigs'
|
||||||
|
import { safeParseJSON } from '@/shared/utils/json'
|
||||||
|
import AuthImage from '@/shared/components/business/AuthImage.vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: boolean
|
||||||
|
config: SystemConfig
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', val: boolean): void
|
||||||
|
(e: 'saved'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const submitting = ref(false)
|
||||||
|
const uploading = ref(false)
|
||||||
|
const description = ref('')
|
||||||
|
const draft = ref<AWRecycleConfig>({ ...defaultAWRecycleConfig })
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
val => {
|
||||||
|
if (!val) return
|
||||||
|
draft.value = parseAWRecycle(props.config.value)
|
||||||
|
description.value = props.config.description || ''
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
function parseAWRecycle(raw: string): AWRecycleConfig {
|
||||||
|
const parsed = safeParseJSON(raw, defaultAWRecycleConfig)
|
||||||
|
return {
|
||||||
|
title:
|
||||||
|
typeof parsed.title === 'string' && parsed.title.trim()
|
||||||
|
? parsed.title.trim()
|
||||||
|
: defaultAWRecycleConfig.title,
|
||||||
|
subtitle:
|
||||||
|
typeof parsed.subtitle === 'string' && parsed.subtitle.trim()
|
||||||
|
? parsed.subtitle.trim()
|
||||||
|
: defaultAWRecycleConfig.subtitle,
|
||||||
|
image_url: typeof parsed.image_url === 'string' ? parsed.image_url.trim() : '',
|
||||||
|
enabled: typeof parsed.enabled === 'boolean' ? parsed.enabled : true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleUpload(event: Event) {
|
||||||
|
const input = event.target as HTMLInputElement
|
||||||
|
const file = input.files?.[0]
|
||||||
|
input.value = ''
|
||||||
|
if (!file) return
|
||||||
|
uploading.value = true
|
||||||
|
try {
|
||||||
|
const uploaded = await uploadAdminFile(file, 'aw-recycle')
|
||||||
|
draft.value.image_url = uploaded.url
|
||||||
|
ElMessage.success('图片已上传')
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(readError(error, '图片上传失败'))
|
||||||
|
} finally {
|
||||||
|
uploading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSave() {
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
const value = JSON.stringify(
|
||||||
|
{
|
||||||
|
title: draft.value.title.trim() || defaultAWRecycleConfig.title,
|
||||||
|
subtitle: draft.value.subtitle.trim() || defaultAWRecycleConfig.subtitle,
|
||||||
|
image_url: draft.value.image_url.trim(),
|
||||||
|
enabled: draft.value.enabled,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)
|
||||||
|
await updateSystemConfig(props.config.key, {
|
||||||
|
value,
|
||||||
|
description: description.value,
|
||||||
|
})
|
||||||
|
ElMessage.success('配置已更新')
|
||||||
|
emit('saved')
|
||||||
|
emit('update:modelValue', false)
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(readError(error, '保存失败'))
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
:model-value="modelValue"
|
||||||
|
title="编辑 AW炫彩回收"
|
||||||
|
width="640px"
|
||||||
|
@update:model-value="emit('update:modelValue', $event)"
|
||||||
|
>
|
||||||
|
<div class="dialog-body">
|
||||||
|
<p class="config-key-label">
|
||||||
|
<strong>{{ config.key }}</strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<el-form label-width="88px">
|
||||||
|
<el-form-item label="启用入口">
|
||||||
|
<el-switch v-model="draft.enabled" active-text="显示" inactive-text="隐藏" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="标题">
|
||||||
|
<el-input v-model="draft.title" maxlength="32" placeholder="AW炫彩回收" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="副标题">
|
||||||
|
<el-input v-model="draft.subtitle" maxlength="64" placeholder="点击查看回收说明" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="展示图片">
|
||||||
|
<div class="image-editor">
|
||||||
|
<el-input v-model="draft.image_url" placeholder="图片 URL" />
|
||||||
|
<div class="image-tools">
|
||||||
|
<AuthImage
|
||||||
|
v-if="draft.image_url"
|
||||||
|
:source="draft.image_url"
|
||||||
|
admin
|
||||||
|
alt="预览"
|
||||||
|
fit="cover"
|
||||||
|
image-class="preview-img"
|
||||||
|
/>
|
||||||
|
<div v-else class="empty-image">暂无图片</div>
|
||||||
|
<label class="upload-trigger">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/jpeg,image/png,image/webp"
|
||||||
|
@change="handleUpload"
|
||||||
|
/>
|
||||||
|
{{ uploading ? '上传中…' : '上传图片' }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<p class="hint">
|
||||||
|
走后台通用上传(scene=aw-recycle,公开可访问)。首页点击入口后展示此图,建议竖图或说明海报。
|
||||||
|
若旧图路径含 misc/ 且无法预览,请重新上传。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注">
|
||||||
|
<el-input v-model="description" type="textarea" :rows="2" placeholder="配置说明" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="emit('update:modelValue', false)">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="submitting" @click="handleSave">保存</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dialog-body {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-key-label {
|
||||||
|
margin: 0;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-editor {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-tools {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 180px auto;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-tools :deep(.preview-img),
|
||||||
|
.empty-image {
|
||||||
|
width: 180px;
|
||||||
|
height: 120px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
object-fit: cover;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-image {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-trigger {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
background: #fff;
|
||||||
|
color: #374151;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-trigger input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
margin: 0;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -27,6 +27,11 @@ import PublishOptionsDialog from '../components/PublishOptionsDialog.vue'
|
|||||||
import SalePriceDialog from '../components/SalePriceDialog.vue'
|
import SalePriceDialog from '../components/SalePriceDialog.vue'
|
||||||
import HomeAnnouncementsDialog from '../components/HomeAnnouncementsDialog.vue'
|
import HomeAnnouncementsDialog from '../components/HomeAnnouncementsDialog.vue'
|
||||||
import HomeBannersDialog from '../components/HomeBannersDialog.vue'
|
import HomeBannersDialog from '../components/HomeBannersDialog.vue'
|
||||||
|
import AwRecycleDialog from '../components/AwRecycleDialog.vue'
|
||||||
|
import {
|
||||||
|
defaultAWRecycleConfig,
|
||||||
|
type AWRecycleConfig,
|
||||||
|
} from '@/features/listings/api/homeConfig'
|
||||||
import ListingPublishAgreementsDialog from '../components/ListingPublishAgreementsDialog.vue'
|
import ListingPublishAgreementsDialog from '../components/ListingPublishAgreementsDialog.vue'
|
||||||
import OrderAgreementsDialog from '../components/OrderAgreementsDialog.vue'
|
import OrderAgreementsDialog from '../components/OrderAgreementsDialog.vue'
|
||||||
import PostRentalNoticeDialog from '../components/PostRentalNoticeDialog.vue'
|
import PostRentalNoticeDialog from '../components/PostRentalNoticeDialog.vue'
|
||||||
@@ -70,6 +75,7 @@ const publishVisible = ref(false)
|
|||||||
const salePriceVisible = ref(false)
|
const salePriceVisible = ref(false)
|
||||||
const announcementsVisible = ref(false)
|
const announcementsVisible = ref(false)
|
||||||
const bannersVisible = ref(false)
|
const bannersVisible = ref(false)
|
||||||
|
const awRecycleVisible = ref(false)
|
||||||
const listingPublishAgreementsVisible = ref(false)
|
const listingPublishAgreementsVisible = ref(false)
|
||||||
const agreementsVisible = ref(false)
|
const agreementsVisible = ref(false)
|
||||||
const postRentalNoticeVisible = ref(false)
|
const postRentalNoticeVisible = ref(false)
|
||||||
@@ -90,6 +96,9 @@ const homeAnnouncementsConfig = computed(
|
|||||||
const homeBannersConfig = computed(
|
const homeBannersConfig = computed(
|
||||||
() => configs.value.find(item => item.key === 'mobile.home_banners') || null
|
() => configs.value.find(item => item.key === 'mobile.home_banners') || null
|
||||||
)
|
)
|
||||||
|
const awRecycleConfig = computed(
|
||||||
|
() => configs.value.find(item => item.key === 'mobile.aw_recycle') || null
|
||||||
|
)
|
||||||
const listingPublishAgreementsConfig = computed(
|
const listingPublishAgreementsConfig = computed(
|
||||||
() => configs.value.find(item => item.key === 'listing.publish_agreements') || null
|
() => configs.value.find(item => item.key === 'listing.publish_agreements') || null
|
||||||
)
|
)
|
||||||
@@ -135,6 +144,7 @@ const regularConfigs = computed(() =>
|
|||||||
item.key !== 'listing.publish_cooldown_minutes' &&
|
item.key !== 'listing.publish_cooldown_minutes' &&
|
||||||
item.key !== 'mobile.home_announcements' &&
|
item.key !== 'mobile.home_announcements' &&
|
||||||
item.key !== 'mobile.home_banners' &&
|
item.key !== 'mobile.home_banners' &&
|
||||||
|
item.key !== 'mobile.aw_recycle' &&
|
||||||
item.key !== 'listing.publish_agreements' &&
|
item.key !== 'listing.publish_agreements' &&
|
||||||
item.key !== 'order.agreements' &&
|
item.key !== 'order.agreements' &&
|
||||||
item.key !== 'profile.post_rental_notice' &&
|
item.key !== 'profile.post_rental_notice' &&
|
||||||
@@ -180,6 +190,13 @@ const homeStats = computed(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const awRecycleStats = computed(() => {
|
||||||
|
const item = parseAWRecycle(awRecycleConfig.value?.value || '')
|
||||||
|
return {
|
||||||
|
enabledLabel: item.enabled ? (item.image_url ? '已配置图' : '已开启') : '已隐藏',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const agreementStats = computed(() => {
|
const agreementStats = computed(() => {
|
||||||
const agreements = parseOrderAgreements(orderAgreementsConfig.value?.value || '')
|
const agreements = parseOrderAgreements(orderAgreementsConfig.value?.value || '')
|
||||||
return {
|
return {
|
||||||
@@ -236,6 +253,8 @@ function openEdit(row: SystemConfig) {
|
|||||||
announcementsVisible.value = true
|
announcementsVisible.value = true
|
||||||
} else if (row.key === 'mobile.home_banners') {
|
} else if (row.key === 'mobile.home_banners') {
|
||||||
bannersVisible.value = true
|
bannersVisible.value = true
|
||||||
|
} else if (row.key === 'mobile.aw_recycle') {
|
||||||
|
awRecycleVisible.value = true
|
||||||
} else if (row.key === 'listing.publish_agreements') {
|
} else if (row.key === 'listing.publish_agreements') {
|
||||||
listingPublishAgreementsVisible.value = true
|
listingPublishAgreementsVisible.value = true
|
||||||
} else if (row.key === 'order.agreements') {
|
} else if (row.key === 'order.agreements') {
|
||||||
@@ -269,6 +288,22 @@ function parseHomeBanners(raw: string) {
|
|||||||
return cloneHomeBanners(mergeHomeConfig({ banners: Array.isArray(parsed) ? parsed : [] }).banners)
|
return cloneHomeBanners(mergeHomeConfig({ banners: Array.isArray(parsed) ? parsed : [] }).banners)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseAWRecycle(raw: string): AWRecycleConfig {
|
||||||
|
const parsed = safeParseJSON(raw, defaultAWRecycleConfig)
|
||||||
|
return {
|
||||||
|
title:
|
||||||
|
typeof parsed.title === 'string' && parsed.title.trim()
|
||||||
|
? parsed.title.trim()
|
||||||
|
: defaultAWRecycleConfig.title,
|
||||||
|
subtitle:
|
||||||
|
typeof parsed.subtitle === 'string' && parsed.subtitle.trim()
|
||||||
|
? parsed.subtitle.trim()
|
||||||
|
: defaultAWRecycleConfig.subtitle,
|
||||||
|
image_url: typeof parsed.image_url === 'string' ? parsed.image_url.trim() : '',
|
||||||
|
enabled: typeof parsed.enabled === 'boolean' ? parsed.enabled : true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function parseOrderAgreements(raw: string) {
|
function parseOrderAgreements(raw: string) {
|
||||||
const parsed = safeParseJSON(raw, defaultOrderAgreements)
|
const parsed = safeParseJSON(raw, defaultOrderAgreements)
|
||||||
return {
|
return {
|
||||||
@@ -479,20 +514,26 @@ function formatConfigValue(row: SystemConfig) {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section v-if="homeAnnouncementsConfig || homeBannersConfig" class="publish-config-panel">
|
<section
|
||||||
|
v-if="homeAnnouncementsConfig || homeBannersConfig || awRecycleConfig"
|
||||||
|
class="publish-config-panel"
|
||||||
|
>
|
||||||
<div class="publish-config-main">
|
<div class="publish-config-main">
|
||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">Home Content</p>
|
<p class="eyebrow">Home Content</p>
|
||||||
<h2>移动端首页运营配置</h2>
|
<h2>首页运营配置</h2>
|
||||||
<span>管理首页公告滚动内容和顶部轮播图,保存后移动端会从接口读取最新配置。</span>
|
<span>管理首页公告、轮播图与 AW炫彩回收入口展示图,保存后 PC/移动端从接口读取。</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-actions">
|
<div class="panel-actions">
|
||||||
<el-button v-if="homeAnnouncementsConfig" @click="openEdit(homeAnnouncementsConfig)"
|
<el-button v-if="homeAnnouncementsConfig" @click="openEdit(homeAnnouncementsConfig)"
|
||||||
>编辑公告</el-button
|
>编辑公告</el-button
|
||||||
>
|
>
|
||||||
<el-button v-if="homeBannersConfig" type="primary" @click="openEdit(homeBannersConfig)"
|
<el-button v-if="homeBannersConfig" @click="openEdit(homeBannersConfig)"
|
||||||
>编辑轮播图</el-button
|
>编辑轮播图</el-button
|
||||||
>
|
>
|
||||||
|
<el-button v-if="awRecycleConfig" type="primary" @click="openEdit(awRecycleConfig)"
|
||||||
|
>编辑 AW炫彩回收</el-button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="publish-stat-grid home-stat-grid">
|
<div class="publish-stat-grid home-stat-grid">
|
||||||
@@ -504,17 +545,17 @@ function formatConfigValue(row: SystemConfig) {
|
|||||||
<strong>{{ homeStats.bannerCount }}</strong>
|
<strong>{{ homeStats.bannerCount }}</strong>
|
||||||
<span>轮播图</span>
|
<span>轮播图</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="publish-stat">
|
||||||
|
<strong>{{ awRecycleStats.enabledLabel }}</strong>
|
||||||
|
<span>AW炫彩回收</span>
|
||||||
|
</div>
|
||||||
<div class="publish-stat">
|
<div class="publish-stat">
|
||||||
<strong>接口</strong>
|
<strong>接口</strong>
|
||||||
<span>/api/mobile-home-config</span>
|
<span>/api/mobile-home-config</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="publish-stat">
|
<div class="publish-stat">
|
||||||
<strong>公告</strong>
|
<strong>更新</strong>
|
||||||
<span>{{ formatHomeConfigStatus(homeAnnouncementsConfig, '未初始化') }}</span>
|
<span>{{ formatHomeConfigStatus(awRecycleConfig || homeBannersConfig, '未初始化') }}</span>
|
||||||
</div>
|
|
||||||
<div class="publish-stat">
|
|
||||||
<strong>轮播</strong>
|
|
||||||
<span>{{ formatHomeConfigStatus(homeBannersConfig, '未初始化') }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -767,6 +808,13 @@ function formatConfigValue(row: SystemConfig) {
|
|||||||
@saved="loadConfigs"
|
@saved="loadConfigs"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<AwRecycleDialog
|
||||||
|
v-if="awRecycleConfig"
|
||||||
|
v-model="awRecycleVisible"
|
||||||
|
:config="awRecycleConfig"
|
||||||
|
@saved="loadConfigs"
|
||||||
|
/>
|
||||||
|
|
||||||
<ListingPublishAgreementsDialog
|
<ListingPublishAgreementsDialog
|
||||||
v-if="listingPublishAgreementsConfig"
|
v-if="listingPublishAgreementsConfig"
|
||||||
v-model="listingPublishAgreementsVisible"
|
v-model="listingPublishAgreementsVisible"
|
||||||
|
|||||||
@@ -11,10 +11,25 @@ export interface HomeBannerSlide {
|
|||||||
image_url?: string
|
image_url?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AWRecycleConfig {
|
||||||
|
title: string
|
||||||
|
subtitle: string
|
||||||
|
image_url: string
|
||||||
|
enabled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export interface MobileHomeConfig {
|
export interface MobileHomeConfig {
|
||||||
announcements: string[]
|
announcements: string[]
|
||||||
banners: HomeBannerSlide[]
|
banners: HomeBannerSlide[]
|
||||||
publish_options: ListingPublishOptions
|
publish_options: ListingPublishOptions
|
||||||
|
aw_recycle: AWRecycleConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultAWRecycleConfig: AWRecycleConfig = {
|
||||||
|
title: 'AW炫彩回收',
|
||||||
|
subtitle: '点击查看回收说明',
|
||||||
|
image_url: '',
|
||||||
|
enabled: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultHomeAnnouncements = [
|
export const defaultHomeAnnouncements = [
|
||||||
@@ -75,10 +90,26 @@ export function mergeHomeConfig(config?: Partial<MobileHomeConfig>): MobileHomeC
|
|||||||
})
|
})
|
||||||
.filter(item => item.title || item.image_url) || []
|
.filter(item => item.title || item.image_url) || []
|
||||||
|
|
||||||
|
const awRaw = config?.aw_recycle
|
||||||
|
const awRecycle: AWRecycleConfig = {
|
||||||
|
title:
|
||||||
|
typeof awRaw?.title === 'string' && awRaw.title.trim()
|
||||||
|
? awRaw.title.trim()
|
||||||
|
: defaultAWRecycleConfig.title,
|
||||||
|
subtitle:
|
||||||
|
typeof awRaw?.subtitle === 'string' && awRaw.subtitle.trim()
|
||||||
|
? awRaw.subtitle.trim()
|
||||||
|
: defaultAWRecycleConfig.subtitle,
|
||||||
|
image_url:
|
||||||
|
typeof awRaw?.image_url === 'string' ? awRaw.image_url.trim() : defaultAWRecycleConfig.image_url,
|
||||||
|
enabled: typeof awRaw?.enabled === 'boolean' ? awRaw.enabled : defaultAWRecycleConfig.enabled,
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
announcements: announcements.length ? announcements : defaultHomeAnnouncements,
|
announcements: announcements.length ? announcements : defaultHomeAnnouncements,
|
||||||
banners: banners.length ? banners : defaultHomeBanners,
|
banners: banners.length ? banners : defaultHomeBanners,
|
||||||
publish_options: mergeListingPublishOptions(config?.publish_options),
|
publish_options: mergeListingPublishOptions(config?.publish_options),
|
||||||
|
aw_recycle: awRecycle,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,15 @@
|
|||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { RouterLink, useRoute, useRouter } from 'vue-router'
|
import { RouterLink, useRoute, useRouter } from 'vue-router'
|
||||||
import {
|
import {
|
||||||
|
defaultAWRecycleConfig,
|
||||||
defaultHomeAnnouncements,
|
defaultHomeAnnouncements,
|
||||||
defaultHomeBanners,
|
defaultHomeBanners,
|
||||||
fetchMobileHomeConfig,
|
fetchMobileHomeConfig,
|
||||||
|
type AWRecycleConfig,
|
||||||
type HomeBannerSlide,
|
type HomeBannerSlide,
|
||||||
} from '@/features/listings/api/homeConfig'
|
} from '@/features/listings/api/homeConfig'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import AuthImage from '@/shared/components/business/AuthImage.vue'
|
||||||
import {
|
import {
|
||||||
emptyListingPublishOptions,
|
emptyListingPublishOptions,
|
||||||
type ListingPublishOptions,
|
type ListingPublishOptions,
|
||||||
@@ -37,6 +41,8 @@ import ListingCard from '../components/ListingCard.vue'
|
|||||||
|
|
||||||
const announcements = ref<string[]>(defaultHomeAnnouncements)
|
const announcements = ref<string[]>(defaultHomeAnnouncements)
|
||||||
const banners = ref<HomeBannerSlide[]>(defaultHomeBanners)
|
const banners = ref<HomeBannerSlide[]>(defaultHomeBanners)
|
||||||
|
const awRecycle = ref<AWRecycleConfig>({ ...defaultAWRecycleConfig })
|
||||||
|
const awRecycleDialogVisible = ref(false)
|
||||||
const publishOptions = ref<ListingPublishOptions>(emptyListingPublishOptions)
|
const publishOptions = ref<ListingPublishOptions>(emptyListingPublishOptions)
|
||||||
const sortBy = ref('recommended')
|
const sortBy = ref('recommended')
|
||||||
const sortOptions = [
|
const sortOptions = [
|
||||||
@@ -149,16 +155,26 @@ async function loadHome() {
|
|||||||
const [, config] = await Promise.all([loadListingsPage(true), fetchMobileHomeConfig()])
|
const [, config] = await Promise.all([loadListingsPage(true), fetchMobileHomeConfig()])
|
||||||
announcements.value = config.announcements
|
announcements.value = config.announcements
|
||||||
banners.value = config.banners
|
banners.value = config.banners
|
||||||
|
awRecycle.value = config.aw_recycle
|
||||||
publishOptions.value = config.publish_options
|
publishOptions.value = config.publish_options
|
||||||
} catch {
|
} catch {
|
||||||
announcements.value = defaultHomeAnnouncements
|
announcements.value = defaultHomeAnnouncements
|
||||||
banners.value = defaultHomeBanners
|
banners.value = defaultHomeBanners
|
||||||
|
awRecycle.value = { ...defaultAWRecycleConfig }
|
||||||
publishOptions.value = emptyListingPublishOptions
|
publishOptions.value = emptyListingPublishOptions
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openAWRecycle() {
|
||||||
|
if (!awRecycle.value.image_url) {
|
||||||
|
ElMessage.warning('回收说明图片尚未配置,请稍后再试')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
awRecycleDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
function updateFilters(partial: Partial<typeof filters>) {
|
function updateFilters(partial: Partial<typeof filters>) {
|
||||||
Object.assign(filters, partial)
|
Object.assign(filters, partial)
|
||||||
}
|
}
|
||||||
@@ -275,8 +291,36 @@ loadHome()
|
|||||||
<strong>撞车 <em>NEW</em></strong>
|
<strong>撞车 <em>NEW</em></strong>
|
||||||
<small>选购商品 · 支付后进群联系客服</small>
|
<small>选购商品 · 支付后进群联系客服</small>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
<button
|
||||||
|
v-if="awRecycle.enabled"
|
||||||
|
type="button"
|
||||||
|
class="biz-entry aw-recycle"
|
||||||
|
@click="openAWRecycle"
|
||||||
|
>
|
||||||
|
<strong>{{ awRecycle.title }}</strong>
|
||||||
|
<small>{{ awRecycle.subtitle }}</small>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
v-model="awRecycleDialogVisible"
|
||||||
|
:title="awRecycle.title"
|
||||||
|
width="720px"
|
||||||
|
class="aw-recycle-dialog"
|
||||||
|
align-center
|
||||||
|
destroy-on-close
|
||||||
|
>
|
||||||
|
<AuthImage
|
||||||
|
v-if="awRecycle.image_url"
|
||||||
|
:source="awRecycle.image_url"
|
||||||
|
:alt="awRecycle.title"
|
||||||
|
fit="contain"
|
||||||
|
image-class="aw-recycle-image"
|
||||||
|
:preview-src-list="[awRecycle.image_url]"
|
||||||
|
/>
|
||||||
|
<el-empty v-else description="暂无展示图片" />
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
<HomeFilters
|
<HomeFilters
|
||||||
:filters="filters"
|
:filters="filters"
|
||||||
:total-listings="totalListings"
|
:total-listings="totalListings"
|
||||||
@@ -353,7 +397,7 @@ loadHome()
|
|||||||
|
|
||||||
.biz-entry-grid {
|
.biz-entry-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,6 +410,9 @@ loadHome()
|
|||||||
border: 1px solid #eef1f5;
|
border: 1px solid #eef1f5;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
font: inherit;
|
||||||
transition:
|
transition:
|
||||||
border-color 0.18s ease,
|
border-color 0.18s ease,
|
||||||
box-shadow 0.18s ease;
|
box-shadow 0.18s ease;
|
||||||
@@ -396,6 +443,26 @@ loadHome()
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.biz-entry.aw-recycle {
|
||||||
|
border-color: #e8edff;
|
||||||
|
background: linear-gradient(135deg, #ffffff 0%, #f5f8ff 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.aw-recycle-image) {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 70vh;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.biz-entry-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.infinite-load-state {
|
.infinite-load-state {
|
||||||
padding: 6px 0 18px;
|
padding: 6px 0 18px;
|
||||||
color: #94a3b8;
|
color: #94a3b8;
|
||||||
|
|||||||
@@ -345,8 +345,8 @@
|
|||||||
|
|
||||||
.biz-entry-grid {
|
.biz-entry-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
gap: 10px;
|
gap: 8px;
|
||||||
margin: 0 0 12px;
|
margin: 0 0 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,14 +356,22 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding: 12px 12px 12px 14px;
|
padding: 12px 10px 12px 12px;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border: 1px solid #eef1f5;
|
border: 1px solid #eef1f5;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
font: inherit;
|
||||||
box-shadow: 0 4px 14px rgba(23, 35, 61, 0.04);
|
box-shadow: 0 4px 14px rgba(23, 35, 61, 0.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.biz-entry.aw-recycle {
|
||||||
|
border-color: #e8edff;
|
||||||
|
background: linear-gradient(135deg, #ffffff 0%, #f5f8ff 100%);
|
||||||
|
}
|
||||||
|
|
||||||
.biz-entry strong {
|
.biz-entry strong {
|
||||||
color: #17233d;
|
color: #17233d;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
import { onBeforeRouteLeave, RouterLink, useRoute, useRouter } from 'vue-router'
|
import { onBeforeRouteLeave, RouterLink, useRoute, useRouter } from 'vue-router'
|
||||||
import { showToast } from 'vant'
|
|
||||||
import MobileBottomNav from '@/components/MobileBottomNav.vue'
|
import MobileBottomNav from '@/components/MobileBottomNav.vue'
|
||||||
import { ensureSupportChat, resolveSupportScene } from '@/features/chats/api/chats'
|
import { ensureSupportChat, resolveSupportScene } from '@/features/chats/api/chats'
|
||||||
import { formatCent, formatMoney } from '@/shared/utils/money'
|
import { formatCent, formatMoney } from '@/shared/utils/money'
|
||||||
@@ -15,11 +14,15 @@ import {
|
|||||||
type PublicListingQuery,
|
type PublicListingQuery,
|
||||||
} from '@/features/listings/api/listings'
|
} from '@/features/listings/api/listings'
|
||||||
import {
|
import {
|
||||||
|
defaultAWRecycleConfig,
|
||||||
defaultHomeAnnouncements,
|
defaultHomeAnnouncements,
|
||||||
defaultHomeBanners,
|
defaultHomeBanners,
|
||||||
fetchMobileHomeConfig,
|
fetchMobileHomeConfig,
|
||||||
|
type AWRecycleConfig,
|
||||||
type HomeBannerSlide,
|
type HomeBannerSlide,
|
||||||
} from '@/features/listings/api/homeConfig'
|
} from '@/features/listings/api/homeConfig'
|
||||||
|
import { showImagePreview, showToast } from 'vant'
|
||||||
|
import { fetchFileBlobByURL } from '@/shared/api/files'
|
||||||
import MobileHomeFilterSheet, { type FilterSection } from './MobileHomeFilterSheet.vue'
|
import MobileHomeFilterSheet, { type FilterSection } from './MobileHomeFilterSheet.vue'
|
||||||
import {
|
import {
|
||||||
clearStoredHomeQuery,
|
clearStoredHomeQuery,
|
||||||
@@ -76,6 +79,7 @@ const refreshing = ref(false)
|
|||||||
const searchValue = ref('')
|
const searchValue = ref('')
|
||||||
const announcements = ref<string[]>(defaultHomeAnnouncements)
|
const announcements = ref<string[]>(defaultHomeAnnouncements)
|
||||||
const bannerSlides = ref<HomeBannerSlide[]>(defaultHomeBanners)
|
const bannerSlides = ref<HomeBannerSlide[]>(defaultHomeBanners)
|
||||||
|
const awRecycle = ref<AWRecycleConfig>({ ...defaultAWRecycleConfig })
|
||||||
const supportLoading = ref(false)
|
const supportLoading = ref(false)
|
||||||
const showBackTop = ref(false)
|
const showBackTop = ref(false)
|
||||||
const mobilePageSize = 10
|
const mobilePageSize = 10
|
||||||
@@ -457,6 +461,7 @@ async function loadHomeConfig() {
|
|||||||
const config = await fetchMobileHomeConfig()
|
const config = await fetchMobileHomeConfig()
|
||||||
announcements.value = config.announcements
|
announcements.value = config.announcements
|
||||||
bannerSlides.value = config.banners
|
bannerSlides.value = config.banners
|
||||||
|
awRecycle.value = config.aw_recycle
|
||||||
publishOptions.value = config.publish_options
|
publishOptions.value = config.publish_options
|
||||||
homeCache.saveHomeConfig({
|
homeCache.saveHomeConfig({
|
||||||
announcements: config.announcements,
|
announcements: config.announcements,
|
||||||
@@ -466,10 +471,39 @@ async function loadHomeConfig() {
|
|||||||
} catch {
|
} catch {
|
||||||
announcements.value = defaultHomeAnnouncements
|
announcements.value = defaultHomeAnnouncements
|
||||||
bannerSlides.value = defaultHomeBanners
|
bannerSlides.value = defaultHomeBanners
|
||||||
|
awRecycle.value = { ...defaultAWRecycleConfig }
|
||||||
publishOptions.value = emptyListingPublishOptions
|
publishOptions.value = emptyListingPublishOptions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function openAWRecycle() {
|
||||||
|
const source = awRecycle.value.image_url
|
||||||
|
if (!source) {
|
||||||
|
showToast({ message: '回收说明图片尚未配置', icon: 'warning-o' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// 私有 /api/files/object 需带鉴权拉取;公开 /api/public/files 可直接预览
|
||||||
|
let previewURL = source
|
||||||
|
if (
|
||||||
|
source.includes('/api/files/object') ||
|
||||||
|
source.includes('/api/admin/files/object')
|
||||||
|
) {
|
||||||
|
const blob = await fetchFileBlobByURL(source)
|
||||||
|
previewURL = URL.createObjectURL(blob)
|
||||||
|
}
|
||||||
|
showImagePreview({
|
||||||
|
images: [previewURL],
|
||||||
|
closeable: true,
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
showToast({
|
||||||
|
message: '图片加载失败,请在后台重新上传(需使用公开图)',
|
||||||
|
icon: 'cross',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function onRefresh() {
|
async function onRefresh() {
|
||||||
refreshing.value = true
|
refreshing.value = true
|
||||||
try {
|
try {
|
||||||
@@ -477,6 +511,7 @@ async function onRefresh() {
|
|||||||
const [, nextHomeConfig] = await Promise.all([loadListings(true), fetchMobileHomeConfig()])
|
const [, nextHomeConfig] = await Promise.all([loadListings(true), fetchMobileHomeConfig()])
|
||||||
announcements.value = nextHomeConfig.announcements
|
announcements.value = nextHomeConfig.announcements
|
||||||
bannerSlides.value = nextHomeConfig.banners
|
bannerSlides.value = nextHomeConfig.banners
|
||||||
|
awRecycle.value = nextHomeConfig.aw_recycle
|
||||||
publishOptions.value = nextHomeConfig.publish_options
|
publishOptions.value = nextHomeConfig.publish_options
|
||||||
homeCache.saveHomeConfig({
|
homeCache.saveHomeConfig({
|
||||||
announcements: nextHomeConfig.announcements,
|
announcements: nextHomeConfig.announcements,
|
||||||
@@ -842,6 +877,15 @@ syncMobileHomeQuery()
|
|||||||
<small>选购商品 · 一键找客服</small>
|
<small>选购商品 · 一键找客服</small>
|
||||||
<em class="biz-entry-badge">NEW</em>
|
<em class="biz-entry-badge">NEW</em>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
<button
|
||||||
|
v-if="awRecycle.enabled"
|
||||||
|
type="button"
|
||||||
|
class="biz-entry aw-recycle"
|
||||||
|
@click="openAWRecycle"
|
||||||
|
>
|
||||||
|
<strong>{{ awRecycle.title }}</strong>
|
||||||
|
<small>{{ awRecycle.subtitle }}</small>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="zone-strip" aria-label="账号专区">
|
<div class="zone-strip" aria-label="账号专区">
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const IMAGE_UPLOAD_MAX_SIDE: Record<string, number> = {
|
|||||||
chat: 1280,
|
chat: 1280,
|
||||||
announcement: 1600,
|
announcement: 1600,
|
||||||
'home-banner': 1920,
|
'home-banner': 1920,
|
||||||
|
'aw-recycle': 1920,
|
||||||
listing: 1920,
|
listing: 1920,
|
||||||
dispute: 1920,
|
dispute: 1920,
|
||||||
handoff: 1920,
|
handoff: 1920,
|
||||||
@@ -15,6 +16,7 @@ const IMAGE_UPLOAD_QUALITY: Record<string, number> = {
|
|||||||
chat: 0.8,
|
chat: 0.8,
|
||||||
announcement: 0.84,
|
announcement: 0.84,
|
||||||
'home-banner': 0.84,
|
'home-banner': 0.84,
|
||||||
|
'aw-recycle': 0.84,
|
||||||
listing: 0.84,
|
listing: 0.84,
|
||||||
dispute: 0.86,
|
dispute: 0.86,
|
||||||
handoff: 0.86,
|
handoff: 0.86,
|
||||||
|
|||||||
Reference in New Issue
Block a user