init: UserBaseFront (frontend)

This commit is contained in:
chenxing
2026-08-10 14:41:13 +08:00
commit eea881b616
174 changed files with 32833 additions and 0 deletions
+295
View File
@@ -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>