526 lines
14 KiB
Vue
526 lines
14 KiB
Vue
<template>
|
||
<div class="pages system-setting">
|
||
|
||
<!-- 主要内容区域 -->
|
||
<div class="page-content">
|
||
<a-tabs v-model:activeKey="activeTab" @change="handleTabChange" class="setting-tabs">
|
||
<template #rightExtra>
|
||
<a-button type="primary" @click="handleAddConfig">
|
||
<template #icon>
|
||
<PlusOutlined />
|
||
</template>
|
||
新增配置
|
||
</a-button>
|
||
</template>
|
||
<a-tab-pane v-for="category in categories" :key="category.name" :tab="category.title">
|
||
<a-form :label-col="{ span: 4 }" :wrapper-col="{ span: 16 }" class="setting-form">
|
||
<a-form-item v-for="field in fields.filter(f => f.category === category.name)"
|
||
:key="field.name" :label="field.title" :required="field.required">
|
||
<div class="form-item-content">
|
||
<div class="form-input-wrapper">
|
||
<!-- 文本输入 -->
|
||
<a-input v-if="field.type === 'text'" v-model:value="formData[field.name]"
|
||
:placeholder="field.placeholder || '请输入'" allow-clear />
|
||
<!-- 文本域 -->
|
||
<a-textarea v-else-if="field.type === 'textarea'"
|
||
v-model:value="formData[field.name]"
|
||
:placeholder="field.placeholder || '请输入'" :rows="4"
|
||
:maxlength="field.maxLength" :show-count="field.maxLength > 0"
|
||
allow-clear />
|
||
<!-- 数字输入 -->
|
||
<a-input-number v-else-if="field.type === 'number'"
|
||
v-model:value="formData[field.name]"
|
||
:placeholder="field.placeholder || '请输入'" :min="field.min" :max="field.max"
|
||
:precision="field.precision || 0" style="width: 100%" />
|
||
<!-- 开关 -->
|
||
<a-switch v-else-if="field.type === 'switch'"
|
||
v-model:checked="formData[field.name]" :checked-children="启用"
|
||
:un-checked-children="禁用" />
|
||
<!-- 下拉选择 -->
|
||
<a-select v-else-if="field.type === 'select'"
|
||
v-model:value="formData[field.name]"
|
||
:placeholder="field.placeholder || '请选择'" style="width: 100%" allow-clear>
|
||
<a-select-option v-for="option in field.options" :key="option.value"
|
||
:value="option.value">
|
||
{{ option.label }}
|
||
</a-select-option>
|
||
</a-select>
|
||
<!-- 多选 -->
|
||
<a-select v-else-if="field.type === 'multiselect'"
|
||
v-model:value="formData[field.name]"
|
||
:placeholder="field.placeholder || '请选择'" mode="multiple"
|
||
style="width: 100%" allow-clear>
|
||
<a-select-option v-for="option in field.options" :key="option.value"
|
||
:value="option.value">
|
||
{{ option.label }}
|
||
</a-select-option>
|
||
</a-select>
|
||
<!-- 日期时间 -->
|
||
<a-date-picker v-else-if="field.type === 'datetime'"
|
||
v-model:value="formData[field.name]"
|
||
:placeholder="field.placeholder || '请选择'" style="width: 100%" show-time
|
||
format="YYYY-MM-DD HH:mm:ss" allow-clear />
|
||
<!-- 颜色选择器 -->
|
||
<div v-else-if="field.type === 'color'" class="color-picker-wrapper">
|
||
<div class="color-preview"
|
||
:style="{ backgroundColor: formData[field.name] }"></div>
|
||
<a-input v-model:value="formData[field.name]"
|
||
placeholder="请输入颜色值(如:#ff0000)" allow-clear class="color-text" />
|
||
</div>
|
||
<!-- 图片上传 -->
|
||
<div v-else-if="field.type === 'image'" class="image-uploader-wrapper">
|
||
<sc-upload v-model="formData[field.name]" :max-count="1" :tip="field.tip"
|
||
upload-text="上传图片" />
|
||
</div>
|
||
<!-- 默认文本输入 -->
|
||
<a-input v-else v-model:value="formData[field.name]"
|
||
:placeholder="field.placeholder || '请输入'" allow-clear />
|
||
</div>
|
||
<div class="form-actions">
|
||
<a-tooltip title="编辑配置项">
|
||
<EditOutlined class="action-icon edit-icon"
|
||
@click="handleEditField(field)" />
|
||
</a-tooltip>
|
||
</div>
|
||
</div>
|
||
<div v-if="field.tip" class="field-tip">
|
||
<InfoCircleOutlined class="tip-icon" />
|
||
{{ field.tip }}
|
||
</div>
|
||
</a-form-item>
|
||
<a-form-item :wrapper-col="{ offset: 4, span: 16 }">
|
||
<div style="display: flex; gap: 10px;">
|
||
<a-button type="primary" size="large" :loading="saving" @click="handleSave">
|
||
<template #icon>
|
||
<SaveOutlined />
|
||
</template>
|
||
保存配置
|
||
</a-button>
|
||
<a-button size="large" @click="handleReset">
|
||
<template #icon>
|
||
<RedoOutlined />
|
||
</template>
|
||
重置
|
||
</a-button>
|
||
</div>
|
||
</a-form-item>
|
||
</a-form>
|
||
|
||
<!-- 空状态 -->
|
||
<a-empty v-if="fields.filter(f => f.category === category.name).length === 0"
|
||
description="暂无配置项">
|
||
<a-button type="primary" @click="handleAddConfig">
|
||
<template #icon>
|
||
<PlusOutlined />
|
||
</template>
|
||
添加配置
|
||
</a-button>
|
||
</a-empty>
|
||
</a-tab-pane>
|
||
</a-tabs>
|
||
</div>
|
||
|
||
<!-- 配置弹窗 -->
|
||
<ConfigModal v-model:visible="modalVisible" :is-edit="isEditMode" :categories="categories" :initial-data="currentEditData" @confirm="handleModalConfirm" />
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive, onMounted } from 'vue'
|
||
import { message } from 'ant-design-vue'
|
||
import scUpload from '@/components/scUpload/index.vue'
|
||
import systemApi from '@/api/system'
|
||
import ConfigModal from './components/ConfigModal.vue'
|
||
|
||
defineOptions({
|
||
name: 'SystemSetting'
|
||
})
|
||
|
||
const activeTab = ref('basic')
|
||
const saving = ref(false)
|
||
|
||
// 配置分类
|
||
const categories = ref([])
|
||
|
||
// 配置字段
|
||
const fields = ref([])
|
||
|
||
// 表单数据
|
||
const formData = reactive({})
|
||
|
||
// 弹窗相关
|
||
const modalVisible = ref(false)
|
||
const isEditMode = ref(false)
|
||
const currentEditData = ref({})
|
||
|
||
// 获取配置字段
|
||
const fetchFields = async () => {
|
||
try {
|
||
const res = await systemApi.setting.fields.get()
|
||
if (res.code === 1) {
|
||
const configData = res.data || []
|
||
|
||
// 根据 group 字段提取分类
|
||
const groupMap = new Map()
|
||
configData.forEach((item) => {
|
||
if (item.group && !groupMap.has(item.group)) {
|
||
const groupTitles = {
|
||
base: '基础设置',
|
||
upload: '上传设置',
|
||
email: '邮件设置',
|
||
sms: '短信设置',
|
||
security: '安全设置'
|
||
}
|
||
groupMap.set(item.group, {
|
||
name: item.group,
|
||
title: groupTitles[item.group] || item.group
|
||
})
|
||
}
|
||
})
|
||
categories.value = Array.from(groupMap.values())
|
||
|
||
// 将配置项转换为前端需要的格式
|
||
fields.value = configData.map((item) => ({
|
||
id: item.id,
|
||
name: item.name,
|
||
title: item.title || item.label,
|
||
value: item.values,
|
||
type: mapFieldType(item.type),
|
||
category: item.group,
|
||
placeholder: item.options?.placeholder || item.remark,
|
||
tip: item.remark,
|
||
required: item.required || false,
|
||
options: mapFieldOptions(item.type, item.options?.options || []),
|
||
sort: item.sort,
|
||
min: item.options?.min,
|
||
max: item.options?.max,
|
||
precision: item.options?.precision,
|
||
maxLength: item.options?.maxLength
|
||
}))
|
||
|
||
// 初始化表单数据
|
||
fields.value.forEach((field) => {
|
||
if (field.type === 'number') {
|
||
formData[field.name] = field.value ? Number(field.value) : 0
|
||
} else if (field.type === 'switch') {
|
||
formData[field.name] = field.value === '1' || field.value === true
|
||
} else if (field.type === 'multiselect') {
|
||
formData[field.name] = field.value ? (Array.isArray(field.value) ? field.value : field.value.split(',')) : []
|
||
} else {
|
||
formData[field.name] = field.value || ''
|
||
}
|
||
})
|
||
|
||
// 设置第一个 tab 为默认激活
|
||
if (categories.value.length > 0) {
|
||
activeTab.value = categories.value[0].name
|
||
}
|
||
}
|
||
} catch (error) {
|
||
message.error('获取配置字段失败')
|
||
console.error('获取配置字段失败:', error)
|
||
}
|
||
}
|
||
|
||
// 映射字段类型
|
||
const mapFieldType = (backendType) => {
|
||
const typeMap = {
|
||
string: 'text',
|
||
text: 'text',
|
||
textarea: 'textarea',
|
||
number: 'number',
|
||
boolean: 'switch',
|
||
switch: 'switch',
|
||
select: 'select',
|
||
radio: 'select',
|
||
multiselect: 'multiselect',
|
||
checkbox: 'multiselect',
|
||
datetime: 'datetime',
|
||
date: 'datetime',
|
||
color: 'color',
|
||
image: 'image',
|
||
file: 'file'
|
||
}
|
||
return typeMap[backendType] || 'text'
|
||
}
|
||
|
||
// 映射字段选项
|
||
const mapFieldOptions = (type, options) => {
|
||
if (options && options.length > 0) {
|
||
return options.map((opt) => ({
|
||
label: opt.label || opt.name || opt,
|
||
value: opt.value || opt.key || opt
|
||
}))
|
||
}
|
||
return []
|
||
}
|
||
|
||
// 切换 Tab
|
||
const handleTabChange = (key) => {
|
||
activeTab.value = key
|
||
}
|
||
|
||
// 添加配置
|
||
const handleAddConfig = () => {
|
||
isEditMode.value = false
|
||
currentEditData.value = {
|
||
category: activeTab.value,
|
||
name: '',
|
||
title: '',
|
||
type: 'text',
|
||
value: '',
|
||
tip: '',
|
||
required: false
|
||
}
|
||
modalVisible.value = true
|
||
}
|
||
|
||
// 编辑字段
|
||
const handleEditField = (field) => {
|
||
isEditMode.value = true
|
||
currentEditData.value = {
|
||
id: field.id,
|
||
category: field.category,
|
||
name: field.name,
|
||
title: field.title,
|
||
type: field.type,
|
||
value: formData[field.name],
|
||
tip: field.tip || '',
|
||
remark: field.tip || '',
|
||
placeholder: field.placeholder,
|
||
options: field.options || [],
|
||
required: field.required || false
|
||
}
|
||
modalVisible.value = true
|
||
}
|
||
|
||
// 弹窗确认处理
|
||
const handleModalConfirm = async (values) => {
|
||
try {
|
||
let res
|
||
if (isEditMode.value) {
|
||
// 编辑模式
|
||
res = await systemApi.setting.edit.post({
|
||
id: currentEditData.value.id,
|
||
...values,
|
||
name: currentEditData.value.name
|
||
})
|
||
if (res.code === 1) {
|
||
message.success('编辑成功')
|
||
modalVisible.value = false
|
||
|
||
// 更新表单数据
|
||
formData[currentEditData.value.name] = values.value
|
||
|
||
// 更新字段信息
|
||
const fieldIndex = fields.value.findIndex((f) => f.name === currentEditData.value.name)
|
||
if (fieldIndex > -1) {
|
||
fields.value[fieldIndex].title = values.title
|
||
fields.value[fieldIndex].value = values.value
|
||
fields.value[fieldIndex].tip = values.tip || values.remark
|
||
fields.value[fieldIndex].placeholder = values.placeholder
|
||
}
|
||
}
|
||
} else {
|
||
// 添加模式
|
||
res = await systemApi.setting.add.post(values)
|
||
if (res.code === 1) {
|
||
message.success('添加成功')
|
||
modalVisible.value = false
|
||
// 重新获取配置字段
|
||
await fetchFields()
|
||
}
|
||
}
|
||
} catch (error) {
|
||
if (error.errorFields) {
|
||
return // 表单验证错误
|
||
}
|
||
message.error(isEditMode.value ? '编辑失败' : '添加失败')
|
||
console.error(isEditMode.value ? '编辑配置失败:' : '添加配置失败:', error)
|
||
}
|
||
}
|
||
|
||
// 保存配置
|
||
const handleSave = async () => {
|
||
try {
|
||
saving.value = true
|
||
// 处理多选和开关类型的值
|
||
const saveData = {}
|
||
Object.keys(formData).forEach((key) => {
|
||
const field = fields.value.find((f) => f.name === key)
|
||
if (field) {
|
||
if (field.type === 'multiselect') {
|
||
saveData[key] = Array.isArray(formData[key]) ? formData[key].join(',') : formData[key]
|
||
} else if (field.type === 'switch') {
|
||
saveData[key] = formData[key] ? '1' : '0'
|
||
} else {
|
||
saveData[key] = formData[key]
|
||
}
|
||
}
|
||
})
|
||
|
||
const res = await systemApi.setting.save.post(saveData)
|
||
if (res.code === 1) {
|
||
message.success('保存成功')
|
||
}
|
||
} catch (error) {
|
||
message.error('保存失败')
|
||
console.error('保存配置失败:', error)
|
||
} finally {
|
||
saving.value = false
|
||
}
|
||
}
|
||
|
||
// 重置配置
|
||
const handleReset = () => {
|
||
fields.value.forEach((field) => {
|
||
if (field.type === 'number') {
|
||
formData[field.name] = field.value ? Number(field.value) : 0
|
||
} else if (field.type === 'switch') {
|
||
formData[field.name] = field.value === '1' || field.value === true
|
||
} else if (field.type === 'multiselect') {
|
||
formData[field.name] = field.value ? (Array.isArray(field.value) ? field.value : field.value.split(',')) : []
|
||
} else {
|
||
formData[field.name] = field.value || ''
|
||
}
|
||
})
|
||
message.info('已重置')
|
||
}
|
||
|
||
onMounted(() => {
|
||
fetchFields()
|
||
})
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.system-setting {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
overflow: hidden;
|
||
background: #f5f5f5;
|
||
|
||
.page-content {
|
||
flex: 1;
|
||
overflow: hidden;
|
||
padding: 16px;
|
||
|
||
.setting-tabs {
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background: #ffffff;
|
||
padding: 0 10px;
|
||
border-radius: 10px;
|
||
|
||
:deep(.ant-tabs-tab-btn) {
|
||
padding: 0 15px;
|
||
}
|
||
|
||
:deep(.ant-tabs-nav) {
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
:deep(.ant-tabs-content) {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding-right: 8px;
|
||
|
||
&::-webkit-scrollbar {
|
||
width: 6px;
|
||
}
|
||
|
||
&::-webkit-scrollbar-thumb {
|
||
background: #d9d9d9;
|
||
border-radius: 3px;
|
||
|
||
&:hover {
|
||
background: #bfbfbf;
|
||
}
|
||
}
|
||
|
||
&::-webkit-scrollbar-track {
|
||
background: transparent;
|
||
}
|
||
}
|
||
|
||
:deep(.ant-tabs-tabpane) {
|
||
height: 100%;
|
||
overflow-y: auto;
|
||
}
|
||
}
|
||
|
||
.setting-form {
|
||
.form-item-content {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 12px;
|
||
|
||
.form-input-wrapper {
|
||
flex: 1;
|
||
}
|
||
|
||
.form-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
padding-top: 4px;
|
||
|
||
.action-icon {
|
||
font-size: 16px;
|
||
color: #8c8c8c;
|
||
cursor: pointer;
|
||
transition: color 0.2s;
|
||
|
||
&:hover {
|
||
color: #1890ff;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.field-tip {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 4px;
|
||
margin-top: 6px;
|
||
font-size: 12px;
|
||
color: #8c8c8c;
|
||
line-height: 1.5;
|
||
|
||
.tip-icon {
|
||
margin-top: 2px;
|
||
flex-shrink: 0;
|
||
}
|
||
}
|
||
|
||
.image-uploader-wrapper {
|
||
width: 100%;
|
||
}
|
||
|
||
.color-picker-wrapper {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
|
||
.color-preview {
|
||
width: 40px;
|
||
height: 32px;
|
||
border: 1px solid #d9d9d9;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
padding: 2px;
|
||
|
||
&:hover {
|
||
border-color: #409eff;
|
||
}
|
||
}
|
||
|
||
.color-text {
|
||
flex: 1;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style>
|