Skip to content

基础 CRUD 页面模板

vue
<template>
	<div class="crud-page">
		<FilterForm
			class="common-search"
			:model-value="searchModel"
			:config="searchFormConfig"
			:loading="gridOptions.loading"
			@query="handleQuery"
			@reset="handleReset"
		/>

		<div class="common-table">
			<div class="toolbar">
				<FkButton type="primary" @click="handleCreate">新建</FkButton>
			</div>

			<VxeGrid ref="gridRef" v-bind="gridOptions">
				<template #action="{ row }">
					<FkButton type="text" @click="handleEdit(row)">编辑</FkButton>
					<FkButton type="text" status="danger" @click="handleDelete(row)">删除</FkButton>
				</template>
			</VxeGrid>
		</div>
	</div>
</template>

<script setup lang="ts">
import { onMounted, reactive, useTemplateRef } from 'vue';
import { FilterForm, VxeGrid, createDynamicFormPop, pop } from '@erp/biz';
import { type CrudItem, type QueryParams, createItem, deleteItem, updateItem } from './mock-api';
import { getGridOptions, getSearchForm } from './crud-page';
import type { DynamicFormI, VxeGridInstance } from '@erp/biz';

const searchFormConfig = getSearchForm();
const searchModel = reactive<QueryParams>({
	keyword: '',
	status: '',
});

const gridRef = useTemplateRef<VxeGridInstance>('gridRef');
const gridOptions = getGridOptions(searchModel);

function reloadGrid() {
	gridRef.value?.commitProxy('reload');
}

function handleQuery() {
	reloadGrid();
}

function handleReset() {
	searchModel.keyword = '';
	searchModel.status = '';
	reloadGrid();
}

function createFormConfig(mode: 'create' | 'edit') {
	return {
		title: mode === 'create' ? '新建' : '编辑',
		showSide: false,
		cols: 2,
		groups: [
			{
				label: '基础信息',
				fields: [
					{
						key: 'name',
						label: '名称',
						type: 'text',
						required: true,
						rules: { required: true, message: '请输入名称', type: 'string' },
					},
					{
						key: 'code',
						label: '编码',
						type: 'text',
						required: true,
						rules: { required: true, message: '请输入编码', type: 'string' },
					},
					{
						key: 'status',
						label: '状态',
						type: 'select',
						required: true,
						options: [
							{ label: '启用', value: '启用' },
							{ label: '禁用', value: '禁用' },
						],
					},
					{
						key: 'remark',
						label: '备注',
						type: 'textarea',
					},
				],
			},
		],
	} as DynamicFormI;
}

async function handleCreate() {
	const result = await createDynamicFormPop<Omit<CrudItem, 'id'>>({
		vm: { name: '', code: '', status: '启用', remark: '' },
		config: createFormConfig('create'),
		modalConfig: { title: '新建', width: 620, id: 'crud-template-create' },
	});
	if (!result) return;

	await createItem(result);
	pop.success('创建成功');
	reloadGrid();
}

async function handleEdit(row: CrudItem) {
	const result = await createDynamicFormPop<Omit<CrudItem, 'id'>>({
		vm: {
			name: row.name,
			code: row.code,
			status: row.status,
			remark: row.remark || '',
		},
		config: createFormConfig('edit'),
		modalConfig: { title: '编辑', width: 620, id: `crud-template-edit-${row.id}` },
	});
	if (!result) return;

	await updateItem(row.id, result);
	pop.success('保存成功');
	reloadGrid();
}

async function handleDelete(row: CrudItem) {
	await pop.confirm(`确定删除「${row.name}」吗?`, {
		title: '删除确认',
		okButtonProps: { status: 'danger' },
	});
	const loading = pop.loading('正在删除...');
	await deleteItem(row.id);
	loading.close();
	pop.success('删除成功');
	reloadGrid();
}

onMounted(() => {
	reloadGrid();
});
</script>

