更新配置页面

This commit is contained in:
2026-02-19 12:06:13 +08:00
parent f0f0763ceb
commit f0af965412
2 changed files with 112 additions and 117 deletions

View File

@@ -2,24 +2,24 @@
<a-modal :open="dialogVisible" :title="isEdit ? '编辑配置项' : '新增配置项'" @cancel="handleCancel" :width="600" :confirm-loading="loading" @ok="handleOk"> <a-modal :open="dialogVisible" :title="isEdit ? '编辑配置项' : '新增配置项'" @cancel="handleCancel" :width="600" :confirm-loading="loading" @ok="handleOk">
<a-form ref="formRef" :model="formData" :rules="formRules" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }"> <a-form ref="formRef" :model="formData" :rules="formRules" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
<a-form-item label="配置名称" name="name"> <a-form-item label="配置名称" name="name">
<a-input v-model:value="formData.name" placeholder="请输入配置名称" :disabled="record?.is_system" /> <a-input v-model:value="formData.name" placeholder="请输入配置名称" />
</a-form-item> </a-form-item>
<a-form-item label="配置键" name="key"> <a-form-item v-if="!isEdit" label="配置键" name="key">
<a-input v-model:value="formData.key" :placeholder="isEdit ? '' : '请输入配置键,如: site_name'" :disabled="record?.is_system"> <a-input v-model:value="formData.key" :placeholder="isEdit ? '' : '请输入配置键,如: site_name'">
<template #prefix> <template #prefix>
<key-outlined /> <key-outlined />
</template> </template>
</a-input> </a-input>
<div v-if="!isEdit" class="form-tip">建议使用小写字母下划线命名</div> <div class="form-tip">建议使用小写字母下划线命名</div>
</a-form-item> </a-form-item>
<a-form-item label="分组" name="group"> <a-form-item v-if="!isEdit" label="分组" name="group">
<a-select v-model:value="formData.group" placeholder="请选择分组" :options="groupOptions" :disabled="record?.is_system" show-search :filter-option="filterOption" /> <a-select v-model:value="formData.group" placeholder="请选择分组" :options="groupOptions" show-search :filter-option="filterOption" />
</a-form-item> </a-form-item>
<a-form-item label="配置类型" name="type"> <a-form-item v-if="!isEdit" label="配置类型" name="type">
<a-select v-model:value="formData.type" placeholder="请选择配置类型" :disabled="record?.is_system" @change="handleTypeChange"> <a-select v-model:value="formData.type" placeholder="请选择配置类型" @change="handleTypeChange">
<a-select-option value="string">字符串</a-select-option> <a-select-option value="string">字符串</a-select-option>
<a-select-option value="text">文本</a-select-option> <a-select-option value="text">文本</a-select-option>
<a-select-option value="number">数字</a-select-option> <a-select-option value="number">数字</a-select-option>
@@ -32,7 +32,7 @@
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item v-if="['select', 'radio', 'checkbox'].includes(formData.type)" label="选项配置" name="options"> <a-form-item v-if="!isEdit && ['select', 'radio', 'checkbox'].includes(formData.type)" label="选项配置" name="options">
<div class="options-editor"> <div class="options-editor">
<div v-for="(option, index) in formData.options" :key="index" class="option-item"> <div v-for="(option, index) in formData.options" :key="index" class="option-item">
<a-input v-model:value="option.label" placeholder="选项标签" style="flex: 1; margin-right: 8px" /> <a-input v-model:value="option.label" placeholder="选项标签" style="flex: 1; margin-right: 8px" />
@@ -60,26 +60,26 @@
<span v-else>{{ getDisplayValue(formData.default_value) }}</span> <span v-else>{{ getDisplayValue(formData.default_value) }}</span>
</a-form-item> </a-form-item>
<a-form-item label="验证规则" name="validation"> <a-form-item v-if="!isEdit" label="验证规则" name="validation">
<a-input v-model:value="formData.validation" placeholder="如: required|email|max:100" :disabled="record?.is_system" /> <a-input v-model:value="formData.validation" placeholder="如: required|email|max:100" />
<div v-if="!isEdit" class="form-tip">可选使用 Laravel 验证规则语法</div> <div class="form-tip">可选使用 Laravel 验证规则语法</div>
</a-form-item> </a-form-item>
<a-form-item label="描述" name="description"> <a-form-item label="描述" name="description">
<a-textarea v-model:value="formData.description" placeholder="请输入配置描述" :rows="2" /> <a-textarea v-model:value="formData.description" placeholder="请输入配置描述" :rows="2" />
</a-form-item> </a-form-item>
<a-form-item label="排序" name="sort"> <a-form-item v-if="!isEdit" label="排序" name="sort">
<a-input-number v-model:value="formData.sort" :min="0" :max="9999" style="width: 100%" /> <a-input-number v-model:value="formData.sort" :min="0" :max="9999" style="width: 100%" />
<div v-if="!isEdit" class="form-tip">数字越小排序越靠前</div> <div class="form-tip">数字越小排序越靠前</div>
</a-form-item> </a-form-item>
<a-form-item label="状态" name="status"> <a-form-item v-if="!isEdit" label="状态" name="status">
<a-switch v-model:checked="formData.status" checked-children="启用" un-checked-children="禁用" :disabled="record?.is_system" /> <a-switch v-model:checked="formData.status" checked-children="启用" un-checked-children="禁用" />
</a-form-item> </a-form-item>
<a-form-item v-if="record?.is_system" label="系统配置"> <a-form-item v-if="record?.is_system" label="系统配置">
<a-tag color="orange">系统配置项</a-tag> <a-tag color="orange">系统配置项 - 仅可修改配置值</a-tag>
</a-form-item> </a-form-item>
</a-form> </a-form>
</a-modal> </a-modal>

