Files
dpb/frontend/web/src/views/module_example/demo/index.vue
T
34047007@qq.com b95053c52c init: 初始化 dpb 桃育种系统代码库
前后端 + 后端 FastAPI 全量源码、部署脚本与文档。
2026-08-06 00:17:49 +08:00

692 lines
20 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- 示例 CRUD与角色页同一套 Fa 布局弹窗与 Crud 一致FaDialog + crud-embed-dialog -->
<template>
<div class="fa-full-height">
<FaSearchBar
v-show="showSearchBar"
ref="searchBarRef"
v-model="searchForm"
:items="demoBusinessSearchItems"
:rules="searchBarRules"
:is-expand="false"
:show-expand="true"
:show-reset="true"
:show-search="true"
:disabled-search="false"
:default-expanded="false"
include-audit
@search="handleSearch"
@reset="onResetSearch"
/>
<ElCard class="fa-table-card" :style="{ 'margin-top': showSearchBar ? '12px' : '0' }">
<FaTableHeader
v-model:columns="columnChecks"
v-model:showSearchBar="showSearchBar"
:loading="loading"
@refresh="refreshData"
>
<template #left>
<FaTableHeaderLeft
:remove-ids="selectedIds"
:perm-create="['module_example:demo:create']"
:perm-import="['module_example:demo:import']"
:perm-export="['module_example:demo:export']"
:perm-delete="['module_example:demo:delete']"
:perm-patch="['module_example:demo:patch']"
:delete-loading="batchDeleting"
:create-loading="createLoading"
@add="handleAdd"
@import="openImport"
@export="openExport"
@delete="handleBatchDelete"
@more="runBatchStatus"
/>
</template>
</FaTableHeader>
<FaTable
ref="faTableRef"
:loading="loading"
:data="data"
:columns="columns"
:pagination="pagination"
@selection-change="onTableSelectionChange"
@pagination:size-change="handleSizeChange"
@pagination:current-change="handleCurrentChange"
/>
</ElCard>
<FaDialog
v-model="dialogVisible.visible"
:title="dialogVisible.title"
width="920px"
dialog-class="crud-embed-dialog"
modal-class="crud-embed-dialog"
:form-mode="dialogVisible.type"
:confirm-loading="submitLoading"
@cancel="handleCloseDialog"
@close="handleCloseDialog"
@confirm="handleSubmit()"
>
<template v-if="dialogVisible.type === 'detail'">
<FaDescriptions
:column="4"
:data="detailFormData"
:items="demoDetailItems"
max-height="70vh"
>
<template #json_val="{ row }">
<FaJsonPretty v-if="row?.json_val != null" :value="row?.json_val" height="140px" />
</template>
</FaDescriptions>
</template>
<template v-else>
<FaForm
:key="demoFormRenderKey"
scrollbar
max-height="70vh"
ref="dataFormRef"
v-model="formData"
:items="demoDialogFormItems"
:rules="rules"
label-suffix=":"
:label-width="100"
label-position="right"
:span="12"
:gutter="16"
:show-reset="false"
:show-submit="false"
class="crud-dialog-art-form"
>
<template #json_val>
<div class="flex flex-col gap-2">
<div
v-for="(item, index) in metadataList"
:key="item.key"
class="flex items-center gap-2"
>
<ElInput v-model="item.key" placeholder="键" />
<ElInput v-model="item.value" placeholder="值" />
<ElButton
type="primary"
:icon="Plus"
circle
@click="metadataList.push({ key: '', value: '' })"
/>
<ElButton
type="danger"
:icon="Delete"
circle
@click="metadataList.splice(index, 1)"
/>
</div>
<ElButton
v-if="metadataList.length === 0"
type="primary"
:icon="Plus"
@click="metadataList.push({ key: '', value: '' })"
>
添加元数据
</ElButton>
</div>
</template>
</FaForm>
</template>
</FaDialog>
<FaImportDialog
v-model="importVisible"
:content-config="demoImportContentConfig"
default-template-file-name="demo_import_template.xlsx"
@upload="handleCrudImportUpload"
/>
<FaExportDialog
v-model="exportVisible"
:content-config="demoExportContentConfig"
:query-params="exportQueryParams"
:page-data="data"
:selection-data="selectedRows"
/>
</div>
</template>
<script setup lang="ts">
import { Plus, Delete } from "@element-plus/icons-vue";
import type { TableOperationAction } from "@/utils/table";
import { renderTableOperationCell, resolveStatusColumns, stripPaginationParams, toCrudCols } from "@utils";
import { useCrudForm } from "@/hooks/core/useCrudForm";
import { ResultEnum } from "@/enums/api/result.enum";
import type { IContentConfig, IObject } from "@/components/modal/types";
import type { AuditSearchFormParams } from "@/components/forms/fa-search-bar/auditSearchFormItems";
import type { FormItem } from "@/components/forms/fa-form/index.vue";
import DemoAPI, {
type DemoForm,
type DemoPageQuery,
type DemoTable,
} from "@/api/module_example/demo";
import type { ColumnOption } from "@/types/component";
import FaDescriptions from "@/components/others/fa-descriptions/index.vue";
import FaForm from "@/components/forms/fa-form/index.vue";
import FaTableHeader from "@/components/tables/fa-table-header/index.vue";
import { msg } from "@/utils/message";
defineOptions({
name: "Demo",
inheritAttrs: false,
});
// 常量定义
const STATUS_OPTIONS = [
{ label: "启用", value: 0 },
{ label: "停用", value: 1 },
] as const;
const createInitialFormData = (): DemoForm => ({
id: undefined,
name: "",
status: 0,
description: undefined,
int_val: undefined,
bigint_val: undefined,
float_val: undefined,
bool_val: true,
date_val: undefined,
time_val: undefined,
datetime_val: undefined,
text_val: undefined,
json_val: undefined,
});
type DemoSearchFormParams = {
name?: string;
status?: number;
} & AuditSearchFormParams;
const searchForm = ref<DemoSearchFormParams>({
name: undefined,
status: undefined,
created_id: undefined,
updated_id: undefined,
created_time: [],
updated_time: [],
});
/** 搜索区域默认展开展示 */
const showSearchBar = ref(true);
const searchBarRef = ref<{ validate: () => Promise<boolean> } | null>(null);
const searchBarRules: Record<string, unknown> = {};
/** 名称、状态;创建人/更新人/时间由 FaSearchBar 的 includeAudit 属性追加 */
const demoBusinessSearchItems = computed(() => [
{
label: "名称",
key: "name",
type: "input",
placeholder: "请输入名称",
clearable: true,
span: 6,
},
{
label: "状态",
key: "status",
type: "select",
props: {
placeholder: "请选择状态",
options: STATUS_OPTIONS,
clearable: true,
},
span: 6,
},
]);
const faTableRef = ref<{ elTableRef?: { clearSelection: () => void } } | null>(null);
const { selectedRows, selectedIds, batchDeleting, onTableSelectionChange } =
useTableSelection<DemoTable>();
const createLoading = ref(false);
const {
columns,
columnChecks,
data,
loading,
pagination,
searchParams,
getData,
replaceSearchParams,
resetSearchParams,
handleSizeChange,
handleCurrentChange,
refreshData,
refreshCreate,
refreshUpdate,
refreshRemove,
} = useTable({
core: {
apiFn: DemoAPI.getDemoList,
apiParams: {
page_no: 1,
page_size: 10,
},
columnsFactory: resolveStatusColumns<DemoTable>((): ColumnOption<DemoTable>[] => [
{ type: "globalIndex", width: 56, label: "序号" },
{ type: "selection", width: 48, fixed: "left" },
{ prop: "name", label: "名称", minWidth: 120, showOverflowTooltip: true },
{
prop: "status",
label: "状态",
width: 88,
status: {
0: { type: "success", text: "启用" },
1: { type: "info", text: "停用" },
},
},
{ prop: "int_val", label: "整数", minWidth: 88, showOverflowTooltip: true },
{ prop: "bigint_val", label: "大整数", minWidth: 100, showOverflowTooltip: true },
{ prop: "float_val", label: "浮点数", minWidth: 88, showOverflowTooltip: true },
{
prop: "bool_val",
label: "布尔",
width: 80,
status: {
true: { type: "success", text: "是" },
false: { type: "danger", text: "否" },
},
},
{ prop: "date_val", label: "日期", minWidth: 112, showOverflowTooltip: true },
{ prop: "time_val", label: "时间", minWidth: 96, showOverflowTooltip: true },
{ prop: "datetime_val", label: "日期时间", minWidth: 168, showOverflowTooltip: true },
{ prop: "text_val", label: "长文本", minWidth: 120, showOverflowTooltip: true },
{ prop: "description", label: "描述", minWidth: 120, showOverflowTooltip: true },
{
prop: "created_time",
label: "创建时间",
width: 168,
sortable: true,
showOverflowTooltip: true,
},
{
prop: "updated_time",
label: "更新时间",
width: 168,
sortable: true,
showOverflowTooltip: true,
},
{
prop: "created_by",
label: "创建人",
minWidth: 100,
formatter: (row: DemoTable) => row.created_by?.name ?? "—",
},
{
prop: "updated_by",
label: "更新人",
minWidth: 100,
formatter: (row: DemoTable) => row.updated_by?.name ?? "—",
},
{
prop: "operation",
label: "操作",
width: 180,
fixed: "right",
align: "center",
formatter: (row: DemoTable) => formatDemoOperationCell(row),
},
]),
},
});
/** 供 CrudImportModal / CrudExportModal 的列配置(与 CrudContent.cols 结构一致) */
const demoCrudCols = toCrudCols(columns);
const exportQueryParams = computed(() => {
return stripPaginationParams(searchParams as Record<string, unknown>);
});
const demoImportContentConfig = computed<IContentConfig>(() => ({
permPrefix: "module_example:demo",
cols: demoCrudCols.value,
indexAction: async () => ({}),
importTemplate: () => DemoAPI.downloadTemplateDemo(),
}));
const demoExportContentConfig = computed(() => ({
permPrefix: "module_example:demo",
cols: demoCrudCols.value,
exportsBlobAction: async (params: IObject) => {
const merged = {
...(exportQueryParams.value as unknown as Record<string, unknown>),
...params,
} as unknown as DemoPageQuery;
const res = await DemoAPI.exportDemo(merged);
return res.data as Blob;
},
}));
const { dialogVisible } = useCrudDialog();
const detailFormData = ref<DemoTable>({});
const demoDetailItems: import("@/components/others/fa-descriptions/index.vue").DescriptionsItem[] =
[
{ label: "名称", prop: "name" },
{ label: "UUID", prop: "uuid" },
{
label: "状态",
prop: "status",
tag: {
map: { "0": { type: "success", text: "启用" }, "1": { type: "danger", text: "停用" } },
},
},
{ label: "整数", prop: "int_val" },
{ label: "大整数", prop: "bigint_val" },
{ label: "浮点数", prop: "float_val" },
{
label: "布尔值",
prop: "bool_val",
tag: {
map: { true: { type: "success", text: "是" }, false: { type: "danger", text: "否" } },
},
},
{ label: "日期", prop: "date_val" },
{ label: "时间", prop: "time_val" },
{ label: "日期时间", prop: "datetime_val" },
{ label: "长文本", prop: "text_val" },
{ label: "元数据", prop: "json_val", slot: "json_val" },
{ label: "描述", prop: "description" },
{ label: "创建人", prop: "created_by.name" },
{ label: "更新人", prop: "updated_by.name" },
{ label: "创建时间", prop: "created_time" },
{ label: "更新时间", prop: "updated_time" },
];
// 示例页新增/编辑表单 —— 元数据字段用具名插槽渲染
const demoDialogFormItems: FormItem[] = [
{
key: "name",
label: "名称",
type: "input",
props: { placeholder: "请输入名称", maxlength: 50 },
},
{
key: "status",
label: "状态",
type: "radiogroup",
props: {
options: [
{ label: "启用", value: 0 },
{ label: "停用", value: 1 },
],
},
},
{ key: "int_val", label: "整数", type: "number", props: { placeholder: "请输入整数" } },
{ key: "bigint_val", label: "大整数", type: "number", props: { placeholder: "请输入大整数" } },
{
key: "float_val",
label: "浮点数",
type: "number",
props: { placeholder: "请输入浮点数", step: 0.01, precision: 2 },
},
{ key: "bool_val", label: "布尔值", type: "switch" },
{
key: "date_val",
label: "日期",
type: "date",
props: { placeholder: "请选择日期", valueFormat: "YYYY-MM-DD", style: "width: 100%" },
},
{
key: "time_val",
label: "时间",
type: "timepicker",
props: { placeholder: "请选择时间", valueFormat: "HH:mm:ss", style: "width: 100%" },
},
{
key: "datetime_val",
label: "日期时间",
type: "datetime",
props: {
placeholder: "请选择日期时间",
valueFormat: "YYYY-MM-DD HH:mm:ss",
style: "width: 100%",
},
},
{
key: "text_val",
label: "长文本",
type: "input",
props: { type: "textarea", rows: 4, placeholder: "请输入长文本" },
},
{
key: "description",
label: "描述",
type: "input",
props: {
type: "textarea",
rows: 4,
maxlength: 100,
showWordLimit: true,
placeholder: "请输入描述",
},
},
{ key: "json_val", label: "元数据", type: "input" /* 由 #json_val 插槽接管 */ },
];
const formData = ref<DemoForm>(createInitialFormData());
const rules = reactive({
name: [{ required: true, message: "请输入名称", trigger: "blur" }],
status: [{ required: true, message: "请选择状态", trigger: "blur" }],
});
const dataFormRef = ref<InstanceType<typeof FaForm> | null>(null);
const demoFormRenderKey = ref(0);
const metadataList = ref<{ key: string; value: string }[]>([]);
const crud = useCrudForm<DemoForm>({
formData,
initialFormData: createInitialFormData(),
dialogVisible,
dataFormRef,
formRenderKey: demoFormRenderKey,
detailApi: DemoAPI.getDemoDetail,
createApi: DemoAPI.createDemo,
updateApi: DemoAPI.updateDemo,
titles: { create: "新增", update: "修改", detail: "详情" },
detailFormData,
onCreateSuccess: async () => {
await refreshCreate();
},
onUpdateSuccess: async () => {
await refreshUpdate();
},
});
const { submitLoading } = crud;
/** json_val → metadataList(打开修改弹窗时回填) */
function syncJsonValToMetadataList(val: unknown) {
if (val && typeof val === "object") {
metadataList.value = Object.entries(val).map(([key, value]) => ({
key,
value: String(value),
}));
} else {
metadataList.value = [];
}
}
/** metadataList → json_val(提交前转换) */
function buildJsonValFromMetadataList(): Record<string, string> | undefined {
const entries = metadataList.value.filter((item) => item.key.trim());
if (entries.length === 0) return undefined;
const obj: Record<string, string> = {};
entries.forEach((item) => {
obj[item.key.trim()] = item.value;
});
return obj;
}
async function handleOpenDialog(type: "create" | "update" | "detail", id?: number) {
if (type === "create") {
metadataList.value = [];
}
await crud.handleOpenDialog(type, id);
if (type === "update") {
syncJsonValToMetadataList(formData.value.json_val);
}
}
async function handleCloseDialog() {
metadataList.value = [];
await crud.handleCloseDialog();
}
async function handleSubmit() {
formData.value.json_val = buildJsonValFromMetadataList();
await crud.handleSubmit();
}
const { importVisible, exportVisible, openImport, openExport } = useImportExport();
const handleSearch = async (params: DemoSearchFormParams) => {
await searchBarRef.value?.validate();
replaceSearchParams({
name: params.name,
status: params.status,
created_id: params.created_id ?? undefined,
updated_id: params.updated_id ?? undefined,
created_time:
Array.isArray(params.created_time) && params.created_time.length === 2
? params.created_time
: undefined,
updated_time:
Array.isArray(params.updated_time) && params.updated_time.length === 2
? params.updated_time
: undefined,
} as Record<string, unknown>);
await getData();
};
const onResetSearch = async () => {
searchForm.value = {
name: undefined,
status: undefined,
created_id: undefined,
updated_id: undefined,
created_time: [],
updated_time: [],
};
await resetSearchParams();
};
function buildDemoRowActions(row: DemoTable): TableOperationAction[] {
const all: TableOperationAction[] = [
{
key: "detail",
label: "详情",
artType: "view",
perm: "module_example:demo:detail",
run: () => void handleOpenDialog("detail", row.id),
},
{
key: "edit",
label: "编辑",
artType: "edit",
icon: "ri:edit-2-line",
perm: "module_example:demo:update",
run: () => void handleOpenDialog("update", row.id),
},
{
key: "delete",
label: "删除",
artType: "delete",
icon: "ri:delete-bin-4-line",
perm: "module_example:demo:delete",
run: () => deleteDemoRow(row),
},
];
return all;
}
function formatDemoOperationCell(row: DemoTable) {
return renderTableOperationCell(buildDemoRowActions(row), {
wrapperClass: "inline-flex flex-wrap items-center justify-end gap-1",
});
}
async function handleAdd() {
createLoading.value = true;
try {
await handleOpenDialog("create");
} finally {
createLoading.value = false;
}
}
const deleteDemoRow = async (row: DemoTable) => {
if (!row.id) return;
try {
await confirmDelete(`确定删除「${row.name ?? row.id}」吗?此操作不可恢复!`);
await DemoAPI.deleteDemo([row.id!]);
faTableRef.value?.elTableRef?.clearSelection();
await refreshRemove();
} catch {
// 用户取消
}
};
async function handleBatchDelete() {
const ids = selectedIds.value;
if (ids.length === 0) return;
try {
await confirmBatchDelete(ids.length);
batchDeleting.value = true;
await DemoAPI.deleteDemo(ids);
faTableRef.value?.elTableRef?.clearSelection();
await refreshRemove();
} catch {
// 用户取消
} finally {
batchDeleting.value = false;
}
}
async function runBatchStatus(value: "enable" | "disable") {
const ids = selectedIds.value;
if (ids.length === 0) {
msg.warning("请先在列表中勾选数据");
return;
}
try {
await confirmAction(
`确认对选中的 ${ids.length} 条数据${value === "enable" ? "启用" : "停用"}`,
"批量设置"
);
const status = value === "enable" ? 0 : 1;
await DemoAPI.batchDemo({ ids, status });
// 成功 / 失败提示由 axios 拦截器统一处理
faTableRef.value?.elTableRef?.clearSelection();
await refreshData();
} catch {
// 用户取消
}
}
async function handleCrudImportUpload(formData: FormData) {
try {
const res = await DemoAPI.importDemo(formData);
if (res.data.code === ResultEnum.SUCCESS) {
msg.success(res.data.msg || "导入成功");
importVisible.value = false;
await refreshData();
}
// 非 SUCCESS 分支提示由 axios 拦截器统一处理
} catch (error: unknown) {
if (import.meta.env.DEV) console.error("[Import]", error);
/* 接口错误已由拦截器提示 */
}
}
</script>