feat: 为多个管理页面添加统一的操作反馈提示,重构设置页面UI

1. 为用户、角色、权限管理页添加增删改成功/失败的全局提示,统一弹窗样式与时长
2. 重构系统设置管理页面的UI,替换element组件为原生html实现,优化布局样式
This commit is contained in:
jayhgq 2026-05-27 16:26:36 +08:00
parent 82cf0192e8
commit 2c78e254e1
6 changed files with 373 additions and 180 deletions

View File

@ -1,18 +1,44 @@
<template> <template>
<fs-page> <fs-page>
<fs-crud ref="crudRef" v-bind="crudBinding" /> <fs-crud ref="crudRef" v-bind="crudBinding" @add-success="onAddSuccess" @edit-success="onEditSuccess" @del-success="onDelSuccess" @add-error="onError" @edit-error="onError" @del-error="onError" />
</fs-page> </fs-page>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onMounted } from "vue"; import { onMounted } from "vue";
import { useFs } from "@fast-crud/fast-crud"; import { useFs } from "@fast-crud/fast-crud";
import { ElMessage } from "element-plus";
import { createPermissionCrudOptions } from "./permission-crud"; import { createPermissionCrudOptions } from "./permission-crud";
const { crudBinding, crudRef, crudExpose } = useFs({ const { crudBinding, crudRef, crudExpose } = useFs({
createCrudOptions: createPermissionCrudOptions, createCrudOptions: createPermissionCrudOptions,
}); });
const onAddSuccess = () => {
ElMessage.success({ message: "添加成功", duration: 5000 });
};
const onEditSuccess = () => {
ElMessage.success({ message: "修改成功", duration: 5000 });
};
const onDelSuccess = () => {
ElMessage.success({ message: "删除成功", duration: 5000 });
};
const onError = (error: any) => {
let message = "操作失败";
if (error.response?.data) {
const data = error.response.data;
if (data.message) {
message = data.message;
} else if (data.detail) {
message = typeof data.detail === "string" ? data.detail : "操作失败";
}
}
ElMessage.error({ message, duration: 5000 });
};
onMounted(() => { onMounted(() => {
crudExpose.doRefresh(); crudExpose.doRefresh();
}); });

View File

