签入版本

This commit is contained in:
2026-10-10 12:20:37 +08:00
parent f54d7735b0
commit 49b04a1ec4
108 changed files with 1459 additions and 1982 deletions
@@ -93,61 +93,61 @@ export function exampleBatchDelete(data: any) {
```typescript
// ui/src/views/tool/example/columns.ts
import { h } from 'vue';
import { ElAvatar, ElTag } from 'element-plus';
import { Tag, Avatar } from 'ant-design-vue';
export const columns = [
{
type: 'selection', // 多选框列
},
{
label: 'ID',
prop: 'id',
title: 'ID',
dataIndex: 'id',
fixed: 'left',
width: 50,
},
{
label: '名称',
prop: 'name',
title: '名称',
dataIndex: 'name',
minWidth: 120,
},
{
label: '状态',
prop: 'statusText',
title: '状态',
dataIndex: 'status',
key: 'status',
minWidth: 100,
// 选项标签渲染:按值映射文字与颜色
render(record) {
customRender({ record }) {
const statusMap = {
0: { text: '禁用', type: 'danger' },
1: { text: '启用', type: 'success' },
0: { text: '禁用', color: 'error' },
1: { text: '启用', color: 'success' },
};
const status = statusMap[record.row.status];
return h(ElTag, { type: status?.type || 'info' }, {
default: () => status?.text || record.row.statusText,
});
const status = statusMap[record.status];
return h(
Tag,
{ color: status?.color || 'default' },
{ default: () => status?.text || record.statusText },
);
},
},
{
label: '头像',
prop: 'avatar',
title: '头像',
dataIndex: 'avatar',
key: 'avatar',
minWidth: 80,
// 图片渲染:以方形头像形式展示
render(record) {
return h(ElAvatar, {
customRender({ record }) {
return h(Avatar, {
size: 48,
src: record.row.avatar,
src: record.avatar,
shape: 'square',
fit: 'fill',
});
},
},
{
label: '创建人',
prop: 'createUser',
title: '创建人',
dataIndex: 'createUser',
minWidth: 100,
},
{
label: '创建时间',
prop: 'createTime',
title: '创建时间',
dataIndex: 'createTime',
width: 180,
},
];
@@ -157,13 +157,17 @@ export const columns = [
| 字段 | 类型 | 说明 |
|------|------|------|
| `type` | string | 列类型:`selection`(多选框)/ `index`(序号) |
| `label` | string | 表头标题 |
| `prop` | string | 数据字段名(camelCase) |
| `title` | string | 表头标题 |
| `dataIndex` | string | 数据字段名(camelCase) |
| `key` | string | 列唯一标识(自定义渲染时必填,供 `#bodyCell` 插槽匹配) |
| `fixed` | string | 固定列:`left` / `right` |
| `width` | number | 固定宽度 |
| `minWidth` | number | 最小宽度(自适应) |
| `render` | function | 自定义渲染函数 |
| `customRender` | function | 自定义渲染函数,入参 `{ record, text, index }` |
:::tip 说明
多选框列由表格的 `row-selection` 属性开启,无需在 columns 中配置。表格行渲染通过 `#bodyCell` 插槽配合 `column.key` 分发,详见步骤四。
:::
## 步骤三:创建查询表单配置
@@ -173,21 +177,21 @@ import { FormSchema } from '@/components/Form/index';
export const schemas: FormSchema[] = [
{
field: 'name',
name: 'name',
component: 'Input',
label: '名称',
componentProps: {
placeholder: '请输入名称',
clearable: true,
allowClear: true,
},
},
{
field: 'status',
name: 'status',
component: 'Select',
label: '状态',
componentProps: {
placeholder: '请选择状态',
clearable: true,
allowClear: true,
options: [
{ label: '启用', value: 1 },
{ label: '禁用', value: 0 },
@@ -208,143 +212,179 @@ export const schemas: FormSchema[] = [
## 步骤四:创建列表页
列表页基于 `PageWrapper` + `BasicForm` + `BasicTable` 组合式组件构建:
```vue
<!-- ui/src/views/tool/example/index.vue -->
<template>
<div>
<!-- 查询表单 -->
<SearchForm :schemas="querySchemas" @search="handleSearch" @reset="handleReset" />
<PageWrapper>
<!-- 查询表单卡片 -->
<a-card :bordered="false" class="pt-3 mb-3 proCard">
<BasicForm @register="register" @submit="handleSubmit" @reset="handleReset" />
</a-card>
<!-- 操作栏 -->
<el-row :gutter="10" style="margin-bottom: 10px;">
<el-col :span="1.5">
<el-button type="primary" @click="handleAdd">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" :disabled="!selectedIds.length" @click="handleBatchDelete">
批量删除
</el-button>
</el-col>
</el-row>
<!-- 数据表格 -->
<el-table :data="tableData" @selection-change="handleSelectionChange" v-loading="loading">
<el-table-column v-for="col in columns" v-bind="col" :key="col.prop || col.type">
<template v-if="col.render" #default="scope">
<component :is="() => col.render(scope)" />
<!-- 数据表格卡片 -->
<a-card :bordered="false" class="proCard">
<BasicTable
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="tableRef"
scroll-x="1200"
:row-selection="{ onChange: onSelectionChange }"
virtual-scroll
>
<!-- 表格顶部工具栏 -->
<template #tableTitle>
<a-space>
<a-button type="primary" @click="handleAdd" v-perm="['sys:example:add']">
<template #icon><PlusOutlined /></template>
新增
</a-button>
<a-button
type="danger"
@click="handleDelete()"
:disabled="!selectionData.length"
v-perm="['sys:example:batchDelete']"
>
<template #icon><DeleteOutlined /></template>
批量删除
</a-button>
</a-space>
</template>
</el-table-column>
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button type="primary" link @click="handleDetail(row)">详情</el-button>
<el-button type="danger" link @click="handleDelete(row.id)">删除</el-button>
<!-- 操作列渲染 -->
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<a-space>
<a-button type="primary" @click="handleEdit(record.id)" v-perm="['sys:example:update']">
<template #icon><EditOutlined /></template>
编辑
</a-button>
<a-button type="primary" @click="handleDetail(record.id)" v-perm="['sys:example:detail']">
<template #icon><EyeOutlined /></template>
详情
</a-button>
<a-button type="primary" danger @click="handleDelete(record.id)" v-perm="['sys:example:delete']">
<template #icon><DeleteOutlined /></template>
删除
</a-button>
</a-space>
</template>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="pageNo"
v-model:page-size="pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@change="fetchData"
style="margin-top: 10px; justify-content: flex-end;"
/>
<!-- 编辑弹窗 -->
<EditDialog ref="editRef" @success="fetchData" />
</BasicTable>
</a-card>
<!-- 编辑弹窗(主键为 0 时表示新增) -->
<editDialog v-if="editVisible" :id="currentId" v-model:visible="editVisible" @success="reloadTable('noRefresh')" />
<!-- 详情弹窗 -->
<DetailDialog v-model:visible="detailVisible" :id="detailId" />
</div>
<detailDialog v-if="detailVisible" :id="currentId" v-model:visible="detailVisible" @success="reloadTable('noRefresh')" />
</PageWrapper>
</template>
<script setup lang="ts">
import { getExamplePage, exampleDelete, exampleBatchDelete } from '@/api/tool/example';
import { columns } from './columns';
import { schemas as querySchemas } from './querySchemas';
import { ElMessage, ElMessageBox } from 'element-plus';
import EditDialog from './edit.vue';
import DetailDialog from './detail.vue';
<script lang="ts" setup>
import { reactive, ref, nextTick, defineAsyncComponent } from 'vue';
import { useForm } from '@/components/Form/index';
import { getExamplePage, exampleDelete, exampleBatchDelete } from '@/api/tool/example';
import { columns } from './columns';
import { schemas } from './querySchemas';
import { PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons-vue';
import { Modal, message } from 'ant-design-vue';
const editRef = ref();
const loading = ref(false);
const tableData = ref([]);
const pageNo = ref(1);
const pageSize = ref(20);
const total = ref(0);
const selectedIds = ref<number[]>([]);
const detailVisible = ref(false);
const detailId = ref(0);
// 动态导入弹窗组件
const editDialog = defineAsyncComponent(() => import('./edit.vue'));
const detailDialog = defineAsyncComponent(() => import('./detail.vue'));
// 加载数据
const fetchData = async () => {
loading.value = true;
try {
const res = await getExamplePage({
pageNo: pageNo.value,
pageSize: pageSize.value,
});
tableData.value = res.records;
total.value = res.total;
} finally {
loading.value = false;
const currentId = ref(0);
const editVisible = ref(false);
const detailVisible = ref(false);
const selectionData = ref([]);
const tableRef = ref();
// 查询参数(与搜索表单字段一一对应)
const formParams = reactive({
name: '',
status: '',
});
// 加载表格数据:合并搜索条件与分页参数
const loadDataTable = async (params: any) => {
return await getExamplePage({ ...formParams, ...params });
};
// 刷新表格:传入非空值保持当前页码,否则回到第一页
function reloadTable(noRefresh = '') {
tableRef.value.reload(noRefresh ? {} : { pageNo: 1 });
}
};
// 搜索
const handleSearch = (params) => {
pageNo.value = 1;
fetchData();
};
// 注册搜索表单
const [register, { getFieldsValue }] = useForm({
rowProps: { gutter: [16, 0] },
colProps: { xs: 24, sm: 24, md: 12, lg: 8, xl: 6 },
labelCol: { span: 6, offset: 0 },
schemas,
});
// 重置
const handleReset = () => {
pageNo.value = 1;
fetchData();
};
// 提交搜索:把条件写回 formParams,从第一页重新加载
function handleSubmit(values: Recordable) {
handleReset();
for (const key in values) {
formParams[key] = values[key];
}
reloadTable();
}
// 新增
const handleAdd = () => {
editRef.value.open();
};
// 重置搜索条件
function handleReset() {
for (const key in formParams) {
formParams[key] = '';
}
}
// 编辑
const handleEdit = (row) => {
editRef.value.open(row);
};
// 打开新增弹窗
const handleAdd = async () => {
currentId.value = 0;
await nextTick();
editVisible.value = true;
};
// 详情
const handleDetail = (row) => {
detailId.value = row.id;
detailVisible.value = true;
};
// 打开编辑弹窗
const handleEdit = async (id: number) => {
currentId.value = id;
await nextTick();
editVisible.value = true;
};
// 删除
const handleDelete = async (id) => {
await ElMessageBox.confirm('确认删除该记录?', '提示', { type: 'warning' });
await exampleDelete(id);
ElMessage.success('删除成功');
fetchData();
};
// 打开详情弹窗
const handleDetail = async (id: number) => {
currentId.value = id;
await nextTick();
detailVisible.value = true;
};
// 批量删除
const handleBatchDelete = async () => {
await ElMessageBox.confirm(`确认删除选中的 ${selectedIds.value.length} 条记录?`, '提示', { type: 'warning' });
await exampleBatchDelete({ ids: selectedIds.value });
ElMessage.success('删除成功');
fetchData();
};
// 删除数据:不传 id 时为批量删除
async function handleDelete(id?: number) {
if (!id) {
if (!selectionData.value.length) {
message.warning('请选择要删除的数据');
return;
}
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的案例吗?',
onOk: async () => {
id ? await exampleDelete(id) : await exampleBatchDelete(selectionData.value);
message.success('删除成功');
reloadTable();
},
});
}
// 多选变化
const handleSelectionChange = (rows) => {
selectedIds.value = rows.map(row => row.id);
};
onMounted(() => fetchData());
// 表格选中项变化
function onSelectionChange(keys: any[]) {
selectionData.value = keys;
}
</script>
```
@@ -353,93 +393,119 @@ onMounted(() => fetchData());
```vue
<!-- ui/src/views/tool/example/edit.vue -->
<template>
<el-dialog v-model="visible" :title="formData.id ? '编辑' : '新增'" width="500px">
<el-form ref="formRef" :model="formData" :rules="rules" label-width="80px">
<el-form-item label="名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入名称" />
</el-form-item>
<el-form-item label="类型" prop="type">
<el-select v-model="formData.type" placeholder="请选择类型">
<el-option v-for="item in typeOptions" :key="item.value" :label="item.name" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio :value="1">启用</el-radio>
<el-radio :value="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number v-model="formData.sort" :min="0" :max="9999" />
</el-form-item>
</el-form>
<a-modal
v-model:visible="props.visible"
:title="props.id ? '编辑案例' : '新增案例'"
width="500px"
:maskClosable="false"
@cancel="dialogClose"
>
<a-form ref="formRef" :model="formData" :label-col="{ style: { width: '85px' } }">
<a-form-item
label="名称"
name="name"
:rules="{ required: true, message: '请输入名称', trigger: 'blur' }"
>
<a-input v-model:value="formData.name" placeholder="请输入名称" allow-clear />
</a-form-item>
<a-form-item
label="类型"
name="type"
:rules="{ required: true, message: '请选择类型', trigger: 'change' }"
>
<a-select
v-model:value="formData.type"
placeholder="请选择类型"
allow-clear
:options="typeOptions"
/>
</a-form-item>
<a-form-item label="状态" name="status">
<a-radio-group v-model:value="formData.status">
<a-radio :value="1">启用</a-radio>
<a-radio :value="0">禁用</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="排序" name="sort">
<a-input-number v-model:value="formData.sort" :min="0" :max="9999" />
</a-form-item>
</a-form>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :loading="submitting" @click="handleSubmit">确定</el-button>
<span class="dialog-footer">
<a-button @click="dialogClose">取消</a-button>
<a-button :loading="subLoading" type="primary" @click="submit">确定</a-button>
</span>
</template>
</el-dialog>
</a-modal>
</template>
<script setup lang="ts">
import { exampleAdd, exampleUpdate, getExampleDetail } from '@/api/tool/example';
import { getDictItemList } from '@/api/common/index';
import { ElMessage } from 'element-plus';
<script lang="ts" setup>
import type { FormInstance } from 'ant-design-vue';
import { shallowRef } from 'vue';
import { exampleAdd, exampleUpdate, getExampleDetail } from '@/api/tool/example';
import { getDictItemList } from '@/api/common/index';
import { message } from 'ant-design-vue';
import { useLockFn } from '@/utils/useLockFn';
const emit = defineEmits(['success']);
const emit = defineEmits(['success', 'update:visible']);
const visible = ref(false);
const submitting = ref(false);
const formRef = ref();
const typeOptions = ref([]);
const formRef = shallowRef<FormInstance>();
const typeOptions = ref([]);
const formData = ref({
id: 0,
name: '',
type: '',
status: 1,
sort: 0,
});
const props = defineProps({
visible: { type: Boolean, required: true, default: false },
id: { type: Number, required: true, default: 0 },
});
const rules = {
name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
type: [{ required: true, message: '请选择类型', trigger: 'change' }],
};
const formData = reactive({
id: 0,
name: '',
type: '',
status: 1,
sort: 0,
});
// 打开弹窗
const open = async (row?) => {
visible.value = true;
formData.value = { id: 0, name: '', type: '', status: 1, sort: 0 };
// 提交表单
const handleSubmit = async () => {
// 表单验证
await formRef.value?.validate();
// 加载字典选项
typeOptions.value = await getDictItemList('example_type');
// 按主键是否存在决定调用新增或更新
props.id ? await exampleUpdate(formData) : await exampleAdd(formData);
// 编辑时加载详情
if (row?.id) {
const detail = await getExampleDetail(row.id);
Object.assign(formData.value, detail);
}
};
// 提交
const handleSubmit = async () => {
await formRef.value.validate();
submitting.value = true;
try {
if (formData.value.id) {
await exampleUpdate(formData.value);
ElMessage.success('修改成功');
} else {
await exampleAdd(formData.value);
ElMessage.success('新增成功');
}
visible.value = false;
message.success('操作成功');
emit('update:visible', false);
emit('success');
} finally {
submitting.value = false;
}
};
};
defineExpose({ open });
// 防抖处理,防止重复提交
const { isLock: subLoading, lockFn: submit } = useLockFn(handleSubmit);
// 关闭弹窗
const dialogClose = () => {
emit('update:visible', false);
};
// 加载详情 / 初始化
watch(
() => props.visible,
async (visible) => {
if (!visible) return;
// 加载字典选项
typeOptions.value = await getDictItemList('example_type');
// 编辑时回填数据
if (props.id) {
const data = await getExampleDetail(props.id);
for (const key in formData) {
if (data[key] != null) {
formData[key] = data[key];
}
}
}
},
{ immediate: true },
);
</script>
```
@@ -448,87 +514,98 @@ defineExpose({ open });
```vue
<!-- ui/src/views/tool/example/detail.vue -->
<template>
<el-dialog v-model="props.visible" title="案例详情" width="750" :close-on-click-modal="false">
<el-descriptions column="2" border>
<el-descriptions-item label="名称:">
<a-modal v-model:visible="props.visible" title="案例详情" width="800px" @cancel="dialogClose">
<a-descriptions class="margin-top" :column="2" bordered :labelStyle="{ width: '120px' }">
<a-descriptions-item label="名称">
{{ formData.name || '-' }}
</el-descriptions-item>
<el-descriptions-item label="类型:">
</a-descriptions-item>
<a-descriptions-item label="类型">
{{ formData.typeText || '-' }}
</el-descriptions-item>
<el-descriptions-item label="状态:">
<el-tag :type="formData.status === 1 ? 'success' : 'danger'">
</a-descriptions-item>
<a-descriptions-item label="状态">
<a-tag :color="formData.status === 1 ? 'success' : 'error'">
{{ formData.statusText || '-' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="排序:">
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="排序">
{{ formData.sort ?? '-' }}
</el-descriptions-item>
<el-descriptions-item label="头像:">
<el-image
</a-descriptions-item>
<a-descriptions-item label="头像">
<Image
v-if="formData.avatar"
:src="formData.avatar"
:preview-src-list="[formData.avatar]"
fit="cover"
style="width: 80px; height: 80px"
:width="80"
:height="80"
/>
<span v-else>-</span>
</el-descriptions-item>
<el-descriptions-item label="创建人:">
</a-descriptions-item>
<a-descriptions-item label="创建人">
{{ formData.createUser || '-' }}
</el-descriptions-item>
<el-descriptions-item label="创建时间:">
</a-descriptions-item>
<a-descriptions-item label="创建时间">
{{ formData.createTime || '-' }}
</el-descriptions-item>
</el-descriptions>
</a-descriptions-item>
</a-descriptions>
<template #footer>
<el-button @click="emit('update:visible', false)">关闭</el-button>
<span class="dialog-footer">
<a-button @click="dialogClose">关闭</a-button>
</span>
</template>
</el-dialog>
</a-modal>
</template>
<script lang="ts" setup>
import { getExampleDetail } from '@/api/tool/example';
import { Image } from 'ant-design-vue';
import { getExampleDetail } from '@/api/tool/example';
const emit = defineEmits(['update:visible']);
const emit = defineEmits(['update:visible', 'success']);
const props = defineProps({
visible: { type: Boolean, required: true, default: false },
id: { type: Number, required: true, default: 0 },
});
const props = defineProps({
visible: { type: Boolean, required: true, default: false },
id: { type: Number, required: true, default: 0 },
});
const formData = reactive({
name: '',
type: '',
typeText: '',
status: 0,
statusText: '',
sort: 0,
avatar: '',
createUser: '',
createTime: '',
});
const formData = reactive({
name: '',
type: '',
typeText: '',
status: 0,
statusText: '',
sort: 0,
avatar: '',
createUser: '',
createTime: '',
});
// 加载详情
const loadDetail = async () => {
try {
const data = await getExampleDetail(props.id);
for (const key in formData) {
if (data[key] != null) {
formData[key] = data[key];
// 加载详情
const loadDetail = async () => {
try {
const data = await getExampleDetail(props.id);
for (const key in formData) {
if (data[key] != null) {
formData[key] = data[key];
}
}
} catch (error) {
console.error('获取详情失败', error);
}
} catch (error) {
console.error('获取详情失败', error);
}
};
};
// 监听弹窗显示
watch(() => props.visible, (newVal) => {
if (newVal && props.id) {
loadDetail();
}
}, { immediate: true });
// 关闭弹窗
const dialogClose = () => {
emit('update:visible', false);
};
// 监听弹窗显示
watch(
() => props.visible,
(newVal) => {
if (newVal && props.id) {
loadDetail();
}
},
{ immediate: true },
);
</script>
```