上线系统配置功能

This commit is contained in:
zhangtao
2025-01-09 10:36:43 +08:00
parent 5aea8479d0
commit 4e15f2ad3d
39 changed files with 529 additions and 502 deletions
+13 -6
View File
@@ -24,19 +24,26 @@ export function createConfig(body) {
});
}
export function updateConfig(body) {
export function batchConfig(body) {
return request({
url: "/api/v1/system/config/update",
url: "/api/v1/system/config/batch",
method: "put",
data: body,
});
}
export function deleteConfig(params) {
export function uploadFile(body) {
return request({
url: "/api/v1/system/config/delete",
method: "delete",
params: params,
url: "/api/v1/system/config/upload",
method: "post",
data: body,
headers: { "Content-Type": "multipart/form-data" },
});
}
export function getInitConfig() {
return request({
url: "/api/v1/system/config/init",
method: "get",
});
}
+31 -4
View File
@@ -11,8 +11,8 @@
>
<!-- Logo 区域 -->
<div class="logo-container">
<a-image src="/logo.png" :preview="false" :width="28" :height="28" />
<h1 class="logo-title" v-show="!menuState.collapsed">fastapi-vue-admin</h1>
<a-image :src="initConfigState.web_favicon" :preview="false" :width="28" :height="28" />
<h1 class="logo-title" v-show="!menuState.collapsed">{{ initConfigState.web_title }}</h1>
</div>
<a-menu
@@ -147,6 +147,7 @@
<script lang="ts" setup>
import { reactive, computed, watch, onMounted, inject, type Ref } from "vue";
import type { MenuProps, ItemType } from "ant-design-vue";
import { message } from 'ant-design-vue';
import { useRouter, useRoute } from "vue-router";
import storage from 'store';
import store from '@/store';
@@ -166,6 +167,7 @@ import {
} from '@ant-design/icons-vue';
import { logout } from '@/api/system/auth';
import { getNoticeList } from '@/api/system/notice'
import { getInitConfig } from "@/api/system/config"
// 通知公告获取
const dataSource = reactive({
@@ -327,13 +329,38 @@ const noticeState = reactive({
});
const initConfigState = reactive({
web_title: '',
web_favicon: '',
});
const initConfig = () => {
getInitConfig()
.then(response => {
const { status_code, data } = response.data;
if (status_code === 200) {
const configData = JSON.parse(data);
configData.forEach(item => {
if (item.fied_key === 'web_title') {
initConfigState.web_title = item.fied_value;
} else if (item.fied_key === 'web_favicon') {
initConfigState.web_favicon = item.fied_value;
}
});
}
})
.catch(error => {
message.error('获取系统配置失败');
console.error(error); // 打印错误信息以便调试
});
};
// 在页面加载时获取通知列表
onMounted(() => {
initMenu();
handleNoticeList();
initConfig();
});
</script>
<style lang="scss" scoped>
+3 -1
View File
@@ -74,6 +74,7 @@
:loading="loading"
:dataSource="cacheNames"
:pagination="false"
:scroll="{ y: 600 }"
rowKey="cache_name"
>
<a-table-column key="cache_name" title="缓存名称" align="center" :ellipsis="true">
@@ -109,6 +110,7 @@
:loading="subLoading"
:dataSource="cacheKeys.map(key => ({ cacheKey: key }))"
:pagination="false"
:scroll="{ y: 600 }"
rowKey="cacheKey"
>
<a-table-column key="cacheKey" title="缓存键名" align="center" :ellipsis="true">
@@ -147,7 +149,7 @@
<a-form-item label="缓存内容:" name="cache_value">
<a-textarea
v-model:value="cacheForm.cache_value"
:rows="8"
:rows="19"
readonly
/>
</a-form-item>
+89 -11
View File
@@ -1,15 +1,15 @@
<template>
<a-layout>
<!-- 页面主体 -->
<div class="container">
<div class="container" :style="{ backgroundImage: `url(${initConfigState.login_background})` }">
<a-layout-content :style="contentStyle">
<div class="header">
<div class="logo">
<a-image src="/logo.png" :preview="false" />
<a-image :src="initConfigState.login_logo" :preview="false" />
</div>
<div class="title">FastAPI Vue Admin</div>
<div class="title">{{ initConfigState.login_title }}</div>
</div>
<div class="desc">FastAPI Vue Admin 是完全开源的权限管理系统</div>
<div class="desc">{{ initConfigState.login_description }}</div>
<div class="login-main" style="width: 330px; margin: 0 auto;">
<a-tabs centered>
@@ -62,13 +62,28 @@
<!-- 页面底部 -->
<a-layout-footer :style="footerStyle">
<div class="footer-copyright">
<a-button type="link" href="https://gitee.com/tao__tao/fastapi_vue_admin.git">
<icon-font type="icon-copyright" :style="{ fontSize: '16px' }" />
Copyright © 2024 insistence.tech All Rights Reserved. 皖ICP备2023021369号-1
</a-button>
<div class="footer-content">
<div class="footer-links">
<a-button type="link" :href="initConfigState.code_url">
<icon-font type="icon-copyright" :style="{ fontSize: '16px' }" />
{{ initConfigState.copyright }} |
</a-button>
<a-button type="link" :href="initConfigState.help_url">
{{ initConfigState.help_name }} |
</a-button>
<a-button type="link" :href="initConfigState.privacy_url">
{{ initConfigState.privacy_name }} |
</a-button>
<a-button type="link" :href="initConfigState.clause_url">
{{ initConfigState.clause_name }}
</a-button>
</div>
<div class="footer-record">
{{ initConfigState.keep_record }}
</div>
</div>
</a-layout-footer>
</div>
<!-- 弹窗区域 -->
@@ -146,6 +161,7 @@ import { save_token } from "@/utils/util"
import { message } from 'ant-design-vue';
import { login, getCaptcha } from "@/api/system/auth"
import { registerUser, forgetPassword } from "@/api/system/user"
import { getInitConfig } from "@/api/system/config"
import type { LoginForm, CaptchaState, ForgetPasswordForm, RegisterForm } from './types'
const router = useRouter();
@@ -271,7 +287,70 @@ const requestCaptcha = () => {
.catch(() => captchaState.enable = false);
};
onMounted(requestCaptcha);
const initConfigState = reactive({
login_title: '',
login_description: '/logo.png',
login_logo: '',
login_background: '/background.png',
copyright: '',
copyright_name: '',
keep_record: '',
keep_record_name: '',
help_url: '',
help_name: '',
privacy_url: '',
privacy_name: '',
clause_url: '',
clause_name: '',
code_url: '',
});
const initConfig = () => {
getInitConfig()
.then(response => {
const { status_code, data } = response.data;
if (status_code === 200) {
const configData = JSON.parse(data);
configData.forEach(item => {
if (item.fied_key === 'login_title') {
initConfigState.login_title = item.fied_value;
} else if (item.fied_key === 'login_description') {
initConfigState.login_description = item.fied_value;
} else if (item.fied_key === 'login_logo') {
initConfigState.login_logo = item.fied_value;
} else if (item.fied_key === 'login_background') {
initConfigState.login_background = item.fied_value;
} else if (item.fied_key === 'copyright') {
initConfigState.copyright = item.fied_value;
initConfigState.copyright_name = item.name;
} else if (item.fied_key === 'keep_record') {
initConfigState.keep_record = item.fied_value;
initConfigState.keep_record_name = item.name;
} else if (item.fied_key === 'help_url') {
initConfigState.help_url = item.fied_value;
initConfigState.help_name = item.name;
} else if (item.fied_key === 'privacy_url') {
initConfigState.privacy_url = item.fied_value;
initConfigState.privacy_name = item.name;
} else if (item.fied_key === 'clause_url') {
initConfigState.clause_url = item.fied_value;
initConfigState.clause_name = item.name;
} else if (item.fied_key === 'code_url') {
initConfigState.code_url = item.fied_value;
}
});
}
})
.catch(error => {
message.error('获取系统配置失败');
console.error(error); // 打印错误信息以便调试
});
};
onMounted(() => {
requestCaptcha();
initConfig();
});
</script>
<style lang="scss" scoped>
@@ -282,7 +361,6 @@ onMounted(requestCaptcha);
}
.container {
background-image: url("/background.png");
background-size: 100% 100%;
.desc {
+160 -357
View File
@@ -1,376 +1,179 @@
<template>
<div>
<!-- 页面头部 -->
<page-header />
<div>
<!-- 页面头部 -->
<page-header />
<!-- 搜索表单 -->
<div class="tree-search-wrapper">
<a-card :bordered="false">
<a-form :model="queryState" @finish="onFinish">
<a-row>
<a-col flex="0 1 450px">
<a-form-item name="name" label="名称" style="max-width: 300px;">
<a-input v-model:value="queryState.name" placeholder="请输入名称" allowClear />
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col>
<a-space>
<a-button type="primary" html-type="submit" :loading="tableLoading">查询</a-button>
<a-button @click="resetFields">重置</a-button>
</a-space>
</a-col>
</a-row>
</a-form>
</a-card>
<!-- 配置项编辑区域 -->
<div class="config-edit-wrapper">
<a-card title="系统配置" :bordered="false">
<!-- 动态生成配置项 -->
<template v-for="group in updateState" :key="group.id">
<a-card :title="group.name" :bordered="false" style="margin-bottom: 24px;">
<a-row :gutter="16">
<template v-for="config in group.children" :key="config.id">
<a-col :span="12">
<!-- 图片上传类型 -->
<a-form-item v-if="config.fied_key === 'web_favicon' || config.fied_key === 'login_logo' || config.fied_key === 'login_background'" :label="config.name">
<a-upload
v-model:file-list="config.fileList"
list-type="picture-card"
:before-upload="beforeUpload"
:custom-request="(options) => handleUpload(options, config)"
>
<div v-if="!config.fileList || config.fileList.length === 0">
<plus-outlined />
<div style="margin-top: 8px">上传图片</div>
</div>
</a-upload>
</a-form-item>
<!-- 输入框类型 -->
<a-form-item v-else :label="config.name">
<a-input v-model:value="config.fied_value" :placeholder="`请输入${config.name}`" allowClear />
</a-form-item>
</a-col>
</template>
</a-row>
</a-card>
</template>
<!-- 保存按钮 -->
<div style="text-align: right; margin-top: 24px;">
<a-button type="primary" @click="handleSave">保存</a-button>
</div>
<!-- 表格区域 -->
<div class="table-wrapper">
<a-card title="配置列表" :bordered="false" :headStyle="{ borderBottom: 'none', padding: '20px 24px' }"
:bodyStyle="{ padding: '0 24px', minHeight: 'calc(100vh - 400px)' }">
<template #extra>
<a-space>
<a-button type="primary" :icon="h(PlusOutlined)" @click="modalHandle('create')">新建</a-button>
<a-button type="primary" @click="openSystemConfigModal">系统配置</a-button>
</a-space>
</template>
<!-- 树形表格 -->
<a-table :rowKey="record => record.id"
:columns="columns"
:data-source="dataSource"
:loading="tableLoading"
:scroll="{ x: 500, y: 'calc(100vh - 500px)' }"
:row-selection="rowSelection"
:pagination="false"
:style="{ minHeight: '500px' }">
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'action'">
<a-space size="middle">
<a @click="modalHandle('view', record)">查看</a>
<a @click="modalHandle('update', record)">修改</a>
<a-popconfirm title="确定要删除吗?" ok-text="确定" cancel-text="取消"
@confirm="deleteRow(record)">
<a style="color: red;">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
</a-card>
</div>
<!-- 系统配置弹窗 -->
<a-modal v-model:open="systemConfigModalVisible" title="系统配置" :width="800" :destroyOnClose="true"
:confirmLoading="systemConfigModalLoading" @ok="handleSystemConfigSubmit">
<!-- 基础配置部分 -->
<a-card title="基础配置" :bordered="false" style="margin-bottom: 16px;">
<a-form ref="systemConfigForm" :model="systemConfigState" :label-col="{ span: 5 }"
:wrapper-col="{ span: 15 }">
<!-- 基础配置的顶级配置项 -->
<a-form-item v-for="config in systemConfigState.filter(item => item.fied_key === 'base')"
:key="config.id" :name="config.id" :label="config.name"
:rules="[{ required: true, message: `请输入${config.name}` }]">
<a-input v-model:value="config.fied_value" :placeholder="`请输入${config.name}`" allowClear />
</a-form-item>
<!-- 基础配置的子配置项 -->
<a-form-item v-for="config in systemConfigState.filter(item => item.parent_id === 1)"
:key="config.id" :name="config.id" :label="config.name"
:rules="[{ required: true, message: `请输入${config.name}` }]">
<a-input v-model:value="config.fied_value" :placeholder="`请输入${config.name}`" allowClear />
</a-form-item>
</a-form>
</a-card>
<!-- 登录页配置部分 -->
<a-card title="登录页配置" :bordered="false">
<a-form ref="systemConfigForm" :model="systemConfigState" :label-col="{ span: 5 }"
:wrapper-col="{ span: 15 }">
<!-- 登录页配置的顶级配置项 -->
<a-form-item v-for="config in systemConfigState.filter(item => item.fied_key === 'login')"
:key="config.id" :name="config.id" :label="config.name"
:rules="[{ required: true, message: `请输入${config.name}` }]">
<a-input v-model:value="config.fied_value" :placeholder="`请输入${config.name}`" allowClear />
</a-form-item>
<!-- 登录页配置的子配置项 -->
<a-form-item v-for="config in systemConfigState.filter(item => item.parent_id === 4)"
:key="config.id" :name="config.id" :label="config.name"
:rules="[{ required: true, message: `请输入${config.name}` }]">
<a-input v-model:value="config.fied_value" :placeholder="`请输入${config.name}`" allowClear />
</a-form-item>
</a-form>
</a-card>
</a-modal>
</a-card>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, onMounted, h, unref } from 'vue';
import { message, Modal, Table } from 'ant-design-vue';
import type { MenuProps, TableColumnsType } from 'ant-design-vue';
import { PlusOutlined, DownOutlined, CheckOutlined, StopOutlined } from '@ant-design/icons-vue';
import { listToTree, cloneDeep, isEmpty } from '@/utils/util';
import { getConfigList, createConfig, updateConfig, deleteConfig } from '@/api/system/config';
import PageHeader from '@/components/PageHeader.vue'
import type { searchDataType, tableDataType } from './types';
import { reactive, onMounted } from 'vue';
import { message } from 'ant-design-vue';
import { PlusOutlined } from '@ant-design/icons-vue';
import { getConfigList, batchConfig, uploadFile } from '@/api/system/config';
import PageHeader from '@/components/PageHeader.vue';
import type { tableDataType } from './types'
// 响应式数据
const createForm = ref();
const updateForm = ref();
const tableLoading = ref(false);
const openModal = ref(false);
const modalTitle = ref('');
const modalSubmitLoading = ref(false);
const detailStateLoading = ref(false);
const dataSource = ref<tableDataType[]>([]);
const selectedRowKeys = ref<tableDataType['id'][]>([]);
interface ConfigGroup extends tableDataType {
children: tableDataType[];
fileList?: any[];
}
const queryState = reactive<searchDataType>({
name: null,
});
const createState = reactive<tableDataType>({
name: '',
order: 1,
parent_id: undefined,
fied_key: null,
fied_value: '',
const updateState = reactive<ConfigGroup[]>([]);
});
const updateState = reactive<tableDataType>({
id: undefined,
name: '',
order: 1,
parent_id: undefined,
fied_key: null,
fied_value: '',
});
const detailState = ref<tableDataType>({});
// 加载配置数据
const loadConfigData = async () => {
try {
const response = await getConfigList({});
const items = response.data.data.items;
const columns = reactive<TableColumnsType>([
{
title: '配置名称',
dataIndex: 'name',
ellipsis: true,
key: 'name',
// width: 160
},
{
title: '排序',
dataIndex: 'order',
key: 'order',
// align: 'center',
ellipsis: true,
// width: 100
},
{
title: '父级',
dataIndex: 'parent_id',
key: 'parent_id',
ellipsis: true,
// align: 'center',
// width: 100
},
{
title: '键',
dataIndex: 'fied_key',
key: 'fied_key',
ellipsis: true,
// align: 'center'
},
{
title: '值',
dataIndex: 'fied_value',
key: 'fied_value',
ellipsis: true,
// align: 'center'
},
{
title: '操作',
dataIndex: 'action',
key: 'action',
align: 'center',
width: 150
}
]);
// 将配置项按父级分组
const groups = items
.filter(item => item.parent_id === null) // 获取顶级配置项
.map(group => ({
...group,
children: items.filter(item => item.parent_id === group.id) // 获取子配置项
}));
const rowSelection = computed(() => {
return {
selectedRowKeys: unref(selectedRowKeys),
onChange: (selectingRowKeys: tableDataType['id'][]) => {
selectedRowKeys.value = selectingRowKeys;
},
hideDefaultSelections: true,
selections: [
Table.SELECTION_ALL,
Table.SELECTION_INVERT,
Table.SELECTION_NONE
]
}
});
// 初始化图片上传列表
groups.forEach(group => {
group.children.forEach(config => {
if (config.fied_key === 'web_favicon' || config.fied_key === 'login_logo' || config.fied_key === 'login_background') {
config.fileList = config.fied_value ? [{ url: config.fied_value }] : [];
}
});
});
// 避免直接修改 reactive 对象,使用重新赋值的方式
updateState.length = 0; // 清空数组
updateState.push(...groups); // 添加新数据
} catch (error) {
console.error('加载配置数据失败:', error);
message.error('加载配置数据失败');
}
};
// 图片上传前的校验
const beforeUpload = (file: File) => {
const isImage = file.type.startsWith('image/');
if (!isImage) {
message.error('只能上传图片文件!');
}
return isImage;
};
// 自定义上传逻辑
const handleUpload = async (options: any, config: any) => {
const { file, onSuccess, onError } = options;
try {
const formData = new FormData();
formData.append('file', file);
const response = await uploadFile(formData);
const fileUrl = response.data.data.file_url; // 使用 file_url 更新配置项的值
// 更新配置项的值
config.fied_value = fileUrl;
config.fileList = [{ url: fileUrl }]; // 更新文件列表显示
onSuccess(response, file);
message.success('上传成功');
} catch (error) {
onError(error);
console.error('上传失败:', error);
message.error('上传失败');
}
};
// 保存配置
const handleSave = async () => {
try {
// 提取所有配置项(包括父级和子级)
const configs = updateState.flatMap(group => [
{
id: group.id,
name: group.name,
order: group.order,
fied_key: group.fied_key,
fied_value: group.fied_value,
parent_id: group.parent_id,
},
...group.children.map(config => ({
id: config.id,
name: config.name,
order: config.order,
fied_key: config.fied_key,
fied_value: config.fied_value,
parent_id: config.parent_id,
})),
]);
// 调用批量保存接口,直接发送数组
await batchConfig(configs); // 修改为直接发送数组
message.success('配置保存成功');
} catch (error) {
console.error('保存配置失败:', error);
message.error('配置保存失败');
}
};
// 生命周期钩子
onMounted(() => loadingData());
// 查询
const onFinish = () => {
loadingData();
};
// 加载表格数据
const loadingData = () => {
tableLoading.value = true;
let params = {};
if (queryState.name) {
params['name'] = queryState.name;
}
getConfigList(params).then(response => {
const result = response.data;
dataSource.value = listToTree(result.data.items);
}).catch(error => {
console.log(error);
}).finally(() => {
tableLoading.value = false;
});
};
// 重置查询
const resetFields = () => {
Object.keys(queryState).forEach((key: string) => {
delete queryState[key];
});
loadingData();
};
// 删除
const deleteRow = (row: tableDataType) => {
deleteConfig({ id: row.id }).then(response => {
const result = response.data;
message.success(result.msg);
loadingData();
}).catch(error => {
console.log(error);
});
};
// 弹窗关键字处理
const modalHandle = (modalType: string, record?: tableDataType) => {
modalTitle.value = modalType;
openModal.value = true;
if (modalType === 'view' && record !== undefined) {
detailStateLoading.value = true;
detailState.value = record
detailStateLoading.value = false;
} else if (modalType === 'update' && record !== undefined) {
Object.keys(updateState).forEach(key => {
updateState[key] = record[key];
})
}
};
// 弹窗提交(详情/新建/修改)
const handleModalSumbit = () => {
modalSubmitLoading.value = true;
if (modalTitle.value === 'view') {
modalSubmitLoading.value = false;
openModal.value = false;
} else if (modalTitle.value === 'create') {
createForm.value.validate().then(() => {
const createBody = cloneDeep(createState);
Object.keys(createBody).forEach(key => {
if (isEmpty(createBody[key])) {
delete createBody[key];
}
});
createConfig(createBody).then(response => {
const result = response.data;
modalSubmitLoading.value = false;
openModal.value = false;
Object.keys(createState).forEach(key => delete createState[key]);
createState.order = 1;
message.success(result.msg);
loadingData();
}).catch(error => {
console.error(error);
});
}).catch(error => {
modalSubmitLoading.value = false;
console.error(error)
})
} else if (modalTitle.value === 'update') {
updateForm.value.validate().then(() => {
updateConfig(updateState).then(response => {
modalSubmitLoading.value = false;
openModal.value = false;
const result = response.data;
message.success(result.msg);
loadingData();
}).catch(error => {
modalSubmitLoading.value = false;
console.error(error)
});
}).catch(error => {
modalSubmitLoading.value = false;
console.error(error)
})
}
};
// 系统配置弹窗状态
const systemConfigModalVisible = ref(false);
const systemConfigModalLoading = ref(false);
const systemConfigState = ref<tableDataType[]>([]);
// 打开系统配置弹窗
const openSystemConfigModal = async () => {
systemConfigModalVisible.value = true;
systemConfigModalLoading.value = true;
try {
const response = await getConfigList({});
systemConfigState.value = listToTree(response.data.items);
} catch (error) {
console.error(error);
} finally {
systemConfigModalLoading.value = false;
}
};
// 提交系统配置
const handleSystemConfigSubmit = async () => {
systemConfigModalLoading.value = true;
try {
const updatePromises = systemConfigState.value.map(config =>
updateConfig(config)
);
await Promise.all(updatePromises);
message.success('系统配置更新成功');
systemConfigModalVisible.value = false;
loadingData(); // 重新加载数据
} catch (error) {
console.error(error);
} finally {
systemConfigModalLoading.value = false;
}
};
onMounted(() => {
loadConfigData();
});
</script>
<style lang="scss" scoped>
.tree-search-wrapper {
margin-block-end: 16px;
.config-edit-wrapper {
margin-block-end: 16px;
}
.ant-card {
margin-bottom: 24px;
&:last-child {
margin-bottom: 0;
}
}
</style>
+2 -7
View File
@@ -1,15 +1,10 @@
export interface searchDataType {
name?: string
}
export interface tableDataType {
id?: number;
index?: number;
name?: string;
order?: number;
fied_key?: string;
fied_value?: string;
parent_id?: number;
parent_name?: string;
fileList?: any[];
children?: tableDataType[];
}
}