first
This commit is contained in:
294
admin-web/src/pages/system-config/index.vue
Normal file
294
admin-web/src/pages/system-config/index.vue
Normal file
@@ -0,0 +1,294 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import type { UploadRequestOptions } from "element-plus";
|
||||
import { adminApi, type AdminSystemConfigGroupItem } from "../../api/admin";
|
||||
|
||||
const loading = ref(false);
|
||||
const savingGroupCode = ref("");
|
||||
const uploadingKey = ref("");
|
||||
const groups = ref<AdminSystemConfigGroupItem[]>([]);
|
||||
const groupSnapshots = ref<Record<string, Record<string, string>>>({});
|
||||
|
||||
const groupOrder = ["file_storage", "mini_program", "h5", "payment", "sms"];
|
||||
|
||||
function cloneSnapshot(groupsList: AdminSystemConfigGroupItem[]) {
|
||||
return Object.fromEntries(
|
||||
groupsList.map((group) => [
|
||||
group.group_code,
|
||||
Object.fromEntries(group.items.map((item) => [item.config_key, item.value])),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
function sortGroups(groupsList: AdminSystemConfigGroupItem[]) {
|
||||
return [...groupsList].sort((a, b) => {
|
||||
const aIndex = groupOrder.indexOf(a.group_code);
|
||||
const bIndex = groupOrder.indexOf(b.group_code);
|
||||
const safeA = aIndex === -1 ? groupOrder.length : aIndex;
|
||||
const safeB = bIndex === -1 ? groupOrder.length : bIndex;
|
||||
return safeA - safeB;
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchConfigs() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await adminApi.getSystemConfigs();
|
||||
groups.value = sortGroups(response.data.groups);
|
||||
groupSnapshots.value = cloneSnapshot(groups.value);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
ElMessage.error("系统配置加载失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function isGroupDirty(group: AdminSystemConfigGroupItem) {
|
||||
const snapshot = groupSnapshots.value[group.group_code] || {};
|
||||
return group.items.some((item) => (snapshot[item.config_key] || "") !== item.value);
|
||||
}
|
||||
|
||||
function markGroupSnapshot(group: AdminSystemConfigGroupItem) {
|
||||
groupSnapshots.value[group.group_code] = Object.fromEntries(
|
||||
group.items.map((item) => [item.config_key, item.value]),
|
||||
);
|
||||
}
|
||||
|
||||
function markFieldSnapshot(groupCode: string, configKey: string, value: string) {
|
||||
groupSnapshots.value[groupCode] = {
|
||||
...(groupSnapshots.value[groupCode] || {}),
|
||||
[configKey]: value,
|
||||
};
|
||||
}
|
||||
|
||||
async function saveGroup(group: AdminSystemConfigGroupItem) {
|
||||
savingGroupCode.value = group.group_code;
|
||||
try {
|
||||
const items = group.items.map((item) => ({
|
||||
config_group: group.group_code,
|
||||
config_key: item.config_key,
|
||||
config_value: item.value,
|
||||
}));
|
||||
await adminApi.saveSystemConfigs(items);
|
||||
markGroupSnapshot(group);
|
||||
ElMessage.success(`${group.group_name}已保存`);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
ElMessage.error(`${group.group_name}保存失败`);
|
||||
} finally {
|
||||
savingGroupCode.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
function uploadKey(groupCode: string, configKey: string) {
|
||||
return `${groupCode}.${configKey}`;
|
||||
}
|
||||
|
||||
function isFieldVisible(group: AdminSystemConfigGroupItem, item: AdminSystemConfigGroupItem["items"][number]) {
|
||||
if (!item.visible_when) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const dependency = group.items.find((field) => field.config_key === item.visible_when?.config_key);
|
||||
return (dependency?.value || "") === item.visible_when.equals;
|
||||
}
|
||||
|
||||
function uploadedFileName(value: string) {
|
||||
if (!value) return "";
|
||||
const normalized = value.replace(/\\/g, "/");
|
||||
return normalized.split("/").pop() || value;
|
||||
}
|
||||
|
||||
async function handleUpload(options: UploadRequestOptions, groupCode: string, configKey: string) {
|
||||
const file = options.file as File;
|
||||
const key = uploadKey(groupCode, configKey);
|
||||
uploadingKey.value = key;
|
||||
|
||||
try {
|
||||
const response = await adminApi.uploadSystemConfigFile(groupCode, configKey, file);
|
||||
const group = groups.value.find((item) => item.group_code === groupCode);
|
||||
const field = group?.items.find((item) => item.config_key === configKey);
|
||||
if (field) {
|
||||
field.value = response.data.config_value;
|
||||
markFieldSnapshot(groupCode, configKey, response.data.config_value);
|
||||
}
|
||||
|
||||
ElMessage.success(`${response.data.file_name} 上传成功`);
|
||||
options.onSuccess?.(response.data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
ElMessage.error("文件上传失败");
|
||||
} finally {
|
||||
uploadingKey.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(fetchConfigs);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-loading="loading">
|
||||
<el-card class="panel-card" shadow="never">
|
||||
<div class="filters-row" style="justify-content: space-between;">
|
||||
<div>
|
||||
<div style="font-size: 18px; font-weight: 700;">系统配置</div>
|
||||
<div style="color: var(--admin-text-subtle); margin-top: 6px;">
|
||||
按模块独立维护配置,每个模块单独保存,避免一次提交修改整页全部参数。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card
|
||||
v-for="group in groups"
|
||||
:key="group.group_code"
|
||||
class="panel-card"
|
||||
shadow="never"
|
||||
>
|
||||
<div class="config-group__header">
|
||||
<div>
|
||||
<div style="font-size: 16px; font-weight: 700;">{{ group.group_name }}</div>
|
||||
<div style="color: var(--admin-text-subtle); margin-top: 6px;">
|
||||
{{ group.group_desc }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-group__actions">
|
||||
<span v-if="isGroupDirty(group)" class="config-group__dirty">本模块有未保存修改</span>
|
||||
<el-button
|
||||
type="primary"
|
||||
:disabled="!isGroupDirty(group)"
|
||||
:loading="savingGroupCode === group.group_code"
|
||||
@click="saveGroup(group)"
|
||||
>
|
||||
保存本模块
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-form label-position="top">
|
||||
<el-row :gutter="16">
|
||||
<el-col
|
||||
v-for="item in group.items"
|
||||
:key="`${group.group_code}-${item.config_key}`"
|
||||
v-show="isFieldVisible(group, item)"
|
||||
:span="item.field_type === 'textarea' ? 24 : 12"
|
||||
>
|
||||
<el-form-item :label="item.title">
|
||||
<template v-if="item.field_type === 'file'">
|
||||
<div class="config-upload">
|
||||
<div class="config-upload__meta">
|
||||
<div class="config-upload__label">
|
||||
{{ item.value ? `已上传:${uploadedFileName(item.value)}` : item.placeholder }}
|
||||
</div>
|
||||
<div class="config-upload__path" v-if="item.value">
|
||||
{{ item.value }}
|
||||
</div>
|
||||
</div>
|
||||
<el-upload
|
||||
:show-file-list="false"
|
||||
accept=".pem"
|
||||
:http-request="(options: UploadRequestOptions) => handleUpload(options, group.group_code, item.config_key)"
|
||||
>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
:loading="uploadingKey === uploadKey(group.group_code, item.config_key)"
|
||||
>
|
||||
{{ item.value ? "重新上传" : "上传 PEM 文件" }}
|
||||
</el-button>
|
||||
</el-upload>
|
||||
</div>
|
||||
</template>
|
||||
<el-select
|
||||
v-else-if="item.field_type === 'select'"
|
||||
v-model="item.value"
|
||||
style="width: 100%"
|
||||
:placeholder="item.placeholder"
|
||||
>
|
||||
<el-option
|
||||
v-for="option in item.options || []"
|
||||
:key="`${group.group_code}-${item.config_key}-${option.value}`"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input
|
||||
v-else-if="item.field_type !== 'textarea'"
|
||||
v-model="item.value"
|
||||
:type="item.field_type === 'password' ? 'password' : 'text'"
|
||||
show-password
|
||||
:placeholder="item.placeholder"
|
||||
/>
|
||||
<el-input
|
||||
v-else
|
||||
v-model="item.value"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
:placeholder="item.placeholder"
|
||||
/>
|
||||
<div style="margin-top: 6px; color: var(--admin-text-subtle); font-size: 12px;">
|
||||
{{ item.remark }}
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.config-group__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.config-group__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.config-group__dirty {
|
||||
color: var(--admin-warning, #b7791f);
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.config-upload {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 14px 16px;
|
||||
border: 1px solid var(--admin-border, #e5ddd1);
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(180deg, rgba(255, 251, 244, 0.92) 0%, rgba(247, 242, 233, 0.72) 100%);
|
||||
}
|
||||
|
||||
.config-upload__meta {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.config-upload__label {
|
||||
color: var(--admin-text, #2f2a22);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.config-upload__path {
|
||||
margin-top: 6px;
|
||||
color: var(--admin-text-subtle, #8f866f);
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user