更新配置页面

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-form ref="formRef" :model="formData" :rules="formRules" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
<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 label="配置键" name="key">
<a-input v-model:value="formData.key" :placeholder="isEdit ? '' : '请输入配置键,如: site_name'" :disabled="record?.is_system">
<a-form-item v-if="!isEdit" label="配置键" name="key">
<a-input v-model:value="formData.key" :placeholder="isEdit ? '' : '请输入配置键,如: site_name'">
<template #prefix>
<key-outlined />
</template>
</a-input>
<div v-if="!isEdit" class="form-tip">建议使用小写字母下划线命名</div>
<div class="form-tip">建议使用小写字母下划线命名</div>
</a-form-item>
<a-form-item label="分组" name="group">
<a-select v-model:value="formData.group" placeholder="请选择分组" :options="groupOptions" :disabled="record?.is_system" show-search :filter-option="filterOption" />
<a-form-item v-if="!isEdit" label="分组" name="group">
<a-select v-model:value="formData.group" placeholder="请选择分组" :options="groupOptions" show-search :filter-option="filterOption" />
</a-form-item>
<a-form-item label="配置类型" name="type">
<a-select v-model:value="formData.type" placeholder="请选择配置类型" :disabled="record?.is_system" @change="handleTypeChange">
<a-form-item v-if="!isEdit" label="配置类型" name="type">
<a-select v-model:value="formData.type" placeholder="请选择配置类型" @change="handleTypeChange">
<a-select-option value="string">字符串</a-select-option>
<a-select-option value="text">文本</a-select-option>
<a-select-option value="number">数字</a-select-option>
@@ -32,7 +32,7 @@
</a-select>
</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 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" />
@@ -60,26 +60,26 @@
<span v-else>{{ getDisplayValue(formData.default_value) }}</span>
</a-form-item>
<a-form-item label="验证规则" name="validation">
<a-input v-model:value="formData.validation" placeholder="如: required|email|max:100" :disabled="record?.is_system" />
<div v-if="!isEdit" class="form-tip">可选使用 Laravel 验证规则语法</div>
<a-form-item v-if="!isEdit" label="验证规则" name="validation">
<a-input v-model:value="formData.validation" placeholder="如: required|email|max:100" />
<div class="form-tip">可选使用 Laravel 验证规则语法</div>
</a-form-item>
<a-form-item label="描述" name="description">
<a-textarea v-model:value="formData.description" placeholder="请输入配置描述" :rows="2" />
</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%" />
<div v-if="!isEdit" class="form-tip">数字越小排序越靠前</div>
<div class="form-tip">数字越小排序越靠前</div>
</a-form-item>
<a-form-item label="状态" name="status">
<a-switch v-model:checked="formData.status" checked-children="启用" un-checked-children="禁用" :disabled="record?.is_system" />
<a-form-item v-if="!isEdit" label="状态" name="status">
<a-switch v-model:checked="formData.status" checked-children="启用" un-checked-children="禁用" />
</a-form-item>
<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>
</a-modal>

View File

@@ -7,9 +7,9 @@
<a-button type="primary" size="small" @click="handleAddConfig"> <PlusOutlined /> 新增配置项 </a-button>
</template>
<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)">
<div class="config-input-wrapper" @mouseenter="showEditIcon(config.id)" @mouseleave="hideEditIcon">
<div class="config-input-wrapper">
<!-- 不同类型的配置项 -->
<!-- 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'" />
@@ -51,7 +51,7 @@
</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 />
编辑
</a-button>
@@ -95,7 +95,6 @@ const configs = ref({})
const formData = reactive({})
const jsonStrings = reactive({})
const jsonErrors = reactive({})
const visibleEditIcon = ref(null)
// 是否可以编辑系统配置
const canEditSystem = ref(false)
@@ -122,16 +121,6 @@ const getColumnSpan = (type) => {
return 8
}
// ===== 显示编辑图标 =====
const showEditIcon = (id) => {
visibleEditIcon.value = id
}
// ===== 隐藏编辑图标 =====
const hideEditIcon = () => {
visibleEditIcon.value = null
}
// ===== 加载配置分组 =====
const loadGroups = async () => {
try {
@@ -262,35 +251,40 @@ const handleSave = async () => {
return
}
// 检查是否有需要保存的配置
const configList = Object.values(configs.value).flat()
if (configList.length === 0) {
message.warning('暂无配置项可保存')
return
}
// 构建更新数据
const updates = {}
const updates = []
Object.keys(formData).forEach((key) => {
const config = Object.values(configs.value)
.flat()
.find((c) => c.key === key)
const config = configList.find((c) => c.key === key)
if (config) {
let value = formData[key]
// 转换 checkbox 类型为数组字符串
if (config.type === 'checkbox') {
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 {
saving.value = true
const promises = Object.entries(updates).map(([key, 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 })
})
const promises = updates.map((update) => systemApi.config.edit.put(update.id, { value: update.value }))
await Promise.all(promises)
message.success('保存成功')
// 重新加载配置
@@ -320,7 +314,7 @@ const handleReset = () => {
// ===== 编辑配置项 =====
const handleEditConfig = (config) => {
currentConfig.value = { ...config }
currentConfig.value = config
dialog.save = true
}
@@ -356,6 +350,7 @@ onMounted(async () => {
flex-direction: column;
height: 100%;
padding: 0;
background: #f5f5f5;
.content-wrapper {
flex: 1;
@@ -363,23 +358,63 @@ onMounted(async () => {
display: flex;
background: #fff;
margin: 16px;
padding: 10px;
border-radius: 8px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
.config-tabs {
width: 100%;
display: flex;
:deep(.ant-tabs-content-holder) {
flex: 1;
overflow-y: auto;
flex-direction: column;
: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) {
text-align: left;
padding: 12px 16px;
padding: 12px 20px;
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) {
background-color: #e6f7ff;
:deep(.ant-tabs-ink-bar) {
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 {
padding: 12px 24px;
padding: 16px 24px;
border-top: 1px solid #f0f0f0;
background: #fafafa;
background: #fff;
display: flex;
justify-content: flex-end;
}
align-items: center;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.04);
z-index: 10;
.config-item {
margin-bottom: 16px;
.config-item-wrapper {
padding: 16px;
border: 1px solid #f0f0f0;
.ant-btn {
border-radius: 6px;
transition: all 0.2s;
&:hover {
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;
}
}
}
height: 36px;
padding: 0 24px;
font-weight: 500;
}
}
}
.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>