fix: 修复动态表单保存校验失败等问题,优化演示页源码入口与注释
This commit is contained in:
1 parent
92a7046634
commit
443c34ed20
22 files changed
+186
-274
No files matched your search
@@ -1,4 +1 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4 4h6v6H4V4Zm10 0h6v6h-6V4ZM4 14h6v6H4v-6Z" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/>
|
||||
<path d="M14 14h2.5v2.5H14V14Zm4 0h2v2m-6 2h2v2m2-2h2v2h-2v-2Z" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<svg t="1788842704919" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="24588" width="200" height="200"><path d="M928.016126 543.908618 95.983874 543.908618c-17.717453 0-31.994625-14.277171-31.994625-31.994625S78.26642 479.919368 95.983874 479.919368l832.032253 0c17.717453 0 31.994625 14.277171 31.994625 31.994625S945.73358 543.908618 928.016126 543.908618z" fill="#575B66" p-id="24589"></path><path d="M832.032253 928.016126 639.892491 928.016126c-17.717453 0-31.994625-14.277171-31.994625-31.994625s14.277171-31.994625 31.994625-31.994625l191.967747 0c17.717453 0 31.994625-14.277171 31.994625-31.994625l0-159.973123c0-17.717453 14.277171-31.994625 31.994625-31.994625s31.994625 14.277171 31.994625 31.994625l0 159.973123C928.016126 884.840585 884.840585 928.016126 832.032253 928.016126z" fill="#575B66" p-id="24590"></path><path d="M351.94087 928.016126l-159.973123 0c-52.980346 0-95.983874-43.003528-95.983874-95.983874l0-159.973123c0-17.717453 14.277171-31.994625 31.994625-31.994625S159.973123 654.341676 159.973123 672.05913l0 159.973123c0 17.717453 14.449185 31.994625 31.994625 31.994625l159.973123 0c17.717453 0 31.994625 14.277171 31.994625 31.994625C383.935495 913.738955 369.658324 928.016126 351.94087 928.016126z" fill="#575B66" p-id="24591"></path><path d="M127.978498 383.935495c-17.717453 0-31.994625-14.277171-31.994625-31.994625l0-159.973123c0-52.980346 43.003528-95.983874 95.983874-95.983874l159.973123 0c17.717453 0 31.994625 14.277171 31.994625 31.994625S369.658324 159.973123 351.94087 159.973123l-159.973123 0c-17.545439 0-31.994625 14.449185-31.994625 31.994625l0 159.973123C159.973123 369.658324 145.695952 383.935495 127.978498 383.935495z" fill="#575B66" p-id="24592"></path><path d="M896.021502 383.935495c-17.717453 0-31.994625-14.277171-31.994625-31.994625l0-159.973123c0-17.545439-14.277171-31.994625-31.994625-31.994625L639.892491 159.973123c-17.717453 0-31.994625-14.277171-31.994625-31.994625s14.277171-31.994625 31.994625-31.994625l191.967747 0c52.980346 0 95.983874 43.003528 95.983874 95.983874l0 159.973123C928.016126 369.658324 913.738955 383.935495 896.021502 383.935495z" fill="#575B66" p-id="24593"></path></svg>
|
||||
|
Before Width: | Height: | Size: 359 B After Width: | Height: | Size: 2.2 KiB |
@@ -1,14 +1,18 @@
|
||||
<!-- 字典组件示例 -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<el-link
|
||||
href="https://gitee.com/youlaiorg/vue3-element-admin/blob/master/src/views/demo/component/dictionary.vue"
|
||||
type="primary"
|
||||
target="_blank"
|
||||
<el-button
|
||||
class="mb-[20px]"
|
||||
tag="a"
|
||||
href="https://gitee.com/youlaiorg/vue3-element-admin/blob/master/src/views/demo/component/dictionary.vue"
|
||||
target="_blank"
|
||||
link
|
||||
type="info"
|
||||
size="small"
|
||||
>
|
||||
示例源码 请点击>>>
|
||||
</el-link>
|
||||
<el-icon class="mr-1"><Link /></el-icon>
|
||||
示例源码
|
||||
</el-button>
|
||||
<el-form>
|
||||
<el-form-item label="性别">
|
||||
<DictSelect v-model="stringValue" code="gender" />
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
<!-- 图标选择器示例 -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<el-link
|
||||
href="https://gitee.com/youlaiorg/vue3-element-admin/blob/master/src/views/demo/component/icon-select.vue"
|
||||
type="primary"
|
||||
target="_blank"
|
||||
<el-button
|
||||
class="mb-10"
|
||||
tag="a"
|
||||
href="https://gitee.com/youlaiorg/vue3-element-admin/blob/master/src/views/demo/component/icon-select.vue"
|
||||
target="_blank"
|
||||
link
|
||||
type="info"
|
||||
size="small"
|
||||
>
|
||||
示例源码 请点击>>>
|
||||
</el-link>
|
||||
<el-icon class="mr-1"><Link /></el-icon>
|
||||
示例源码
|
||||
</el-button>
|
||||
<IconSelect v-model="iconName" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
<!-- 列表选择器示例 -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<el-link
|
||||
href="https://gitee.com/youlaiorg/vue3-element-admin/blob/master/src/views/demo/component/table-select/index.vue"
|
||||
type="primary"
|
||||
target="_blank"
|
||||
<el-button
|
||||
class="mb-10"
|
||||
tag="a"
|
||||
href="https://gitee.com/youlaiorg/vue3-element-admin/blob/master/src/views/demo/component/table-select/index.vue"
|
||||
target="_blank"
|
||||
link
|
||||
type="info"
|
||||
size="small"
|
||||
>
|
||||
示例源码 请点击>>>
|
||||
</el-link>
|
||||
<el-icon class="mr-1"><Link /></el-icon>
|
||||
示例源码
|
||||
</el-button>
|
||||
<table-select :text="text" :select-config="selectConfig" @confirm-click="handleConfirm">
|
||||
<template #status="scope">
|
||||
<el-tag :type="scope.row[scope.prop as string] === 1 ? 'success' : 'info'">
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
<!-- wangEditor富文本编辑器示例 -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<el-link
|
||||
href="https://gitee.com/youlaiorg/vue3-element-admin/blob/master/src/views/demo/component/wang-editor.vue"
|
||||
type="primary"
|
||||
target="_blank"
|
||||
<el-button
|
||||
class="mb-[20px]"
|
||||
tag="a"
|
||||
href="https://gitee.com/youlaiorg/vue3-element-admin/blob/master/src/views/demo/component/wang-editor.vue"
|
||||
target="_blank"
|
||||
link
|
||||
type="info"
|
||||
size="small"
|
||||
>
|
||||
示例源码 请点击>>>
|
||||
</el-link>
|
||||
<el-icon class="mr-1"><Link /></el-icon>
|
||||
示例源码
|
||||
</el-button>
|
||||
<WangEditor v-model="value" height="400px" />
|
||||
|
||||
<div style="margin-top: 10px">
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
<!-- 文件上传组件示例 -->
|
||||
<!-- 自适应表格操作栏组件示例 -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<el-link
|
||||
href="https://gitee.com/youlaiorg/vue3-element-admin/blob/master/src/views/demo/table/auto-operation-column.vue"
|
||||
type="primary"
|
||||
target="_blank"
|
||||
<el-button
|
||||
class="mb-10"
|
||||
tag="a"
|
||||
href="https://gitee.com/youlaiorg/vue3-element-admin/blob/master/src/views/demo/table/auto-operation-column.vue"
|
||||
target="_blank"
|
||||
link
|
||||
type="info"
|
||||
size="small"
|
||||
>
|
||||
示例源码 请点击>>>
|
||||
</el-link>
|
||||
<el-icon class="mr-1"><Link /></el-icon>
|
||||
示例源码
|
||||
</el-button>
|
||||
|
||||
<div>
|
||||
<h3>自适应表格操作栏</h3>
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
<template>
|
||||
<div class="page-container h-full flex flex-1 flex-col">
|
||||
<div class="mb-10">
|
||||
<el-link
|
||||
<el-button
|
||||
tag="a"
|
||||
href="https://gitee.com/youlaiorg/vue3-element-admin/blob/master/src/views/demo/table/curd-single.vue"
|
||||
type="primary"
|
||||
target="_blank"
|
||||
link
|
||||
type="info"
|
||||
size="small"
|
||||
>
|
||||
整合版示例源码 请点击 >>>
|
||||
</el-link>
|
||||
<el-icon class="mr-1"><Link /></el-icon>
|
||||
示例源码
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 搜索 -->
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
<template>
|
||||
<div class="page-container h-full flex flex-1 flex-col">
|
||||
<div class="flex-x-between mb-10">
|
||||
<el-link
|
||||
<el-button
|
||||
tag="a"
|
||||
href="https://gitee.com/youlaiorg/vue3-element-admin/blob/master/src/views/demo/table/curd/index.vue"
|
||||
type="primary"
|
||||
target="_blank"
|
||||
link
|
||||
type="info"
|
||||
size="small"
|
||||
>
|
||||
示例源码 请点击>>>
|
||||
</el-link>
|
||||
<el-icon class="mr-1"><Link /></el-icon>
|
||||
示例源码
|
||||
</el-button>
|
||||
<el-button type="primary" plain round size="small" @click="isA = !isA">切换示例</el-button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -34,12 +34,7 @@ defineOptions({
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* 表单渲染组件
|
||||
*
|
||||
* @description 填写页、公开填写页、设计器预览页共用,保证三处渲染效果一致;
|
||||
* 规则加载与提交落库由各承载页实现,本组件只负责渲染、校验与数据抛出
|
||||
*/
|
||||
/** 表单渲染组件(填写/公开/预览三页共用;规则加载与提交由承载页实现) */
|
||||
const props = defineProps<{
|
||||
/** 表单标题(卡片头展示) */
|
||||
title: string;
|
||||
@@ -63,10 +58,7 @@ const emit = defineEmits<{
|
||||
const formApi = ref();
|
||||
const formData = ref<Record<string, unknown>>({});
|
||||
|
||||
/**
|
||||
* 从成功态返回填写态时清空已提交数据:
|
||||
* form-create 随 v-if 重新挂载会重新应用规则默认值,此处仅需清掉残留提交值
|
||||
*/
|
||||
// 返回填写态时清掉残留提交值(重新挂载会重新应用规则默认值)
|
||||
watch(
|
||||
() => props.submitted,
|
||||
(submitted) => {
|
||||
@@ -76,9 +68,7 @@ watch(
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 校验通过后抛出表单数据(浅拷贝,避免承载页异步提交期间数据被继续编辑污染)
|
||||
*/
|
||||
// 抛出表单数据(浅拷贝,防异步提交期间被继续编辑污染)
|
||||
function handleSubmit(): void {
|
||||
emit("submit", { ...formData.value });
|
||||
}
|
||||
|
||||
@@ -91,13 +91,7 @@ defineOptions({
|
||||
name: "FormPublishDialog",
|
||||
});
|
||||
|
||||
/**
|
||||
* 表单发布/入口管理向导容器
|
||||
*
|
||||
* @description 三步向导(方式选择 → 入口配置 → 发布完成)的状态与编排容器:
|
||||
* 数据加载、保存编排(发布 → 生成菜单 → 保存公开开关)在容器完成,
|
||||
* 各步骤展示细节由 publish/ 目录下的步骤组件承担
|
||||
*/
|
||||
/** 发布/入口管理三步向导容器 */
|
||||
const props = defineProps<{
|
||||
/** 表单ID */
|
||||
formId: string;
|
||||
@@ -119,7 +113,7 @@ const visible = defineModel("modelValue", {
|
||||
default: false,
|
||||
});
|
||||
|
||||
/** 表单是否已发布(决定向导模式:发布流程 / 入口管理直达第②步) */
|
||||
/** 表单是否已发布(已发布直达入口配置步骤) */
|
||||
const isPublished = computed(() => props.status === FormStatus.PUBLISHED);
|
||||
|
||||
/** 当前向导步骤 */
|
||||
@@ -149,13 +143,10 @@ const entryConfigRef = ref<InstanceType<typeof EntryConfigStep>>();
|
||||
/** 目录类型标识(sys_menu.type:C 目录) */
|
||||
const MENU_TYPE_CATALOG = "C";
|
||||
|
||||
/** 默认挂载目录名称:挂在"动态表单"目录下,管理菜单不授权给填写角色即不渲染 */
|
||||
/** 默认挂载目录名称 */
|
||||
const DEFAULT_CATALOG_NAME = "表单中心";
|
||||
|
||||
/**
|
||||
* 是否展示菜单入口配置块:
|
||||
* 已发布(入口管理)统一管理两种入口;未发布按第①步所选方式渲染
|
||||
*/
|
||||
/** 是否展示菜单入口配置块(已发布恒显示,未发布按第①步所选) */
|
||||
const showMenuConfig = computed(() =>
|
||||
isPublished.value ? true : selectedMethods.value.includes("menu")
|
||||
);
|
||||
@@ -176,7 +167,7 @@ const shareUrl = computed(
|
||||
() => `${window.location.origin}${window.location.pathname}#/f/${props.formKey}`
|
||||
);
|
||||
|
||||
/** 第③步分享汇总可见:本次配置了分享且公开开关开启 */
|
||||
/** 第③步分享汇总可见(配置了分享且开关开启) */
|
||||
const shareResultVisible = computed(() => showShareConfig.value && shareEnabled.value);
|
||||
|
||||
/** 第③步副标题 */
|
||||
@@ -194,9 +185,7 @@ watch(visible, async (newVisible) => {
|
||||
await loadWizardData();
|
||||
});
|
||||
|
||||
/**
|
||||
* 加载向导数据:目录树、角色选项、已生成菜单配置回显
|
||||
*/
|
||||
// 加载目录树、角色选项与已生成菜单配置
|
||||
async function loadWizardData(): Promise<void> {
|
||||
const [menuTree, roles, menu] = await Promise.all([
|
||||
MenuAPI.getList({}),
|
||||
@@ -206,20 +195,19 @@ async function loadWizardData(): Promise<void> {
|
||||
catalogTree.value = buildCatalogTree(menuTree);
|
||||
roleOptions.value = roles;
|
||||
|
||||
// 回显已生成的菜单配置;未生成过时菜单名默认表单名、父级默认"表单中心"
|
||||
// 未生成过菜单时:名称默认表单名、父级默认"表单中心"
|
||||
menuConfig.value = menu;
|
||||
Object.assign(menuForm, {
|
||||
menuName: menu?.menuName ?? props.formName,
|
||||
parentId: menu?.parentId ?? findDefaultCatalogId(),
|
||||
roleIds: menu?.roleIds ? [...menu.roleIds] : [],
|
||||
});
|
||||
// 回显公开开关(列表行传入,未开启过为 0)
|
||||
// 回显公开开关
|
||||
shareEnabled.value = props.isPublic === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换发布方式选中态(可单选可全选)
|
||||
*
|
||||
* 切换发布方式选中态
|
||||
* @param method 发布方式
|
||||
*/
|
||||
function toggleMethod(method: PublishMethod): void {
|
||||
@@ -233,7 +221,6 @@ function toggleMethod(method: PublishMethod): void {
|
||||
|
||||
/**
|
||||
* 构建目录树(仅目录类型可挂载表单菜单)
|
||||
*
|
||||
* @param menus 菜单树
|
||||
*/
|
||||
function buildCatalogTree(menus: MenuItem[]): CatalogNode[] {
|
||||
@@ -247,9 +234,7 @@ function buildCatalogTree(menus: MenuItem[]): CatalogNode[] {
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找默认挂载目录ID:递归查找"表单中心"(已生成过则预选);不存在返回空串(留空=后端自动创建)
|
||||
*/
|
||||
// 递归查找"表单中心"目录ID;不存在返回空串(留空由后端自动创建)
|
||||
function findDefaultCatalogId(): string {
|
||||
const find = (nodes: CatalogNode[]): string => {
|
||||
for (const node of nodes) {
|
||||
@@ -266,14 +251,8 @@ function findDefaultCatalogId(): string {
|
||||
return find(catalogTree.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 菜单路径面包屑:选中目录的名称链 + 菜单名称
|
||||
* 留空(默认目录)时展示后端实际挂载位置:动态表单 / 表单中心
|
||||
*/
|
||||
/** 菜单位置面包屑(目录名称链 + 菜单名) */
|
||||
const menuBreadcrumb = computed(() => {
|
||||
if (!menuForm.parentId) {
|
||||
return ["动态表单", DEFAULT_CATALOG_NAME, menuForm.menuName];
|
||||
}
|
||||
const names: string[] = [];
|
||||
const find = (nodes: CatalogNode[], chain: string[]): boolean => {
|
||||
for (const node of nodes) {
|
||||
@@ -297,14 +276,8 @@ const grantedRoleNames = computed(() =>
|
||||
.map((role) => role.label)
|
||||
);
|
||||
|
||||
/**
|
||||
* 生成菜单的完整路由路径:目录路径链 + 表单标识
|
||||
* 留空(默认目录)时为后端标准挂载位置:/form/center/{formKey}
|
||||
*/
|
||||
/** 菜单完整路由路径(目录路径链 + 表单标识) */
|
||||
const menuRoutePath = computed(() => {
|
||||
if (!menuForm.parentId) {
|
||||
return `/form/center/${props.formKey}`;
|
||||
}
|
||||
let routePath = "";
|
||||
const find = (nodes: CatalogNode[], basePath: string): boolean => {
|
||||
for (const node of nodes) {
|
||||
@@ -324,9 +297,7 @@ const menuRoutePath = computed(() => {
|
||||
return routePath;
|
||||
});
|
||||
|
||||
/**
|
||||
* 保存入口配置:未发布表单先发布(版本+1),再按所选方式生成菜单/保存公开开关
|
||||
*/
|
||||
// 保存入口配置:未发布先发布,再生成菜单/保存公开开关
|
||||
async function handleSaveEntry(): Promise<void> {
|
||||
if (showMenuConfig.value) {
|
||||
const valid = (await entryConfigRef.value?.validate()) ?? true;
|
||||
@@ -341,14 +312,18 @@ async function handleSaveEntry(): Promise<void> {
|
||||
if (showMenuConfig.value) {
|
||||
await FormAPI.saveFormMenu(props.formId, {
|
||||
menuName: menuForm.menuName,
|
||||
// 留空不传:后端自动使用/创建默认目录"表单中心"
|
||||
parentId: menuForm.parentId || undefined,
|
||||
parentId: menuForm.parentId,
|
||||
roleIds: menuForm.roleIds,
|
||||
});
|
||||
}
|
||||
if (showShareConfig.value) {
|
||||
// isPublic 独立提交:仅配置分享入口时才写开关,避免菜单-only 保存意外重置公开状态
|
||||
await FormAPI.update(props.formId, { isPublic: shareEnabled.value ? 1 : 0 });
|
||||
// isPublic 独立提交,避免菜单-only 保存意外重置公开状态
|
||||
// formKey/formName 原样回传满足后端非空校验(已发布态后端不更新元数据)
|
||||
await FormAPI.update(props.formId, {
|
||||
formKey: props.formKey,
|
||||
formName: props.formName,
|
||||
isPublic: shareEnabled.value ? 1 : 0,
|
||||
});
|
||||
}
|
||||
step.value = 2;
|
||||
emit("success");
|
||||
@@ -357,9 +332,7 @@ async function handleSaveEntry(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制分享链接
|
||||
*/
|
||||
// 复制分享链接
|
||||
function handleCopyLink(): void {
|
||||
navigator.clipboard
|
||||
?.writeText(shareUrl.value)
|
||||
@@ -367,9 +340,7 @@ function handleCopyLink(): void {
|
||||
.catch(() => ElMessage.warning("复制失败,请手动复制"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 前往查看:重载动态路由(菜单为新建,当前路由表未包含)后跳转
|
||||
*/
|
||||
// 重载动态路由(新建菜单需重新拉取路由表)后跳转
|
||||
async function handleGoView(): Promise<void> {
|
||||
if (!menuRoutePath.value) {
|
||||
ElMessage.warning("未能解析菜单路径,请刷新页面后从侧边栏进入");
|
||||
@@ -381,9 +352,7 @@ async function handleGoView(): Promise<void> {
|
||||
router.push(menuRoutePath.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭向导并重置步骤
|
||||
*/
|
||||
// 关闭向导并重置步骤
|
||||
function handleClose(): void {
|
||||
step.value = 0;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
ref="menuFormRef"
|
||||
:model="menuForm"
|
||||
:rules="rules"
|
||||
label-width="80px"
|
||||
label-width="92px"
|
||||
class="entry-section__form"
|
||||
>
|
||||
<el-form-item label="菜单名称" prop="menuName">
|
||||
@@ -22,10 +22,7 @@
|
||||
<template #label>
|
||||
<div class="flex-y-center">
|
||||
上级菜单
|
||||
<el-tooltip placement="bottom">
|
||||
<template #content>
|
||||
留空时后端自动创建"表单中心"目录(挂在"动态表单"下);仅目录类型可选
|
||||
</template>
|
||||
<el-tooltip content="表单入口挂载的目录位置,仅目录类型可选" placement="bottom">
|
||||
<el-icon class="ml-1 cursor-pointer">
|
||||
<QuestionFilled />
|
||||
</el-icon>
|
||||
@@ -34,10 +31,9 @@
|
||||
</template>
|
||||
<el-tree-select
|
||||
v-model="menuForm.parentId"
|
||||
placeholder="留空默认:表单中心"
|
||||
placeholder="请选择上级菜单"
|
||||
:data="catalogTree"
|
||||
filterable
|
||||
clearable
|
||||
check-strictly
|
||||
:render-after-expand="false"
|
||||
class="!w-full"
|
||||
@@ -86,7 +82,7 @@
|
||||
|
||||
<!-- 对外分享分区 -->
|
||||
<EntrySection v-if="showShare" kind="share" title="对外分享" sub="匿名链接/二维码进入">
|
||||
<el-form label-width="80px" class="entry-section__form">
|
||||
<el-form label-width="92px" class="entry-section__form">
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="flex-y-center">
|
||||
@@ -142,12 +138,7 @@ defineOptions({
|
||||
name: "FormPublishEntryConfigStep",
|
||||
});
|
||||
|
||||
/**
|
||||
* 入口配置(向导第②步)
|
||||
*
|
||||
* @description 菜单入口(名称/目录/角色)与公开分享开关的表单分区;
|
||||
* 表单数据与开关由容器持有(defineModel 双向绑定),校验能力通过 expose 提供给容器保存前调用
|
||||
*/
|
||||
/** 入口配置(向导第②步;数据由容器持有,校验经 expose 供容器保存前调用) */
|
||||
const props = defineProps<{
|
||||
/** 是否展示菜单入口配置块 */
|
||||
showMenu: boolean;
|
||||
@@ -171,11 +162,12 @@ const shareEnabled = defineModel<boolean>("shareEnabled", { required: true });
|
||||
|
||||
const menuFormRef = ref<FormInstance>();
|
||||
|
||||
/** 菜单表单校验规则:菜单名称必填;上级菜单留空 = 默认目录"表单中心"(后端自动创建) */
|
||||
/** 菜单表单校验规则:菜单名称与上级菜单必填 */
|
||||
const rules = computed<FormRules<FormMenuFormData>>(() =>
|
||||
props.showMenu
|
||||
? {
|
||||
menuName: [{ required: true, message: "请输入菜单名称", trigger: "blur" }],
|
||||
parentId: [{ required: true, message: "请选择上级菜单", trigger: "change" }],
|
||||
}
|
||||
: {}
|
||||
);
|
||||
@@ -187,8 +179,7 @@ function handleSelectAllRoles(): void {
|
||||
|
||||
/**
|
||||
* 校验菜单表单(容器保存前调用)
|
||||
*
|
||||
* @return 校验是否通过;未配置菜单入口时恒通过
|
||||
* @return 未配置菜单入口时恒通过
|
||||
*/
|
||||
function validate(): Promise<boolean> {
|
||||
if (!props.showMenu || !menuFormRef.value) {
|
||||
@@ -213,6 +204,11 @@ defineExpose({ validate });
|
||||
|
||||
.entry-section__form {
|
||||
padding: 16px 14px 0;
|
||||
|
||||
// label 带提示图标,禁止折行
|
||||
:deep(.el-form-item__label) {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
/* 角色多选下拉 header:全选 + 计数 */
|
||||
|
||||
@@ -19,12 +19,7 @@ defineOptions({
|
||||
name: "FormPublishEntrySection",
|
||||
});
|
||||
|
||||
/**
|
||||
* 入口分区卡片
|
||||
*
|
||||
* @description 发布向导第②步(入口配置)与第③步(发布完成)共用的分区容器,
|
||||
* 左侧 3px 色条区分两种形态(蓝=系统内嵌 绿=对外分享)
|
||||
*/
|
||||
/** 入口分区卡片(发布向导②③步共用,左侧色条区分内嵌/分享形态) */
|
||||
defineProps<{
|
||||
/** 分区形态:menu 系统内嵌 / share 对外分享 */
|
||||
kind: "menu" | "share";
|
||||
|
||||
@@ -47,11 +47,7 @@ defineOptions({
|
||||
name: "FormPublishMethodStep",
|
||||
});
|
||||
|
||||
/**
|
||||
* 发布方式选择(向导第①步)
|
||||
*
|
||||
* @description 卡片可单选可全选;已生成过入口/已开启分享的形态带标签提示
|
||||
*/
|
||||
/** 发布方式选择(向导第①步;卡片单选/全选,已配置过的入口带标签提示) */
|
||||
defineProps<{
|
||||
/** 选中的发布方式 */
|
||||
selected: PublishMethod[];
|
||||
|
||||
@@ -55,11 +55,7 @@ defineOptions({
|
||||
name: "FormPublishResultStep",
|
||||
});
|
||||
|
||||
/**
|
||||
* 发布完成汇总(向导第③步)
|
||||
*
|
||||
* @description 展示保存后的最终入口配置:菜单位置面包屑、可见角色、分享二维码与链接
|
||||
*/
|
||||
/** 发布完成汇总(向导第③步:菜单位置、可见角色、分享二维码与链接) */
|
||||
const props = defineProps<{
|
||||
/** 是否展示菜单入口汇总 */
|
||||
showMenu: boolean;
|
||||
@@ -85,7 +81,7 @@ const emit = defineEmits<{
|
||||
/** 分享二维码画布 */
|
||||
const qrCanvasRef = ref<HTMLCanvasElement>();
|
||||
|
||||
// 本组件仅在向导第③步渲染(v-else-if 挂载),此时配置已定,挂载后绘制一次即可
|
||||
// 仅在第③步渲染(v-else-if 挂载),配置已定,挂载后绘制一次即可
|
||||
onMounted(async () => {
|
||||
if (!props.shareVisible) return;
|
||||
await nextTick();
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
/**
|
||||
* 表单规则加载与渲染状态
|
||||
*
|
||||
* @description
|
||||
* 填写页(render.vue)、公开填写页(share.vue)、设计器预览页(preview.vue)共用,
|
||||
* 保证三处走同一条渲染管线:相同的规则解析、提交按钮策略与成功态切换
|
||||
* 填写页(render)、公开页(share)、预览页(preview)共用同一渲染管线
|
||||
*/
|
||||
|
||||
import { ref, shallowRef } from "vue";
|
||||
@@ -25,17 +23,15 @@ export interface FormRuleSource {
|
||||
export function useFormRenderer<T extends FormRuleSource = FormRuleSource>(
|
||||
loader: () => Promise<T | null | undefined>
|
||||
) {
|
||||
// shallowRef:规则与配置整体加载整体替换,无需深层响应式;
|
||||
// 深响应式代理会破坏 form-create Rule 内部 Creator 结构(模板 UnwrapRef 深映射导致类型失配)
|
||||
// shallowRef:深响应式代理会破坏 form-create Rule 内部 Creator 结构
|
||||
const rule = shallowRef<Rule[]>([]);
|
||||
const option = shallowRef<Options>({ submitBtn: true });
|
||||
const loading = ref(false);
|
||||
const submitted = ref(false);
|
||||
|
||||
/**
|
||||
* 加载并解析规则,返回接口原始数据
|
||||
*
|
||||
* 强制开启提交按钮:填写页必须有提交入口,设计器保存时可能关掉了它
|
||||
* 加载并解析规则
|
||||
* @returns 接口原始数据
|
||||
*/
|
||||
async function load(): Promise<T | undefined> {
|
||||
loading.value = true;
|
||||
@@ -43,6 +39,7 @@ export function useFormRenderer<T extends FormRuleSource = FormRuleSource>(
|
||||
const data = await loader();
|
||||
rule.value = data?.formJson ? JSON.parse(data.formJson) : [];
|
||||
const parsedOption: Options = data?.optionsJson ? JSON.parse(data.optionsJson) : {};
|
||||
// 强制开启提交按钮:填写页必须有提交入口,设计器保存时可能关掉了它
|
||||
option.value = { ...parsedOption, submitBtn: true };
|
||||
return data ?? undefined;
|
||||
} finally {
|
||||
@@ -50,7 +47,7 @@ export function useFormRenderer<T extends FormRuleSource = FormRuleSource>(
|
||||
}
|
||||
}
|
||||
|
||||
/** 回到填写态(已填数据清空由 FormRenderer 处理) */
|
||||
// 回到填写态(已填数据清空由 FormRenderer 处理)
|
||||
function refill(): void {
|
||||
submitted.value = false;
|
||||
}
|
||||
|
||||
@@ -55,7 +55,8 @@
|
||||
{{ formatCellValue(getFieldValue(scope.row as FormDataItem, field.field), field) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="提交人" width="120" align="center" show-overflow-tooltip>
|
||||
<!-- 弹性列:吸收剩余宽度,避免表格右侧留白 -->
|
||||
<el-table-column label="提交人" min-width="120" align="center" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
{{ (scope.row as FormDataItem).createByName || "匿名" }}
|
||||
</template>
|
||||
@@ -140,10 +141,10 @@ defineOptions({
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
/** 表单唯一标识(列表"数据"按钮 query 携带,复刻 dict-item 模式) */
|
||||
/** 表单唯一标识(列表"数据"按钮携带) */
|
||||
const formKey = computed(() => String(route.query.formKey ?? ""));
|
||||
|
||||
/** 页面标题(列表页携带,如【入职信息采集】数据) */
|
||||
/** 页面标题(列表页携带) */
|
||||
const title = computed(() => String(route.query.title ?? "表单数据"));
|
||||
|
||||
const tableWrapperRef = ref<HTMLElement | null>(null);
|
||||
@@ -152,10 +153,10 @@ const { toggle: toggleFullscreen } = useFullscreen(tableWrapperRef);
|
||||
/** 表单名称(导出文件名用) */
|
||||
const formName = ref("");
|
||||
|
||||
/** 导出中状态(防止重复触发全量拉取) */
|
||||
/** 导出中状态(防重复触发) */
|
||||
const exporting = ref(false);
|
||||
|
||||
/** 表单规则(详情抽屉只读回显复用;shallowRef 避免深代理破坏 Rule 内部的 Creator 结构) */
|
||||
/** 表单规则(shallowRef 避免深代理破坏 Rule 内部 Creator 结构) */
|
||||
const rule = shallowRef<Rule[]>([]);
|
||||
|
||||
/** 从表单规则提取的字段元数据(field + title + 选项映射) */
|
||||
@@ -202,11 +203,7 @@ onMounted(async () => {
|
||||
fetchData();
|
||||
});
|
||||
|
||||
/**
|
||||
* 行数据解析缓存(行ID -> field/value 映射)
|
||||
*
|
||||
* 避免模板渲染时每行每列各解析一次同一行 dataJson
|
||||
*/
|
||||
/** 行数据解析缓存(行ID -> 字段值映射,避免模板重复解析 dataJson) */
|
||||
const rowDataMap = computed(() => {
|
||||
const map = new Map<string, Record<string, unknown>>();
|
||||
list.value.forEach((row) => map.set(row.id, parseDataJson(row.dataJson)));
|
||||
@@ -215,7 +212,6 @@ const rowDataMap = computed(() => {
|
||||
|
||||
/**
|
||||
* 取行数据的指定字段值
|
||||
*
|
||||
* @param row 数据行
|
||||
* @param field 字段名
|
||||
*/
|
||||
@@ -223,33 +219,27 @@ function getFieldValue(row: FormDataItem, field: string): unknown {
|
||||
return rowDataMap.value.get(row.id)?.[field];
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开详情抽屉并只读回显
|
||||
*
|
||||
* 规则优先加载后端返回的提交时版本快照,防止表单改版(字段删除/改名)导致历史数据回显漂移;
|
||||
* 无快照(快照表启用前提交的历史数据)回退当前定义
|
||||
*/
|
||||
// 打开详情抽屉只读回显(优先提交时版本快照,防止表单改版后历史数据漂移)
|
||||
async function handleDetailClick(row: FormDataItem): Promise<void> {
|
||||
const detail = await FormAPI.getFormDataDetail(formKey.value, row.id);
|
||||
detailRow.value = detail;
|
||||
detailData.value = detail.dataJson ? JSON.parse(detail.dataJson) : {};
|
||||
// JSON.parse 产出的规则与页面级渲染规则隔离,可安全写入 disabled 只读态
|
||||
// 独立 JSON.parse,与页面渲染规则隔离,可安全置为只读
|
||||
detailRule.value = detail.formJson
|
||||
? JSON.parse(detail.formJson)
|
||||
: JSON.parse(JSON.stringify(rule.value));
|
||||
// 全局配置随快照回显;强制隐藏提交/重置按钮(快照配置可能开启了它们)
|
||||
// 强制隐藏提交/重置按钮(快照配置可能开启)
|
||||
const parsedOption: Options = detail.optionsJson ? JSON.parse(detail.optionsJson) : {};
|
||||
detailOption.value = { ...parsedOption, submitBtn: false, resetBtn: false };
|
||||
detailState.visible = true;
|
||||
// 抽屉内 form-create 挂载完成后再禁用(api 就绪时机在 nextTick 之后)
|
||||
// form-create 挂载完成后再禁用
|
||||
await nextTick();
|
||||
detailApi.value?.disabled(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除单个或批量表单数据
|
||||
*
|
||||
* @param id 指定时删除单条数据;不指定时删除表格勾选项
|
||||
* @param id 指定时删除单条,否则删除勾选项
|
||||
*/
|
||||
async function handleDelete(id?: string): Promise<void> {
|
||||
const dataIds = id ?? selectedIds.value.join(",");
|
||||
@@ -279,18 +269,13 @@ async function handleDelete(id?: string): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/** 导出分页大小(单页拉取行数,与导出上限配合控制内存占用) */
|
||||
/** 导出分页大小 */
|
||||
const EXPORT_PAGE_SIZE = 500;
|
||||
|
||||
/** 导出页数上限(超出截断并提示,防止超大表单拖垮浏览器) */
|
||||
/** 导出页数上限(超出截断提示) */
|
||||
const EXPORT_MAX_PAGES = 100;
|
||||
|
||||
/**
|
||||
* 导出全量表单数据为 Excel
|
||||
*
|
||||
* 动态列 = 表单全部字段(不受列表展示列数上限约束)+ 提交人/版本/提交时间;
|
||||
* 数据按分页循环拉取全量,exceljs 动态导入按需加载
|
||||
*/
|
||||
// 导出全量数据为 Excel(全部字段 + 提交人/版本/时间;分页拉取,exceljs 按需加载)
|
||||
async function handleExport(): Promise<void> {
|
||||
if (exporting.value) return;
|
||||
exporting.value = true;
|
||||
@@ -333,7 +318,6 @@ async function handleExport(): Promise<void> {
|
||||
const data = parseDataJson(row.dataJson);
|
||||
const record: Record<string, unknown> = {};
|
||||
fields.value.forEach((field) => {
|
||||
// 与列表展示同规则:选项 value 翻译为 label,时间格式化
|
||||
record[field.field] = formatCellValue(data[field.field], field);
|
||||
});
|
||||
record.createByName = row.createByName || "匿名";
|
||||
|
||||
@@ -35,38 +35,45 @@ defineOptions({
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
/** 表单 ID(设计器始终由列表页"设计"按钮带参进入) */
|
||||
/** 表单 ID(列表页"设计"按钮携带) */
|
||||
const formId = computed(() => String(route.query.id ?? ""));
|
||||
|
||||
/** 页面标题(列表页携带,如【入职信息采集】表单设计) */
|
||||
/** 页面标题(列表页携带) */
|
||||
const title = computed(() => String(route.query.title ?? "表单设计"));
|
||||
|
||||
const designerRef = ref();
|
||||
|
||||
/** 设计器配置:隐藏自带保存按钮,保存统一走工具栏 */
|
||||
/** 隐藏设计器自带保存按钮,统一走工具栏 */
|
||||
const designerConfig = { showSaveBtn: false };
|
||||
|
||||
/** 保存中状态 */
|
||||
const saving = ref(false);
|
||||
|
||||
/** 回显已有规则(空规则为空白画布) */
|
||||
/** 表单定义元数据(保存时回传以满足后端非空校验) */
|
||||
const formMeta = ref<{ formKey?: string; formName?: string }>({});
|
||||
|
||||
// 回显已有规则(空规则为空白画布)
|
||||
onMounted(async () => {
|
||||
if (!formId.value) {
|
||||
ElMessage.error("缺少表单ID参数");
|
||||
return;
|
||||
}
|
||||
const data = await FormAPI.getFormData(formId.value);
|
||||
formMeta.value = { formKey: data.formKey, formName: data.formName };
|
||||
if (data.formJson) {
|
||||
designerRef.value?.setRule(JSON.parse(data.formJson));
|
||||
}
|
||||
if (data.optionsJson) {
|
||||
designerRef.value?.setOption(JSON.parse(data.optionsJson));
|
||||
if (data.optionsJson || data.formName) {
|
||||
const option = data.optionsJson ? JSON.parse(data.optionsJson) : {};
|
||||
// 画布「表单名称」面板回显读 option.formName,而 getOption 保存产出的是面板字段原名
|
||||
// formCreateFormName,键名不对称需转换;画布未填时用表单定义名称预填
|
||||
option.formName = option.formName || option.formCreateFormName || data.formName || "";
|
||||
delete option.formCreateFormName;
|
||||
designerRef.value?.setOption(option);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 保存设计器产出的规则与全局配置
|
||||
*/
|
||||
// 保存设计器产出的规则与全局配置
|
||||
async function handleSave(): Promise<void> {
|
||||
if (!formId.value) {
|
||||
ElMessage.error("缺少表单ID参数");
|
||||
@@ -76,25 +83,19 @@ async function handleSave(): Promise<void> {
|
||||
const optionsJson = JSON.stringify(designerRef.value?.getOption() ?? {});
|
||||
saving.value = true;
|
||||
try {
|
||||
await FormAPI.update(formId.value, { formJson, optionsJson });
|
||||
await FormAPI.update(formId.value, { ...formMeta.value, formJson, optionsJson });
|
||||
ElMessage.success("保存成功");
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回表单管理列表
|
||||
*/
|
||||
// 返回表单列表
|
||||
function handleBack(): void {
|
||||
router.back();
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行态真预览:跳预览页用与填写承载页相同的渲染管线查看效果
|
||||
*
|
||||
* 预览读取的是已保存规则,未保存的改动不会体现,需先保存再预览
|
||||
*/
|
||||
// 预览读取已保存规则,未保存改动不体现
|
||||
function handlePreview(): void {
|
||||
if (!formId.value) {
|
||||
ElMessage.error("缺少表单ID参数");
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<el-card ref="tableWrapperRef" class="page-content" shadow="never">
|
||||
<div class="page-toolbar">
|
||||
<div class="page-toolbar__left">
|
||||
<!-- 类型过滤视图:空串=全部;切换即查询,替代原"类型列+搜索区类型下拉"双冗余 -->
|
||||
<!-- 类型过滤:空串=全部,切换即查询 -->
|
||||
<el-radio-group v-model="params.category" @change="handleQuery">
|
||||
<el-radio-button value="">全部</el-radio-button>
|
||||
<el-radio-button v-for="(label, value) in categoryOptions" :key="value" :value="value">
|
||||
@@ -110,6 +110,7 @@
|
||||
设计
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="scope.row.status === FormStatus.PUBLISHED"
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
@@ -329,7 +330,7 @@ const dialogState = reactive({
|
||||
visible: false,
|
||||
});
|
||||
|
||||
/** 新增表单默认值:类型缺省普通表单(工作流表单需明确选择) */
|
||||
/** 新增表单默认值:类型缺省普通表单 */
|
||||
const initialFormData: FormDefinitionData = {
|
||||
category: "normal",
|
||||
};
|
||||
@@ -348,9 +349,7 @@ const rules: FormRules<FormDefinitionData> = {
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* 重置表单数据和验证状态
|
||||
*/
|
||||
// 重置表单数据和校验状态
|
||||
function resetForm(): void {
|
||||
formDefinitionFormRef.value?.resetFields();
|
||||
formDefinitionFormRef.value?.clearValidate();
|
||||
@@ -364,17 +363,13 @@ function openDialog(): void {
|
||||
dialogState.visible = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭表单定义弹窗并清理临时状态
|
||||
*/
|
||||
// 关闭弹窗并重置
|
||||
function closeDialog(): void {
|
||||
dialogState.visible = false;
|
||||
resetForm();
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开新增表单定义弹窗:类型按当前过滤视图预选(仍可修改)
|
||||
*/
|
||||
// 打开新增弹窗(类型按当前过滤视图预选)
|
||||
function handleCreateClick(): void {
|
||||
resetForm();
|
||||
editingStatus.value = null;
|
||||
@@ -384,9 +379,8 @@ function handleCreateClick(): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开编辑表单定义弹窗并回填数据
|
||||
*
|
||||
* @param row 当前表单行(携带状态,用于判断类型可否修改)
|
||||
* 打开编辑弹窗并回填数据
|
||||
* @param row 当前表单行(携带状态,判断类型可否修改)
|
||||
*/
|
||||
async function handleEditClick(row: FormDefinitionItem): Promise<void> {
|
||||
resetForm();
|
||||
@@ -397,9 +391,7 @@ async function handleEditClick(row: FormDefinitionItem): Promise<void> {
|
||||
openDialog();
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验并提交表单定义
|
||||
*/
|
||||
// 校验并提交
|
||||
async function handleSubmit(): Promise<void> {
|
||||
const valid = await formDefinitionFormRef.value?.validate().then(
|
||||
() => true,
|
||||
@@ -411,7 +403,10 @@ async function handleSubmit(): Promise<void> {
|
||||
try {
|
||||
const id = formData.id;
|
||||
if (id) {
|
||||
await FormAPI.update(id, formData);
|
||||
// 仅提交元数据:formData 由回显数据整体赋值,含 formJson/optionsJson,
|
||||
// 整体回写会用打开弹窗时的旧规则覆盖期间设计器保存的新规则
|
||||
const { formKey, formName, category, description } = formData;
|
||||
await FormAPI.update(id, { formKey, formName, category, description });
|
||||
ElMessage.success("修改成功");
|
||||
} else {
|
||||
await FormAPI.create(formData);
|
||||
@@ -424,9 +419,7 @@ async function handleSubmit(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布向导状态("发布"与已发布表单的"入口管理"共用)
|
||||
*/
|
||||
/** 发布向导状态(发布/入口管理共用) */
|
||||
const publishState = reactive({
|
||||
visible: false,
|
||||
formId: "",
|
||||
@@ -438,8 +431,7 @@ const publishState = reactive({
|
||||
});
|
||||
|
||||
/**
|
||||
* 打开发布向导(未发布走发布流程,已发布直达入口配置)
|
||||
*
|
||||
* 打开发布向导(已发布表单直达入口配置)
|
||||
* @param row 当前表单行
|
||||
*/
|
||||
function openPublishDialog(row: FormDefinitionItem): void {
|
||||
@@ -453,16 +445,13 @@ function openPublishDialog(row: FormDefinitionItem): void {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布向导保存成功后刷新列表(表单状态与版本可能已变更)
|
||||
*/
|
||||
// 发布后刷新列表(状态与版本可能已变更)
|
||||
function handlePublishSuccess(): void {
|
||||
fetchData();
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转到指定表单页面并检查路由是否已注册
|
||||
*
|
||||
* @param name 路由名称(FormDesigner/FormData)
|
||||
* @param query 路由参数
|
||||
*/
|
||||
@@ -482,7 +471,6 @@ function openFormPage(name: string, query: Record<string, string>): void {
|
||||
|
||||
/**
|
||||
* 跳转到表单数据页
|
||||
*
|
||||
* @param row 当前表单行
|
||||
*/
|
||||
function openDataPage(row: FormDefinitionItem): void {
|
||||
@@ -491,7 +479,6 @@ function openDataPage(row: FormDefinitionItem): void {
|
||||
|
||||
/**
|
||||
* 停用表单(已发出去的访问入口立即失效)
|
||||
*
|
||||
* @param id 表单 ID
|
||||
*/
|
||||
async function handleDisable(id: string): Promise<void> {
|
||||
@@ -512,8 +499,7 @@ async function handleDisable(id: string): Promise<void> {
|
||||
|
||||
/**
|
||||
* 删除单个或批量表单定义
|
||||
*
|
||||
* @param id 指定时删除单个表单;不指定时删除表格勾选项
|
||||
* @param id 指定时删除单个表单,否则删除勾选项
|
||||
*/
|
||||
async function handleDelete(id?: string): Promise<void> {
|
||||
const formIds = id ?? selectedIds.value.join(",");
|
||||
@@ -545,7 +531,6 @@ async function handleDelete(id?: string): Promise<void> {
|
||||
|
||||
/**
|
||||
* 跳转到表单设计器页面
|
||||
*
|
||||
* @param row 当前表单行
|
||||
*/
|
||||
function openDesigner(row: FormDefinitionItem): void {
|
||||
|
||||
@@ -37,10 +37,10 @@ defineOptions({
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
/** 表单 ID(设计器"预览"按钮携参进入) */
|
||||
/** 表单 ID(设计器"预览"按钮携带) */
|
||||
const formId = computed(() => String(route.query.id ?? ""));
|
||||
|
||||
/** 页面标题(设计器携带,如【入职信息采集】表单预览) */
|
||||
/** 页面标题(设计器携带) */
|
||||
const title = computed(() => String(route.query.title ?? "表单预览"));
|
||||
|
||||
// 走表单定义接口拉草稿规则:render 接口仅返回已发布表单,预览恰恰要覆盖未发布态
|
||||
@@ -56,12 +56,12 @@ onMounted(async () => {
|
||||
await load();
|
||||
});
|
||||
|
||||
/** 预览提交:校验通过即进入成功态,不落库 */
|
||||
// 预览不落库,校验通过即进入成功态
|
||||
function handlePreviewSubmit(): void {
|
||||
submitted.value = true;
|
||||
}
|
||||
|
||||
/** 返回设计器继续编辑 */
|
||||
// 返回设计器
|
||||
function handleBack(): void {
|
||||
router.back();
|
||||
}
|
||||
|
||||
@@ -36,12 +36,7 @@ defineOptions({
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
/**
|
||||
* 表单唯一标识
|
||||
*
|
||||
* 优先取菜单路由参数(meta.params 由后端菜单 params 转换而来,刷新不丢失);
|
||||
* query 兜底:侧边栏点击跳转时 LayoutSidebarItem 会将 meta.params 透传为 query
|
||||
*/
|
||||
/** 表单唯一标识(优先菜单路由 meta.params,query 兜底) */
|
||||
const formKey = computed(() => {
|
||||
const metaParams = route.meta.params as Record<string, unknown> | undefined;
|
||||
return String(metaParams?.formKey ?? route.query.formKey ?? "");
|
||||
@@ -63,8 +58,7 @@ onMounted(async () => {
|
||||
});
|
||||
|
||||
/**
|
||||
* 提交表单数据(FormRenderer 校验通过后触发)
|
||||
*
|
||||
* 提交表单数据(校验通过后触发)
|
||||
* @param data 表单数据(field -> value 映射)
|
||||
*/
|
||||
async function handleSubmit(data: Record<string, unknown>): Promise<void> {
|
||||
@@ -79,14 +73,10 @@ async function handleSubmit(data: Record<string, unknown>): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否有权查看收集数据(复用 form:data:list 数据查询权限)
|
||||
*
|
||||
* 无权限(匿名分享等)不显示"查看已提交数据"按钮,避免点击后无路由可跳
|
||||
*/
|
||||
/** 是否有权查看收集数据(无权限时不显示"查看已提交数据"按钮) */
|
||||
const canViewData = computed(() => hasPerm(["form:data:list"]));
|
||||
|
||||
/** 跳转数据列表(带 formKey 定位表单,列表按提交时间倒序,本次提交在首位) */
|
||||
// 跳转数据列表(按提交时间倒序,本次提交在首位)
|
||||
function handleViewData(): void {
|
||||
router.push({
|
||||
name: "FormData",
|
||||
|
||||
@@ -50,7 +50,7 @@ const formKey = computed(() => String(route.params.formKey ?? ""));
|
||||
const renderData = ref<FormRenderData>();
|
||||
const submitting = ref(false);
|
||||
|
||||
/** 加载失败提示(接口异常时 request 拦截器已弹错误消息,此处只负责兜底文案) */
|
||||
/** 加载失败兜底文案(错误消息由拦截器统一弹出) */
|
||||
const loadError = ref("");
|
||||
|
||||
const { rule, option, loading, submitted, load, refill } = useFormRenderer(() =>
|
||||
@@ -70,8 +70,7 @@ onMounted(async () => {
|
||||
});
|
||||
|
||||
/**
|
||||
* 匿名提交表单数据(FormRenderer 校验通过后触发)
|
||||
*
|
||||
* 匿名提交表单数据(校验通过后触发)
|
||||
* @param data 表单数据(field -> value 映射)
|
||||
*/
|
||||
async function handleSubmit(data: Record<string, unknown>): Promise<void> {
|
||||
@@ -81,7 +80,7 @@ async function handleSubmit(data: Record<string, unknown>): Promise<void> {
|
||||
await FormAPI.submitPublicFormData(formKey.value, data);
|
||||
submitted.value = true;
|
||||
} catch {
|
||||
// 提交失败(限流/校验不通过)消息由 request 拦截器统一弹出,无需重复提示;停留填写态可重试
|
||||
// 失败不重复提示(拦截器已弹),停留填写态可重试
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
/**
|
||||
* 表单字段解析
|
||||
*
|
||||
* @description 从 form-create 规则提取字段元数据、解析提交数据并格式化展示值
|
||||
* 表单字段解析(规则字段提取、提交数据解析与展示格式化)
|
||||
*/
|
||||
|
||||
/** 表单字段元数据 */
|
||||
@@ -15,12 +13,8 @@ export interface FormFieldMeta {
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归提取表单字段元数据
|
||||
*
|
||||
* 布局容器(row/col)自身无 field,其子节点递归收集;
|
||||
* 参数为 JSON 解析产物,结构未经校验,按 unknown 逐层探测
|
||||
*
|
||||
* @param rules form-create 规则(JSON 解析值)
|
||||
* 递归提取表单字段元数据(布局容器的子节点递归收集)
|
||||
* @param rules form-create 规则(JSON 解析产物,结构未校验)
|
||||
*/
|
||||
export function extractFields(rules: unknown): FormFieldMeta[] {
|
||||
const result: FormFieldMeta[] = [];
|
||||
@@ -45,11 +39,8 @@ export function extractFields(rules: unknown): FormFieldMeta[] {
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取字段选项映射(value -> label)
|
||||
*
|
||||
* 非对象或缺失 value/label 的脏项跳过
|
||||
*
|
||||
* @param options 规则里的 options 数组(JSON 解析值)
|
||||
* 提取字段选项映射(value -> label,脏项跳过)
|
||||
* @param options 规则 options 数组
|
||||
*/
|
||||
export function extractOptionMap(options: unknown): Map<string, string> {
|
||||
const optionMap = new Map<string, string>();
|
||||
@@ -66,7 +57,6 @@ export function extractOptionMap(options: unknown): Map<string, string> {
|
||||
|
||||
/**
|
||||
* 解析提交数据(field -> value 映射,解析失败按空数据兜底)
|
||||
*
|
||||
* @param dataJson 数据 JSON 字符串
|
||||
*/
|
||||
export function parseDataJson(dataJson?: string): Record<string, unknown> {
|
||||
@@ -79,9 +69,8 @@ export function parseDataJson(dataJson?: string): Record<string, unknown> {
|
||||
|
||||
/**
|
||||
* 格式化单元格展示值(选项翻译、数组拼接、对象序列化、空值占位)
|
||||
*
|
||||
* @param value 字段值
|
||||
* @param field 字段元数据(选项类字段 value 翻译为 label,未命中映射时原样展示)
|
||||
* @param field 字段元数据(选项类字段翻译 label,未命中原样展示)
|
||||
*/
|
||||
export function formatCellValue(value: unknown, field?: FormFieldMeta): string {
|
||||
if (value === null || value === undefined || value === "") return "-";
|
||||
|
||||
Reference in new issue
Block a user