969 lines
34 KiB
Vue
969 lines
34 KiB
Vue
<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>
|