This commit is contained in:
wushumin
2026-05-11 15:28:27 +08:00
commit 9aac78b8da
289 changed files with 67193 additions and 0 deletions

View 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>