Files
UserhomeFront/src/views/app/ModuleManage.vue
T

296 lines
11 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="main-wrapper">
<div class="main-container">
<div class="search-wrap">
<Button class="fl" type="primary" @click="handleAdd">新增模块</Button>
<Button class="fr" icon="ios-arrow-back" @click="goBack">返回</Button>
</div>
<h4 class="mt-10 second-title">{{ appName }} - 模块管理</h4>
<Table class="module-table" :columns="columns" :data="flatData" border>
<template #name="{ row }">
<div class="module-name-cell" :style="{ paddingLeft: row._depth * 28 + 'px' }">
<SvgIcon v-if="row._depth > 0" name="tree-line" :size="16" color="#999" />
<Icon type="ios-cube-outline" size="16" />
<span class="module-name-text">{{ row.name }}</span>
</div>
</template>
</Table>
</div>
<!-- 新增 / 编辑弹窗 -->
<Modal v-model="editModal" :mask-closable="false" width="600" :title="modalTitle">
<Form ref="formData" class="mt-8 mr-4 mb-12" :model="form" :rules="ruleValidate" :label-width="110">
<FormItem v-if="form.parentId != null" label="父模块名称:">
<Input :model-value="parentNameForDisplay" disabled />
</FormItem>
<FormItem label="模块名称:" prop="name">
<Input placeholder="请输入模块名称" v-model="form.name" />
</FormItem>
<FormItem label="模块路径:" prop="path">
<Input placeholder="请输入模块路径" v-model="form.path" />
</FormItem>
<FormItem label="自定义ID:" prop="customId">
<InputNumber :style="{ width: '100%' }" placeholder="请输入自定义ID" v-model="form.customId" />
</FormItem>
<FormItem label="排序号:" prop="sortNo">
<InputNumber :style="{ width: '100%' }" placeholder="请输入排序号" v-model="form.sortNo" />
</FormItem>
</Form>
<template #footer>
<Button type="primary" :loading="submitting" @click="onSave">提交</Button>
<Button @click="onReset">取消</Button>
</template>
</Modal>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, nextTick } from "vue";
import { useRoute, useRouter } from "vue-router";
import { debounce } from "lodash";
import SvgIcon from "@/components/SvgIcon.vue";
import MyConfirm from "@/components/MyConfirm";
import useCurrentInstance from "@/utils/hook/useCurrentInstance";
const { $utils } = useCurrentInstance();
const route = useRoute();
const router = useRouter();
const appId = computed(() => Number(route.query.id));
const appName = computed(() => (route.query.name as string) || '');
/* ---------------- 数据加载(无分页) ---------------- */
const { data: moduleList, send: fetchModuleList } = useRequest(
Apis.AppSettings.moduleList({
params: { appId: appId.value },
cacheFor: null,
}),
{ initialData: [], immediate: true }
);
/* ---------------- 递归扁平化(伪树形,支持任意层级) ----------------
* 注意:必须剥掉 children,否则 view-ui-plus Table 会把行当成树节点处理,
* 触发内部树形展开逻辑并读到 null 子节点,报 `_isChecked` of null 错误。 */
function flattenTree(list: any[], depth = 0): any[] {
const result: any[] = [];
(list ?? []).forEach((node: any) => {
const { children, ...rest } = node;
const hasChildren = Array.isArray(children) && children.length > 0;
result.push({ ...rest, _depth: depth, _isParent: node.parentId == null, _hasChildren: hasChildren });
if (hasChildren) {
result.push(...flattenTree(children, depth + 1));
}
});
return result;
}
const flatData = computed(() => flattenTree(moduleList.value));
/* ---------------- 表格列 ---------------- */
const columns = [
{ title: "模块名称", key: "name", minWidth: 260, slot: "name" },
{ title: "ID", key: "customId", width: 120, align: "center", render: (h: any, { row }: any) => h("span", row.customId || "-") },
{ title: "路径", key: "path", minWidth: 200, render: (h: any, { row }: any) => h("span", row.path || "-") },
{ title: "排序号", key: "sortNo", width: 100, align: "center", render: (h: any, { row }: any) => h("span", row.sortNo ?? "-") },
{
title: "操作",
key: "action",
width: 220,
align: "center",
render: (h: any, { row }: any) => {
const actions: any[] = [
$utils.operatingTextHandle(h, '编辑', '', () => handleEdit(row)),
$utils.operatingTextHandle(h, '删除', 'text-error', () => handleDelete(row)),
];
if (row.level < 5) {
actions.splice(1, 0, $utils.operatingTextHandle(h, '添加子模块', '', () => handleAddChild(row)));
}
return h("div", { style: { display: "flex", justifyContent: "center", gap: "4px" } }, actions);
},
},
];
/* ---------------- 弹窗状态 ---------------- */
const refFormData = useTemplateRef('formData');
const editModal = ref(false);
const currentId = ref<number | null>(null);
const submitting = ref(false);
const parentNameForDisplay = ref('');
const form = ref<{
id: number | null;
name: string;
path: string;
customId: number | null;
parentId: number | null;
sortNo: number | null;
}>({
id: null,
name: '',
path: '',
customId: null,
parentId: null,
sortNo: null,
});
const ruleValidate = ref({
name: [{ required: true, message: "请输入模块名称", trigger: "blur" }],
path: [{ required: true, message: "请输入模块路径", trigger: "blur" }],
customId: [{ required: true, type: "number", message: "请输入自定义ID", trigger: "change" }],
});
const openingIsChild = ref(false);
const modalTitle = computed(() => {
if (currentId.value != null) return '编辑模块';
return openingIsChild.value ? '新增子模块' : '新增模块';
});
/* ---------------- 返回应用列表(keepAlive 记忆筛选) ---------------- */
function goBack() {
router.push("/app-manage/list");
}
/* ---------------- 由嵌套 moduleList 递归构建 id->name 映射(兜底后端未返回 parentName) ---------------- */
function buildIdNameMap(list: any[]): Record<number, string> {
const map: Record<number, string> = {};
(list ?? []).forEach((n: any) => {
if (n.id != null) map[n.id] = n.name;
if (Array.isArray(n.children) && n.children.length) Object.assign(map, buildIdNameMap(n.children));
});
return map;
}
const parentNameMap = computed(() => buildIdNameMap(moduleList.value));
/* ---------------- 弹窗赋值统一入口(先打开弹窗 → nextTick 内 resetFields 清空校验 → 再写入值) ----------------
* 注意:view-ui-plus 的 resetFields() 会把所有字段重置回 mount 初值(parentId 会变成 null),
* 因此必须先 resetFields,再赋值,否则值被清掉:
* - 「父模块名称」整行因 v-if(parentId!=null) 不成立而消失
* - 保存时子模块的 parentId 被清空、变成顶层模块 */
function openEditModal(item: any) {
currentId.value = item?.id ?? null;
editModal.value = true;
nextTick(() => {
refFormData.value?.resetFields();
const f = form.value;
f.id = item?.id ?? null;
f.name = item?.name || '';
f.path = item?.path || '';
f.customId = item?.customId != null ? Number(item.customId) : null;
f.parentId = item?.parentId ?? null;
f.sortNo = item?.sortNo != null ? Number(item.sortNo) : null;
parentNameForDisplay.value = f.parentId != null
? (parentNameMap.value[f.parentId] || '')
: '';
});
}
/* ---------------- 新增模块 ---------------- */
function handleAdd() {
openingIsChild.value = false;
openEditModal({ id: null, name: '', path: '', customId: null, parentId: null, sortNo: null });
}
/* ---------------- 新增子模块 ---------------- */
function handleAddChild(parent: any) {
openingIsChild.value = true;
openEditModal({ id: null, name: '', path: '', customId: null, parentId: parent.id, sortNo: null });
}
/* ---------------- 编辑 ---------------- */
function handleEdit(item: any) {
openingIsChild.value = item.parentId != null;
openEditModal(item);
}
/* ---------------- 删除 ---------------- */
async function handleDelete(item: any) {
await MyConfirm({
msg: `确定删除模块「${item.name || item.id}」吗?`,
info: item._hasChildren ? "该模块下存在子模块,删除后子模块也将被删除!" : "删除后不可恢复!",
async onOk() {
try {
await Apis.AppSettings.deleteModule({ pathParams: { id: item.id } });
Message.success("删除成功!");
await fetchModuleList();
} catch (e: any) {
Message.error(e?.message || '删除失败');
}
},
});
}
/* ---------------- 提交(防抖) ---------------- */
const onSave = debounce(onRawSave, 500);
async function onRawSave() {
try {
const valid = await refFormData.value?.validate();
if (!valid) return;
} catch {
return;
}
submitting.value = true;
try {
if (currentId.value == null) {
await Apis.AppSettings.addModule({
data: {
appId: appId.value,
name: form.value.name,
path: form.value.path,
customId: form.value.customId == null ? '' : String(form.value.customId),
parentId: form.value.parentId ?? undefined,
sortNo: form.value.sortNo == null ? undefined : Number(form.value.sortNo),
},
});
} else {
await Apis.AppSettings.editModule({
pathParams: { id: currentId.value },
data: {
id: currentId.value,
appId: appId.value,
name: form.value.name,
path: form.value.path,
customId: form.value.customId == null ? '' : String(form.value.customId),
parentId: form.value.parentId ?? undefined,
sortNo: form.value.sortNo == null ? undefined : Number(form.value.sortNo),
},
});
}
Message.success("操作成功!");
editModal.value = false;
await fetchModuleList();
} catch (error) {
console.error(error);
} finally {
submitting.value = false;
}
}
function onReset() {
refFormData.value?.resetFields();
currentId.value = null;
editModal.value = false;
}
</script>
<style lang="scss" scoped>
.module-table {
:deep(.ivu-table-cell) {
padding-left: 12px;
padding-right: 12px;
}
}
.module-name-cell {
display: flex;
align-items: center;
.module-name-text {
margin-left: 6px;
}
}
</style>