init: UserBaseFront (frontend)
This commit is contained in:
@@ -0,0 +1,295 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user