Files
hfb_sys/frontend/src/features/admin/views/AdminSystemConfigsView.vue
T

969 lines
34 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 { parseRenterGrowthConfig } from '@/features/admin/api/renterGrowth'
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 AwRecycleDialog from '../components/AwRecycleDialog.vue'
import {
defaultAWRecycleConfig,
defaultCooperationFeedbackConfig,
type AWRecycleConfig,
type CooperationFeedbackConfig,
} from '@/features/listings/api/homeConfig'
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 RenterGrowthConfigDialog from '../components/RenterGrowthConfigDialog.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 awRecycleVisible = ref(false)
const cooperationFeedbackVisible = ref(false)
const listingPublishAgreementsVisible = ref(false)
const agreementsVisible = ref(false)
const postRentalNoticeVisible = ref(false)
const generalVisible = ref(false)
const renterGrowthVisible = 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 renterGrowthConfig = computed(
() => configs.value.find(item => item.key === 'renter.growth_level_rules') || 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 awRecycleConfig = computed(
() => configs.value.find(item => item.key === 'mobile.aw_recycle') || null
)
const cooperationFeedbackConfig = computed(
() => configs.value.find(item => item.key === 'mobile.cooperation_feedback') || 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', 'PP-OCRv6', '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 !== 'mobile.aw_recycle' &&
item.key !== 'mobile.cooperation_feedback' &&
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' &&
item.key !== 'renter.growth_level_rules'
)
)
// 只读计算属性,用于渲染卡片上的统计信息
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 awRecycleStats = computed(() => {
const item = parseAWRecycle(awRecycleConfig.value?.value || '')
return {
enabledLabel: item.enabled ? (item.image_url ? '已配置图' : '已开启') : '已隐藏',
}
})
const cooperationFeedbackStats = computed(() => {
const item = parseCooperationFeedback(cooperationFeedbackConfig.value?.value || '')
return {
enabledLabel: item.enabled ? (item.image_url ? '已配置图' : '已开启') : '已隐藏',
}
})
const renterGrowthStats = computed(() => {
const config = parseRenterGrowthConfig(renterGrowthConfig.value?.value || '')
return {
enabled: config.enabled,
pointsPerYuan: config.points_per_yuan,
levels: config.levels.filter(level => level.code !== 'normal'),
}
})
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 === 'mobile.aw_recycle') {
awRecycleVisible.value = true
} else if (row.key === 'mobile.cooperation_feedback') {
cooperationFeedbackVisible.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 === 'renter.growth_level_rules') {
renterGrowthVisible.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 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 parseCooperationFeedback(raw: string): CooperationFeedbackConfig {
const parsed = safeParseJSON(raw, defaultCooperationFeedbackConfig)
return {
title:
typeof parsed.title === 'string' && parsed.title.trim()
? parsed.title.trim()
: defaultCooperationFeedbackConfig.title,
subtitle:
typeof parsed.subtitle === 'string' && parsed.subtitle.trim()
? parsed.subtitle.trim()
: defaultCooperationFeedbackConfig.subtitle,
image_url: typeof parsed.image_url === 'string' ? parsed.image_url.trim() : '',
enabled:
typeof parsed.enabled === 'boolean'
? parsed.enabled
: defaultCooperationFeedbackConfig.enabled,
}
}
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="renterGrowthConfig" class="publish-config-panel">
<div class="publish-config-main">
<div>
<p class="eyebrow">Renter Growth</p>
<h2>租客成长等级</h2>
<span>管理完成订单后的积分换算、各等级租金支付比例与等级免押额度。</span>
</div>
<el-button type="primary" @click="openEdit(renterGrowthConfig)">编辑成长等级</el-button>
</div>
<div class="publish-stat-grid home-stat-grid">
<div class="publish-stat">
<strong>{{ renterGrowthStats.enabled ? '已启用' : '已停用' }}</strong>
<span>成长权益</span>
</div>
<div class="publish-stat">
<strong>{{ renterGrowthStats.pointsPerYuan }}</strong>
<span>每元积分</span>
</div>
<div v-for="level in renterGrowthStats.levels" :key="level.code" class="publish-stat">
<strong>¥{{ level.deposit_free_quota_cent / 100 }}</strong>
<span>{{ level.name }}免押</span>
</div>
</div>
</section>
<section
v-if="
homeAnnouncementsConfig || homeBannersConfig || awRecycleConfig || cooperationFeedbackConfig
"
class="publish-config-panel"
>
<div class="publish-config-main">
<div>
<p class="eyebrow">Home Content</p>
<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" @click="openEdit(homeBannersConfig)"
>编辑轮播图</el-button
>
<el-button v-if="awRecycleConfig" type="primary" @click="openEdit(awRecycleConfig)"
>编辑 AW炫彩回收</el-button
>
<el-button
v-if="cooperationFeedbackConfig"
type="primary"
@click="openEdit(cooperationFeedbackConfig)"
>编辑 合作与反馈</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>{{ awRecycleStats.enabledLabel }}</strong>
<span>AW炫彩回收</span>
</div>
<div class="publish-stat">
<strong>{{ cooperationFeedbackStats.enabledLabel }}</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(
cooperationFeedbackConfig || awRecycleConfig || 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"
/>
<AwRecycleDialog
v-if="awRecycleConfig"
v-model="awRecycleVisible"
:config="awRecycleConfig"
:defaults="defaultAWRecycleConfig"
dialog-title="编辑 AW炫彩回收"
upload-scene="aw-recycle"
image-hint="走后台通用上传(scene=aw-recycle,公开可访问)。首页点击入口后展示此图,建议竖图或说明海报。"
@saved="loadConfigs"
/>
<AwRecycleDialog
v-if="cooperationFeedbackConfig"
v-model="cooperationFeedbackVisible"
:config="cooperationFeedbackConfig"
:defaults="defaultCooperationFeedbackConfig"
dialog-title="编辑 合作与反馈"
upload-scene="cooperation-feedback"
image-hint="走后台通用上传(scene=cooperation-feedback,公开可访问)。首页点击入口后展示此图,建议上传合作或反馈联系方式海报。"
@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"
/>
<RenterGrowthConfigDialog
v-if="renterGrowthConfig"
v-model="renterGrowthVisible"
:config="renterGrowthConfig"
@saved="loadConfigs"
/>
<GeneralConfigDialog
v-if="currentEditingConfig"
v-model="generalVisible"
:config="currentEditingConfig"
@saved="loadConfigs"
/>
</section>
</template>
<style scoped src="./AdminSystemConfigsView.css"></style>