签入脚本

This commit is contained in:
小蚂蚁云
2026-10-20 11:37:25 +08:00
commit 7f5e8470af
909 changed files with 4045080 additions and 0 deletions
+135
View File
@@ -0,0 +1,135 @@
import { http } from '@/utils/http/axios';
/**
* [[$.ModuleComment]]模块接口
*
* 提供[[$.ModuleComment]]的增删改查及导入导出接口调用。
*/
/**
* 获取[[$.ModuleComment]]分页列表
*
* @param params 查询参数,包含 pageNo、pageSize 及业务筛选条件
* @returns 分页结果,含 records、total、size、current、pages
*/
export function get[[$.ModelClass]]Page(params?) {
return http.request({
url: '/[[$.RoutePrefix]]/page',
method: 'GET',
params,
});
}
/**
* 获取全部[[$.ModuleComment]]列表
*
* @param params 查询参数
* @returns 返回结果
*/
export function get[[$.ModelClass]]List(params?) {
return http.request({
url: '/[[$.RoutePrefix]]/list',
method: 'GET',
params,
});
}
/**
* 根据 ID 获取[[$.ModuleComment]]详情
*
* @param [[$.PrimaryPk]] [[$.ModuleComment]]ID
* @returns 详情数据
*/
export function get[[$.ModelClass]]Detail([[$.PrimaryPk]]) {
return http.request({
url: '/[[$.RoutePrefix]]/detail/' + [[$.PrimaryPk]],
method: 'get',
});
}
/**
* 添加[[$.ModuleComment]]
*
* @param data [[$.ModuleComment]]数据
* @returns 新记录 ID
*/
export function [[$.ModuleCamel]]Add(data: any) {
return http.request({
url: '/[[$.RoutePrefix]]/add',
method: 'POST',
data,
});
}
/**
* 更新[[$.ModuleComment]]
*
* @param data [[$.ModuleComment]]数据,需包含主键 [[$.PrimaryPk]]
* @returns 操作结果
*/
export function [[$.ModuleCamel]]Update(data: any) {
return http.request({
url: '/[[$.RoutePrefix]]/update',
method: 'PUT',
data,
});
}
/**
* 删除[[$.ModuleComment]]
*
* @param [[$.PrimaryPk]] [[$.ModuleComment]]ID
* @returns 操作结果
*/
export function [[$.ModuleCamel]]Delete([[$.PrimaryPk]]) {
return http.request({
url: '/[[$.RoutePrefix]]/delete/' + [[$.PrimaryPk]],
method: 'DELETE',
});
}
/**
* 批量删除[[$.ModuleComment]]
*
* @param data 含 ids 数组的对象,形如 { ids: [1, 2, 3] }
* @returns 操作结果
*/
export function [[$.ModuleCamel]]BatchDelete(data: any) {
return http.request({
url: '/[[$.RoutePrefix]]/batchDelete',
method: 'DELETE',
data,
});
}
[[- if $.HasImport]]
/**
* 导入[[$.ModuleComment]]
*
* @param data 文件数据(FormData)
* @returns 导入结果,含成功数量与失败明细
*/
export function [[$.ModuleCamel]]Import(data) {
return http.request({
url: '/[[$.RoutePrefix]]/import',
method: 'POST',
data,
});
}
[[- end]]
[[- if $.HasExport]]
/**
* 导出[[$.ModuleComment]]
*
* @param params 查询参数
* @returns 导出文件的 URL
*/
export function [[$.ModuleCamel]]Export(params) {
return http.request({
url: '/[[$.RoutePrefix]]/export',
method: 'GET',
params,
});
}
[[- end]]
+218
View File
@@ -0,0 +1,218 @@
<template>
<!-- [[$.ModuleComment]]详情弹窗 -->
<el-dialog
v-model="props.visible"
title="[[$.ModuleComment]]详情"
width="750"
:close-on-click-modal="false"
:before-close="dialogClose"
>
<el-descriptions column="2" border>
[[- range .FormFields]]
<el-descriptions-item label="[[.Label]]:" label-class-name="des-width">
[[- if .IsImage]]
<!-- 图片:支持点击预览 -->
<el-image
v-if="formData.[[.CamelName]]"
:src="formData.[[.CamelName]]"
:preview-src-list="[formData.[[.CamelName]]]"
fit="cover"
style="width: 80px; height: 80px"
/>
<span v-else>-</span>
[[- else if .IsRichText]]
<!-- 富文本:直接渲染 HTML 内容 -->
<div class="rich-text-content" v-html="formData.[[.CamelName]]"></div>
[[- else if eq .Type "BooleanField"]]
<!-- 布尔:用标签区分是与否 -->
<el-tag :type="formData.[[.CamelName]] ? 'success' : 'danger'">
{{ formData.[[.CamelName]] ? "是" : "否" }}
</el-tag>
[[- else if and (eq .Type "IntegerField") (.Choices)]]
<!-- 选项:按选项映射为可读文字 -->
<span>{{ getChoiceLabel('[[.CamelName]]', formData.[[.CamelName]]) }}</span>
[[- else]]
<!-- 普通文本:空值显示短横线 -->
{{ formData.[[.CamelName]] || '-' }}
[[- end]]
</el-descriptions-item>
[[- end]]
</el-descriptions>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogClose">关闭</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { get[[$.ModelClass]]Detail } from '@/api/[[$.ApiPath]]';
import { onMounted, reactive, watch } from 'vue';
[[- if $.HasImageField]]
import { ElImage } from 'element-plus';
[[- end]]
const emit = defineEmits(['update:visible']);
/**
* 表单数据
*
* 字段与 form_fields 一一对应,按类型初始化默认值:
* 字符串为空串、数字为 0、布尔为 false。
*/
const formData = reactive({
id: '',
[[- range .FormFields]]
[[.CamelName]]: [[if eq .Type "CharField"]]''[[else if eq .Type "IntegerField"]]0[[else if eq .Type "BooleanField"]]false[[else if eq .Type "FloatField"]]0[[else]]''[[end]],
[[- end]]
});
/**
* 组件接收的参数
*
* @property visible 弹窗是否显示,配合 update:visible 双向绑定
* @property [[$.PrimaryPk]] [[$.ModuleComment]]ID,为 0 时展示空数据
*/
const props = defineProps({
visible: {
type: Boolean,
required: true,
default: false,
},
[[$.PrimaryPk]]: {
type: Number,
required: true,
default: 0,
},
});
/**
* 获取选项字段的显示文字
*
* 按字段名与值从 choiceMap 中查找标签,
* 未匹配到时回退为原值,仍为空则返回短横线。
*
* @param fieldName 字段名(驼峰)
* @param value 字段值
* @returns 选项标签或原始值
*/
const getChoiceLabel = (fieldName: string, value: any) => {
const choiceMap: Record<string, Record<string, string>> = {
[[- range .FormFields]]
[[- if and (eq .Type "IntegerField") (.Choices)]]
[[.CamelName]]: {
[[- range .Choices]]
[[.Value]]: '[[.Label]]',
[[- end]]
},
[[- end]]
[[- end]]
};
return choiceMap[fieldName]?.[value] || value || '-';
};
/**
* 关闭弹窗
*
* 通过 update:visible 通知父组件隐藏。
*/
const dialogClose = () => {
emit('update:visible', false);
};
/**
* 加载详情数据
*
* 按主键请求详情接口,逐字段回填到 formData;
* 仅覆盖接口返回的非空字段,避免空值冲掉默认值。
* 请求失败时在控制台输出错误,不阻断页面。
*/
const setFormData = async () => {
try {
const data = await get[[$.ModelClass]]Detail(props.[[$.PrimaryPk]]);
for (const key in formData) {
if (data[key] != null && data[key] != undefined) {
//@ts-ignore
formData[key] = data[key];
}
}
} catch (error) {
console.error('获取[[$.ModuleComment]]详情失败', error);
}
};
/**
* 重置表单数据
*
* 按字段当前类型还原为初始值,用于无主键时清空展示。
*/
const resetFormData = () => {
for (const key in formData) {
//@ts-ignore
if (typeof formData[key] === 'string') {
//@ts-ignore
formData[key] = '';
} else if (typeof formData[key] === 'number') {
//@ts-ignore
formData[key] = 0;
} else if (typeof formData[key] === 'boolean') {
//@ts-ignore
formData[key] = false;
}
}
};
/**
* 监听弹窗显示状态
*
* 显示时:有主键则加载详情,无主键则重置为空数据。
* immediate 保证首次挂载即按当前状态处理。
*/
watch(
() => props.visible,
(newVal) => {
if (newVal && props.[[$.PrimaryPk]]) {
setFormData();
} else if (newVal && !props.[[$.PrimaryPk]]) {
resetFormData();
}
},
{ immediate: true },
);
/**
* 组件挂载后处理
*
* 若初始即为显示状态且带主键,则主动加载一次详情。
*/
onMounted(() => {
if (props.visible && props.[[$.PrimaryPk]]) {
setFormData();
}
});
</script>
<style lang="scss" scoped>
/* 标签列固定宽度 */
:deep(.des-width) {
width: 120px;
}
/* 长文本自动换行,避免撑破布局 */
:deep(.el-descriptions__body .el-descriptions__table .el-descriptions__cell) {
word-break: break-all;
}
/* 富文本容器:限高滚动,内嵌图片自适应宽度 */
.rich-text-content {
max-height: 300px;
overflow-y: auto;
border: 1px solid #e4e7ed;
padding: 10px;
border-radius: 4px;
background-color: #f8f9fa;
:deep(img) {
max-width: 100%;
}
}
</style>
+378
View File
@@ -0,0 +1,378 @@
<template>
<!-- [[$.ModuleComment]]新增/编辑弹窗(树形结构) -->
<el-dialog
v-model="props.visible"
:title="props.[[$.PrimaryPk]] ? '编辑' : '新增'"
width="600"
:close-on-click-modal="false"
:before-close="dialogClose"
>
<el-form ref="formRef" :model="formData" label-width="80px">
[[- range .FormFields]]
[[- if eq .CamelName $.ParentIdCamel]]
<!-- [[.Label]](上级选择) -->
<el-form-item
label="[[.Label]]"
prop="[[.CamelName]]"
[[- if .Required]]
:rules="{ required: true, message: '请选择[[.Label]]', trigger: 'change' }"
[[- end]]
>
<el-tree-select
class="flex-1"
v-model="formData.[[.CamelName]]"
:data="treeOptions"
clearable
node-key="[[$.PrimaryPk]]"
:props="{
label: '[[$.DisplayFieldCamel]]',
}"
:default-expand-all="true"
placeholder="请选择[[.Label]]"
check-strictly
/>
</el-form-item>
[[- else if .IsImage]]
<!-- [[.Label]](图片上传) -->
<el-form-item
label="[[.Label]]"
prop="[[.CamelName]]"
[[- if .Required]]
:rules="{ required: true, message: '请上传[[.Label]]', trigger: 'change' }"
[[- end]]
>
<UploadImg
@changeFileName="(name) => (formData.[[.CamelName]]ImgName = name)"
:fileType="['image/jpeg', 'image/png', 'image/jpg', 'image/gif', 'image/webp']"
name="[[$.AppName]]"
:fileSize="20"
v-model:image-url="formData.[[.CamelName]]"
>
<template #tip>支持扩展名: jpg png jpeg gif webp; 文件大小不超过20M</template>
</UploadImg>
</el-form-item>
[[- else if and (eq .Type "CharField") (.IsTextarea)]]
<!-- [[.Label]](多行文本) -->
<el-form-item
label="[[.Label]]"
prop="[[.CamelName]]"
[[- if .Required]]
:rules="{ required: true, message: '请输入[[.Label]]', trigger: 'blur' }"
[[- end]]
>
<el-input
class="flex-1"
v-model="formData.[[.CamelName]]"
type="textarea"
placeholder="请输入[[.Label]]"
:rows="3"
clearable
/>
</el-form-item>
[[- else if eq .Type "CharField"]]
<!-- [[.Label]](单行文本) -->
<el-form-item
label="[[.Label]]"
prop="[[.CamelName]]"
[[- if .Required]]
:rules="{ required: true, message: '请输入[[.Label]]', trigger: 'blur' }"
[[- end]]
>
<el-input v-model="formData.[[.CamelName]]" placeholder="请输入[[.Label]]" clearable />
</el-form-item>
[[- else if and (eq .Type "IntegerField") (.Choices)]]
<!-- [[.Label]]([[if le (len .Choices) 3]]单选[[else]]下拉选择[[end]]) -->
<el-form-item
label="[[.Label]]"
prop="[[.CamelName]]"
[[- if .Required]]
:rules="{ required: true, message: '请选择[[.Label]]', trigger: 'change' }"
[[- end]]
>
[[- if le (len .Choices) 3]]
<el-radio-group v-model="formData.[[.CamelName]]">
[[- range .Choices]]
<el-radio :value="[[.Value]]">[[.Label]]</el-radio>
[[- end]]
</el-radio-group>
[[- else]]
<el-select
v-model="formData.[[.CamelName]]"
placeholder="请选择[[.Label]]"
clearable
filterable
>
<el-option
v-for="item in optionsData.[[.CamelName]]List"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
[[- end]]
</el-form-item>
[[- else if and (eq .Type "IntegerField") (eq .CamelName "sort")]]
<!-- [[.Label]](排序) -->
<el-form-item
label="[[.Label]]"
prop="[[.CamelName]]"
[[- if .Required]]
:rules="{ required: true, message: '请输入[[.Label]]', trigger: 'blur' }"
[[- end]]
>
<div>
<el-input-number v-model="formData.[[.CamelName]]" :min="[[.MinValue]]" :max="[[.MaxValue]]" placeholder="请输入[[.Label]]" />
<div class="form-tips">数值越小越排前</div>
</div>
</el-form-item>
[[- else if and (eq .Type "IntegerField") (not .Choices)]]
<!-- [[.Label]](数字输入) -->
<el-form-item
label="[[.Label]]"
prop="[[.CamelName]]"
[[- if .Required]]
:rules="{ required: true, message: '请输入[[.Label]]', trigger: 'blur' }"
[[- end]]
>
<el-input-number v-model="formData.[[.CamelName]]" :min="[[.MinValue]]" :max="[[.MaxValue]]" />
</el-form-item>
[[- else if eq .Type "BooleanField"]]
<!-- [[.Label]](开关) -->
<el-form-item label="[[.Label]]" prop="[[.CamelName]]">
<el-switch v-model="formData.[[.CamelName]]" :active-value="1" :inactive-value="0" />
</el-form-item>
[[- else if eq .Type "DateField"]]
<!-- [[.Label]](日期选择) -->
<el-form-item
label="[[.Label]]"
prop="[[.CamelName]]"
[[- if .Required]]
:rules="{ required: true, message: '请选择[[.Label]]', trigger: 'change' }"
[[- end]]
>
<el-date-picker
v-model="formData.[[.CamelName]]"
type="date"
placeholder="请选择[[.Label]]"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
/>
</el-form-item>
[[- else if eq .Type "DateTimeField"]]
<!-- [[.Label]](日期时间选择) -->
<el-form-item
label="[[.Label]]"
prop="[[.CamelName]]"
[[- if .Required]]
:rules="{ required: true, message: '请选择[[.Label]]', trigger: 'change' }"
[[- end]]
>
<el-date-picker
v-model="formData.[[.CamelName]]"
type="datetime"
placeholder="请选择[[.Label]]"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
[[- else if eq .Type "TextField"]]
<!-- [[.Label]](文本域) -->
<el-form-item
label="[[.Label]]"
prop="[[.CamelName]]"
[[- if .Required]]
:rules="{ required: true, message: '请输入[[.Label]]', trigger: 'blur' }"
[[- end]]
>
<el-input class="flex-1" type="textarea" v-model="formData.[[.CamelName]]" placeholder="请输入[[.Label]]" :rows="3" />
</el-form-item>
[[- else if eq .Type "DecimalField"]]
<!-- [[.Label]](小数输入) -->
<el-form-item
label="[[.Label]]"
prop="[[.CamelName]]"
[[- if .Required]]
:rules="{ required: true, message: '请输入[[.Label]]', trigger: 'blur' }"
[[- end]]
>
<el-input-number
v-model="formData.[[.CamelName]]"
:min="[[.MinValue]]"
:max="[[.MaxValue]]"
:precision="2"
:step="0.1"
/>
</el-form-item>
[[- end]]
[[- end]]
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogClose">取消</el-button>
<el-button :loading="subLoading" type="primary" @click="submit"> 确定 </el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import type { FormInstance } from 'element-plus';
import {
[[$.ModuleCamel]]Add,
[[$.ModuleCamel]]Update,
get[[$.ModelClass]]List,
get[[$.ModelClass]]Detail,
} from '@/api/[[$.ApiPath]]';
import { onMounted, reactive, ref, shallowRef, watch } from 'vue';
import { message, buildTree } from '@/utils/auth';
import { useLockFn } from '@/utils/useLockFn';
[[- if $.HasImageField]]
import UploadImg from '@/components/Upload/Image.vue';
[[- end]]
const emit = defineEmits(['success', 'update:visible']);
const formRef = shallowRef<FormInstance>();
// 树形选项数据,供上级选择器使用
const treeOptions = ref<any[]>([]);
/**
* 下拉选项数据
*
* 仅当选项数超过 3 个时生成,供 el-select 使用;
* 选项不超过 3 个的字段在模板中直接用 el-radio 渲染,不在此维护。
*/
const optionsData = reactive({
[[- range .FormFields]]
[[- if and (eq .Type "IntegerField") (.Choices) (gt (len .Choices) 3)]]
[[.CamelName]]List: [
[[- range .Choices]]
{
id: [[if eq .ValueType "string"]]'[[.Value]]'[[else]][[.Value]][[end]],
name: '[[.Label]]',
},
[[- end]]
],
[[- end]]
[[- end]]
});
/**
* 表单数据
*/
const formData = reactive({
id: '',
[[- range .FormFields]]
[[.CamelName]]: [[if or (eq .Type "IntegerField") (eq .Type "BooleanField") (eq .Type "FloatField") (eq .Type "DecimalField")]][[.Default]][[else]]''[[end]],
[[- end]]
[[- range .FormFields]]
[[- if .IsImage]]
[[.CamelName]]ImgName: '',
[[- end]]
[[- end]]
});
/**
* 定义接收的参数
*/
const props = defineProps({
visible: {
type: Boolean,
required: true,
default: false,
},
[[$.PrimaryPk]]: {
type: Number,
required: true,
default: 0,
},
parentId: {
type: Number,
default: 0,
},
});
const dialogClose = () => {
emit('update:visible', false);
};
/**
* 加载树形选项数据
*
* 请求全量列表后通过 buildTree 组装为树,
* 并在根节点前置一个"顶级"虚拟节点(id=0),
* 使新增时可将父级设为顶级。
*/
const get[[$.ModelClass]]Tree = async () => {
const data: any = await get[[$.ModelClass]]List();
const tree: any = [{ id: 0, [[$.DisplayFieldCamel]]: '顶级', children: [] }];
const lists = buildTree(data);
tree[0].children.push(...lists);
treeOptions.value = tree;
};
/**
* 提交表单
*/
const handleSubmit = async () => {
await formRef.value?.validate();
props.[[$.PrimaryPk]] ? await [[$.ModuleCamel]]Update(formData) : await [[$.ModuleCamel]]Add(formData);
message('操作成功');
emit('update:visible', false);
emit('success');
};
const { isLock: subLoading, lockFn: submit } = useLockFn(handleSubmit);
/**
* 设置表单数据
*/
const setFormData = (data: Record<any, any>) => {
for (const key in formData) {
if (data[key] != null && data[key] != undefined) {
formData[key] = data[key];
}
}
};
/**
* 获取详情
*/
const getDetail = async () => {
const data = await get[[$.ModelClass]]Detail(props.[[$.PrimaryPk]]);
setFormData(data);
};
/**
* 重置表单
*/
const resetForm = () => {
if (formRef.value) {
formRef.value.resetFields();
}
formData.[[$.ParentIdCamel]] = props.parentId || 0;
};
onMounted(() => {
get[[$.ModelClass]]Tree();
if (props.[[$.PrimaryPk]]) {
getDetail();
} else {
resetForm();
}
});
watch(
() => props.visible,
(newVal) => {
if (newVal && props.[[$.PrimaryPk]]) {
getDetail();
} else if (newVal && !props.[[$.PrimaryPk]]) {
resetForm();
get[[$.ModelClass]]Tree();
}
},
);
</script>
<style lang="scss" scoped></style>
+303
View File
@@ -0,0 +1,303 @@
<template>
<!-- [[$.ModuleComment]]列表页(树形结构) -->
<div class="[[$.AppName]]-index">
<!-- 顶部工具栏 -->
<el-card :bordered="false" class="pt-3 mb-3 proCard">
<div>
<!-- 新增顶级节点 -->
<el-button type="primary" @click="handleAdd()" v-perm="['[[$.PermissionPrefix]]:add']">
<template #icon>
<el-icon>
<plus />
</el-icon>
</template>
新增[[$.ModuleComment]]
</el-button>
<!-- 展开/折叠全部节点 -->
<el-button @click="handleExpand"> 展开/折叠</el-button>
[[- if $.HasExport]]
<!-- 导出 -->
<el-button type="" @click="handleExport" :loading="exportLoading" :disabled="exportLoading" v-perm="['[[$.PermissionPrefix]]:export']">
<template #icon>
<el-icon><Download /></el-icon>
</template>
导出
</el-button>
[[- end]]
</div>
</el-card>
<!-- 树形表格 -->
<el-card :bordered="false" class="pt-3 mb-3 proCard">
<el-table
border
v-loading="loading"
ref="tableRef"
:data="tableData"
row-key="[[$.PrimaryPk]]"
:height="tableHeight"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
>
[[- range $field := .ListFields]]
[[- if $field.Choices]]
<!-- [[.Label]](选项标签) -->
<el-table-column
label="[[.Label]]"
prop="[[.CamelName]]"
align="center"
min-width="100"
>
<template #default="{ row }">
[[- range $i, $choice := .Choices]]
<el-tag [[if eq $i 0]]v-if="row.[[$field.CamelName]] == [[.Value]]"[[end]]
[[if gt $i 0]]v-else-if="row.[[$field.CamelName]] == [[.Value]]"[[end]]
[[if .Type]]type="[[.Type]]"[[end]]>[[.Label]]</el-tag>
[[- end]]
</template>
</el-table-column>
[[- else if .IsImage]]
<!-- [[.Label]](图片,支持点击预览) -->
<el-table-column
label="[[.Label]]"
prop="[[.CamelName]]"
align="center"
min-width="100"
>
<template #default="{ row }">
<el-image
v-if="row.[[.CamelName]]"
:src="row.[[.CamelName]]"
:preview-src-list="[row.[[.CamelName]]]"
fit="cover"
style="width: 40px; height: 40px; border-radius: 4px;"
/>
<span v-else>-</span>
</template>
</el-table-column>
[[- else]]
<!-- [[.Label]](普通文本,超长省略) -->
<el-table-column
label="[[.Label]]"
prop="[[.CamelName]]"
min-width="150"
[[- if ne .CamelName $.DisplayFieldCamel]]
align="center"
[[- end]]
show-overflow-tooltip
/>
[[- end]]
[[- end]]
<!-- 操作栏:含新增子级、编辑、详情、删除 -->
<el-table-column align="center" label="操作" width="400" fixed="right">
<template #default="{ row }">
<el-button
type="primary"
icon="Plus"
@click="handleAdd(row.id)"
v-perm="['[[$.PermissionPrefix]]:add']"
>
新增
</el-button>
<el-button
type="warning"
icon="Edit"
@click="handleEdit(row)"
v-perm="['[[$.PermissionPrefix]]:update']"
>
编辑
</el-button>
<el-button
icon="View"
@click="handleDetail(row)"
v-perm="['[[$.PermissionPrefix]]:detail']"
>
详情
</el-button>
<el-button
type="danger"
icon="Delete"
@click="handleDelete(row)"
v-perm="['[[$.PermissionPrefix]]:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 新增/编辑弹窗,parentId 用于新增时预选上级 -->
<editDialog
ref="editRef"
v-if="editVisible"
:[[$.PrimaryPk]]="[[$.PrimaryPk]]Value"
:parentId="parentId"
v-model:visible="editVisible"
@success="getLists"
/>
<!-- 详情弹窗 -->
<detailDialog
v-if="detailVisible"
:[[$.PrimaryPk]]="[[$.PrimaryPk]]Value"
v-model:visible="detailVisible"
@success="getLists"
/>
</div>
</template>
<script lang="ts" setup name="[[$.ModelClass]]Index">
import { defineAsyncComponent, nextTick, onMounted, ref, shallowRef } from 'vue';
import { get[[$.ModelClass]]List, [[$.ModuleCamel]]Delete[[if $.HasExport]], [[$.ModuleCamel]]Export[[end]] } from '@/api/[[$.ApiPath]]';
import type { ElTable } from 'element-plus';
import { confirm, message, buildTree } from '@/utils/auth';
// 动态导入编辑对话框组件
const editDialog = defineAsyncComponent(() => import('./edit.vue'));
// 动态导入详情对话框组件
const detailDialog = defineAsyncComponent(() => import('./detail.vue'));
// 表格引用
const tableRef = shallowRef<InstanceType<typeof ElTable>>();
// 表格高度,按视口高度减去固定区域动态计算
const tableHeight = document.body.clientHeight - 320;
// 响应式状态
const isExpand = ref(false); // 当前展开状态,供"展开/折叠"切换
const loading = ref(false); // 表格加载状态
const editVisible = ref(false); // 编辑弹窗显示状态
const detailVisible = ref(false); // 详情对话框显示状态
[[- if $.HasExport]]
const exportLoading = ref(false); // 导出按钮加载状态
[[- end]]
const [[$.PrimaryPk]]Value = ref(0); // 当前操作的主键值
const parentId = ref(0); // 新增子级时的父级 ID
const tableData = ref([]); // 表格数据(树形结构)
/**
* 加载数据列表
*
* 请求全量列表后通过 buildTree 组装为树,
* 赋值给 tableData 供树形表格渲染。
* 无论成功失败都会关闭 loading。
*/
const getLists = async () => {
loading.value = true;
try {
const data = await get[[$.ModelClass]]List();
tableData.value = buildTree(data);
loading.value = false;
} catch (error) {
loading.value = false;
}
};
/**
* 打开新增弹窗
*
* 不传 parentIdParam 时新增顶级节点,
* 传入时新增该节点的子级(父级预选)。
*
* @param parentIdParam 上级ID(可选)
*/
const handleAdd = async (parentIdParam?: number) => {
[[$.PrimaryPk]]Value.value = 0;
parentId.value = parentIdParam || 0;
await nextTick();
editVisible.value = true;
};
/**
* 打开编辑弹窗
*
* @param row 当前行数据
*/
const handleEdit = async (row: any) => {
[[$.PrimaryPk]]Value.value = row.id;
await nextTick();
editVisible.value = true;
};
/**
* 打开详情弹窗
*
* @param row 当前行数据
*/
const handleDetail = async (row: any) => {
[[$.PrimaryPk]]Value.value = row.id;
await nextTick();
detailVisible.value = true;
};
/**
* 删除数据
*
* 先弹出确认框,确认后调用删除接口并刷新列表;
* 失败时恢复 loading 状态。
*
* @param row 当前行数据
*/
const handleDelete = async (row: any) => {
await confirm('确定要删除?');
try {
loading.value = true;
await [[$.ModuleCamel]]Delete(row.id);
message('删除成功');
getLists();
} catch (e) {
loading.value = false;
}
};
/**
* 切换全部节点的展开/折叠
*
* 翻转 isExpand 后递归调用 toggleExpand。
*/
const handleExpand = () => {
isExpand.value = !isExpand.value;
toggleExpand(tableData.value, isExpand.value);
};
/**
* 递归展开/折叠所有节点
*
* 借助 el-table 的 toggleRowExpansion 控制每一行,
* 若该行仍有 children 则继续递归。
*
* @param children 子级数据
* @param unfold 是否展开
*/
const toggleExpand = (children: any[], unfold = true) => {
for (const key in children) {
tableRef.value?.toggleRowExpansion(children[key], unfold);
if (children[key].children) {
toggleExpand(children[key].children!, unfold);
}
}
};
/**
* 组件挂载后加载列表
*/
onMounted(() => {
getLists();
});
[[- if $.HasExport]]
/**
* 导出数据
*
* 树形结构导出不带筛选条件,拿到文件 URL 后新窗口打开;
* 导出期间按钮保持 loading,避免重复点击。
*/
const handleExport = async () => {
exportLoading.value = true;
const data = await [[$.ModuleCamel]]Export({});
window.open(data);
exportLoading.value = false;
message('导出成功');
};
[[- end]]
</script>
<style scoped></style>