View File

@@ -7,9 +7,9 @@
<a-button type="primary" size="small" @click="handleAddConfig"> <PlusOutlined /> 新增配置项 </a-button> <a-button type="primary" size="small" @click="handleAddConfig"> <PlusOutlined /> 新增配置项 </a-button>
</template> </template>
<a-tab-pane v-for="group in groups" :key="group.value" :tab="group.label"> <a-tab-pane v-for="group in groups" :key="group.value" :tab="group.label">
<a-form :model="formData"> <a-form :model="formData" :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }">
<a-form-item :label="config.name" v-for="config in configs[group.value]" :key="config.id" :span="getColumnSpan(config.type)"> <a-form-item :label="config.name" v-for="config in configs[group.value]" :key="config.id" :span="getColumnSpan(config.type)">
<div class="config-input-wrapper" @mouseenter="showEditIcon(config.id)" @mouseleave="hideEditIcon"> <div class="config-input-wrapper">
<!-- 不同类型的配置项 --> <!-- 不同类型的配置项 -->
<!-- string/text 类型 --> <!-- string/text 类型 -->
<a-input v-if="['string', 'text'].includes(config.type)" v-model:value="formData[config.key]" :placeholder="config.description" :rows="config.type === 'text' ? 3 : 1" :type="config.type === 'text' ? 'textarea' : 'text'" /> <a-input v-if="['string', 'text'].includes(config.type)" v-model:value="formData[config.key]" :placeholder="config.description" :rows="config.type === 'text' ? 3 : 1" :type="config.type === 'text' ? 'textarea' : 'text'" />
@@ -51,7 +51,7 @@
</div> </div>
<!-- 编辑图标 --> <!-- 编辑图标 -->
<a-button v-show="visibleEditIcon === config.id" type="link" size="small" class="edit-btn" @click="handleEditConfig(config)"> <a-button type="link" size="small" class="edit-btn" @click="handleEditConfig(config)">
<EditOutlined /> <EditOutlined />
编辑 编辑
</a-button> </a-button>
@@ -95,7 +95,6 @@ const configs = ref({})
const formData = reactive({}) const formData = reactive({})
const jsonStrings = reactive({}) const jsonStrings = reactive({})
const jsonErrors = reactive({}) const jsonErrors = reactive({})
const visibleEditIcon = ref(null)
// 是否可以编辑系统配置 // 是否可以编辑系统配置
const canEditSystem = ref(false) const canEditSystem = ref(false)
@@ -122,16 +121,6 @@ const getColumnSpan = (type) => {
return 8 return 8
} }
// ===== 显示编辑图标 =====
const showEditIcon = (id) => {
visibleEditIcon.value = id
}
// ===== 隐藏编辑图标 =====
const hideEditIcon = () => {
visibleEditIcon.value = null
}
// ===== 加载配置分组 ===== // ===== 加载配置分组 =====
const loadGroups = async () => { const loadGroups = async () => {
try { try {
@@ -262,35 +251,40 @@ const handleSave = async () => {
return return
} }
// 检查是否有需要保存的配置
const configList = Object.values(configs.value).flat()
if (configList.length === 0) {
message.warning('暂无配置项可保存')
return
}
// 构建更新数据 // 构建更新数据
const updates = {} const updates = []
Object.keys(formData).forEach((key) => { Object.keys(formData).forEach((key) => {
const config = Object.values(configs.value) const config = configList.find((c) => c.key === key)
.flat()
.find((c) => c.key === key)
if (config) { if (config) {
let value = formData[key] let value = formData[key]
// 转换 checkbox 类型为数组字符串 // 转换 checkbox 类型为数组字符串
if (config.type === 'checkbox') { if (config.type === 'checkbox') {
value = JSON.stringify(value) value = JSON.stringify(value)
} }
updates[key] = value // 转换 file 类型为 JSON 字符串
else if (config.type === 'file' && Array.isArray(value) && value.length > 0) {
value = JSON.stringify(value)
}
updates.push({ id: config.id, value })
} }
}) })
if (updates.length === 0) {
message.warning('暂无配置项需要保存')
return
}
// 批量更新 // 批量更新
try { try {
saving.value = true saving.value = true
const promises = Object.entries(updates).map(([key, value]) => { const promises = updates.map((update) => systemApi.config.edit.put(update.id, { value: update.value }))
const config = Object.values(configs.value)
.flat()
.find((c) => c.key === key)
if (!config || (config.is_system && !canEditSystem.value)) {
return Promise.resolve()
}
return systemApi.config.edit.put(config.id, { value })
})
await Promise.all(promises) await Promise.all(promises)
message.success('保存成功') message.success('保存成功')
// 重新加载配置 // 重新加载配置
@@ -320,7 +314,7 @@ const handleReset = () => {
// ===== 编辑配置项 ===== // ===== 编辑配置项 =====
const handleEditConfig = (config) => { const handleEditConfig = (config) => {
currentConfig.value = { ...config } currentConfig.value = config
dialog.save = true dialog.save = true
} }
@@ -356,6 +350,7 @@ onMounted(async () => {
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
padding: 0; padding: 0;
background: #f5f5f5;
.content-wrapper { .content-wrapper {
flex: 1; flex: 1;
@@ -363,23 +358,63 @@ onMounted(async () => {
display: flex; display: flex;
background: #fff; background: #fff;
margin: 16px; margin: 16px;
padding: 10px; border-radius: 8px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
.config-tabs { .config-tabs {
width: 100%; width: 100%;
display: flex; display: flex;
:deep(.ant-tabs-content-holder) { flex-direction: column;
flex: 1;
overflow-y: auto; :deep(.ant-tabs-nav) {
margin-bottom: 0;
padding: 0 8px;
background: #fafafa;
border-bottom: 1px solid #f0f0f0;
border-radius: 8px 8px 0 0;
} }
:deep(.ant-tabs-tab) { :deep(.ant-tabs-tab) {
text-align: left; padding: 12px 20px;
padding: 12px 16px; font-size: 14px;
font-weight: 500;
transition: all 0.3s;
&:hover {
color: #1890ff;
}
&.ant-tabs-tab-active {
color: #1890ff;
background: #fff;
border-top: 2px solid #1890ff;
}
} }
:deep(.ant-tabs-left > .ant-tabs-nav .ant-tabs-tab-active) { :deep(.ant-tabs-ink-bar) {
background-color: #e6f7ff; display: none;
}
:deep(.ant-tabs-content) {
flex: 1;
overflow: hidden;
display: flex;
}
:deep(.ant-tabs-tabpane) {
height: 100%;
overflow: hidden;
display: flex;
}
:deep(.ant-tabs-content-holder) {
flex: 1;
overflow-y: auto;
padding: 16px;
}
:deep(.ant-form) {
width: 100%;
} }
} }
@@ -389,74 +424,34 @@ onMounted(async () => {
} }
.footer-bar { .footer-bar {
padding: 12px 24px; padding: 16px 24px;
border-top: 1px solid #f0f0f0; border-top: 1px solid #f0f0f0;
background: #fafafa; background: #fff;
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
} align-items: center;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.04);
z-index: 10;
.config-item { .ant-btn {
margin-bottom: 16px;
.config-item-wrapper {
padding: 16px;
border: 1px solid #f0f0f0;
border-radius: 6px; border-radius: 6px;
transition: all 0.2s; height: 36px;
padding: 0 24px;
&:hover { font-weight: 500;
border-color: #d9d9d9;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.config-label {
margin-bottom: 8px;
.label-text {
display: block;
font-size: 14px;
font-weight: 500;
color: #262626;
margin-bottom: 4px;
}
.label-desc {
display: block;
font-size: 12px;
color: #8c8c8c;
}
}
.config-input-wrapper {
position: relative;
.edit-btn {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
padding: 4px 8px;
font-size: 12px;
}
.config-upload {
:deep(.ant-upload-list) {
margin-top: 8px;
}
}
.json-editor-wrapper {
position: relative;
.json-error {
margin-top: 4px;
color: #ff4d4f;
font-size: 12px;
}
}
}
} }
} }
} }
.config-input-wrapper {
display: flex;
align-items: center;
justify-items: center;
.edit-btn {
padding: 4px 8px;
font-size: 12px;
background: rgba(255, 255, 255, 0.95);
border-radius: 4px;
}
}
</style> </style>