签入版本
This commit is contained in:
@@ -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?: Record<string, any>) {
|
||||
return http.request({
|
||||
url: '/[[$.RoutePrefix]]/page',
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取全部[[$.ModuleComment]]列表
|
||||
*
|
||||
* @param params 查询参数
|
||||
* @returns [[$.ModuleComment]]列表
|
||||
*/
|
||||
export function get[[$.ModelClass]]List(params?: Record<string, any>) {
|
||||
return http.request({
|
||||
url: '/[[$.RoutePrefix]]/list',
|
||||
method: 'GET',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 ID 获取[[$.ModuleComment]]详情
|
||||
*
|
||||
* @param [[$.PrimaryPk]] [[$.ModuleComment]]ID
|
||||
* @returns [[$.ModuleComment]]详情数据
|
||||
*/
|
||||
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]]
|
||||
@@ -0,0 +1,82 @@
|
||||
import { h } from 'vue';
|
||||
import { ElAvatar, ElTag } from 'element-plus';
|
||||
|
||||
/**
|
||||
* [[$.ModuleComment]]表格列配置
|
||||
*
|
||||
* 定义列表页表格的显示列,包含多选框、ID、各业务字段及审计字段。
|
||||
* 字段按类型自动选择渲染方式:图片用 ElAvatar、选项用 ElTag、
|
||||
* 链接用 a 标签,其余为普通文本。
|
||||
*/
|
||||
export const columns = [
|
||||
{
|
||||
type: 'selection', // 多选框列
|
||||
},
|
||||
{
|
||||
label: 'ID',
|
||||
prop: 'id',
|
||||
fixed: 'left',
|
||||
width: 50,
|
||||
},
|
||||
[[- range .ListFields]]
|
||||
{
|
||||
label: '[[.Label]]',
|
||||
prop: '[[.CamelName]]',
|
||||
minWidth: [[if eq .Type "CharField"]]120[[else if eq .Type "IntegerField"]]100[[else if eq .Type "DateTimeField"]]180[[else]]120[[end]],
|
||||
[[- if .IsImage]]
|
||||
// 图片渲染:以方形头像形式展示
|
||||
render(record) {
|
||||
return h(ElAvatar, {
|
||||
size: 48,
|
||||
src: record.row.[[.CamelName]],
|
||||
shape: 'square',
|
||||
fit: 'fill',
|
||||
});
|
||||
},
|
||||
[[- else if .Choices]]
|
||||
// 选项标签渲染:按选项值映射文字与标签颜色
|
||||
render(record) {
|
||||
const statusMap = {
|
||||
[[- range .Choices]]
|
||||
[[.Value]]: { text: '[[.Label]]', type: '[[.Type]]' },
|
||||
[[- end]]
|
||||
};
|
||||
const status = statusMap[record.row.[[.CamelName]]];
|
||||
return h(
|
||||
ElTag,
|
||||
{
|
||||
type: status?.type || 'info',
|
||||
},
|
||||
{
|
||||
default: () => status?.text || record.row.[[.CamelName]],
|
||||
},
|
||||
);
|
||||
},
|
||||
[[- else if and (eq .Type "CharField") ((isUrlField .CamelName))]]
|
||||
// 链接渲染:新窗口打开
|
||||
render(record) {
|
||||
return h(
|
||||
'a',
|
||||
{
|
||||
href: record.row.[[.CamelName]],
|
||||
target: '_blank',
|
||||
},
|
||||
'点击查看',
|
||||
);
|
||||
},
|
||||
[[- else]]
|
||||
// 普通文本渲染
|
||||
[[- end]]
|
||||
},
|
||||
[[- end]]
|
||||
{
|
||||
label: '创建人',
|
||||
prop: 'createUser',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
label: '创建时间',
|
||||
prop: 'createTime',
|
||||
width: 180,
|
||||
},
|
||||
];
|
||||
@@ -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>
|
||||
@@ -0,0 +1,410 @@
|
||||
<template>
|
||||
<!-- [[$.ModuleComment]]新增/编辑弹窗 -->
|
||||
<el-dialog
|
||||
v-model="props.visible"
|
||||
:title="props.[[$.PrimaryPk]] ? '编辑[[$.ModuleComment]]' : '新增[[$.ModuleComment]]'"
|
||||
[[- if $.HasRichTextField]]
|
||||
width="850"
|
||||
[[- else]]
|
||||
width="500"
|
||||
[[- end]]
|
||||
:close-on-click-modal="false"
|
||||
:before-close="dialogClose"
|
||||
>
|
||||
<!-- 表单 -->
|
||||
<el-form class="ls-form" ref="formRef" :model="formData" label-width="80px">
|
||||
[[- range .FormFields]]
|
||||
[[- if .IsImage]]
|
||||
<!-- [[.Comment]](图片上传) -->
|
||||
<el-form-item
|
||||
label="[[.Label]]"
|
||||
prop="[[.CamelName]]"
|
||||
[[- if .Required]]
|
||||
:rules="{ required: true, message: '请上传[[.Comment]]', 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 .IsRichText]]
|
||||
<!-- [[.Comment]](富文本编辑器) -->
|
||||
<el-form-item
|
||||
label="[[.Label]]"
|
||||
prop="[[.CamelName]]"
|
||||
[[- if .Required]]
|
||||
:rules="{ required: true, message: '请输入[[.Comment]]', trigger: 'blur' }"
|
||||
[[- end]]
|
||||
>
|
||||
<div class="flex">
|
||||
<el-form-item class="flex-1" label-width="0">
|
||||
<Editor
|
||||
ref="[[.CamelName]]EditorRef"
|
||||
:height="editorHeight"
|
||||
class="flex-1"
|
||||
name="[[$.AppName]]"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form-item>
|
||||
[[- else if and (eq .Type "CharField") ((containsAny .Comment "备注" "介绍" "描述" "说明"))]]
|
||||
<!-- [[.Comment]](多行文本) -->
|
||||
<el-form-item
|
||||
label="[[.Label]]"
|
||||
prop="[[.CamelName]]"
|
||||
[[- if .Required]]
|
||||
:rules="{ required: true, message: '请输入[[.Comment]]', trigger: 'blur' }"
|
||||
[[- end]]
|
||||
>
|
||||
<el-input type="textarea" v-model="formData.[[.CamelName]]" placeholder="请输入[[.Comment]]" :rows="3" clearable />
|
||||
</el-form-item>
|
||||
[[- else if eq .Type "CharField"]]
|
||||
<!-- [[.Comment]](单行文本) -->
|
||||
<el-form-item
|
||||
label="[[.Label]]"
|
||||
prop="[[.CamelName]]"
|
||||
[[- if .Required]]
|
||||
:rules="{ required: true, message: '请输入[[.Comment]]', trigger: 'blur' }"
|
||||
[[- end]]
|
||||
>
|
||||
<el-input class="ls-input" v-model="formData.[[.CamelName]]" placeholder="请输入[[.Comment]]" clearable />
|
||||
</el-form-item>
|
||||
[[- else if and (eq .Type "IntegerField") (.Choices)]]
|
||||
<!-- [[.Comment]]([[if le (len .Choices) 3]]单选[[else]]下拉选择[[end]]) -->
|
||||
<el-form-item
|
||||
label="[[.Label]]"
|
||||
prop="[[.CamelName]]"
|
||||
[[- if .Required]]
|
||||
:rules="{ required: true, message: '请选择[[.Comment]]', 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="请选择[[.Comment]]"
|
||||
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") (not .Choices)]]
|
||||
<!-- [[.Comment]](数字输入) -->
|
||||
<el-form-item
|
||||
label="[[.Label]]"
|
||||
prop="[[.CamelName]]"
|
||||
[[- if .Required]]
|
||||
:rules="{ required: true, message: '请输入[[.Comment]]', trigger: 'blur' }"
|
||||
[[- end]]
|
||||
>
|
||||
<el-input-number v-model="formData.[[.CamelName]]" :min="[[.MinValue]]" :max="[[.MaxValue]]" />
|
||||
</el-form-item>
|
||||
[[- else if eq .Type "BooleanField"]]
|
||||
<!-- [[.Comment]](开关) -->
|
||||
<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"]]
|
||||
<!-- [[.Comment]](日期选择) -->
|
||||
<el-form-item
|
||||
label="[[.Label]]"
|
||||
prop="[[.CamelName]]"
|
||||
[[- if .Required]]
|
||||
:rules="{ required: true, message: '请选择[[.Comment]]', trigger: 'change' }"
|
||||
[[- end]]
|
||||
>
|
||||
<el-date-picker
|
||||
v-model="formData.[[.CamelName]]"
|
||||
type="date"
|
||||
placeholder="请选择[[.Comment]]"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
/>
|
||||
</el-form-item>
|
||||
[[- else if eq .Type "DateTimeField"]]
|
||||
<!-- [[.Comment]](日期时间选择) -->
|
||||
<el-form-item
|
||||
label="[[.Label]]"
|
||||
prop="[[.CamelName]]"
|
||||
[[- if .Required]]
|
||||
:rules="{ required: true, message: '请选择[[.Comment]]', trigger: 'change' }"
|
||||
[[- end]]
|
||||
>
|
||||
<el-date-picker
|
||||
v-model="formData.[[.CamelName]]"
|
||||
type="datetime"
|
||||
placeholder="请选择[[.Comment]]"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
/>
|
||||
</el-form-item>
|
||||
[[- else if eq .Type "TextField"]]
|
||||
<!-- [[.Comment]](文本域) -->
|
||||
<el-form-item
|
||||
label="[[.Label]]"
|
||||
prop="[[.CamelName]]"
|
||||
[[- if .Required]]
|
||||
:rules="{ required: true, message: '请输入[[.Comment]]', trigger: 'blur' }"
|
||||
[[- end]]
|
||||
>
|
||||
<el-input type="textarea" v-model="formData.[[.CamelName]]" placeholder="请输入[[.Comment]]" :rows="3" />
|
||||
</el-form-item>
|
||||
[[- else if or (eq .Type "FloatField") (eq .Type "DecimalField")]]
|
||||
<!-- [[.Comment]](小数输入) -->
|
||||
<el-form-item
|
||||
label="[[.Label]]"
|
||||
prop="[[.CamelName]]"
|
||||
[[- if .Required]]
|
||||
:rules="{ required: true, message: '请输入[[.Comment]]', 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 { get[[$.ModelClass]]Detail, [[$.ModuleCamel]]Add, [[$.ModuleCamel]]Update } from '@/api/[[$.ApiPath]]';
|
||||
import { onMounted, reactive, shallowRef, watch } from 'vue';
|
||||
import { message } from '@/utils/auth';
|
||||
import { useLockFn } from '@/utils/useLockFn';
|
||||
[[- if $.HasImageField]]
|
||||
import UploadImg from '@/components/Upload/Image.vue';
|
||||
[[- end]]
|
||||
[[- if $.HasRichTextField]]
|
||||
import Editor from '@/components/Editor/tinymce.vue';
|
||||
[[- end]]
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits(['success', 'update:visible']);
|
||||
|
||||
// 表单引用
|
||||
const formRef = shallowRef<FormInstance>();
|
||||
|
||||
[[- if $.HasRichTextField]]
|
||||
// 富文本编辑器高度,按视口高度动态计算
|
||||
const editorHeight = document.body.clientHeight - 400;
|
||||
|
||||
// 富文本编辑器引用
|
||||
[[- range .FormFields]]
|
||||
[[- if .IsRichText]]
|
||||
const [[.CamelName]]EditorRef = ref();
|
||||
[[- end]]
|
||||
[[- end]]
|
||||
[[- end]]
|
||||
|
||||
/**
|
||||
* 下拉选项数据
|
||||
*
|
||||
* 仅当选项数超过 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]]
|
||||
});
|
||||
|
||||
/**
|
||||
* 表单数据
|
||||
*
|
||||
* 字段与 form_fields 一一对应,按类型初始化默认值;
|
||||
* 图片字段额外维护一个 {字段名}ImgName,用于回显原始文件名。
|
||||
*/
|
||||
const formData = reactive({
|
||||
id: '', // ID字段,新增时为0
|
||||
[[- 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: '', // [[.Comment]]文件名
|
||||
[[- 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,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 提交表单
|
||||
*
|
||||
* 处理流程:
|
||||
* 1. 同步富文本编辑器内容到 formData;
|
||||
* 2. 触发表单校验,未通过则中断;
|
||||
* 3. 按主键是否存在决定调用新增或更新接口;
|
||||
* 4. 成功后提示并通知父组件刷新列表、关闭弹窗。
|
||||
*/
|
||||
const handleSubmit = async () => {
|
||||
// 获取富文本编辑器内容
|
||||
[[- range .FormFields]]
|
||||
[[- if .IsRichText]]
|
||||
if ([[.CamelName]]EditorRef.value) {
|
||||
formData.[[.CamelName]] = [[.CamelName]]EditorRef.value.myValue;
|
||||
}
|
||||
[[- end]]
|
||||
[[- end]]
|
||||
|
||||
// 表单验证
|
||||
await formRef.value?.validate();
|
||||
|
||||
// 调用接口
|
||||
props.[[$.PrimaryPk]] ? await [[$.ModuleCamel]]Update(formData) : await [[$.ModuleCamel]]Add(formData);
|
||||
|
||||
message('操作成功');
|
||||
emit('update:visible', false);
|
||||
emit('success');
|
||||
};
|
||||
|
||||
/**
|
||||
* 关闭弹窗
|
||||
*
|
||||
* 通过 update:visible 通知父组件隐藏。
|
||||
*/
|
||||
const dialogClose = () => {
|
||||
emit('update:visible', false);
|
||||
};
|
||||
|
||||
// 防抖处理,防止重复提交
|
||||
const { isLock: subLoading, lockFn: submit } = useLockFn(handleSubmit);
|
||||
|
||||
/**
|
||||
* 加载详情数据
|
||||
*
|
||||
* 按主键请求详情,逐字段回填到 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];
|
||||
}
|
||||
}
|
||||
// 设置富文本编辑器内容
|
||||
[[- range .FormFields]]
|
||||
[[- if .IsRichText]]
|
||||
if ([[.CamelName]]EditorRef.value && formData.[[.CamelName]]) {
|
||||
[[.CamelName]]EditorRef.value.myValue = formData.[[.CamelName]];
|
||||
}
|
||||
[[- end]]
|
||||
[[- end]]
|
||||
} catch (error) {
|
||||
console.error('获取[[$.ModuleComment]]详情失败', error);
|
||||
message('获取数据失败', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 重置表单
|
||||
*
|
||||
* 调用 el-form 的 resetFields 清空校验状态与字段值,
|
||||
* 并同步清空富文本编辑器内容。
|
||||
*/
|
||||
const resetForm = () => {
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields();
|
||||
}
|
||||
// 清空富文本编辑器内容
|
||||
[[- range .FormFields]]
|
||||
[[- if .IsRichText]]
|
||||
if ([[.CamelName]]EditorRef.value) {
|
||||
[[.CamelName]]EditorRef.value.myValue = '';
|
||||
}
|
||||
[[- end]]
|
||||
[[- end]]
|
||||
};
|
||||
|
||||
/**
|
||||
* 组件挂载后处理
|
||||
*
|
||||
* 编辑场景加载详情,新增场景重置表单。
|
||||
*/
|
||||
onMounted(() => {
|
||||
if (props.[[$.PrimaryPk]]) {
|
||||
setFormData();
|
||||
} else {
|
||||
resetForm();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 监听弹窗显示状态
|
||||
*
|
||||
* 显示时:编辑场景加载详情,新增场景重置表单。
|
||||
*/
|
||||
watch(
|
||||
() => props.visible,
|
||||
(newVal) => {
|
||||
if (newVal && props.[[$.PrimaryPk]]) {
|
||||
setFormData();
|
||||
} else if (newVal && !props.[[$.PrimaryPk]]) {
|
||||
resetForm();
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
@@ -0,0 +1,337 @@
|
||||
<template>
|
||||
<PageWrapper>
|
||||
<!-- 搜索表单卡片 -->
|
||||
<el-card :bordered="false" class="pt-3 mb-3 proCard">
|
||||
<BasicForm @register="register" @submit="handleSubmit" @reset="handleReset" />
|
||||
</el-card>
|
||||
|
||||
<!-- 数据表格卡片 -->
|
||||
<el-card :bordered="false" class="proCard">
|
||||
<BasicTable
|
||||
:columns="columns"
|
||||
:request="loadDataTable"
|
||||
:row-key="(row) => row.id"
|
||||
ref="tableRef"
|
||||
:actionColumn="actionColumn"
|
||||
@selection-change="onSelectionChange"
|
||||
>
|
||||
<!-- 表格顶部工具栏 -->
|
||||
<template #tableTitle>
|
||||
<!-- 新增按钮 -->
|
||||
<el-button type="primary" @click="handleAdd" v-perm="['[[$.PermissionPrefix]]:add']">
|
||||
<template #icon>
|
||||
<el-icon class="el-input__icon">
|
||||
<PlusOutlined />
|
||||
</el-icon>
|
||||
</template>
|
||||
添加[[$.ModuleComment]]
|
||||
</el-button>
|
||||
|
||||
<!-- 批量删除按钮 -->
|
||||
<el-button
|
||||
type="danger"
|
||||
@click="handleDelete()"
|
||||
:disabled="!selectionData.length"
|
||||
v-perm="['[[$.PermissionPrefix]]:batchDelete']"
|
||||
>
|
||||
<template #icon>
|
||||
<el-icon class="el-input__icon">
|
||||
<Delete />
|
||||
</el-icon>
|
||||
</template>
|
||||
批量删除
|
||||
</el-button>
|
||||
[[- if $.HasImport]]
|
||||
<!-- 导入按钮 -->
|
||||
<el-button type="" @click="importVisible = true" v-perm="['[[$.PermissionPrefix]]:import']">
|
||||
<template #icon>
|
||||
<el-icon class="el-input__icon">
|
||||
<Upload />
|
||||
</el-icon>
|
||||
</template>
|
||||
导入
|
||||
</el-button>
|
||||
[[- end]]
|
||||
[[- if $.HasExport]]
|
||||
<!-- 导出按钮 -->
|
||||
<el-button
|
||||
type=""
|
||||
@click="handleExport"
|
||||
:loading="exportLoading"
|
||||
:disabled="exportLoading"
|
||||
v-perm="['[[$.PermissionPrefix]]:export']"
|
||||
>
|
||||
<template #icon>
|
||||
<el-icon class="el-input__icon">
|
||||
<Download />
|
||||
</el-icon>
|
||||
</template>
|
||||
导出
|
||||
</el-button>
|
||||
[[- end]]
|
||||
</template>
|
||||
</BasicTable>
|
||||
</el-card>
|
||||
|
||||
<!-- 新增/编辑弹窗,主键为 0 时表示新增 -->
|
||||
<editDialog
|
||||
v-if="editVisible"
|
||||
:[[$.PrimaryPk]]="currentId"
|
||||
v-model:visible="editVisible"
|
||||
@success="reloadTable('noRefresh')"
|
||||
/>
|
||||
<!-- 详情弹窗 -->
|
||||
<detailDialog
|
||||
v-if="detailVisible"
|
||||
:[[$.PrimaryPk]]="currentId"
|
||||
v-model:visible="detailVisible"
|
||||
@success="reloadTable('noRefresh')"
|
||||
/>
|
||||
[[- if $.HasImport]]
|
||||
<!-- 导入弹窗,templateCode 对应后端模板编码 -->
|
||||
<fileUpload
|
||||
v-if="importVisible"
|
||||
v-model:visible="importVisible"
|
||||
api="/api/[[$.RoutePrefix]]/import"
|
||||
templateCode="[[$.Module]]_import"
|
||||
dialogTitle="导入[[$.ModuleComment]]"
|
||||
@success="reloadTable()"
|
||||
/>
|
||||
[[- end]]
|
||||
</PageWrapper>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, h, nextTick, defineAsyncComponent } from 'vue';
|
||||
import { ColProps } from 'element-plus';
|
||||
import { schemas } from './querySchemas';
|
||||
import { useForm } from '@/components/Form/index';
|
||||
import { TableAction } from '@/components/Table';
|
||||
import { get[[$.ModelClass]]Page, [[$.ModuleCamel]]Delete, [[$.ModuleCamel]]BatchDelete[[if $.HasExport]], [[$.ModuleCamel]]Export[[end]] } from '@/api/[[$.ApiPath]]';
|
||||
import { columns } from './columns';
|
||||
import { PlusOutlined } from '@vicons/antd';
|
||||
import { message, confirm } from '@/utils/auth';
|
||||
|
||||
// 动态导入编辑对话框组件
|
||||
const editDialog = defineAsyncComponent(() => import('./edit.vue'));
|
||||
// 动态导入详情对话框组件
|
||||
const detailDialog = defineAsyncComponent(() => import('./detail.vue'));
|
||||
[[- if $.HasImport]]
|
||||
// 动态导入文件上传组件
|
||||
const fileUpload = defineAsyncComponent(() => import('@/components/importFile/index.vue'));
|
||||
[[- end]]
|
||||
// 响应式状态定义
|
||||
const currentId = ref(0); // 当前操作的ID
|
||||
const editVisible = ref(false); // 编辑对话框显示状态
|
||||
const detailVisible = ref(false); // 详情对话框显示状态
|
||||
[[- if $.HasImport]]
|
||||
const importVisible = ref(false); // 导入对话框显示状态
|
||||
[[- end]]
|
||||
[[- if $.HasExport]]
|
||||
const exportLoading = ref(false); // 导出按钮加载状态
|
||||
[[- end]]
|
||||
const selectionData = ref([]); // 表格选中数据
|
||||
const tableRef = ref(); // 表格组件引用
|
||||
|
||||
/**
|
||||
* 查询参数
|
||||
*
|
||||
* 与搜索表单字段一一对应,提交后合并到分页请求中。
|
||||
*/
|
||||
const formParams = reactive({
|
||||
[[- range .FilterFields]]
|
||||
[[.Name]]: '',
|
||||
[[- end]]
|
||||
});
|
||||
|
||||
/**
|
||||
* 操作栏配置
|
||||
*
|
||||
* 固定右侧显示,包含编辑、详情、删除三个按钮;
|
||||
* 每个按钮通过 auth 声明所需权限,由 v-perm 指令控制显隐。
|
||||
*/
|
||||
const actionColumn = reactive({
|
||||
width: 300,
|
||||
label: '操作',
|
||||
prop: 'action',
|
||||
fixed: 'right',
|
||||
render(record) {
|
||||
return h(TableAction, {
|
||||
style: 'button',
|
||||
actions: [
|
||||
{
|
||||
label: '编辑',
|
||||
icon: 'Edit',
|
||||
type: 'warning',
|
||||
onClick: handleEdit.bind(null, record),
|
||||
auth: ['[[$.PermissionPrefix]]:update'],
|
||||
},
|
||||
{
|
||||
label: '详情',
|
||||
icon: 'View',
|
||||
type: '',
|
||||
onClick: handleDetail.bind(null, record),
|
||||
auth: ['[[$.PermissionPrefix]]:detail'],
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
icon: 'Delete',
|
||||
type: 'danger',
|
||||
onClick: handleDelete.bind(null, record),
|
||||
auth: ['[[$.PermissionPrefix]]:delete'],
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 加载表格数据
|
||||
*
|
||||
* 合并搜索条件与分页参数后请求分页接口,
|
||||
* 返回值直接交给 BasicTable 渲染。
|
||||
*
|
||||
* @param params 分页参数,含 pageNo、pageSize
|
||||
* @returns 分页结果
|
||||
*/
|
||||
const loadDataTable = async (params: any) => {
|
||||
const result = await get[[$.ModelClass]]Page({ ...formParams, ...params });
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* 刷新表格数据
|
||||
*
|
||||
* @param noRefresh 传入非空值时保持当前页码,否则回到第一页
|
||||
*/
|
||||
function reloadTable(noRefresh = '') {
|
||||
tableRef.value.reload(noRefresh ? {} : { pageNo: 1 });
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册搜索表单
|
||||
*
|
||||
* 配置表单布局、字段来源与提交重置行为,
|
||||
* 返回 register 供模板绑定、getFieldsValue 供导出取值。
|
||||
*/
|
||||
const [register, { getFieldsValue }] = useForm({
|
||||
labelWidth: 80,
|
||||
layout: 'horizontal',
|
||||
colProps: { span: 6 } as ColProps,
|
||||
submitOnReset: true,
|
||||
schemas,
|
||||
});
|
||||
|
||||
/**
|
||||
* 提交搜索
|
||||
*
|
||||
* 先把当前条件写回 formParams,再从第一页重新加载。
|
||||
*
|
||||
* @param values 表单值
|
||||
*/
|
||||
function handleSubmit(values: Recordable) {
|
||||
handleReset();
|
||||
for (const key in values) {
|
||||
formParams[key] = values[key];
|
||||
}
|
||||
reloadTable();
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置搜索条件
|
||||
*
|
||||
* 清空 formParams 中的所有筛选项,回到无条件状态。
|
||||
*/
|
||||
function handleReset() {
|
||||
for (const key in formParams) {
|
||||
formParams[key] = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开新增弹窗
|
||||
*
|
||||
* 主键置 0 表示新增,nextTick 保证弹窗组件重新创建后再显示。
|
||||
*/
|
||||
const handleAdd = async () => {
|
||||
currentId.value = 0;
|
||||
await nextTick();
|
||||
editVisible.value = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 打开编辑弹窗
|
||||
*
|
||||
* @param record 当前行数据
|
||||
*/
|
||||
const handleEdit = async (record: Recordable) => {
|
||||
currentId.value = record.row.id;
|
||||
await nextTick();
|
||||
editVisible.value = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 打开详情弹窗
|
||||
*
|
||||
* @param record 当前行数据
|
||||
*/
|
||||
const handleDetail = async (record: Recordable) => {
|
||||
currentId.value = record.row.id;
|
||||
await nextTick();
|
||||
detailVisible.value = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除数据
|
||||
*
|
||||
* 不传 record 时为批量删除,从选中行取 ID 并做非空校验;
|
||||
* 传 record 时为单条删除。两种情况都先弹出确认框。
|
||||
*
|
||||
* @param record 当前行数据(可选)
|
||||
*/
|
||||
async function handleDelete(record: Recordable) {
|
||||
let ids = [];
|
||||
if (!record) {
|
||||
// 批量删除:获取选中行的ID
|
||||
ids = selectionData.value.map(({ id }) => id);
|
||||
if (!ids.length) {
|
||||
message('请选择要删除的数据', 'warning');
|
||||
return;
|
||||
}
|
||||
}
|
||||
// 确认删除
|
||||
await confirm('确定要删除选中的[[$.ModuleComment]]吗?');
|
||||
// 执行删除操作
|
||||
record ? await [[$.ModuleCamel]]Delete(record.row.id) : await [[$.ModuleCamel]]BatchDelete(ids);
|
||||
message('删除成功');
|
||||
reloadTable();
|
||||
}
|
||||
|
||||
/**
|
||||
* 表格选中项变化回调
|
||||
*
|
||||
* 把选中行同步到 selectionData,供批量删除使用。
|
||||
*
|
||||
* @param value 选中的数据
|
||||
*/
|
||||
function onSelectionChange(value) {
|
||||
selectionData.value = value;
|
||||
}
|
||||
[[- if $.HasExport]]
|
||||
/**
|
||||
* 导出数据
|
||||
*
|
||||
* 以当前搜索条件请求导出接口,拿到文件 URL 后新窗口打开;
|
||||
* 导出期间按钮保持 loading,避免重复点击。
|
||||
*/
|
||||
const handleExport = async () => {
|
||||
exportLoading.value = true;
|
||||
const data = await [[$.ModuleCamel]]Export(getFieldsValue());
|
||||
window.open(data);
|
||||
exportLoading.value = false;
|
||||
message('导出成功');
|
||||
};
|
||||
[[- end]]
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,37 @@
|
||||
import { FormSchema } from '@/components/Form/index';
|
||||
|
||||
/**
|
||||
* [[$.ModuleComment]]查询表单配置
|
||||
*
|
||||
* 定义列表页搜索区域的表单项,字段来自 filter_fields,
|
||||
* 按字段类型自动选择组件:
|
||||
* - 字符型用 Input;
|
||||
* - 带选项的字段用 Select;
|
||||
* - 纯数字字段用 InputNumber;
|
||||
* - 日期字段用 DatePicker。
|
||||
*/
|
||||
export const schemas: FormSchema[] = [
|
||||
[[- range .FilterFields]]
|
||||
{
|
||||
// [[.Label]]
|
||||
field: '[[.Name]]',
|
||||
component: '[[if eq .Type "CharField"]]Input[[else if .Choices]]Select[[else if and (eq .Type "IntegerField") (not .Choices)]]InputNumber[[else if eq .Type "DateField"]]DatePicker[[else]]Input[[end]]',
|
||||
label: '[[.Label]]',
|
||||
componentProps: {
|
||||
placeholder: '请输入[[.Label]]',
|
||||
clearable: true,
|
||||
[[- if .Choices]]
|
||||
// 选项列表:值用于查询,标签用于展示
|
||||
options: [
|
||||
[[- range .Choices]]
|
||||
{
|
||||
label: '[[.Label]]',
|
||||
value: [[.Value]],
|
||||
},
|
||||
[[- end]]
|
||||
],
|
||||
[[- end]]
|
||||
},
|
||||
},
|
||||
[[- end]]
|
||||
];
|
||||
Reference in New Issue
Block a user