refactor: 重构后台配置管理页面,拆分大 Dialog 为高内聚子组件
This commit is contained in:
@@ -1,5 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ElMessage } from 'element-plus'
|
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -16,26 +15,32 @@ import {
|
|||||||
mergeHomeConfig,
|
mergeHomeConfig,
|
||||||
type HomeBannerSlide,
|
type HomeBannerSlide,
|
||||||
} from '@/api/homeConfig'
|
} from '@/api/homeConfig'
|
||||||
import { uploadAdminFile } from '@/api/files'
|
import { fetchSystemConfigs, type SystemConfig } from '@/api/systemConfigs'
|
||||||
import { fetchSystemConfigs, updateSystemConfig, type SystemConfig } from '@/api/systemConfigs'
|
|
||||||
import { formatDateTime } from '@/utils/time'
|
import { formatDateTime } from '@/utils/time'
|
||||||
|
|
||||||
|
// 导入重构后的 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 GeneralConfigDialog from './components/GeneralConfigDialog.vue'
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const submitting = ref(false)
|
|
||||||
const configs = ref<SystemConfig[]>([])
|
const configs = ref<SystemConfig[]>([])
|
||||||
const activeConfig = ref<SystemConfig | null>(null)
|
const currentEditingConfig = ref<SystemConfig | null>(null)
|
||||||
const value = ref('')
|
|
||||||
const description = ref('')
|
// 各种子 Dialog 的可见性控制
|
||||||
const publishOptionsDraft = ref<ListingPublishOptions>(cloneOptions(emptyListingPublishOptions))
|
const publishVisible = ref(false)
|
||||||
const salePriceConfigDraft = ref<PublishSalePriceConfig>(cloneSalePriceConfig(emptyListingSalePriceConfig))
|
const salePriceVisible = ref(false)
|
||||||
const homeAnnouncementLines = ref(itemsToLines(defaultHomeAnnouncements))
|
const announcementsVisible = ref(false)
|
||||||
const homeBannersDraft = ref<HomeBannerSlide[]>(cloneHomeBanners(defaultHomeBanners))
|
const bannersVisible = ref(false)
|
||||||
const uploadingBannerIndex = ref<number | null>(null)
|
const generalVisible = ref(false)
|
||||||
|
|
||||||
const publishConfig = computed(() => configs.value.find((item) => item.key === 'listing.publish_options') || null)
|
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 salePriceConfig = computed(() => configs.value.find((item) => item.key === 'listing.sale_price_config') || null)
|
||||||
const homeAnnouncementsConfig = computed(() => configs.value.find((item) => item.key === 'mobile.home_announcements') || 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 homeBannersConfig = computed(() => configs.value.find((item) => item.key === 'mobile.home_banners') || null)
|
||||||
|
|
||||||
const regularConfigs = computed(() =>
|
const regularConfigs = computed(() =>
|
||||||
configs.value.filter(
|
configs.value.filter(
|
||||||
(item) =>
|
(item) =>
|
||||||
@@ -45,10 +50,8 @@ const regularConfigs = computed(() =>
|
|||||||
item.key !== 'mobile.home_banners',
|
item.key !== 'mobile.home_banners',
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
const isPublishOptionsConfig = computed(() => activeConfig.value?.key === 'listing.publish_options')
|
|
||||||
const isSalePriceConfig = computed(() => activeConfig.value?.key === 'listing.sale_price_config')
|
// 只读计算属性,用于渲染卡片上的统计信息
|
||||||
const isHomeAnnouncementsConfig = computed(() => activeConfig.value?.key === 'mobile.home_announcements')
|
|
||||||
const isHomeBannersConfig = computed(() => activeConfig.value?.key === 'mobile.home_banners')
|
|
||||||
const publishStats = computed(() => {
|
const publishStats = computed(() => {
|
||||||
const options = safeParsePublishOptions(publishConfig.value?.value || '')
|
const options = safeParsePublishOptions(publishConfig.value?.value || '')
|
||||||
return {
|
return {
|
||||||
@@ -63,6 +66,7 @@ const publishStats = computed(() => {
|
|||||||
regionCount: options.region_options.length,
|
regionCount: options.region_options.length,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const salePriceStats = computed(() => {
|
const salePriceStats = computed(() => {
|
||||||
const config = safeParseSalePriceConfig(salePriceConfig.value?.value || '')
|
const config = safeParseSalePriceConfig(salePriceConfig.value?.value || '')
|
||||||
return {
|
return {
|
||||||
@@ -70,6 +74,7 @@ const salePriceStats = computed(() => {
|
|||||||
ratioCount: config.ratio_adjustment_rules.length,
|
ratioCount: config.ratio_adjustment_rules.length,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const homeStats = computed(() => {
|
const homeStats = computed(() => {
|
||||||
const announcements = parseHomeAnnouncements(homeAnnouncementsConfig.value?.value || '', false)
|
const announcements = parseHomeAnnouncements(homeAnnouncementsConfig.value?.value || '', false)
|
||||||
const banners = parseHomeBanners(homeBannersConfig.value?.value || '', false)
|
const banners = parseHomeBanners(homeBannersConfig.value?.value || '', false)
|
||||||
@@ -79,24 +84,6 @@ const homeStats = computed(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const isStructuredConfig = computed(() => {
|
|
||||||
const trimmed = value.value.trim()
|
|
||||||
return (
|
|
||||||
isPublishOptionsConfig.value ||
|
|
||||||
isSalePriceConfig.value ||
|
|
||||||
isHomeAnnouncementsConfig.value ||
|
|
||||||
isHomeBannersConfig.value ||
|
|
||||||
trimmed.startsWith('{') ||
|
|
||||||
trimmed.startsWith('[')
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const dialogWidth = computed(() => {
|
|
||||||
if (isPublishOptionsConfig.value || isSalePriceConfig.value || isHomeBannersConfig.value) return '920px'
|
|
||||||
if (isHomeAnnouncementsConfig.value) return '680px'
|
|
||||||
return '560px'
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(loadConfigs)
|
onMounted(loadConfigs)
|
||||||
|
|
||||||
async function loadConfigs() {
|
async function loadConfigs() {
|
||||||
@@ -109,64 +96,17 @@ async function loadConfigs() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openEdit(row: SystemConfig) {
|
function openEdit(row: SystemConfig) {
|
||||||
activeConfig.value = row
|
currentEditingConfig.value = row
|
||||||
value.value = row.value
|
|
||||||
description.value = row.description
|
|
||||||
if (row.key === 'listing.publish_options') {
|
if (row.key === 'listing.publish_options') {
|
||||||
publishOptionsDraft.value = parsePublishOptions(row.value)
|
publishVisible.value = true
|
||||||
}
|
} else if (row.key === 'listing.sale_price_config') {
|
||||||
if (row.key === 'listing.sale_price_config') {
|
salePriceVisible.value = true
|
||||||
salePriceConfigDraft.value = parseSalePriceConfig(row.value)
|
} else if (row.key === 'mobile.home_announcements') {
|
||||||
}
|
announcementsVisible.value = true
|
||||||
if (row.key === 'mobile.home_announcements') {
|
} else if (row.key === 'mobile.home_banners') {
|
||||||
homeAnnouncementLines.value = itemsToLines(parseHomeAnnouncements(row.value, true))
|
bannersVisible.value = true
|
||||||
}
|
} else {
|
||||||
if (row.key === 'mobile.home_banners') {
|
generalVisible.value = true
|
||||||
homeBannersDraft.value = parseHomeBanners(row.value, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSave() {
|
|
||||||
if (!activeConfig.value) return
|
|
||||||
submitting.value = true
|
|
||||||
try {
|
|
||||||
if (isPublishOptionsConfig.value) {
|
|
||||||
value.value = JSON.stringify(publishOptionsDraft.value, null, 2)
|
|
||||||
}
|
|
||||||
if (isSalePriceConfig.value) {
|
|
||||||
value.value = JSON.stringify(salePriceConfigDraft.value, null, 2)
|
|
||||||
}
|
|
||||||
if (isHomeAnnouncementsConfig.value) {
|
|
||||||
value.value = JSON.stringify(linesToItems(homeAnnouncementLines.value), null, 2)
|
|
||||||
}
|
|
||||||
if (isHomeBannersConfig.value) {
|
|
||||||
value.value = JSON.stringify(
|
|
||||||
homeBannersDraft.value.filter((item) => item.title.trim() || item.image_url?.trim()),
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
await updateSystemConfig(activeConfig.value.key, {
|
|
||||||
value: value.value,
|
|
||||||
description: description.value,
|
|
||||||
})
|
|
||||||
ElMessage.success('配置已更新')
|
|
||||||
activeConfig.value = null
|
|
||||||
await loadConfigs()
|
|
||||||
} catch (error) {
|
|
||||||
ElMessage.error(readError(error, '保存失败'))
|
|
||||||
} finally {
|
|
||||||
submitting.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function parsePublishOptions(raw: string) {
|
|
||||||
try {
|
|
||||||
const parsed = raw.trim() ? JSON.parse(raw) : emptyListingPublishOptions
|
|
||||||
return cloneOptions(mergeListingPublishOptions(parsed))
|
|
||||||
} catch {
|
|
||||||
ElMessage.warning('发布选项 JSON 解析失败,已清空草稿')
|
|
||||||
return cloneOptions(emptyListingPublishOptions)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,16 +119,6 @@ function safeParsePublishOptions(raw: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseSalePriceConfig(raw: string) {
|
|
||||||
try {
|
|
||||||
const parsed = raw.trim() ? JSON.parse(raw) : emptyListingSalePriceConfig
|
|
||||||
return cloneSalePriceConfig(mergeListingSalePriceConfig(parsed))
|
|
||||||
} catch {
|
|
||||||
ElMessage.warning('出售定价规则 JSON 解析失败,已清空草稿')
|
|
||||||
return cloneSalePriceConfig(emptyListingSalePriceConfig)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function safeParseSalePriceConfig(raw: string) {
|
function safeParseSalePriceConfig(raw: string) {
|
||||||
try {
|
try {
|
||||||
const parsed = raw.trim() ? JSON.parse(raw) : emptyListingSalePriceConfig
|
const parsed = raw.trim() ? JSON.parse(raw) : emptyListingSalePriceConfig
|
||||||
@@ -203,7 +133,6 @@ function parseHomeAnnouncements(raw: string, showWarning = false) {
|
|||||||
const parsed = raw.trim() ? JSON.parse(raw) : defaultHomeAnnouncements
|
const parsed = raw.trim() ? JSON.parse(raw) : defaultHomeAnnouncements
|
||||||
return mergeHomeConfig({ announcements: Array.isArray(parsed) ? parsed : [] }).announcements
|
return mergeHomeConfig({ announcements: Array.isArray(parsed) ? parsed : [] }).announcements
|
||||||
} catch {
|
} catch {
|
||||||
if (showWarning) ElMessage.warning('首页公告 JSON 解析失败,已使用默认配置')
|
|
||||||
return [...defaultHomeAnnouncements]
|
return [...defaultHomeAnnouncements]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -213,7 +142,6 @@ function parseHomeBanners(raw: string, showWarning = false) {
|
|||||||
const parsed = raw.trim() ? JSON.parse(raw) : defaultHomeBanners
|
const parsed = raw.trim() ? JSON.parse(raw) : defaultHomeBanners
|
||||||
return cloneHomeBanners(mergeHomeConfig({ banners: Array.isArray(parsed) ? parsed : [] }).banners)
|
return cloneHomeBanners(mergeHomeConfig({ banners: Array.isArray(parsed) ? parsed : [] }).banners)
|
||||||
} catch {
|
} catch {
|
||||||
if (showWarning) ElMessage.warning('首页轮播 JSON 解析失败,已使用默认配置')
|
|
||||||
return cloneHomeBanners(defaultHomeBanners)
|
return cloneHomeBanners(defaultHomeBanners)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -230,183 +158,6 @@ function cloneHomeBanners(banners: HomeBannerSlide[]) {
|
|||||||
return JSON.parse(JSON.stringify(banners)) as HomeBannerSlide[]
|
return JSON.parse(JSON.stringify(banners)) as HomeBannerSlide[]
|
||||||
}
|
}
|
||||||
|
|
||||||
function linesToItems(value: string) {
|
|
||||||
return value
|
|
||||||
.split('\n')
|
|
||||||
.map((item) => item.trim())
|
|
||||||
.filter(Boolean)
|
|
||||||
}
|
|
||||||
|
|
||||||
function itemsToLines(items: string[]) {
|
|
||||||
return items.join('\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateOptionLines(key: keyof Pick<ListingPublishOptions, 'server_options' | 'face_options' | 'rank_options' | 'insurance_options' | 'level_options' | 'login_method_options' | 'region_options' | 'ban_record_options' | 'ban_evidence_options'>, nextValue: string) {
|
|
||||||
publishOptionsDraft.value[key] = linesToItems(nextValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateSkinGroupOptions(index: number, nextValue: string) {
|
|
||||||
const group = publishOptionsDraft.value.skin_groups[index]
|
|
||||||
if (group) {
|
|
||||||
group.options = linesToItems(nextValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addSkinGroup() {
|
|
||||||
publishOptionsDraft.value.skin_groups.push({
|
|
||||||
key: `group_${Date.now()}`,
|
|
||||||
title: '新皮肤分类',
|
|
||||||
options: [],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeSkinGroup(index: number) {
|
|
||||||
publishOptionsDraft.value.skin_groups.splice(index, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
function addQuantityItem() {
|
|
||||||
publishOptionsDraft.value.quantity_items.push({
|
|
||||||
key: `item_${Date.now()}`,
|
|
||||||
label: '',
|
|
||||||
price: '',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeQuantityItem(index: number) {
|
|
||||||
publishOptionsDraft.value.quantity_items.splice(index, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
function addScreenshotSlot() {
|
|
||||||
publishOptionsDraft.value.screenshot_slots.push({
|
|
||||||
key: `screenshot_${Date.now()}`,
|
|
||||||
label: '',
|
|
||||||
required: false,
|
|
||||||
hint: '',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeScreenshotSlot(index: number) {
|
|
||||||
publishOptionsDraft.value.screenshot_slots.splice(index, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
function addInsuranceBaseRatio() {
|
|
||||||
publishOptionsDraft.value.ratio_config.insurance_base_ratios.push({
|
|
||||||
insurance: '',
|
|
||||||
ratio: 0,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeInsuranceBaseRatio(index: number) {
|
|
||||||
publishOptionsDraft.value.ratio_config.insurance_base_ratios.splice(index, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
function addRatioConfigItem() {
|
|
||||||
publishOptionsDraft.value.ratio_config.config_items.push({
|
|
||||||
key: `config_${Date.now()}`,
|
|
||||||
label: '',
|
|
||||||
kind: 'skin_group',
|
|
||||||
group_key: '',
|
|
||||||
missing_penalty: 1,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeRatioConfigItem(index: number) {
|
|
||||||
publishOptionsDraft.value.ratio_config.config_items.splice(index, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
function addCoinCorrection() {
|
|
||||||
publishOptionsDraft.value.ratio_config.coin_corrections.push({
|
|
||||||
threshold_m: 0,
|
|
||||||
correction: 0,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeCoinCorrection(index: number) {
|
|
||||||
publishOptionsDraft.value.ratio_config.coin_corrections.splice(index, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
function addDepositSkinGroupRule() {
|
|
||||||
publishOptionsDraft.value.deposit_recommend_config.skin_group_rules.push({
|
|
||||||
group_key: '',
|
|
||||||
label: '',
|
|
||||||
amount_per_item: 0,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeDepositSkinGroupRule(index: number) {
|
|
||||||
publishOptionsDraft.value.deposit_recommend_config.skin_group_rules.splice(index, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
function addSaleFixedMarkupRule() {
|
|
||||||
salePriceConfigDraft.value.fixed_markup_rules.push({
|
|
||||||
min_m: 0,
|
|
||||||
max_m: 0,
|
|
||||||
markup_amount: 0,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeSaleFixedMarkupRule(index: number) {
|
|
||||||
salePriceConfigDraft.value.fixed_markup_rules.splice(index, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
function addSaleRatioAdjustmentRule() {
|
|
||||||
salePriceConfigDraft.value.ratio_adjustment_rules.push({
|
|
||||||
min_m: 0,
|
|
||||||
max_m: 0,
|
|
||||||
ratio_subtract: 0,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeSaleRatioAdjustmentRule(index: number) {
|
|
||||||
salePriceConfigDraft.value.ratio_adjustment_rules.splice(index, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
function addHomeBanner() {
|
|
||||||
homeBannersDraft.value.push({
|
|
||||||
eyebrow: '首页推荐',
|
|
||||||
title: '',
|
|
||||||
badge: 'NEW',
|
|
||||||
pill: '',
|
|
||||||
tone: 'blue',
|
|
||||||
image_url: '',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeHomeBanner(index: number) {
|
|
||||||
homeBannersDraft.value.splice(index, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetHomeAnnouncements() {
|
|
||||||
homeAnnouncementLines.value = itemsToLines(defaultHomeAnnouncements)
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetHomeBanners() {
|
|
||||||
homeBannersDraft.value = cloneHomeBanners(defaultHomeBanners)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleHomeBannerUpload(event: Event, index: number) {
|
|
||||||
const input = event.target as HTMLInputElement
|
|
||||||
const file = input.files?.[0]
|
|
||||||
input.value = ''
|
|
||||||
if (!file) return
|
|
||||||
uploadingBannerIndex.value = index
|
|
||||||
try {
|
|
||||||
const uploaded = await uploadAdminFile(file, 'home-banner')
|
|
||||||
const banner = homeBannersDraft.value[index]
|
|
||||||
if (banner) {
|
|
||||||
banner.image_url = uploaded.url
|
|
||||||
if (!banner.title) {
|
|
||||||
banner.title = banner.eyebrow || '首页轮播图'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ElMessage.success('图片已上传')
|
|
||||||
} catch (error) {
|
|
||||||
ElMessage.error(readError(error, '图片上传失败'))
|
|
||||||
} finally {
|
|
||||||
uploadingBannerIndex.value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatHomeConfigStatus(row: SystemConfig | null, fallback: string) {
|
function formatHomeConfigStatus(row: SystemConfig | null, fallback: string) {
|
||||||
if (!row) return fallback
|
if (!row) return fallback
|
||||||
return formatDateTime(row.updated_at, fallback)
|
return formatDateTime(row.updated_at, fallback)
|
||||||
@@ -419,14 +170,6 @@ function formatConfigValue(row: SystemConfig) {
|
|||||||
}
|
}
|
||||||
return row.value
|
return row.value
|
||||||
}
|
}
|
||||||
|
|
||||||
function readError(error: unknown, fallback: string) {
|
|
||||||
if (typeof error === 'object' && error && 'response' in error) {
|
|
||||||
const response = (error as { response?: { data?: { message?: string } } }).response
|
|
||||||
return response?.data?.message || fallback
|
|
||||||
}
|
|
||||||
return fallback
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -559,432 +302,41 @@ function readError(error: unknown, fallback: string) {
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<el-dialog :model-value="!!activeConfig" title="编辑系统配置" :width="dialogWidth" @update:model-value="activeConfig = null">
|
<!-- 弹窗编辑器子组件 -->
|
||||||
<div v-if="activeConfig" class="dialog-body">
|
<PublishOptionsDialog
|
||||||
<p><strong>{{ activeConfig.key }}</strong></p>
|
v-if="publishConfig"
|
||||||
|
v-model="publishVisible"
|
||||||
|
:config="publishConfig"
|
||||||
|
@saved="loadConfigs"
|
||||||
|
/>
|
||||||
|
|
||||||
<div v-if="isPublishOptionsConfig" class="publish-options-editor">
|
<SalePriceDialog
|
||||||
<div class="editor-toolbar">
|
v-if="salePriceConfig"
|
||||||
<span>发布页选项配置</span>
|
v-model="salePriceVisible"
|
||||||
</div>
|
:config="salePriceConfig"
|
||||||
|
@saved="loadConfigs"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="editor-grid">
|
<HomeAnnouncementsDialog
|
||||||
<el-form-item label="区服">
|
v-if="homeAnnouncementsConfig"
|
||||||
<el-input
|
v-model="announcementsVisible"
|
||||||
:model-value="itemsToLines(publishOptionsDraft.server_options)"
|
:config="homeAnnouncementsConfig"
|
||||||
type="textarea"
|
@saved="loadConfigs"
|
||||||
:rows="4"
|
/>
|
||||||
placeholder="一行一个选项"
|
|
||||||
@update:model-value="updateOptionLines('server_options', $event)"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="段位">
|
|
||||||
<el-input
|
|
||||||
:model-value="itemsToLines(publishOptionsDraft.rank_options)"
|
|
||||||
type="textarea"
|
|
||||||
:rows="4"
|
|
||||||
placeholder="一行一个选项"
|
|
||||||
@update:model-value="updateOptionLines('rank_options', $event)"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="保险">
|
|
||||||
<el-input
|
|
||||||
:model-value="itemsToLines(publishOptionsDraft.insurance_options)"
|
|
||||||
type="textarea"
|
|
||||||
:rows="4"
|
|
||||||
placeholder="一行一个选项"
|
|
||||||
@update:model-value="updateOptionLines('insurance_options', $event)"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="体力/负重">
|
|
||||||
<el-input
|
|
||||||
:model-value="itemsToLines(publishOptionsDraft.level_options)"
|
|
||||||
type="textarea"
|
|
||||||
:rows="4"
|
|
||||||
placeholder="一行一个选项"
|
|
||||||
@update:model-value="updateOptionLines('level_options', $event)"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="人脸选项">
|
|
||||||
<el-input
|
|
||||||
:model-value="itemsToLines(publishOptionsDraft.face_options)"
|
|
||||||
type="textarea"
|
|
||||||
:rows="3"
|
|
||||||
placeholder="一行一个选项"
|
|
||||||
@update:model-value="updateOptionLines('face_options', $event)"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="上号方式">
|
|
||||||
<el-input
|
|
||||||
:model-value="itemsToLines(publishOptionsDraft.login_method_options)"
|
|
||||||
type="textarea"
|
|
||||||
:rows="3"
|
|
||||||
placeholder="一行一个选项"
|
|
||||||
@update:model-value="updateOptionLines('login_method_options', $event)"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-form-item label="常用登录地区">
|
<HomeBannersDialog
|
||||||
<el-input
|
v-if="homeBannersConfig"
|
||||||
:model-value="itemsToLines(publishOptionsDraft.region_options)"
|
v-model="bannersVisible"
|
||||||
type="textarea"
|
:config="homeBannersConfig"
|
||||||
:rows="5"
|
@saved="loadConfigs"
|
||||||
placeholder="一行一个省市"
|
/>
|
||||||
@update:model-value="updateOptionLines('region_options', $event)"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<div class="editor-grid">
|
<GeneralConfigDialog
|
||||||
<el-form-item label="封禁记录">
|
v-if="currentEditingConfig"
|
||||||
<el-input
|
v-model="generalVisible"
|
||||||
:model-value="itemsToLines(publishOptionsDraft.ban_record_options)"
|
:config="currentEditingConfig"
|
||||||
type="textarea"
|
@saved="loadConfigs"
|
||||||
:rows="3"
|
/>
|
||||||
placeholder="一行一个选项"
|
|
||||||
@update:model-value="updateOptionLines('ban_record_options', $event)"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="需传安全图">
|
|
||||||
<el-input
|
|
||||||
:model-value="itemsToLines(publishOptionsDraft.ban_evidence_options)"
|
|
||||||
type="textarea"
|
|
||||||
:rows="3"
|
|
||||||
placeholder="选择这些封禁记录时,腾讯安全中心截图必传"
|
|
||||||
@update:model-value="updateOptionLines('ban_evidence_options', $event)"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="editor-block">
|
|
||||||
<div class="editor-block-title">
|
|
||||||
<strong>发布规则</strong>
|
|
||||||
</div>
|
|
||||||
<el-form-item label="最低烽火等级">
|
|
||||||
<el-input-number v-model="publishOptionsDraft.fire_level_min" :min="1" :step="1" class="full-control" />
|
|
||||||
</el-form-item>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="editor-block">
|
|
||||||
<div class="editor-block-title">
|
|
||||||
<strong>押金与价格提示</strong>
|
|
||||||
</div>
|
|
||||||
<el-form-item label="押金提示">
|
|
||||||
<el-input v-model="publishOptionsDraft.price_config.deposit_placeholder" type="textarea" :rows="3" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="价格提示">
|
|
||||||
<el-input v-model="publishOptionsDraft.price_config.price_placeholder" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="比例说明">
|
|
||||||
<el-input v-model="publishOptionsDraft.price_config.ratio_description" />
|
|
||||||
</el-form-item>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="editor-block">
|
|
||||||
<div class="editor-block-title">
|
|
||||||
<strong>智能推荐押金</strong>
|
|
||||||
<el-button size="small" @click="addDepositSkinGroupRule">添加皮肤规则</el-button>
|
|
||||||
</div>
|
|
||||||
<el-form-item label="基础押金">
|
|
||||||
<el-input-number
|
|
||||||
v-model="publishOptionsDraft.deposit_recommend_config.base_amount"
|
|
||||||
:min="0"
|
|
||||||
:step="5"
|
|
||||||
class="full-control"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-table :data="publishOptionsDraft.deposit_recommend_config.skin_group_rules" size="small" border>
|
|
||||||
<el-table-column label="皮肤分组 Key" min-width="150">
|
|
||||||
<template #default="{ row }"><el-input v-model="row.group_key" placeholder="operatorRed" /></template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="名称" min-width="140">
|
|
||||||
<template #default="{ row }"><el-input v-model="row.label" placeholder="干员红皮" /></template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="每个增加押金" min-width="140">
|
|
||||||
<template #default="{ row }"><el-input-number v-model="row.amount_per_item" :min="0" :step="5" /></template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作" width="90">
|
|
||||||
<template #default="{ $index }">
|
|
||||||
<el-button size="small" type="danger" plain @click="removeDepositSkinGroupRule($index)">删除</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="editor-block">
|
|
||||||
<div class="editor-block-title">
|
|
||||||
<strong>比例计算配置</strong>
|
|
||||||
</div>
|
|
||||||
<div class="editor-block-title subtle-title">
|
|
||||||
<span>保险基础比例</span>
|
|
||||||
<el-button size="small" @click="addInsuranceBaseRatio">添加保险比例</el-button>
|
|
||||||
</div>
|
|
||||||
<el-table :data="publishOptionsDraft.ratio_config.insurance_base_ratios" size="small" border>
|
|
||||||
<el-table-column label="保险" min-width="160">
|
|
||||||
<template #default="{ row }"><el-input v-model="row.insurance" placeholder="3*3" /></template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="基础比例" min-width="120">
|
|
||||||
<template #default="{ row }"><el-input-number v-model="row.ratio" :min="0" :step="0.5" /></template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作" width="90">
|
|
||||||
<template #default="{ $index }">
|
|
||||||
<el-button size="small" type="danger" plain @click="removeInsuranceBaseRatio($index)">删除</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
|
|
||||||
<div class="editor-block-title subtle-title">
|
|
||||||
<span>配置项缺失加成</span>
|
|
||||||
<el-button size="small" @click="addRatioConfigItem">添加配置项</el-button>
|
|
||||||
</div>
|
|
||||||
<el-table :data="publishOptionsDraft.ratio_config.config_items" size="small" border>
|
|
||||||
<el-table-column label="Key" min-width="140">
|
|
||||||
<template #default="{ row }"><el-input v-model="row.key" /></template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="名称" min-width="140">
|
|
||||||
<template #default="{ row }"><el-input v-model="row.label" /></template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="类型" min-width="160">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-select v-model="row.kind">
|
|
||||||
<el-option label="皮肤分组" value="skin_group" />
|
|
||||||
<el-option label="满体力" value="max_stamina" />
|
|
||||||
<el-option label="满负重" value="max_load" />
|
|
||||||
</el-select>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="皮肤分组 Key" min-width="150">
|
|
||||||
<template #default="{ row }"><el-input v-model="row.group_key" /></template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="缺失加成" min-width="120">
|
|
||||||
<template #default="{ row }"><el-input-number v-model="row.missing_penalty" :min="0" :step="0.5" /></template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作" width="90">
|
|
||||||
<template #default="{ $index }">
|
|
||||||
<el-button size="small" type="danger" plain @click="removeRatioConfigItem($index)">删除</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
|
|
||||||
<div class="editor-block-title subtle-title">
|
|
||||||
<span>大额币修正</span>
|
|
||||||
<el-button size="small" @click="addCoinCorrection">添加修正</el-button>
|
|
||||||
</div>
|
|
||||||
<el-table :data="publishOptionsDraft.ratio_config.coin_corrections" size="small" border>
|
|
||||||
<el-table-column label="大于 M" min-width="130">
|
|
||||||
<template #default="{ row }"><el-input-number v-model="row.threshold_m" :min="0" :step="10" /></template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="比例加成" min-width="130">
|
|
||||||
<template #default="{ row }"><el-input-number v-model="row.correction" :min="0" :step="0.5" /></template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作" width="90">
|
|
||||||
<template #default="{ $index }">
|
|
||||||
<el-button size="small" type="danger" plain @click="removeCoinCorrection($index)">删除</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="editor-block">
|
|
||||||
<div class="editor-block-title">
|
|
||||||
<strong>皮肤分类</strong>
|
|
||||||
<el-button size="small" @click="addSkinGroup">添加分类</el-button>
|
|
||||||
</div>
|
|
||||||
<div v-for="(group, index) in publishOptionsDraft.skin_groups" :key="`${group.key}-${index}`" class="skin-config-row">
|
|
||||||
<el-input v-model="group.key" placeholder="分类 key" />
|
|
||||||
<el-input v-model="group.title" placeholder="分类名称" />
|
|
||||||
<el-input
|
|
||||||
:model-value="itemsToLines(group.options)"
|
|
||||||
type="textarea"
|
|
||||||
:rows="4"
|
|
||||||
placeholder="皮肤名称,一行一个"
|
|
||||||
@update:model-value="updateSkinGroupOptions(index, $event)"
|
|
||||||
/>
|
|
||||||
<el-button type="danger" plain @click="removeSkinGroup(index)">删除</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="editor-block">
|
|
||||||
<div class="editor-block-title">
|
|
||||||
<strong>额外消耗品</strong>
|
|
||||||
<el-button size="small" @click="addQuantityItem">添加消耗品</el-button>
|
|
||||||
</div>
|
|
||||||
<el-table :data="publishOptionsDraft.quantity_items" size="small" border>
|
|
||||||
<el-table-column label="Key" min-width="150">
|
|
||||||
<template #default="{ row }"><el-input v-model="row.key" /></template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="名称" min-width="150">
|
|
||||||
<template #default="{ row }"><el-input v-model="row.label" /></template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="价格" min-width="130">
|
|
||||||
<template #default="{ row }"><el-input v-model="row.price" /></template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="提示" min-width="220">
|
|
||||||
<template #default="{ row }"><el-input v-model="row.placeholder" /></template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作" width="90">
|
|
||||||
<template #default="{ $index }">
|
|
||||||
<el-button size="small" type="danger" plain @click="removeQuantityItem($index)">删除</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="editor-block">
|
|
||||||
<div class="editor-block-title">
|
|
||||||
<strong>截图材料</strong>
|
|
||||||
<el-button size="small" @click="addScreenshotSlot">添加截图项</el-button>
|
|
||||||
</div>
|
|
||||||
<el-table :data="publishOptionsDraft.screenshot_slots" size="small" border>
|
|
||||||
<el-table-column label="Key" min-width="150">
|
|
||||||
<template #default="{ row }"><el-input v-model="row.key" /></template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="名称" min-width="150">
|
|
||||||
<template #default="{ row }"><el-input v-model="row.label" /></template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="必填" width="90">
|
|
||||||
<template #default="{ row }"><el-switch v-model="row.required" /></template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="提示" min-width="260">
|
|
||||||
<template #default="{ row }"><el-input v-model="row.hint" /></template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作" width="90">
|
|
||||||
<template #default="{ $index }">
|
|
||||||
<el-button size="small" type="danger" plain @click="removeScreenshotSlot($index)">删除</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else-if="isSalePriceConfig" class="publish-options-editor">
|
|
||||||
<div class="editor-toolbar">
|
|
||||||
<span>内部出售定价规则</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="editor-block">
|
|
||||||
<div class="editor-block-title subtle-title">
|
|
||||||
<span>90M 及以下固定加价:出售价格 = 回收价格 + 固定加价</span>
|
|
||||||
<el-button size="small" @click="addSaleFixedMarkupRule">添加固定加价</el-button>
|
|
||||||
</div>
|
|
||||||
<el-table :data="salePriceConfigDraft.fixed_markup_rules" size="small" border>
|
|
||||||
<el-table-column label="最小 M" min-width="120">
|
|
||||||
<template #default="{ row }"><el-input-number v-model="row.min_m" :min="0" :step="10" /></template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="最大 M" min-width="120">
|
|
||||||
<template #default="{ row }"><el-input-number v-model="row.max_m" :min="0" :step="10" /></template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="加价金额/元" min-width="140">
|
|
||||||
<template #default="{ row }"><el-input-number v-model="row.markup_amount" :min="0" :step="1" /></template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作" width="90">
|
|
||||||
<template #default="{ $index }">
|
|
||||||
<el-button size="small" type="danger" plain @click="removeSaleFixedMarkupRule($index)">删除</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
|
|
||||||
<div class="editor-block-title subtle-title">
|
|
||||||
<span>90M 以上比例修正:出售比例 = 回收比例 - 比例修正;最大 M 为 0 表示无上限</span>
|
|
||||||
<el-button size="small" @click="addSaleRatioAdjustmentRule">添加比例修正</el-button>
|
|
||||||
</div>
|
|
||||||
<el-table :data="salePriceConfigDraft.ratio_adjustment_rules" size="small" border>
|
|
||||||
<el-table-column label="最小 M" min-width="120">
|
|
||||||
<template #default="{ row }"><el-input-number v-model="row.min_m" :min="0" :step="10" /></template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="最大 M" min-width="120">
|
|
||||||
<template #default="{ row }"><el-input-number v-model="row.max_m" :min="0" :step="10" /></template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="比例修正" min-width="130">
|
|
||||||
<template #default="{ row }"><el-input-number v-model="row.ratio_subtract" :min="0" :step="0.5" /></template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作" width="90">
|
|
||||||
<template #default="{ $index }">
|
|
||||||
<el-button size="small" type="danger" plain @click="removeSaleRatioAdjustmentRule($index)">删除</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else-if="isHomeAnnouncementsConfig" class="home-config-editor">
|
|
||||||
<div class="editor-toolbar">
|
|
||||||
<span>首页公告</span>
|
|
||||||
<el-button size="small" @click="resetHomeAnnouncements">恢复默认公告</el-button>
|
|
||||||
</div>
|
|
||||||
<el-form-item label="公告内容">
|
|
||||||
<el-input
|
|
||||||
v-model="homeAnnouncementLines"
|
|
||||||
type="textarea"
|
|
||||||
:rows="8"
|
|
||||||
placeholder="一行一条公告,移动端会自动轮播展示"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else-if="isHomeBannersConfig" class="home-config-editor">
|
|
||||||
<div class="editor-toolbar">
|
|
||||||
<span>首页轮播图</span>
|
|
||||||
<div class="panel-actions">
|
|
||||||
<el-button size="small" @click="resetHomeBanners">恢复默认轮播</el-button>
|
|
||||||
<el-button size="small" type="primary" @click="addHomeBanner">添加轮播</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<el-table :data="homeBannersDraft" size="small" border>
|
|
||||||
<el-table-column label="图片" min-width="240">
|
|
||||||
<template #default="{ row, $index }">
|
|
||||||
<div class="banner-image-editor">
|
|
||||||
<el-input v-model="row.image_url" placeholder="图片 URL,留空使用文字卡片" />
|
|
||||||
<div class="banner-image-tools">
|
|
||||||
<img v-if="row.image_url" :src="row.image_url" alt="轮播图预览" />
|
|
||||||
<span v-else>无图</span>
|
|
||||||
<label class="upload-trigger">
|
|
||||||
<input type="file" accept="image/jpeg,image/png,image/webp" @change="handleHomeBannerUpload($event, $index)" />
|
|
||||||
{{ uploadingBannerIndex === $index ? '上传中' : '上传图片' }}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="眉标" min-width="160">
|
|
||||||
<template #default="{ row }"><el-input v-model="row.eyebrow" placeholder="如 三角洲行动账号专区" /></template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="主标题" min-width="220">
|
|
||||||
<template #default="{ row }"><el-input v-model="row.title" placeholder="轮播主文案" /></template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="角标" width="110">
|
|
||||||
<template #default="{ row }"><el-input v-model="row.badge" placeholder="HOT" /></template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="胶囊文案" min-width="220">
|
|
||||||
<template #default="{ row }"><el-input v-model="row.pill" placeholder="底部补充文案" /></template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="色调" width="130">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-select v-model="row.tone" placeholder="色调">
|
|
||||||
<el-option label="蓝色" value="blue" />
|
|
||||||
<el-option label="绿色" value="green" />
|
|
||||||
<el-option label="橙色" value="orange" />
|
|
||||||
</el-select>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作" width="90">
|
|
||||||
<template #default="{ $index }">
|
|
||||||
<el-button size="small" type="danger" plain @click="removeHomeBanner($index)">删除</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-input v-else-if="isStructuredConfig" v-model="value" type="textarea" :rows="12" placeholder="配置值 JSON" />
|
|
||||||
<el-input v-else v-model="value" placeholder="配置值" />
|
|
||||||
<el-input v-model="description" type="textarea" :rows="3" placeholder="配置说明" />
|
|
||||||
</div>
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="activeConfig = null">取消</el-button>
|
|
||||||
<el-button type="primary" :loading="submitting" @click="handleSave">保存</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -1053,102 +405,6 @@ function readError(error: unknown, fallback: string) {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.publish-options-editor {
|
|
||||||
display: grid;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-config-editor {
|
|
||||||
display: grid;
|
|
||||||
gap: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.banner-image-editor {
|
|
||||||
display: grid;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.banner-image-tools {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.banner-image-tools img,
|
|
||||||
.banner-image-tools span {
|
|
||||||
width: 68px;
|
|
||||||
height: 38px;
|
|
||||||
border-radius: 6px;
|
|
||||||
background: #f3f4f6;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.banner-image-tools span {
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
color: #9ca3af;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-trigger {
|
|
||||||
position: relative;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 28px;
|
|
||||||
padding: 0 10px;
|
|
||||||
border: 1px solid #dcdfe6;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: #fff;
|
|
||||||
color: #606266;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-trigger input {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
opacity: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-toolbar,
|
|
||||||
.editor-block-title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-toolbar span {
|
|
||||||
color: #30343a;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-block {
|
|
||||||
display: grid;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtle-title {
|
|
||||||
margin-top: 6px;
|
|
||||||
color: #4b5563;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skin-config-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 140px 180px minmax(0, 1fr) 72px;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1100px) {
|
@media (max-width: 1100px) {
|
||||||
.publish-stat-grid {
|
.publish-stat-grid {
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
import { updateSystemConfig, type SystemConfig } from '@/api/systemConfigs'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: boolean
|
||||||
|
config: SystemConfig
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', val: boolean): void
|
||||||
|
(e: 'saved'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const submitting = ref(false)
|
||||||
|
const value = ref('')
|
||||||
|
const description = ref('')
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(val) => {
|
||||||
|
if (val) {
|
||||||
|
value.value = props.config.value || ''
|
||||||
|
description.value = props.config.description || ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
const isStructuredConfig = computed(() => {
|
||||||
|
const trimmed = value.value.trim()
|
||||||
|
return trimmed.startsWith('{') || trimmed.startsWith('[')
|
||||||
|
})
|
||||||
|
|
||||||
|
async function handleSave() {
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
await updateSystemConfig(props.config.key, {
|
||||||
|
value: value.value,
|
||||||
|
description: description.value,
|
||||||
|
})
|
||||||
|
ElMessage.success('配置已更新')
|
||||||
|
emit('saved')
|
||||||
|
emit('update:modelValue', false)
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(readError(error, '保存失败'))
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readError(error: unknown, fallback: string) {
|
||||||
|
if (typeof error === 'object' && error && 'response' in error) {
|
||||||
|
const response = (error as { response?: { data?: { message?: string } } }).response
|
||||||
|
return response?.data?.message || fallback
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
:model-value="modelValue"
|
||||||
|
title="编辑系统配置"
|
||||||
|
width="560px"
|
||||||
|
@update:model-value="emit('update:modelValue', $event)"
|
||||||
|
>
|
||||||
|
<div class="dialog-body">
|
||||||
|
<p class="config-key-label"><strong>{{ config.key }}</strong></p>
|
||||||
|
|
||||||
|
<el-form-item v-if="isStructuredConfig" label="配置值 (JSON)" class="full-control">
|
||||||
|
<el-input v-model="value" type="textarea" :rows="12" placeholder="配置值 JSON" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-else label="配置值" class="full-control">
|
||||||
|
<el-input v-model="value" placeholder="配置值" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="配置说明" class="desc-item">
|
||||||
|
<el-input v-model="description" type="textarea" :rows="3" placeholder="配置说明" />
|
||||||
|
</el-form-item>
|
||||||
|
</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: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-key-label {
|
||||||
|
margin: 0;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.full-control {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc-item {
|
||||||
|
margin-top: 14px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
|
import { defaultHomeAnnouncements, mergeHomeConfig } from '@/api/homeConfig'
|
||||||
|
import { updateSystemConfig, type SystemConfig } from '@/api/systemConfigs'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: boolean
|
||||||
|
config: SystemConfig
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', val: boolean): void
|
||||||
|
(e: 'saved'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const submitting = ref(false)
|
||||||
|
const description = ref('')
|
||||||
|
const homeAnnouncementLines = ref('')
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(val) => {
|
||||||
|
if (val) {
|
||||||
|
homeAnnouncementLines.value = itemsToLines(parseHomeAnnouncements(props.config.value))
|
||||||
|
description.value = props.config.description || ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
function parseHomeAnnouncements(raw: string) {
|
||||||
|
try {
|
||||||
|
const parsed = raw.trim() ? JSON.parse(raw) : defaultHomeAnnouncements
|
||||||
|
return mergeHomeConfig({ announcements: Array.isArray(parsed) ? parsed : [] }).announcements
|
||||||
|
} catch {
|
||||||
|
return [...defaultHomeAnnouncements]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function linesToItems(value: string) {
|
||||||
|
return value
|
||||||
|
.split('\n')
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
function itemsToLines(items: string[]) {
|
||||||
|
return items.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetHomeAnnouncements() {
|
||||||
|
homeAnnouncementLines.value = itemsToLines(defaultHomeAnnouncements)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSave() {
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
const value = JSON.stringify(linesToItems(homeAnnouncementLines.value), 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readError(error: unknown, fallback: string) {
|
||||||
|
if (typeof error === 'object' && error && 'response' in error) {
|
||||||
|
const response = (error as { response?: { data?: { message?: string } } }).response
|
||||||
|
return response?.data?.message || fallback
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
:model-value="modelValue"
|
||||||
|
title="编辑首页公告"
|
||||||
|
width="680px"
|
||||||
|
@update:model-value="emit('update:modelValue', $event)"
|
||||||
|
>
|
||||||
|
<div class="dialog-body">
|
||||||
|
<p class="config-key-label"><strong>{{ config.key }}</strong></p>
|
||||||
|
|
||||||
|
<div class="home-config-editor">
|
||||||
|
<div class="editor-toolbar">
|
||||||
|
<span>首页公告</span>
|
||||||
|
<el-button size="small" @click="resetHomeAnnouncements">恢复默认公告</el-button>
|
||||||
|
</div>
|
||||||
|
<el-form-item label="公告内容" class="full-control">
|
||||||
|
<el-input
|
||||||
|
v-model="homeAnnouncementLines"
|
||||||
|
type="textarea"
|
||||||
|
:rows="8"
|
||||||
|
placeholder="一行一条公告,移动端会自动轮播展示"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form-item label="配置说明" class="desc-item">
|
||||||
|
<el-input v-model="description" type="textarea" :rows="3" placeholder="配置说明" />
|
||||||
|
</el-form-item>
|
||||||
|
</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: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-key-label {
|
||||||
|
margin: 0;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-config-editor {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-toolbar span {
|
||||||
|
color: #30343a;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.full-control {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc-item {
|
||||||
|
margin-top: 14px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,289 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
|
import { uploadAdminFile } from '@/api/files'
|
||||||
|
import { defaultHomeBanners, mergeHomeConfig, type HomeBannerSlide } from '@/api/homeConfig'
|
||||||
|
import { updateSystemConfig, type SystemConfig } from '@/api/systemConfigs'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: boolean
|
||||||
|
config: SystemConfig
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', val: boolean): void
|
||||||
|
(e: 'saved'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const submitting = ref(false)
|
||||||
|
const description = ref('')
|
||||||
|
const homeBannersDraft = ref<HomeBannerSlide[]>([])
|
||||||
|
const uploadingBannerIndex = ref<number | null>(null)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(val) => {
|
||||||
|
if (val) {
|
||||||
|
homeBannersDraft.value = parseHomeBanners(props.config.value)
|
||||||
|
description.value = props.config.description || ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
function parseHomeBanners(raw: string) {
|
||||||
|
try {
|
||||||
|
const parsed = raw.trim() ? JSON.parse(raw) : defaultHomeBanners
|
||||||
|
return cloneHomeBanners(mergeHomeConfig({ banners: Array.isArray(parsed) ? parsed : [] }).banners)
|
||||||
|
} catch {
|
||||||
|
return cloneHomeBanners(defaultHomeBanners)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneHomeBanners(banners: HomeBannerSlide[]) {
|
||||||
|
return JSON.parse(JSON.stringify(banners)) as HomeBannerSlide[]
|
||||||
|
}
|
||||||
|
|
||||||
|
function addHomeBanner() {
|
||||||
|
homeBannersDraft.value.push({
|
||||||
|
eyebrow: '首页推荐',
|
||||||
|
title: '',
|
||||||
|
badge: 'NEW',
|
||||||
|
pill: '',
|
||||||
|
tone: 'blue',
|
||||||
|
image_url: '',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeHomeBanner(index: number) {
|
||||||
|
homeBannersDraft.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetHomeBanners() {
|
||||||
|
homeBannersDraft.value = cloneHomeBanners(defaultHomeBanners)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleHomeBannerUpload(event: Event, index: number) {
|
||||||
|
const input = event.target as HTMLInputElement
|
||||||
|
const file = input.files?.[0]
|
||||||
|
input.value = ''
|
||||||
|
if (!file) return
|
||||||
|
uploadingBannerIndex.value = index
|
||||||
|
try {
|
||||||
|
const uploaded = await uploadAdminFile(file, 'home-banner')
|
||||||
|
const banner = homeBannersDraft.value[index]
|
||||||
|
if (banner) {
|
||||||
|
banner.image_url = uploaded.url
|
||||||
|
if (!banner.title) {
|
||||||
|
banner.title = banner.eyebrow || '首页轮播图'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ElMessage.success('图片已上传')
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(readError(error, '图片上传失败'))
|
||||||
|
} finally {
|
||||||
|
uploadingBannerIndex.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSave() {
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
const value = JSON.stringify(
|
||||||
|
homeBannersDraft.value.filter((item) => item.title.trim() || item.image_url?.trim()),
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readError(error: unknown, fallback: string) {
|
||||||
|
if (typeof error === 'object' && error && 'response' in error) {
|
||||||
|
const response = (error as { response?: { data?: { message?: string } } }).response
|
||||||
|
return response?.data?.message || fallback
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
:model-value="modelValue"
|
||||||
|
title="编辑首页轮播图"
|
||||||
|
width="920px"
|
||||||
|
@update:model-value="emit('update:modelValue', $event)"
|
||||||
|
>
|
||||||
|
<div class="dialog-body">
|
||||||
|
<p class="config-key-label"><strong>{{ config.key }}</strong></p>
|
||||||
|
|
||||||
|
<div class="home-config-editor">
|
||||||
|
<div class="editor-toolbar">
|
||||||
|
<span>首页轮播图</span>
|
||||||
|
<div class="panel-actions">
|
||||||
|
<el-button size="small" @click="resetHomeBanners">恢复默认轮播</el-button>
|
||||||
|
<el-button size="small" type="primary" @click="addHomeBanner">添加轮播</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-table :data="homeBannersDraft" size="small" border>
|
||||||
|
<el-table-column label="图片" min-width="240">
|
||||||
|
<template #default="{ row, $index }">
|
||||||
|
<div class="banner-image-editor">
|
||||||
|
<el-input v-model="row.image_url" placeholder="图片 URL,留空使用文字卡片" />
|
||||||
|
<div class="banner-image-tools">
|
||||||
|
<img v-if="row.image_url" :src="row.image_url" alt="轮播图预览" />
|
||||||
|
<div v-else class="empty-image">无图</div>
|
||||||
|
<label class="upload-trigger">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/jpeg,image/png,image/webp"
|
||||||
|
@change="handleHomeBannerUpload($event, $index)"
|
||||||
|
/>
|
||||||
|
{{ uploadingBannerIndex === $index ? '上传中' : '上传图片' }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="眉标" min-width="160">
|
||||||
|
<template #default="{ row }"><el-input v-model="row.eyebrow" placeholder="如 三角洲行动账号专区" /></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="主标题" min-width="220">
|
||||||
|
<template #default="{ row }"><el-input v-model="row.title" placeholder="轮播主文案" /></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="角标" width="110">
|
||||||
|
<template #default="{ row }"><el-input v-model="row.badge" placeholder="HOT" /></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="胶囊文案" min-width="220">
|
||||||
|
<template #default="{ row }"><el-input v-model="row.pill" placeholder="底部补充文案" /></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="色调" width="130">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-select v-model="row.tone" placeholder="色调">
|
||||||
|
<el-option label="蓝色" value="blue" />
|
||||||
|
<el-option label="绿色" value="green" />
|
||||||
|
<el-option label="橙色" value="orange" />
|
||||||
|
</el-select>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="90">
|
||||||
|
<template #default="{ $index }">
|
||||||
|
<el-button size="small" type="danger" plain @click="removeHomeBanner($index)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form-item label="配置说明" class="desc-item">
|
||||||
|
<el-input v-model="description" type="textarea" :rows="3" placeholder="配置说明" />
|
||||||
|
</el-form-item>
|
||||||
|
</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: 16px;
|
||||||
|
max-height: 65vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-key-label {
|
||||||
|
margin: 0;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-config-editor {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-toolbar span {
|
||||||
|
color: #30343a;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-image-editor {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-image-tools {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-image-tools img,
|
||||||
|
.empty-image {
|
||||||
|
width: 68px;
|
||||||
|
height: 38px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-image {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
color: #9ca3af;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-trigger {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 28px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #fff;
|
||||||
|
color: #606266;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-trigger input {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
opacity: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc-item {
|
||||||
|
margin-top: 14px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,578 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
|
import {
|
||||||
|
emptyListingPublishOptions,
|
||||||
|
mergeListingPublishOptions,
|
||||||
|
type ListingPublishOptions,
|
||||||
|
} from '@/api/listingOptions'
|
||||||
|
import { updateSystemConfig, type SystemConfig } from '@/api/systemConfigs'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: boolean
|
||||||
|
config: SystemConfig
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', val: boolean): void
|
||||||
|
(e: 'saved'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const submitting = ref(false)
|
||||||
|
const description = ref('')
|
||||||
|
const publishOptionsDraft = ref<ListingPublishOptions>(cloneOptions(emptyListingPublishOptions))
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(val) => {
|
||||||
|
if (val) {
|
||||||
|
publishOptionsDraft.value = parsePublishOptions(props.config.value)
|
||||||
|
description.value = props.config.description || ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
function parsePublishOptions(raw: string) {
|
||||||
|
try {
|
||||||
|
const parsed = raw.trim() ? JSON.parse(raw) : emptyListingPublishOptions
|
||||||
|
return cloneOptions(mergeListingPublishOptions(parsed))
|
||||||
|
} catch {
|
||||||
|
ElMessage.warning('发布选项 JSON 解析失败,已清空草稿')
|
||||||
|
return cloneOptions(emptyListingPublishOptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneOptions(options: ListingPublishOptions) {
|
||||||
|
return JSON.parse(JSON.stringify(options)) as ListingPublishOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
function linesToItems(value: string) {
|
||||||
|
return value
|
||||||
|
.split('\n')
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
function itemsToLines(items: string[]) {
|
||||||
|
return items.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateOptionLines(
|
||||||
|
key: keyof Pick<
|
||||||
|
ListingPublishOptions,
|
||||||
|
| 'server_options'
|
||||||
|
| 'face_options'
|
||||||
|
| 'rank_options'
|
||||||
|
| 'insurance_options'
|
||||||
|
| 'level_options'
|
||||||
|
| 'login_method_options'
|
||||||
|
| 'region_options'
|
||||||
|
| 'ban_record_options'
|
||||||
|
| 'ban_evidence_options'
|
||||||
|
>,
|
||||||
|
nextValue: string
|
||||||
|
) {
|
||||||
|
publishOptionsDraft.value[key] = linesToItems(nextValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSkinGroupOptions(index: number, nextValue: string) {
|
||||||
|
const group = publishOptionsDraft.value.skin_groups[index]
|
||||||
|
if (group) {
|
||||||
|
group.options = linesToItems(nextValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addSkinGroup() {
|
||||||
|
publishOptionsDraft.value.skin_groups.push({
|
||||||
|
key: `group_${Date.now()}`,
|
||||||
|
title: '新皮肤分类',
|
||||||
|
options: [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeSkinGroup(index: number) {
|
||||||
|
publishOptionsDraft.value.skin_groups.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function addQuantityItem() {
|
||||||
|
publishOptionsDraft.value.quantity_items.push({
|
||||||
|
key: `item_${Date.now()}`,
|
||||||
|
label: '',
|
||||||
|
price: '',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeQuantityItem(index: number) {
|
||||||
|
publishOptionsDraft.value.quantity_items.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function addScreenshotSlot() {
|
||||||
|
publishOptionsDraft.value.screenshot_slots.push({
|
||||||
|
key: `screenshot_${Date.now()}`,
|
||||||
|
label: '',
|
||||||
|
required: false,
|
||||||
|
hint: '',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeScreenshotSlot(index: number) {
|
||||||
|
publishOptionsDraft.value.screenshot_slots.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function addInsuranceBaseRatio() {
|
||||||
|
publishOptionsDraft.value.ratio_config.insurance_base_ratios.push({
|
||||||
|
insurance: '',
|
||||||
|
ratio: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeInsuranceBaseRatio(index: number) {
|
||||||
|
publishOptionsDraft.value.ratio_config.insurance_base_ratios.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function addRatioConfigItem() {
|
||||||
|
publishOptionsDraft.value.ratio_config.config_items.push({
|
||||||
|
key: `config_${Date.now()}`,
|
||||||
|
label: '',
|
||||||
|
kind: 'skin_group',
|
||||||
|
group_key: '',
|
||||||
|
missing_penalty: 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeRatioConfigItem(index: number) {
|
||||||
|
publishOptionsDraft.value.ratio_config.config_items.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function addCoinCorrection() {
|
||||||
|
publishOptionsDraft.value.ratio_config.coin_corrections.push({
|
||||||
|
threshold_m: 0,
|
||||||
|
correction: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeCoinCorrection(index: number) {
|
||||||
|
publishOptionsDraft.value.ratio_config.coin_corrections.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function addDepositSkinGroupRule() {
|
||||||
|
publishOptionsDraft.value.deposit_recommend_config.skin_group_rules.push({
|
||||||
|
group_key: '',
|
||||||
|
label: '',
|
||||||
|
amount_per_item: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeDepositSkinGroupRule(index: number) {
|
||||||
|
publishOptionsDraft.value.deposit_recommend_config.skin_group_rules.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSave() {
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
const value = JSON.stringify(publishOptionsDraft.value, 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readError(error: unknown, fallback: string) {
|
||||||
|
if (typeof error === 'object' && error && 'response' in error) {
|
||||||
|
const response = (error as { response?: { data?: { message?: string } } }).response
|
||||||
|
return response?.data?.message || fallback
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
:model-value="modelValue"
|
||||||
|
title="编辑发布选项配置"
|
||||||
|
width="920px"
|
||||||
|
@update:model-value="emit('update:modelValue', $event)"
|
||||||
|
>
|
||||||
|
<div class="dialog-body">
|
||||||
|
<p class="config-key-label"><strong>{{ config.key }}</strong></p>
|
||||||
|
|
||||||
|
<div class="publish-options-editor">
|
||||||
|
<div class="editor-toolbar">
|
||||||
|
<span>发布页选项配置</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="editor-grid">
|
||||||
|
<el-form-item label="区服">
|
||||||
|
<el-input
|
||||||
|
:model-value="itemsToLines(publishOptionsDraft.server_options)"
|
||||||
|
type="textarea"
|
||||||
|
:rows="4"
|
||||||
|
placeholder="一行一个选项"
|
||||||
|
@update:model-value="updateOptionLines('server_options', $event)"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="段位">
|
||||||
|
<el-input
|
||||||
|
:model-value="itemsToLines(publishOptionsDraft.rank_options)"
|
||||||
|
type="textarea"
|
||||||
|
:rows="4"
|
||||||
|
placeholder="一行一个选项"
|
||||||
|
@update:model-value="updateOptionLines('rank_options', $event)"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="保险">
|
||||||
|
<el-input
|
||||||
|
:model-value="itemsToLines(publishOptionsDraft.insurance_options)"
|
||||||
|
type="textarea"
|
||||||
|
:rows="4"
|
||||||
|
placeholder="一行一个选项"
|
||||||
|
@update:model-value="updateOptionLines('insurance_options', $event)"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="体力/负重">
|
||||||
|
<el-input
|
||||||
|
:model-value="itemsToLines(publishOptionsDraft.level_options)"
|
||||||
|
type="textarea"
|
||||||
|
:rows="4"
|
||||||
|
placeholder="一行一个选项"
|
||||||
|
@update:model-value="updateOptionLines('level_options', $event)"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="人脸选项">
|
||||||
|
<el-input
|
||||||
|
:model-value="itemsToLines(publishOptionsDraft.face_options)"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
placeholder="一行一个选项"
|
||||||
|
@update:model-value="updateOptionLines('face_options', $event)"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="上号方式">
|
||||||
|
<el-input
|
||||||
|
:model-value="itemsToLines(publishOptionsDraft.login_method_options)"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
placeholder="一行一个选项"
|
||||||
|
@update:model-value="updateOptionLines('login_method_options', $event)"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form-item label="常用登录地区">
|
||||||
|
<el-input
|
||||||
|
:model-value="itemsToLines(publishOptionsDraft.region_options)"
|
||||||
|
type="textarea"
|
||||||
|
:rows="5"
|
||||||
|
placeholder="一行一个省市"
|
||||||
|
@update:model-value="updateOptionLines('region_options', $event)"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<div class="editor-grid">
|
||||||
|
<el-form-item label="封禁记录">
|
||||||
|
<el-input
|
||||||
|
:model-value="itemsToLines(publishOptionsDraft.ban_record_options)"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
placeholder="一行一个选项"
|
||||||
|
@update:model-value="updateOptionLines('ban_record_options', $event)"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="需传安全图">
|
||||||
|
<el-input
|
||||||
|
:model-value="itemsToLines(publishOptionsDraft.ban_evidence_options)"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
placeholder="选择这些封禁记录时,腾讯安全中心截图必传"
|
||||||
|
@update:model-value="updateOptionLines('ban_evidence_options', $event)"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="editor-block">
|
||||||
|
<div class="editor-block-title">
|
||||||
|
<strong>发布规则</strong>
|
||||||
|
</div>
|
||||||
|
<el-form-item label="最低烽火等级">
|
||||||
|
<el-input-number v-model="publishOptionsDraft.fire_level_min" :min="1" :step="1" class="full-control" />
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="editor-block">
|
||||||
|
<div class="editor-block-title">
|
||||||
|
<strong>押金与价格提示</strong>
|
||||||
|
</div>
|
||||||
|
<el-form-item label="押金提示">
|
||||||
|
<el-input v-model="publishOptionsDraft.price_config.deposit_placeholder" type="textarea" :rows="3" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="价格提示">
|
||||||
|
<el-input v-model="publishOptionsDraft.price_config.price_placeholder" class="full-control" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="比例说明">
|
||||||
|
<el-input v-model="publishOptionsDraft.price_config.ratio_description" class="full-control" />
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="editor-block">
|
||||||
|
<div class="editor-block-title">
|
||||||
|
<strong>智能推荐押金</strong>
|
||||||
|
<el-button size="small" @click="addDepositSkinGroupRule">添加皮肤规则</el-button>
|
||||||
|
</div>
|
||||||
|
<el-form-item label="基础押金">
|
||||||
|
<el-input-number
|
||||||
|
v-model="publishOptionsDraft.deposit_recommend_config.base_amount"
|
||||||
|
:min="0"
|
||||||
|
:step="5"
|
||||||
|
class="full-control"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-table :data="publishOptionsDraft.deposit_recommend_config.skin_group_rules" size="small" border>
|
||||||
|
<el-table-column label="皮肤分组 Key" min-width="150">
|
||||||
|
<template #default="{ row }"><el-input v-model="row.group_key" placeholder="operatorRed" /></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="名称" min-width="140">
|
||||||
|
<template #default="{ row }"><el-input v-model="row.label" placeholder="干员红皮" /></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="每个增加押金" min-width="140">
|
||||||
|
<template #default="{ row }"><el-input-number v-model="row.amount_per_item" :min="0" :step="5" /></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="90">
|
||||||
|
<template #default="{ $index }">
|
||||||
|
<el-button size="small" type="danger" plain @click="removeDepositSkinGroupRule($index)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="editor-block">
|
||||||
|
<div class="editor-block-title">
|
||||||
|
<strong>比例计算配置</strong>
|
||||||
|
</div>
|
||||||
|
<div class="editor-block-title subtle-title">
|
||||||
|
<span>保险基础比例</span>
|
||||||
|
<el-button size="small" @click="addInsuranceBaseRatio">添加保险比例</el-button>
|
||||||
|
</div>
|
||||||
|
<el-table :data="publishOptionsDraft.ratio_config.insurance_base_ratios" size="small" border>
|
||||||
|
<el-table-column label="保险" min-width="160">
|
||||||
|
<template #default="{ row }"><el-input v-model="row.insurance" placeholder="3*3" /></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="基础比例" min-width="120">
|
||||||
|
<template #default="{ row }"><el-input-number v-model="row.ratio" :min="0" :step="0.5" /></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="90">
|
||||||
|
<template #default="{ $index }">
|
||||||
|
<el-button size="small" type="danger" plain @click="removeInsuranceBaseRatio($index)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div class="editor-block-title subtle-title">
|
||||||
|
<span>配置项缺失加成</span>
|
||||||
|
<el-button size="small" @click="addRatioConfigItem">添加配置项</el-button>
|
||||||
|
</div>
|
||||||
|
<el-table :data="publishOptionsDraft.ratio_config.config_items" size="small" border>
|
||||||
|
<el-table-column label="Key" min-width="140">
|
||||||
|
<template #default="{ row }"><el-input v-model="row.key" /></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="名称" min-width="140">
|
||||||
|
<template #default="{ row }"><el-input v-model="row.label" /></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="类型" min-width="160">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-select v-model="row.kind">
|
||||||
|
<el-option label="皮肤分组" value="skin_group" />
|
||||||
|
<el-option label="满体力" value="max_stamina" />
|
||||||
|
<el-option label="满负重" value="max_load" />
|
||||||
|
</el-select>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="皮肤分组 Key" min-width="150">
|
||||||
|
<template #default="{ row }"><el-input v-model="row.group_key" /></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="缺失加成" min-width="120">
|
||||||
|
<template #default="{ row }"><el-input-number v-model="row.missing_penalty" :min="0" :step="0.5" /></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="90">
|
||||||
|
<template #default="{ $index }">
|
||||||
|
<el-button size="small" type="danger" plain @click="removeRatioConfigItem($index)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div class="editor-block-title subtle-title">
|
||||||
|
<span>大额币修正</span>
|
||||||
|
<el-button size="small" @click="addCoinCorrection">添加修正</el-button>
|
||||||
|
</div>
|
||||||
|
<el-table :data="publishOptionsDraft.ratio_config.coin_corrections" size="small" border>
|
||||||
|
<el-table-column label="大于 M" min-width="130">
|
||||||
|
<template #default="{ row }"><el-input-number v-model="row.threshold_m" :min="0" :step="10" /></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="比例加成" min-width="130">
|
||||||
|
<template #default="{ row }"><el-input-number v-model="row.correction" :min="0" :step="0.5" /></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="90">
|
||||||
|
<template #default="{ $index }">
|
||||||
|
<el-button size="small" type="danger" plain @click="removeCoinCorrection($index)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="editor-block">
|
||||||
|
<div class="editor-block-title">
|
||||||
|
<strong>皮肤分类</strong>
|
||||||
|
<el-button size="small" @click="addSkinGroup">添加分类</el-button>
|
||||||
|
</div>
|
||||||
|
<div v-for="(group, index) in publishOptionsDraft.skin_groups" :key="`${group.key}-${index}`" class="skin-config-row">
|
||||||
|
<el-input v-model="group.key" placeholder="分类 key" />
|
||||||
|
<el-input v-model="group.title" placeholder="分类名称" />
|
||||||
|
<el-input
|
||||||
|
:model-value="itemsToLines(group.options)"
|
||||||
|
type="textarea"
|
||||||
|
:rows="4"
|
||||||
|
placeholder="皮肤名称,一行一个"
|
||||||
|
@update:model-value="updateSkinGroupOptions(index, $event)"
|
||||||
|
/>
|
||||||
|
<el-button type="danger" plain @click="removeSkinGroup(index)">删除</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="editor-block">
|
||||||
|
<div class="editor-block-title">
|
||||||
|
<strong>额外消耗品</strong>
|
||||||
|
<el-button size="small" @click="addQuantityItem">添加消耗品</el-button>
|
||||||
|
</div>
|
||||||
|
<el-table :data="publishOptionsDraft.quantity_items" size="small" border>
|
||||||
|
<el-table-column label="Key" min-width="150">
|
||||||
|
<template #default="{ row }"><el-input v-model="row.key" /></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="名称" min-width="150">
|
||||||
|
<template #default="{ row }"><el-input v-model="row.label" /></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="价格" min-width="130">
|
||||||
|
<template #default="{ row }"><el-input v-model="row.price" /></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="提示" min-width="220">
|
||||||
|
<template #default="{ row }"><el-input v-model="row.placeholder" /></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="90">
|
||||||
|
<template #default="{ $index }">
|
||||||
|
<el-button size="small" type="danger" plain @click="removeQuantityItem($index)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="editor-block">
|
||||||
|
<div class="editor-block-title">
|
||||||
|
<strong>截图材料</strong>
|
||||||
|
<el-button size="small" @click="addScreenshotSlot">添加截图项</el-button>
|
||||||
|
</div>
|
||||||
|
<el-table :data="publishOptionsDraft.screenshot_slots" size="small" border>
|
||||||
|
<el-table-column label="Key" min-width="150">
|
||||||
|
<template #default="{ row }"><el-input v-model="row.key" /></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="名称" min-width="150">
|
||||||
|
<template #default="{ row }"><el-input v-model="row.label" /></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="必填" width="90">
|
||||||
|
<template #default="{ row }"><el-switch v-model="row.required" /></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="提示" min-width="260">
|
||||||
|
<template #default="{ row }"><el-input v-model="row.hint" /></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="90">
|
||||||
|
<template #default="{ $index }">
|
||||||
|
<el-button size="small" type="danger" plain @click="removeScreenshotSlot($index)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form-item label="配置说明" class="desc-item">
|
||||||
|
<el-input v-model="description" type="textarea" :rows="3" placeholder="配置说明" />
|
||||||
|
</el-form-item>
|
||||||
|
</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: 16px;
|
||||||
|
max-height: 65vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-key-label {
|
||||||
|
margin: 0;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.publish-options-editor {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-toolbar,
|
||||||
|
.editor-block-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-toolbar span {
|
||||||
|
color: #30343a;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-block {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtle-title {
|
||||||
|
margin-top: 6px;
|
||||||
|
color: #4b5563;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skin-config-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 140px 180px minmax(0, 1fr) 72px;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.full-control {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc-item {
|
||||||
|
margin-top: 14px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
|
import {
|
||||||
|
emptyListingSalePriceConfig,
|
||||||
|
mergeListingSalePriceConfig,
|
||||||
|
type PublishSalePriceConfig,
|
||||||
|
} from '@/api/listingOptions'
|
||||||
|
import { updateSystemConfig, type SystemConfig } from '@/api/systemConfigs'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: boolean
|
||||||
|
config: SystemConfig
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', val: boolean): void
|
||||||
|
(e: 'saved'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const submitting = ref(false)
|
||||||
|
const description = ref('')
|
||||||
|
const salePriceConfigDraft = ref<PublishSalePriceConfig>(cloneSalePriceConfig(emptyListingSalePriceConfig))
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(val) => {
|
||||||
|
if (val) {
|
||||||
|
salePriceConfigDraft.value = parseSalePriceConfig(props.config.value)
|
||||||
|
description.value = props.config.description || ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
function parseSalePriceConfig(raw: string) {
|
||||||
|
try {
|
||||||
|
const parsed = raw.trim() ? JSON.parse(raw) : emptyListingSalePriceConfig
|
||||||
|
return cloneSalePriceConfig(mergeListingSalePriceConfig(parsed))
|
||||||
|
} catch {
|
||||||
|
ElMessage.warning('出售定价规则 JSON 解析失败,已清空草稿')
|
||||||
|
return cloneSalePriceConfig(emptyListingSalePriceConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneSalePriceConfig(config: PublishSalePriceConfig) {
|
||||||
|
return JSON.parse(JSON.stringify(config)) as PublishSalePriceConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
function addSaleFixedMarkupRule() {
|
||||||
|
salePriceConfigDraft.value.fixed_markup_rules.push({
|
||||||
|
min_m: 0,
|
||||||
|
max_m: 0,
|
||||||
|
markup_amount: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeSaleFixedMarkupRule(index: number) {
|
||||||
|
salePriceConfigDraft.value.fixed_markup_rules.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function addSaleRatioAdjustmentRule() {
|
||||||
|
salePriceConfigDraft.value.ratio_adjustment_rules.push({
|
||||||
|
min_m: 0,
|
||||||
|
max_m: 0,
|
||||||
|
ratio_subtract: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeSaleRatioAdjustmentRule(index: number) {
|
||||||
|
salePriceConfigDraft.value.ratio_adjustment_rules.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSave() {
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
const value = JSON.stringify(salePriceConfigDraft.value, 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readError(error: unknown, fallback: string) {
|
||||||
|
if (typeof error === 'object' && error && 'response' in error) {
|
||||||
|
const response = (error as { response?: { data?: { message?: string } } }).response
|
||||||
|
return response?.data?.message || fallback
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
:model-value="modelValue"
|
||||||
|
title="编辑出售定价配置"
|
||||||
|
width="920px"
|
||||||
|
@update:model-value="emit('update:modelValue', $event)"
|
||||||
|
>
|
||||||
|
<div class="dialog-body">
|
||||||
|
<p class="config-key-label"><strong>{{ config.key }}</strong></p>
|
||||||
|
|
||||||
|
<div class="publish-options-editor">
|
||||||
|
<div class="editor-toolbar">
|
||||||
|
<span>内部出售定价规则</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="editor-block">
|
||||||
|
<div class="editor-block-title subtle-title">
|
||||||
|
<span>90M 及以下固定加价:出售价格 = 回收价格 + 固定加价</span>
|
||||||
|
<el-button size="small" @click="addSaleFixedMarkupRule">添加固定加价</el-button>
|
||||||
|
</div>
|
||||||
|
<el-table :data="salePriceConfigDraft.fixed_markup_rules" size="small" border>
|
||||||
|
<el-table-column label="最小 M" min-width="120">
|
||||||
|
<template #default="{ row }"><el-input-number v-model="row.min_m" :min="0" :step="10" /></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="最大 M" min-width="120">
|
||||||
|
<template #default="{ row }"><el-input-number v-model="row.max_m" :min="0" :step="10" /></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="加价金额/元" min-width="140">
|
||||||
|
<template #default="{ row }"><el-input-number v-model="row.markup_amount" :min="0" :step="1" /></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="90">
|
||||||
|
<template #default="{ $index }">
|
||||||
|
<el-button size="small" type="danger" plain @click="removeSaleFixedMarkupRule($index)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div class="editor-block-title subtle-title">
|
||||||
|
<span>90M 以上比例修正:出售比例 = 回收比例 - 比例修正;最大 M 为 0 表示无上限</span>
|
||||||
|
<el-button size="small" @click="addSaleRatioAdjustmentRule">添加比例修正</el-button>
|
||||||
|
</div>
|
||||||
|
<el-table :data="salePriceConfigDraft.ratio_adjustment_rules" size="small" border>
|
||||||
|
<el-table-column label="最小 M" min-width="120">
|
||||||
|
<template #default="{ row }"><el-input-number v-model="row.min_m" :min="0" :step="10" /></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="最大 M" min-width="120">
|
||||||
|
<template #default="{ row }"><el-input-number v-model="row.max_m" :min="0" :step="10" /></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="比例修正" min-width="130">
|
||||||
|
<template #default="{ row }"><el-input-number v-model="row.ratio_subtract" :min="0" :step="0.5" /></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="90">
|
||||||
|
<template #default="{ $index }">
|
||||||
|
<el-button size="small" type="danger" plain @click="removeSaleRatioAdjustmentRule($index)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form-item label="配置说明" class="desc-item">
|
||||||
|
<el-input v-model="description" type="textarea" :rows="3" placeholder="配置说明" />
|
||||||
|
</el-form-item>
|
||||||
|
</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: 16px;
|
||||||
|
max-height: 65vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-key-label {
|
||||||
|
margin: 0;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.publish-options-editor {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-toolbar,
|
||||||
|
.editor-block-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-toolbar span {
|
||||||
|
color: #30343a;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-block {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtle-title {
|
||||||
|
margin-top: 6px;
|
||||||
|
color: #4b5563;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc-item {
|
||||||
|
margin-top: 14px;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user