@ -1,5 +1,6 @@
import type { CreateCrudOptions } from "@fast-crud/fast-crud"; import type { CreateCrudOptions } from "@fast-crud/fast-crud";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { ElMessage } from "element-plus";
import { http } from "@/modules/admin/api/http"; import { http } from "@/modules/admin/api/http";
import { toSearchParams } from "@/modules/admin/utils/list-query"; import { toSearchParams } from "@/modules/admin/utils/list-query";
@ -58,6 +59,27 @@ export const createPermissionCrudOptions: CreateCrudOptions<PermissionRow> = ()
if (!row) return; if (!row) return;
await http.delete(`/permissions/${row.id}`); await http.delete(`/permissions/${row.id}`);
}, },
onSuccess: ({ action }: { action: string }) => {
if (action === "add") {
ElMessage.success({ message: "添加成功", duration: 5000 });
} else if (action === "edit") {
ElMessage.success({ message: "修改成功", duration: 5000 });
} else if (action === "del") {
ElMessage.success({ message: "删除成功", duration: 5000 });
}
},
onError: (error: any) => {
let message = "操作失败";
if (error.response?.data) {
const data = error.response.data;
if (data.message) {
message = data.message;
} else if (data.detail) {
message = typeof data.detail === "string" ? data.detail : "操作失败";
}
}
ElMessage.error({ message, duration: 5000 });
},
}, },
rowHandle: { rowHandle: {
buttons: { buttons: {

View File

@ -1,5 +1,6 @@
import type { CreateCrudOptions } from "@fast-crud/fast-crud"; import type { CreateCrudOptions } from "@fast-crud/fast-crud";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { ElMessage } from "element-plus";
import { http } from "@/modules/admin/api/http"; import { http } from "@/modules/admin/api/http";
import { toSearchParams } from "@/modules/admin/utils/list-query"; import { toSearchParams } from "@/modules/admin/utils/list-query";
@ -54,6 +55,27 @@ export function buildRoleCrudOptions(h: RoleCrudHandlers): CreateCrudOptions<Rol
if (!row) return; if (!row) return;
await http.delete(`/roles/${row.id}`); await http.delete(`/roles/${row.id}`);
}, },
onSuccess: ({ action }: { action: string }) => {
if (action === "add") {
ElMessage.success({ message: "添加成功", duration: 5000 });
} else if (action === "edit") {
ElMessage.success({ message: "修改成功", duration: 5000 });
} else if (action === "del") {
ElMessage.success({ message: "删除成功", duration: 5000 });
}
},
onError: (error: any) => {
let message = "操作失败";
if (error.response?.data) {
const data = error.response.data;
if (data.message) {
message = data.message;
} else if (data.detail) {
message = typeof data.detail === "string" ? data.detail : "操作失败";
}
}
ElMessage.error({ message, duration: 5000 });
},
}, },
rowHandle: { rowHandle: {
width: 300, width: 300,

View File

@ -1,146 +1,92 @@
<template> <template>
<fs-page> <div class="setting-container">
<el-card> <div class="header">
<template #header> <h2>系统设置管理</h2>
<div class="flex items-center justify-between"> <button class="add-btn" @click="openAddModal">添加设置</button>
<span>系统设置管理</span> </div>
<el-button type="primary" @click="openAddModal">添加设置</el-button>
</div> <table class="settings-table">
</template> <thead>
<tr>
<el-table :data="settings" border style="width: 100%"> <th>ID</th>
<el-table-column prop="id" label="ID" width="80" /> <th>设置键名</th>
<el-table-column prop="key" label="设置键名" /> <th>设置值</th>
<el-table-column prop="value" label="设置值" :show-overflow-tooltip="true" /> <th>描述</th>
<el-table-column prop="type" label="类型" width="120"> <th>创建时间</th>
<template #default="scope"> <th>操作</th>
{{ getTypeLabel(scope.row.type) }} </tr>
</template> </thead>
</el-table-column> <tbody>
<el-table-column prop="description" label="描述" /> <tr v-for="item in settings" :key="item.id">
<el-table-column prop="createtime" label="创建时间" width="180" /> <td>{{ item.id }}</td>
<el-table-column label="操作" width="180"> <td>{{ item.key }}</td>
<template #default="scope"> <td class="value-cell">{{ item.value }}</td>
<el-button size="small" @click="openEditModal(scope.row)">编辑</el-button> <td>{{ item.description || '-' }}</td>
<el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button> <td>{{ item.createtime }}</td>
</template> <td class="actions">
</el-table-column> <button class="edit-btn" @click="openEditModal(item)">编辑</button>
</el-table> <button class="delete-btn" @click="handleDelete(item)">删除</button>
</el-card> </td>
</tr>
</tbody>
</table>
<!-- 添加/编辑弹窗 --> <!-- 添加/编辑弹窗 -->
<el-dialog :title="isEdit ? '编辑设置' : '添加设置'" :visible.sync="showModal" width="600px"> <div v-if="showModal" class="modal-overlay" @click.self="closeModal">
<el-form :model="form" label-width="120px"> <div class="modal-content">
<el-form-item label="设置键名" prop="key"> <h3>{{ isEdit ? '编辑设置' : '添加设置' }}</h3>
<el-input v-model="form.key" placeholder="请输入设置键名" />
</el-form-item>
<el-form-item label="类型" prop="type"> <div class="form-group">
<el-select v-model="form.type" placeholder="请选择类型"> <label>设置键名 *</label>
<el-option label="字符串" value="string" /> <input v-model="formData.key" type="text" placeholder="请输入设置键名" />
<el-option label="图片" value="image" /> </div>
<el-option label="下拉列表" value="select" />
<el-option label="布尔值" value="boolean" /> <div class="form-group">
</el-select> <label>设置值</label>
</el-form-item> <textarea v-model="formData.value" rows="3" placeholder="请输入设置值"></textarea>
</div>
<!-- 字符串类型 -->
<el-form-item v-if="form.type === 'string'" label="设置值" prop="value"> <div class="form-group">
<el-input v-model="form.value" type="textarea" :rows="3" placeholder="请输入值" /> <label>描述</label>
</el-form-item> <textarea v-model="formData.description" rows="2" placeholder="请输入描述"></textarea>
</div>
<!-- 图片类型 -->
<el-form-item v-if="form.type === 'image'" label="设置值" prop="value"> <div class="modal-footer">
<el-upload <button class="cancel-btn" @click="closeModal">取消</button>
action="#" <button class="submit-btn" @click="handleSubmit">{{ isEdit ? '保存修改' : '确认添加' }}</button>
accept="image/*" </div>
list-type="picture-card" </div>
:show-file-list="false" </div>
:before-upload="handleImageUpload" </div>
:on-remove="handleImageRemove"
>
<div v-if="!form.value">
<el-icon><UploadIcon /></el-icon>
<div>上传图片</div>
</div>
<img v-else :src="form.value" style="width: 100%; height: 100%" />
</el-upload>
</el-form-item>
<!-- 下拉列表类型 -->
<template v-if="form.type === 'select'">
<el-form-item label="选项配置" prop="options">
<el-input v-model="form.options" type="textarea" :rows="2" placeholder="多个选项用 | 分隔选项1|选项2|选项3" />
</el-form-item>
<el-form-item label="设置值" prop="value">
<el-select v-model="form.value" placeholder="请选择">
<el-option v-for="opt in optionsList" :key="opt" :label="opt" :value="opt" />
</el-select>
</el-form-item>
</template>
<!-- 布尔值类型 -->
<el-form-item v-if="form.type === 'boolean'" label="设置值" prop="value">
<el-switch v-model="form.booleanValue" active-text="开启" inactive-text="关闭" />
</el-form-item>
<el-form-item label="描述" prop="description">
<el-input v-model="form.description" type="textarea" :rows="2" placeholder="请输入描述" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showModal = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</fs-page>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch } from "vue"; import { ref } from "vue";
import { ElMessage, ElMessageBox } from "element-plus"; import { ElMessage, ElMessageBox } from "element-plus";
import { http } from "@/modules/admin/api/http"; import { http } from "@/modules/admin/api/http";
import type { SettingRow } from "./setting-crud";
const settings = ref<SettingRow[]>([]); interface SettingItem {
id: number;
key: string;
value: string;
description: string;
createtime: string;
}
const settings = ref<SettingItem[]>([]);
const showModal = ref(false); const showModal = ref(false);
const isEdit = ref(false); const isEdit = ref(false);
const form = ref({ const formData = ref({
id: 0, id: 0,
key: "", key: "",
value: "", value: "",
type: "string",
options: "",
description: "", description: "",
booleanValue: false,
}); });
const optionsList = computed(() => {
if (!form.value.options) return [];
return form.value.options.split("|").filter(Boolean).map((o) => o.trim());
});
watch(() => form.value.type, (newType) => {
if (newType === "boolean") {
form.value.booleanValue = form.value.value === "true";
}
});
const getTypeLabel = (type: string) => {
const types: Record<string, string> = {
string: "字符串",
image: "图片",
select: "下拉列表",
boolean: "布尔值",
};
return types[type] || type;
};
const loadData = async () => { const loadData = async () => {
try { try {
const { data } = await http.get<SettingRow[]>("/settings/"); const { data } = await http.get<SettingItem[]>("/settings/");
settings.value = data; settings.value = data;
} catch (error) { } catch (error) {
ElMessage.error("加载失败"); ElMessage.error("加载失败");
@ -149,83 +95,59 @@ const loadData = async () => {
const openAddModal = () => { const openAddModal = () => {
isEdit.value = false; isEdit.value = false;
form.value = { formData.value = {
id: 0, id: 0,
key: "", key: "",
value: "", value: "",
type: "string",
options: "",
description: "", description: "",
booleanValue: false,
}; };
showModal.value = true; showModal.value = true;
}; };
const openEditModal = (row: SettingRow) => { const openEditModal = (item: SettingItem) => {
isEdit.value = true; isEdit.value = true;
form.value = { formData.value = {
id: row.id, id: item.id,
key: row.key, key: item.key,
value: row.value, value: item.value,
type: row.type, description: item.description,
options: row.options || "",
description: row.description || "",
booleanValue: row.value === "true",
}; };
showModal.value = true; showModal.value = true;
}; };
const handleImageUpload = async (file: any) => { const closeModal = () => {
const formData = new FormData(); showModal.value = false;
formData.append("file", file.raw);
try {
const response = await http.post("/settings/upload", formData, {
headers: { "Content-Type": "multipart/form-data" },
});
form.value.value = response.data.url;
} catch (error) {
ElMessage.error("上传失败");
}
return false;
}; };
const handleImageRemove = () => { const handleDelete = async (item: SettingItem) => {
form.value.value = ""; try {
await ElMessageBox.confirm("确定要删除吗?");
await http.delete(`/settings/${item.id}`);
ElMessage.success("删除成功");
await loadData();
} catch (error) {
ElMessage.error("删除失败");
}
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
if (!form.value.key) { if (!formData.value.key) {
ElMessage.error("请输入设置键名"); ElMessage.warning("请输入设置键名");
return; return;
} }
if (form.value.type === "boolean") {
form.value.value = form.value.booleanValue ? "true" : "false";
}
try { try {
if (isEdit.value) { if (isEdit.value) {
await http.put(`/settings/${form.value.id}`, form.value); await http.put(`/settings/${formData.value.id}`, formData.value);
ElMessage.success("修改成功"); ElMessage.success("修改成功");
} else { } else {
await http.post("/settings/", form.value); await http.post("/settings/", formData.value);
ElMessage.success("添加成功"); ElMessage.success("添加成功");
} }
showModal.value = false; closeModal();
await loadData(); await loadData();
} catch (error: any) { } catch (error) {
ElMessage.error(error.response?.data?.detail || "操作失败"); ElMessage.error("操作失败");
}
};
const handleDelete = async (row: SettingRow) => {
try {
await ElMessageBox.confirm("确定要删除吗?");
await http.delete(`/settings/${row.id}`);
ElMessage.success("删除成功");
await loadData();
} catch {
//
} }
}; };
@ -233,13 +155,168 @@ loadData();
</script> </script>
<style scoped> <style scoped>
.flex { .setting-container {
padding: 20px;
}
.header {
display: flex; display: flex;
}
.items-center {
align-items: center;
}
.justify-between {
justify-content: space-between; justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.header h2 {
margin: 0;
font-size: 18px;
}
.add-btn {
background: #409eff;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
}
.add-btn:hover {
background: #66b1ff;
}
.settings-table {
width: 100%;
border-collapse: collapse;
}
.settings-table th,
.settings-table td {
border: 1px solid #ebeef5;
padding: 12px;
text-align: left;
}
.settings-table th {
background: #f5f7fa;
font-weight: 600;
}
.value-cell {
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.actions {
display: flex;
gap: 8px;
}
.edit-btn {
background: #67c23a;
color: white;
border: none;
padding: 4px 12px;
border-radius: 4px;
cursor: pointer;
}
.edit-btn:hover {
background: #85ce61;
}
.delete-btn {
background: #f56c6c;
color: white;
border: none;
padding: 4px 12px;
border-radius: 4px;
cursor: pointer;
}
.delete-btn:hover {
background: #f78989;
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background: white;
padding: 24px;
border-radius: 8px;
width: 480px;
}
.modal-content h3 {
margin: 0 0 20px 0;
font-size: 16px;
}
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 500;
}
.form-group input,
.form-group textarea {
width: 100%;
padding: 8px;
border: 1px solid #dcdfe6;
border-radius: 4px;
box-sizing: border-box;
}
.form-group textarea {
resize: vertical;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 24px;
}
.cancel-btn {
background: #f5f7fa;
color: #606266;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
}
.cancel-btn:hover {
background: #e4e7ed;
}
.submit-btn {
background: #409eff;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
}
.submit-btn:hover {
background: #66b1ff;
} }
</style> </style>

View File

@ -1,18 +1,55 @@
<template> <template>
<fs-page> <fs-page>
<fs-crud ref="crudRef" v-bind="crudBinding" /> <fs-crud ref="crudRef" v-bind="crudBinding" @add-success="onAddSuccess" @edit-success="onEditSuccess" @del-success="onDelSuccess" @add-error="onError" @edit-error="onError" @del-error="onError" />
</fs-page> </fs-page>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onMounted } from "vue"; import { onMounted } from "vue";
import { useFs } from "@fast-crud/fast-crud"; import { useFs } from "@fast-crud/fast-crud";
import { ElMessage } from "element-plus";
import { createUserCrudOptions } from "./user-crud"; import { createUserCrudOptions } from "./user-crud";
const { crudBinding, crudRef, crudExpose } = useFs({ const { crudBinding, crudRef, crudExpose } = useFs({
createCrudOptions: createUserCrudOptions, createCrudOptions: createUserCrudOptions,
}); });
const onAddSuccess = () => {
ElMessage.success({ message: "添加成功", duration: 5000 });
};
const onEditSuccess = () => {
ElMessage.success({ message: "修改成功", duration: 5000 });
};
const onDelSuccess = () => {
ElMessage.success({ message: "删除成功", duration: 5000 });
};
const onError = (error: any) => {
let message = "操作失败";
if (error.response?.data) {
const data = error.response.data;
if (data.errors && Array.isArray(data.errors)) {
message = data.errors.map((e: any) => e.message).join("");
} else if (data.message) {
message = data.message;
} else if (data.detail) {
const detail = data.detail;
if (typeof detail === "string") {
if (detail.includes("at least 6 characters")) {
message = "密码长度至少为6个字符";
} else {
message = detail;
}
}
}
} else if (error.message) {
message = error.message;
}
ElMessage.error({ message, duration: 5000 });
};
onMounted(() => { onMounted(() => {
crudExpose.doRefresh(); crudExpose.doRefresh();
}); });

View File

@ -71,7 +71,7 @@ export const createUserCrudOptions: CreateCrudOptions<UserRow> = () => {
} else if (error.message) { } else if (error.message) {
message = error.message; message = error.message;
} }
ElMessage.error(message); ElMessage.error({ message, duration: 5000 });
throw error; throw error;
}, },
addRequest: async ({ form }) => { addRequest: async ({ form }) => {
@ -87,6 +87,15 @@ export const createUserCrudOptions: CreateCrudOptions<UserRow> = () => {
if (!row) return; if (!row) return;
await http.delete(`/users/${row.id}`); await http.delete(`/users/${row.id}`);
}, },
onSuccess: ({ action }: { action: string }) => {
if (action === "add") {
ElMessage.success({ message: "添加成功", duration: 5000 });
} else if (action === "edit") {
ElMessage.success({ message: "修改成功", duration: 5000 });
} else if (action === "del") {
ElMessage.success({ message: "删除成功", duration: 5000 });
}
},
}, },
rowHandle: { rowHandle: {
buttons: { buttons: {