新增首页 AW炫彩回收入口,支持后台配置展示图
首页第三入口点击查看回收说明图;配置项 mobile.aw_recycle 可配标题/图片;aw-recycle 走公开文件链路,首页鉴权预览兼容旧私有地址。
This commit is contained in:
@@ -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 HomeAnnouncementsDialog from '../components/HomeAnnouncementsDialog.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 OrderAgreementsDialog from '../components/OrderAgreementsDialog.vue'
|
||||
import PostRentalNoticeDialog from '../components/PostRentalNoticeDialog.vue'
|
||||
@@ -70,6 +75,7 @@ const publishVisible = ref(false)
|
||||
const salePriceVisible = ref(false)
|
||||
const announcementsVisible = ref(false)
|
||||
const bannersVisible = ref(false)
|
||||
const awRecycleVisible = ref(false)
|
||||
const listingPublishAgreementsVisible = ref(false)
|
||||
const agreementsVisible = ref(false)
|
||||
const postRentalNoticeVisible = ref(false)
|
||||
@@ -90,6 +96,9 @@ const homeAnnouncementsConfig = computed(
|
||||
const homeBannersConfig = computed(
|
||||
() => 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(
|
||||
() => 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 !== 'mobile.home_announcements' &&
|
||||
item.key !== 'mobile.home_banners' &&
|
||||
item.key !== 'mobile.aw_recycle' &&
|
||||
item.key !== 'listing.publish_agreements' &&
|
||||
item.key !== 'order.agreements' &&
|
||||
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 agreements = parseOrderAgreements(orderAgreementsConfig.value?.value || '')
|
||||
return {
|
||||
@@ -236,6 +253,8 @@ function openEdit(row: SystemConfig) {
|
||||
announcementsVisible.value = true
|
||||
} else if (row.key === 'mobile.home_banners') {
|
||||
bannersVisible.value = true
|
||||
} else if (row.key === 'mobile.aw_recycle') {
|
||||
awRecycleVisible.value = true
|
||||
} else if (row.key === 'listing.publish_agreements') {
|
||||
listingPublishAgreementsVisible.value = true
|
||||
} else if (row.key === 'order.agreements') {
|
||||
@@ -269,6 +288,22 @@ function parseHomeBanners(raw: string) {
|
||||
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) {
|
||||
const parsed = safeParseJSON(raw, defaultOrderAgreements)
|
||||
return {
|
||||
@@ -479,20 +514,26 @@ function formatConfigValue(row: SystemConfig) {
|
||||
</div>
|
||||
</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>
|
||||
<p class="eyebrow">Home Content</p>
|
||||
<h2>移动端首页运营配置</h2>
|
||||
<span>管理首页公告滚动内容和顶部轮播图,保存后移动端会从接口读取最新配置。</span>
|
||||
<h2>首页运营配置</h2>
|
||||
<span>管理首页公告、轮播图与 AW炫彩回收入口展示图,保存后 PC/移动端从接口读取。</span>
|
||||
</div>
|
||||
<div class="panel-actions">
|
||||
<el-button v-if="homeAnnouncementsConfig" @click="openEdit(homeAnnouncementsConfig)"
|
||||
>编辑公告</el-button
|
||||
>
|
||||
<el-button v-if="homeBannersConfig" type="primary" @click="openEdit(homeBannersConfig)"
|
||||
<el-button v-if="homeBannersConfig" @click="openEdit(homeBannersConfig)"
|
||||
>编辑轮播图</el-button
|
||||
>
|
||||
<el-button v-if="awRecycleConfig" type="primary" @click="openEdit(awRecycleConfig)"
|
||||
>编辑 AW炫彩回收</el-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="publish-stat-grid home-stat-grid">
|
||||
@@ -504,17 +545,17 @@ function formatConfigValue(row: SystemConfig) {
|
||||
<strong>{{ homeStats.bannerCount }}</strong>
|
||||
<span>轮播图</span>
|
||||
</div>
|
||||
<div class="publish-stat">
|
||||
<strong>{{ awRecycleStats.enabledLabel }}</strong>
|
||||
<span>AW炫彩回收</span>
|
||||
</div>
|
||||
<div class="publish-stat">
|
||||
<strong>接口</strong>
|
||||
<span>/api/mobile-home-config</span>
|
||||
</div>
|
||||
<div class="publish-stat">
|
||||
<strong>公告</strong>
|
||||
<span>{{ formatHomeConfigStatus(homeAnnouncementsConfig, '未初始化') }}</span>
|
||||
</div>
|
||||
<div class="publish-stat">
|
||||
<strong>轮播</strong>
|
||||
<span>{{ formatHomeConfigStatus(homeBannersConfig, '未初始化') }}</span>
|
||||
<strong>更新</strong>
|
||||
<span>{{ formatHomeConfigStatus(awRecycleConfig || homeBannersConfig, '未初始化') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -767,6 +808,13 @@ function formatConfigValue(row: SystemConfig) {
|
||||
@saved="loadConfigs"
|
||||
/>
|
||||
|
||||
<AwRecycleDialog
|
||||
v-if="awRecycleConfig"
|
||||
v-model="awRecycleVisible"
|
||||
:config="awRecycleConfig"
|
||||
@saved="loadConfigs"
|
||||
/>
|
||||
|
||||
<ListingPublishAgreementsDialog
|
||||
v-if="listingPublishAgreementsConfig"
|
||||
v-model="listingPublishAgreementsVisible"
|
||||
|
||||
Reference in New Issue
Block a user