Files
vueadmin/src/pages/system/setting/index.vue
2026-01-23 22:41:10 +08:00

526 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>