优化后端可以编辑皮肤等等
This commit is contained in:
@@ -1,7 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
import {
|
||||
defaultListingPublishOptions,
|
||||
mergeListingPublishOptions,
|
||||
type ListingPublishOptions,
|
||||
} from '@/api/listingOptions'
|
||||
import { fetchSystemConfigs, updateSystemConfig, type SystemConfig } from '@/api/systemConfigs'
|
||||
|
||||
const loading = ref(false)
|
||||
@@ -10,6 +15,32 @@ const configs = ref<SystemConfig[]>([])
|
||||
const activeConfig = ref<SystemConfig | null>(null)
|
||||
const value = ref('')
|
||||
const description = ref('')
|
||||
const publishOptionsDraft = ref<ListingPublishOptions>(cloneOptions(defaultListingPublishOptions))
|
||||
|
||||
const publishConfig = computed(() => configs.value.find((item) => item.key === 'listing.publish_options') || null)
|
||||
const regularConfigs = computed(() => configs.value.filter((item) => item.key !== 'listing.publish_options'))
|
||||
const isPublishOptionsConfig = computed(() => activeConfig.value?.key === 'listing.publish_options')
|
||||
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 isStructuredConfig = computed(() => {
|
||||
const trimmed = value.value.trim()
|
||||
return isPublishOptionsConfig.value || trimmed.startsWith('{') || trimmed.startsWith('[')
|
||||
})
|
||||
|
||||
const dialogWidth = computed(() => (isPublishOptionsConfig.value ? '920px' : '560px'))
|
||||
|
||||
onMounted(loadConfigs)
|
||||
|
||||
@@ -26,12 +57,18 @@ function openEdit(row: SystemConfig) {
|
||||
activeConfig.value = row
|
||||
value.value = row.value
|
||||
description.value = row.description
|
||||
if (row.key === 'listing.publish_options') {
|
||||
publishOptionsDraft.value = parsePublishOptions(row.value)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!activeConfig.value) return
|
||||
submitting.value = true
|
||||
try {
|
||||
if (isPublishOptionsConfig.value) {
|
||||
value.value = JSON.stringify(publishOptionsDraft.value, null, 2)
|
||||
}
|
||||
await updateSystemConfig(activeConfig.value.key, {
|
||||
value: value.value,
|
||||
description: description.value,
|
||||
@@ -46,6 +83,100 @@ async function handleSave() {
|
||||
}
|
||||
}
|
||||
|
||||
function parsePublishOptions(raw: string) {
|
||||
try {
|
||||
const parsed = raw.trim() ? JSON.parse(raw) : defaultListingPublishOptions
|
||||
return cloneOptions(mergeListingPublishOptions(parsed))
|
||||
} catch {
|
||||
ElMessage.warning('发布选项 JSON 解析失败,已使用默认配置')
|
||||
return cloneOptions(defaultListingPublishOptions)
|
||||
}
|
||||
}
|
||||
|
||||
function safeParsePublishOptions(raw: string) {
|
||||
try {
|
||||
const parsed = raw.trim() ? JSON.parse(raw) : defaultListingPublishOptions
|
||||
return cloneOptions(mergeListingPublishOptions(parsed))
|
||||
} catch {
|
||||
return cloneOptions(defaultListingPublishOptions)
|
||||
}
|
||||
}
|
||||
|
||||
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'>, 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()}` as never,
|
||||
label: '',
|
||||
price: '',
|
||||
})
|
||||
}
|
||||
|
||||
function removeQuantityItem(index: number) {
|
||||
publishOptionsDraft.value.quantity_items.splice(index, 1)
|
||||
}
|
||||
|
||||
function addScreenshotSlot() {
|
||||
publishOptionsDraft.value.screenshot_slots.push({
|
||||
key: `screenshot_${Date.now()}` as never,
|
||||
label: '',
|
||||
required: false,
|
||||
hint: '',
|
||||
})
|
||||
}
|
||||
|
||||
function removeScreenshotSlot(index: number) {
|
||||
publishOptionsDraft.value.screenshot_slots.splice(index, 1)
|
||||
}
|
||||
|
||||
function resetPublishOptions() {
|
||||
publishOptionsDraft.value = cloneOptions(defaultListingPublishOptions)
|
||||
}
|
||||
|
||||
function formatConfigValue(row: SystemConfig) {
|
||||
const trimmed = row.value.trim()
|
||||
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
||||
return 'JSON 配置'
|
||||
}
|
||||
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
|
||||
@@ -63,9 +194,47 @@ function readError(error: unknown, fallback: string) {
|
||||
<p>管理交接超时、归还超时、短信限流、最低押金和抽成比例。</p>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" class="table-panel" :data="configs">
|
||||
<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>
|
||||
|
||||
<el-table v-loading="loading" class="table-panel" :data="regularConfigs">
|
||||
<el-table-column prop="key" label="配置项" min-width="260" />
|
||||
<el-table-column prop="value" label="当前值" width="150" />
|
||||
<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 prop="updated_at" label="更新时间" min-width="180" />
|
||||
@@ -76,10 +245,157 @@ function readError(error: unknown, fallback: string) {
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog :model-value="!!activeConfig" title="编辑系统配置" width="560px" @update:model-value="activeConfig = null">
|
||||
<el-dialog :model-value="!!activeConfig" title="编辑系统配置" :width="dialogWidth" @update:model-value="activeConfig = null">
|
||||
<div v-if="activeConfig" class="dialog-body">
|
||||
<p><strong>{{ activeConfig.key }}</strong></p>
|
||||
<el-input v-model="value" placeholder="配置值" />
|
||||
|
||||
<div v-if="isPublishOptionsConfig" class="publish-options-editor">
|
||||
<div class="editor-toolbar">
|
||||
<span>发布页选项配置</span>
|
||||
<el-button size="small" @click="resetPublishOptions">恢复默认选项</el-button>
|
||||
</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-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-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>
|
||||
@@ -89,3 +405,101 @@ function readError(error: unknown, fallback: string) {
|
||||
</el-dialog>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.publish-config-panel {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
margin-bottom: 18px;
|
||||
padding: 20px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.publish-config-main {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.publish-config-main h2 {
|
||||
margin: 4px 0 6px;
|
||||
color: #111827;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.publish-config-main span {
|
||||
color: #6b7280;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.publish-stat-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.publish-stat {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.publish-stat strong {
|
||||
color: #1477ff;
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.publish-stat span,
|
||||
.config-value {
|
||||
color: #4b5563;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.skin-config-row {
|
||||
display: grid;
|
||||
grid-template-columns: 140px 180px minmax(0, 1fr) 72px;
|
||||
gap: 8px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.publish-stat-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user