<style scoped lang="scss">
.crud-page {
	height: 800px;
	width: 100%;
	padding: 16px 16px 0;
	display: flex;
	flex-direction: column;

	.common-search {
		width: 100%;
		padding: 12px;
		border-radius: 4px;
		background-color: var(--color-fill-1);
	}

	.common-table {
		flex: 1;
		width: 100%;
		margin-top: 12px;
	}

	.toolbar {
		margin-bottom: 10px;
	}
}
</style>
ts
import { reactive } from 'vue';
import { mergeGridProps } from '@erp/biz';
import { queryList } from './mock-api';
import type { SearchFormI, VxeGridProps } from '@erp/biz';
import type { CrudItem, QueryParams } from './mock-api';

export function getSearchForm() {
	const config: SearchFormI = {
		gridProps: {
			cols: { xxl: 4, xl: 4, lg: 3, md: 2, sm: 2, xs: 2 },
			colGap: 14,
			rowGap: 10,
		},
		labelLayout: 'inner',
		fields: [
			{ key: 'keyword', label: '关键字', type: 'text' },
			{
				key: 'status',
				label: '状态',
				type: 'select',
				options: [
					{ label: '全部', value: '' },
					{ label: '启用', value: '启用' },
					{ label: '禁用', value: '禁用' },
				],
			},
		],
	};
	return config;
}

export function getGridOptions(model: QueryParams) {
	const gridOptions: VxeGridProps<CrudItem> = reactive(
		mergeGridProps({
			id: 'crud-template-grid',
			height: 360,
			columns: [
				{ type: 'seq', width: 60, fixed: 'left', title: '#' },
				{ field: 'name', title: '名称', minWidth: 160 },
				{ field: 'code', title: '编码', minWidth: 160 },
				{ field: 'status', title: '状态', width: 100 },
				{ field: 'remark', title: '备注', minWidth: 220 },
				{
					title: '操作',
					width: 180,
					fixed: 'right',
					slots: { default: 'action' },
				},
			],
			proxyConfig: {
				response: { result: 'list', total: 'total' },
				ajax: {
					query({ page }) {
						return queryList({
							...model,
							page: page.current,
							pageSize: page.pageSize,
						});
					},
				},
			},
		}),
	);
	return gridOptions;
}
ts
export type Status = '启用' | '禁用';

export interface CrudItem {
	id: number;
	name: string;
	code: string;
	status: Status;
	remark?: string;
}

export interface QueryParams {
	keyword?: string;
	status?: '' | Status;
	page?: number;
	pageSize?: number;
}

const db: CrudItem[] = [
	{ id: 1, name: '订单模板', code: 'order_tpl', status: '启用', remark: '订单业务模板' },
	{ id: 2, name: '商品模板', code: 'goods_tpl', status: '禁用', remark: '商品业务模板' },
	{ id: 3, name: '客户模板', code: 'customer_tpl', status: '启用', remark: '客户业务模板' },
];

function wait(ms = 300) {
	return new Promise(resolve => setTimeout(resolve, ms));
}

export async function queryList(params: QueryParams) {
	await wait(300);
	const keyword = (params.keyword || '').trim();
	const status = params.status || '';
	const page = params.page || 1;
	const pageSize = params.pageSize || 10;

	const filtered = db.filter(item => {
		const hitKeyword = !keyword || item.name.includes(keyword) || item.code.includes(keyword);
		const hitStatus = !status || item.status === status;
		return hitKeyword && hitStatus;
	});

	const start = (page - 1) * pageSize;
	return {
		list: filtered.slice(start, start + pageSize),
		total: filtered.length,
	};
}

export async function createItem(payload: Omit<CrudItem, 'id'>) {
	await wait(300);
	const id = Math.max(...db.map(row => row.id), 0) + 1;
	const row: CrudItem = { id, ...payload };
	db.unshift(row);
	return row;
}

export async function updateItem(id: number, payload: Omit<CrudItem, 'id'>) {
	await wait(300);
	const index = db.findIndex(row => row.id === id);
	if (index < 0) {
		throw new Error('数据不存在');
	}
	db[index] = { id, ...payload };
	return db[index];
}

export async function deleteItem(id: number) {
	await wait(300);
	const index = db.findIndex(row => row.id === id);
	if (index >= 0) {
		db.splice(index, 1);
	}
	return true;
}

实例

基于 MIT 许可发布