Files
hfb_sys/frontend/src/features/admin/views/AdminSystemConfigsView.vue
T
2026-06-27 22:26:37 +08:00

890 lines
29 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import {
emptyListingSalePriceConfig,
emptyListingPublishOptions,
mergeListingSalePriceConfig,
mergeListingPublishOptions,
type ListingPublishOptions,
type ListingPublishAgreements,
type PublishSalePriceConfig,
} from '@/features/listings/api/listingOptions'
import {
defaultHomeAnnouncements,
defaultHomeBanners,
mergeHomeConfig,
type HomeBannerSlide,
} from '@/features/listings/api/homeConfig'
import type { OrderAgreements } from '@/features/orders/api/orders'
import { fetchSystemConfigs, type SystemConfig } from '@/features/admin/api/systemConfigs'
import { formatDateTime } from '@/shared/utils/time'
import { safeParseJSON } from '@/shared/utils/json'
import { formatSystemConfigSelectValue } from '@/shared/utils/systemConfigOptions'
// 导入重构后的 Dialog 子组件
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 ListingPublishAgreementsDialog from '../components/ListingPublishAgreementsDialog.vue'
import OrderAgreementsDialog from '../components/OrderAgreementsDialog.vue'
import PostRentalNoticeDialog from '../components/PostRentalNoticeDialog.vue'
import GeneralConfigDialog from '../components/GeneralConfigDialog.vue'
import AutoWelcomeConfig from '../components/AutoWelcomeConfig.vue'
import {
fetchAutoWelcomeMessage,
updateAutoWelcomeMessage,
fetchListingGroupWelcomeMessage,
updateListingGroupWelcomeMessage,
} from '@/features/chats'
const defaultOrderAgreements: OrderAgreements = {
virtual_asset_purchase: {
title: '虚拟资产购买协议',
content: '',
},
renter_agreement: {
title: '租客协议',
content: '',
},
}
const defaultListingPublishAgreements: ListingPublishAgreements = {
virtual_asset_sale: {
title: '虚拟资产出售协议',
content: '',
},
seller_agreement: {
title: '号主协议',
content: '',
},
}
const loading = ref(false)
const configs = ref<SystemConfig[]>([])
const currentEditingConfig = ref<SystemConfig | null>(null)
// 各种子 Dialog 的可见性控制
const publishVisible = ref(false)
const salePriceVisible = ref(false)
const announcementsVisible = ref(false)
const bannersVisible = ref(false)
const listingPublishAgreementsVisible = ref(false)
const agreementsVisible = ref(false)
const postRentalNoticeVisible = ref(false)
const generalVisible = ref(false)
const publishConfig = computed(
() => configs.value.find(item => item.key === 'listing.publish_options') || null
)
const salePriceConfig = computed(
() => configs.value.find(item => item.key === 'listing.sale_price_config') || null
)
const listingPublishCooldownConfig = computed(
() => configs.value.find(item => item.key === 'listing.publish_cooldown_minutes') || null
)
const homeAnnouncementsConfig = computed(
() => configs.value.find(item => item.key === 'mobile.home_announcements') || null
)
const homeBannersConfig = computed(
() => configs.value.find(item => item.key === 'mobile.home_banners') || null
)
const listingPublishAgreementsConfig = computed(
() => configs.value.find(item => item.key === 'listing.publish_agreements') || null
)
const orderAgreementsConfig = computed(
() => configs.value.find(item => item.key === 'order.agreements') || null
)
const postRentalNoticeConfig = computed(
() => configs.value.find(item => item.key === 'profile.post_rental_notice') || null
)
const autoWelcomeConfig = computed(
() => configs.value.find(item => item.key === 'chat.auto_welcome_message') || null
)
const listingGroupWelcomeConfig = computed(
() => configs.value.find(item => item.key === 'chat.listing_group_welcome') || null
)
const renterRetentionConfig = computed(
() => configs.value.find(item => item.key === 'chat.renter_retention_days_after_order_end') || null
)
const paddleOcrTokenConfig = computed(() =>
systemConfigByKey(
'integration.paddle_ocr_token',
'',
'PaddleOCR API Token,用于二维码群名自动识别'
)
)
const paddleOcrJobUrlConfig = computed(() =>
systemConfigByKey(
'integration.paddle_ocr_job_url',
'https://paddleocr.aistudio-app.com/api/v2/ocr/jobs',
'PaddleOCR 异步任务接口地址'
)
)
const paddleOcrModelConfig = computed(() =>
systemConfigByKey('integration.paddle_ocr_model', 'PaddleOCR-VL-1.6', 'PaddleOCR 识别模型')
)
const regularConfigs = computed(() =>
configs.value.filter(
item =>
item.key !== 'listing.publish_options' &&
item.key !== 'listing.sale_price_config' &&
item.key !== 'listing.publish_cooldown_minutes' &&
item.key !== 'mobile.home_announcements' &&
item.key !== 'mobile.home_banners' &&
item.key !== 'listing.publish_agreements' &&
item.key !== 'order.agreements' &&
item.key !== 'profile.post_rental_notice' &&
item.key !== 'chat.auto_welcome_message' &&
item.key !== 'chat.listing_group_welcome' &&
item.key !== 'chat.renter_retention_days_after_order_end' &&
item.key !== 'integration.paddle_ocr_token' &&
item.key !== 'integration.paddle_ocr_job_url' &&
item.key !== 'integration.paddle_ocr_model'
)
)
// 只读计算属性,用于渲染卡片上的统计信息
const publishStats = computed(() => {
const options = safeParsePublishOptions(publishConfig.value?.value || '')
return {
baseCount:
options.server_options.length +
options.rank_options.length +
options.insurance_options.length +
options.level_options.length,
skinCount: options.skin_groups.reduce((sum, group) => sum + group.options.length, 0),
resourceCount: options.quantity_items.length,
screenshotCount: options.screenshot_slots.length,
regionCount: options.region_options.length,
}
})
const salePriceStats = computed(() => {
const config = safeParseSalePriceConfig(salePriceConfig.value?.value || '')
return {
fixedCount: config.fixed_markup_rules.length,
ratioCount: config.ratio_adjustment_rules.length,
}
})
const homeStats = computed(() => {
const announcements = parseHomeAnnouncements(homeAnnouncementsConfig.value?.value || '')
const banners = parseHomeBanners(homeBannersConfig.value?.value || '')
return {
announcementCount: announcements.length,
bannerCount: banners.length,
}
})
const agreementStats = computed(() => {
const agreements = parseOrderAgreements(orderAgreementsConfig.value?.value || '')
return {
virtualTitle: agreements.virtual_asset_purchase.title || '购买协议',
renterTitle: agreements.renter_agreement.title || '租客协议',
virtualWords: countContentWords(agreements.virtual_asset_purchase.content),
renterWords: countContentWords(agreements.renter_agreement.content),
}
})
const listingPublishAgreementStats = computed(() => {
const agreements = parseListingPublishAgreements(
listingPublishAgreementsConfig.value?.value || ''
)
return {
saleTitle: agreements.virtual_asset_sale.title || '出售协议',
sellerTitle: agreements.seller_agreement.title || '号主协议',
saleWords: countContentWords(agreements.virtual_asset_sale.content),
sellerWords: countContentWords(agreements.seller_agreement.content),
}
})
onMounted(loadConfigs)
async function loadConfigs() {
loading.value = true
try {
configs.value = await fetchSystemConfigs()
} finally {
loading.value = false
}
}
function systemConfigByKey(key: string, fallbackValue: string, description: string): SystemConfig {
const existing = configs.value.find(item => item.key === key)
if (existing) return existing
return {
id: 0,
key,
value: fallbackValue,
description,
created_at: '',
updated_at: '',
}
}
function openEdit(row: SystemConfig) {
currentEditingConfig.value = row
if (row.key === 'listing.publish_options') {
publishVisible.value = true
} else if (row.key === 'listing.sale_price_config') {
salePriceVisible.value = true
} else if (row.key === 'mobile.home_announcements') {
announcementsVisible.value = true
} else if (row.key === 'mobile.home_banners') {
bannersVisible.value = true
} else if (row.key === 'listing.publish_agreements') {
listingPublishAgreementsVisible.value = true
} else if (row.key === 'order.agreements') {
agreementsVisible.value = true
} else if (row.key === 'profile.post_rental_notice') {
postRentalNoticeVisible.value = true
} else if (row.key === 'chat.renter_retention_days_after_order_end') {
generalVisible.value = true
} else {
generalVisible.value = true
}
}
function safeParsePublishOptions(raw: string) {
const parsed = safeParseJSON(raw, emptyListingPublishOptions)
return cloneOptions(mergeListingPublishOptions(parsed))
}
function safeParseSalePriceConfig(raw: string) {
const parsed = safeParseJSON(raw, emptyListingSalePriceConfig)
return cloneSalePriceConfig(mergeListingSalePriceConfig(parsed))
}
function parseHomeAnnouncements(raw: string) {
const parsed = safeParseJSON(raw, defaultHomeAnnouncements)
return mergeHomeConfig({ announcements: Array.isArray(parsed) ? parsed : [] }).announcements
}
function parseHomeBanners(raw: string) {
const parsed = safeParseJSON(raw, defaultHomeBanners)
return cloneHomeBanners(mergeHomeConfig({ banners: Array.isArray(parsed) ? parsed : [] }).banners)
}
function parseOrderAgreements(raw: string) {
const parsed = safeParseJSON(raw, defaultOrderAgreements)
return {
virtual_asset_purchase: {
title: readText(
parsed.virtual_asset_purchase?.title,
defaultOrderAgreements.virtual_asset_purchase.title
),
content: readText(
parsed.virtual_asset_purchase?.content,
defaultOrderAgreements.virtual_asset_purchase.content
),
},
renter_agreement: {
title: readText(
parsed.renter_agreement?.title,
defaultOrderAgreements.renter_agreement.title
),
content: readText(
parsed.renter_agreement?.content,
defaultOrderAgreements.renter_agreement.content
),
},
}
}
function parseListingPublishAgreements(raw: string) {
const parsed = safeParseJSON(raw, defaultListingPublishAgreements)
return {
virtual_asset_sale: {
title: readText(
parsed.virtual_asset_sale?.title,
defaultListingPublishAgreements.virtual_asset_sale.title
),
content: readText(
parsed.virtual_asset_sale?.content,
defaultListingPublishAgreements.virtual_asset_sale.content
),
},
seller_agreement: {
title: readText(
parsed.seller_agreement?.title,
defaultListingPublishAgreements.seller_agreement.title
),
content: readText(
parsed.seller_agreement?.content,
defaultListingPublishAgreements.seller_agreement.content
),
},
}
}
function readText(value: unknown, fallback: string) {
return typeof value === 'string' && value.trim() ? value : fallback
}
function countContentWords(value: string) {
return value.replace(/\s/g, '').length
}
function cloneOptions(options: ListingPublishOptions) {
return JSON.parse(JSON.stringify(options)) as ListingPublishOptions
}
function cloneSalePriceConfig(config: PublishSalePriceConfig) {
return JSON.parse(JSON.stringify(config)) as PublishSalePriceConfig
}
function cloneHomeBanners(banners: HomeBannerSlide[]) {
return JSON.parse(JSON.stringify(banners)) as HomeBannerSlide[]
}
function formatHomeConfigStatus(row: SystemConfig | null, fallback: string) {
if (!row) return fallback
return formatDateTime(row.updated_at, fallback)
}
function formatPublishCooldown(value: string) {
const trimmed = value.trim()
if (!/^\d+$/.test(trimmed)) return '配置异常'
const minutes = Number(trimmed)
if (minutes === 0) return '已关闭'
return `${minutes} 分钟`
}
function formatConfigValue(row: SystemConfig) {
if (row.key.includes('token')) {
return row.value.trim() ? '已配置' : '未配置'
}
const trimmed = row.value.trim()
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
return 'JSON 配置'
}
const selectedLabel = formatSystemConfigSelectValue(row.key, row.value)
if (selectedLabel) {
return selectedLabel
}
return row.value
}
</script>
<template>
<section class="page">
<div class="page-header">
<p class="eyebrow">Configs</p>
<h1>系统配置</h1>
<p>管理交接超时归还超时短信限流最低押金和抽成比例</p>
</div>
<section v-if="publishConfig" class="publish-config-panel">
<div class="publish-config-main">
<div>
<p class="eyebrow">Publish Options</p>
<h2>发布表单选项</h2>
<span>管理移动端发布页的区服段位皮肤额外消耗品截图材料和地区选项</span>
</div>
<el-button type="primary" @click="openEdit(publishConfig)">编辑发布选项</el-button>
</div>
<div class="publish-stat-grid">
<div class="publish-stat">
<strong>{{ publishStats.baseCount }}</strong>
<span>基础选项</span>
</div>
<div class="publish-stat">
<strong>{{ publishStats.skinCount }}</strong>
<span>皮肤项</span>
</div>
<div class="publish-stat">
<strong>{{ publishStats.resourceCount }}</strong>
<span>额外消耗品</span>
</div>
<div class="publish-stat">
<strong>{{ publishStats.screenshotCount }}</strong>
<span>截图材料</span>
</div>
<div class="publish-stat">
<strong>{{ publishStats.regionCount }}</strong>
<span>地区</span>
</div>
</div>
</section>
<section v-if="listingPublishCooldownConfig" class="publish-config-panel">
<div class="publish-config-main">
<div>
<p class="eyebrow">Publish Guard</p>
<h2>发布频率限制</h2>
<span>限制同一用户新增发布账号的间隔默认 5 分钟 0 可关闭限制</span>
</div>
<el-button type="primary" @click="openEdit(listingPublishCooldownConfig)">
编辑限制
</el-button>
</div>
<div class="publish-stat-grid home-stat-grid">
<div class="publish-stat">
<strong>{{ formatPublishCooldown(listingPublishCooldownConfig.value) }}</strong>
<span>当前间隔</span>
</div>
<div class="publish-stat">
<strong>用户维度</strong>
<span>每个用户单独计算</span>
</div>
<div class="publish-stat">
<strong>新增发布</strong>
<span>不限制编辑和下架</span>
</div>
<div class="publish-stat">
<strong></strong>
<span>{{ listingPublishCooldownConfig.key }}</span>
</div>
<div class="publish-stat">
<strong>更新</strong>
<span>{{ formatHomeConfigStatus(listingPublishCooldownConfig, '未初始化') }}</span>
</div>
</div>
</section>
<section v-if="salePriceConfig" class="publish-config-panel">
<div class="publish-config-main">
<div>
<p class="eyebrow">Sale Pricing</p>
<h2>内部出售定价规则</h2>
<span>管理出售专用比例计算规则仅用于平台内部定价计算不在发布页展示</span>
</div>
<el-button type="primary" @click="openEdit(salePriceConfig)">编辑出售规则</el-button>
</div>
<div class="publish-stat-grid home-stat-grid">
<div class="publish-stat">
<strong>{{ salePriceStats.fixedCount }}</strong>
<span>固定加价</span>
</div>
<div class="publish-stat">
<strong>{{ salePriceStats.ratioCount }}</strong>
<span>比例修正</span>
</div>
<div class="publish-stat">
<strong>内部</strong>
<span>不展示给发布用户</span>
</div>
<div class="publish-stat">
<strong>配置</strong>
<span>listing.sale_price_config</span>
</div>
<div class="publish-stat">
<strong>更新</strong>
<span>{{ formatHomeConfigStatus(salePriceConfig, '未初始化') }}</span>
</div>
</div>
</section>
<section v-if="homeAnnouncementsConfig || homeBannersConfig" class="publish-config-panel">
<div class="publish-config-main">
<div>
<p class="eyebrow">Home Content</p>
<h2>移动端首页运营配置</h2>
<span>管理首页公告滚动内容和顶部轮播图保存后移动端会从接口读取最新配置</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
>
</div>
</div>
<div class="publish-stat-grid home-stat-grid">
<div class="publish-stat">
<strong>{{ homeStats.announcementCount }}</strong>
<span>公告条数</span>
</div>
<div class="publish-stat">
<strong>{{ homeStats.bannerCount }}</strong>
<span>轮播图</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>
</div>
</div>
</section>
<section v-if="listingPublishAgreementsConfig" class="publish-config-panel">
<div class="publish-config-main">
<div>
<p class="eyebrow">Publish Agreements</p>
<h2>发布协议配置</h2>
<span>管理发布账号前必须勾选确认的虚拟资产出售协议和号主协议</span>
</div>
<el-button type="primary" @click="openEdit(listingPublishAgreementsConfig)"
>编辑发布协议</el-button
>
</div>
<div class="publish-stat-grid home-stat-grid">
<div class="publish-stat">
<strong>2</strong>
<span>协议数量</span>
</div>
<div class="publish-stat">
<strong>{{ listingPublishAgreementStats.saleWords }}</strong>
<span>{{ listingPublishAgreementStats.saleTitle }}字数</span>
</div>
<div class="publish-stat">
<strong>{{ listingPublishAgreementStats.sellerWords }}</strong>
<span>{{ listingPublishAgreementStats.sellerTitle }}字数</span>
</div>
<div class="publish-stat">
<strong>接口</strong>
<span>/api/listing-publish-agreements</span>
</div>
<div class="publish-stat">
<strong>更新</strong>
<span>{{ formatHomeConfigStatus(listingPublishAgreementsConfig, '未初始化') }}</span>
</div>
</div>
</section>
<section v-if="orderAgreementsConfig" class="publish-config-panel">
<div class="publish-config-main">
<div>
<p class="eyebrow">Order Agreements</p>
<h2>下单协议配置</h2>
<span>管理下单前必须阅读并勾选确认的虚拟资产购买协议和租客协议</span>
</div>
<el-button type="primary" @click="openEdit(orderAgreementsConfig)">编辑协议</el-button>
</div>
<div class="publish-stat-grid home-stat-grid">
<div class="publish-stat">
<strong>2</strong>
<span>协议数量</span>
</div>
<div class="publish-stat">
<strong>{{ agreementStats.virtualWords }}</strong>
<span>{{ agreementStats.virtualTitle }}字数</span>
</div>
<div class="publish-stat">
<strong>{{ agreementStats.renterWords }}</strong>
<span>{{ agreementStats.renterTitle }}字数</span>
</div>
<div class="publish-stat">
<strong>接口</strong>
<span>/api/order-agreements</span>
</div>
<div class="publish-stat">
<strong>更新</strong>
<span>{{ formatHomeConfigStatus(orderAgreementsConfig, '未初始化') }}</span>
</div>
</div>
</section>
<section v-if="postRentalNoticeConfig" class="publish-config-panel">
<div class="publish-config-main">
<div>
<p class="eyebrow">Post Rental Notice</p>
<h2>租后须知配置</h2>
<span>管理移动端个人中心展示的租后须知内容用户在实名认证下方可查看</span>
</div>
<el-button type="primary" @click="openEdit(postRentalNoticeConfig)">编辑租后须知</el-button>
</div>
<div class="publish-stat-grid home-stat-grid">
<div class="publish-stat">
<strong>1</strong>
<span>须知文档</span>
</div>
<div class="publish-stat">
<strong>移动端</strong>
<span>个人中心展示</span>
</div>
<div class="publish-stat">
<strong>实名认证</strong>
<span>下方位置</span>
</div>
<div class="publish-stat">
<strong>接口</strong>
<span>/api/post-rental-notice</span>
</div>
<div class="publish-stat">
<strong>更新</strong>
<span>{{ formatHomeConfigStatus(postRentalNoticeConfig, '未初始化') }}</span>
</div>
</div>
</section>
<section v-if="autoWelcomeConfig || listingGroupWelcomeConfig" class="publish-config-panel">
<div class="publish-config-main">
<div>
<p class="eyebrow">Chat Welcome</p>
<h2>群聊自动欢迎语</h2>
<span>分别管理订单群和发布群进入后自动发送的话术支持后台直接编辑</span>
</div>
</div>
<div class="welcome-config-grid">
<AutoWelcomeConfig
v-if="autoWelcomeConfig"
title="订单群自动话术"
description="订单群聊创建后自动发送的欢迎消息"
placeholder="输入订单群自动发送的话术"
empty-hint="未设置"
:fetch-message="fetchAutoWelcomeMessage"
:update-message="updateAutoWelcomeMessage"
/>
<AutoWelcomeConfig
v-if="listingGroupWelcomeConfig"
title="发布群欢迎语"
description="账号群创建后自动发送的欢迎消息"
placeholder="输入发布群创建后自动发送的欢迎语"
empty-hint="未设置"
:fetch-message="fetchListingGroupWelcomeMessage"
:update-message="updateListingGroupWelcomeMessage"
/>
</div>
</section>
<section v-if="renterRetentionConfig" class="publish-config-panel">
<div class="publish-config-main">
<div>
<p class="eyebrow">Chat Retention</p>
<h2>发布群租客保留天数</h2>
<span>订单结束后租客在发布群中保留的天数超时后由后台任务自动移出</span>
</div>
<el-button type="primary" @click="openEdit(renterRetentionConfig)">编辑天数</el-button>
</div>
<div class="publish-stat-grid home-stat-grid">
<div class="publish-stat">
<strong>{{ renterRetentionConfig.value }}</strong>
<span>当前天数</span>
</div>
<div class="publish-stat">
<strong>范围</strong>
<span>3-7 </span>
</div>
<div class="publish-stat">
<strong>任务</strong>
<span>订单结束后自动清退</span>
</div>
<div class="publish-stat">
<strong></strong>
<span>{{ renterRetentionConfig.key }}</span>
</div>
<div class="publish-stat">
<strong>更新</strong>
<span>{{ formatHomeConfigStatus(renterRetentionConfig, '未初始化') }}</span>
</div>
</div>
</section>
<section class="publish-config-panel">
<div class="publish-config-main">
<div>
<p class="eyebrow">PaddleOCR</p>
<h2>二维码群名识别配置</h2>
<span>配置企业微信群二维码上传时使用的 OCR 识别接口保存后无需重新构建前端</span>
</div>
</div>
<div class="publish-stat-grid home-stat-grid">
<div class="publish-stat">
<strong>{{ paddleOcrTokenConfig.value.trim() ? '已配置' : '未配置' }}</strong>
<span>API Token</span>
<el-button size="small" @click="openEdit(paddleOcrTokenConfig)">编辑</el-button>
</div>
<div class="publish-stat">
<strong>接口</strong>
<span>{{ paddleOcrJobUrlConfig.value || '未配置' }}</span>
<el-button size="small" @click="openEdit(paddleOcrJobUrlConfig)">编辑</el-button>
</div>
<div class="publish-stat">
<strong>{{ paddleOcrModelConfig.value || '未配置' }}</strong>
<span>识别模型</span>
<el-button size="small" @click="openEdit(paddleOcrModelConfig)">编辑</el-button>
</div>
<div class="publish-stat">
<strong>直连</strong>
<span>二维码池上传时调用</span>
</div>
<div class="publish-stat">
<strong>更新</strong>
<span>{{ formatHomeConfigStatus(paddleOcrTokenConfig, '未初始化') }}</span>
</div>
</div>
</section>
<el-table v-loading="loading" class="table-panel" :data="regularConfigs">
<el-table-column prop="key" label="配置项" min-width="260" />
<el-table-column label="当前值" min-width="180" show-overflow-tooltip>
<template #default="{ row }">
<el-tag v-if="formatConfigValue(row) === 'JSON 配置'" type="info">JSON 配置</el-tag>
<span v-else class="config-value">{{ formatConfigValue(row) }}</span>
</template>
</el-table-column>
<el-table-column prop="description" label="说明" min-width="260" show-overflow-tooltip />
<el-table-column prop="updated_by" label="更新人" width="100" />
<el-table-column label="更新时间" min-width="180">
<template #default="{ row }">{{ formatDateTime(row.updated_at) }}</template>
</el-table-column>
<el-table-column label="操作" width="100">
<template #default="{ row }">
<el-button size="small" @click="openEdit(row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
<!-- 弹窗编辑器子组件 -->
<PublishOptionsDialog
v-if="publishConfig"
v-model="publishVisible"
:config="publishConfig"
@saved="loadConfigs"
/>
<SalePriceDialog
v-if="salePriceConfig"
v-model="salePriceVisible"
:config="salePriceConfig"
@saved="loadConfigs"
/>
<HomeAnnouncementsDialog
v-if="homeAnnouncementsConfig"
v-model="announcementsVisible"
:config="homeAnnouncementsConfig"
@saved="loadConfigs"
/>
<HomeBannersDialog
v-if="homeBannersConfig"
v-model="bannersVisible"
:config="homeBannersConfig"
@saved="loadConfigs"
/>
<ListingPublishAgreementsDialog
v-if="listingPublishAgreementsConfig"
v-model="listingPublishAgreementsVisible"
:config="listingPublishAgreementsConfig"
@saved="loadConfigs"
/>
<OrderAgreementsDialog
v-if="orderAgreementsConfig"
v-model="agreementsVisible"
:config="orderAgreementsConfig"
@saved="loadConfigs"
/>
<PostRentalNoticeDialog
v-if="postRentalNoticeConfig"
v-model="postRentalNoticeVisible"
:config="postRentalNoticeConfig"
@saved="loadConfigs"
/>
<GeneralConfigDialog
v-if="currentEditingConfig"
v-model="generalVisible"
:config="currentEditingConfig"
@saved="loadConfigs"
/>
</section>
</template>
<style scoped>
.publish-config-panel {
display: grid;
gap: 18px;
margin-bottom: 18px;
padding: 20px;
border: 1px solid #e5e7eb;
border-radius: 8px;
background: #fff;
}
.publish-config-main {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
}
.panel-actions {
display: flex;
align-items: center;
gap: 8px;
}
.publish-config-main h2 {
margin: 4px 0 6px;
color: #111827;
font-size: 20px;
}
.publish-config-main span {
color: #6b7280;
font-size: 13px;
}
.publish-stat-grid {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 10px;
}
.welcome-config-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.home-stat-grid .publish-stat strong {
font-size: 18px;
}
.publish-stat {
display: grid;
gap: 4px;
padding: 12px;
border-radius: 8px;
background: #f8fafc;
}
.publish-stat strong {
color: #1477ff;
font-size: 22px;
line-height: 1;
}
.publish-stat span,
.config-value {
color: #4b5563;
font-size: 13px;
overflow-wrap: anywhere;
}
/* 按钮文字清晰度优化 */
:deep(.el-button--primary) {
font-weight: 700;
}
:deep(.el-button--primary span) {
color: #ffffff !important;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
font-weight: 700;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@media (max-width: 1100px) {
.publish-stat-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
</style>