This commit is contained in:
@@ -3,6 +3,7 @@ import { alovaInstance } from "@/utils/alova/alova-iview";
|
|||||||
import config from "@/assets/config/host.config";
|
import config from "@/assets/config/host.config";
|
||||||
import type {
|
import type {
|
||||||
ApplyVo,
|
ApplyVo,
|
||||||
|
FcFormCatalogPage,
|
||||||
FlowProgressVo,
|
FlowProgressVo,
|
||||||
FlowSummary,
|
FlowSummary,
|
||||||
PageResult,
|
PageResult,
|
||||||
@@ -135,6 +136,20 @@ class SvcDictApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class SvcFormApi {
|
class SvcFormApi {
|
||||||
|
/**
|
||||||
|
* 表单中心目录(v3/TASK_1):事项编辑页的表单选择器数据源。
|
||||||
|
* 走本应用同源代理,浏览器不持 appKey/secret、也不出现 form-center 地址(同 flow 能力的纪律);
|
||||||
|
* 校域由后端从 CAS 会话派生,前端没有 schId 入参——这是有意的,别"顺手加一个"。
|
||||||
|
*/
|
||||||
|
centerForms(siteCode: string, filter: { keyword?: string; page?: number; pageSize?: number } = {}) {
|
||||||
|
return get<FcFormCatalogPage>(`${SVC}/formCenter/forms`, {
|
||||||
|
siteCode,
|
||||||
|
keyword: filter.keyword || undefined,
|
||||||
|
page: filter.page,
|
||||||
|
pageSize: filter.pageSize
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/** 本地表单清单(存量回显用;v3 起不再新增定义,save/publish 已在后端明确退出) */
|
||||||
list(siteCode: string) {
|
list(siteCode: string) {
|
||||||
return get<SvcFormVo[]>(`${SVC}/form/list`, { siteCode });
|
return get<SvcFormVo[]>(`${SVC}/form/list`, { siteCode });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,6 +97,9 @@ export interface SvcItemEditVo {
|
|||||||
ruleCategoryId?: number;
|
ruleCategoryId?: number;
|
||||||
coverImage?: string;
|
coverImage?: string;
|
||||||
formCode?: string;
|
formCode?: string;
|
||||||
|
/** 表单中心绑定坐标(v3/TASK_1,成对;保存时后端会再向中心核验一次才落库) */
|
||||||
|
fcFormUuid?: string;
|
||||||
|
fcFormVersion?: number;
|
||||||
flowGuid?: string;
|
flowGuid?: string;
|
||||||
applyUrl?: string;
|
applyUrl?: string;
|
||||||
sortNo?: number;
|
sortNo?: number;
|
||||||
@@ -127,7 +130,13 @@ export interface SvcItemSaveRequest {
|
|||||||
noticeCategoryId?: number;
|
noticeCategoryId?: number;
|
||||||
ruleCategoryId?: number;
|
ruleCategoryId?: number;
|
||||||
coverImage?: string;
|
coverImage?: string;
|
||||||
|
/**
|
||||||
|
* 本地表单 code:v3/TASK_1 起**只读**——编辑存量事项时原样回传即保留历史绑定,
|
||||||
|
* 写一个新值会被后端拒(保存链路的 form_code 已停写)。新表单一律走 fcFormUuid + fcFormVersion。
|
||||||
|
*/
|
||||||
formCode?: string;
|
formCode?: string;
|
||||||
|
fcFormUuid?: string;
|
||||||
|
fcFormVersion?: number;
|
||||||
flowGuid?: string;
|
flowGuid?: string;
|
||||||
applyUrl?: string;
|
applyUrl?: string;
|
||||||
// ===== 服务承诺(P1-1):与后端 SvcModels.SvcItemSaveRequest 同名,勿改 =====
|
// ===== 服务承诺(P1-1):与后端 SvcModels.SvcItemSaveRequest 同名,勿改 =====
|
||||||
@@ -149,6 +158,29 @@ export interface SvcItemAttachmentVo {
|
|||||||
fileSize?: number;
|
fileSize?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== 表单中心目录(v3/TASK_1)=====
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 目录行 = 后端 `SvcFormCatalogVo`(形状对齐 form-center `GET /oapi/form/forms` 的 items)。
|
||||||
|
* 浏览器不直连表单中心:这条走本应用同源代理 `/api/svc/formCenter/forms`,
|
||||||
|
* 校域由后端从 CAS 会话派生,应用签名也不在前端出现。
|
||||||
|
*/
|
||||||
|
export interface FcFormCatalogItem {
|
||||||
|
formUuid: string;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
appCode: string;
|
||||||
|
/** 中心当前发布版本:选中即成对存下 formUuid + version */
|
||||||
|
version: number;
|
||||||
|
audience: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FcFormCatalogPage {
|
||||||
|
items: FcFormCatalogItem[];
|
||||||
|
/** 中心侧"已发布 ∩ 本应用可读"过滤后的总条数(先过滤再分页) */
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
// ===== 表单 =====
|
// ===== 表单 =====
|
||||||
|
|
||||||
export interface SvcFormVo {
|
export interface SvcFormVo {
|
||||||
|
|||||||
+94
-334
@@ -1,364 +1,124 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
/**
|
/**
|
||||||
* 表单设计器(独立全屏页,/svc/form/designer):三栏布局迁移自旧 FormManage,
|
* 表单设计器(v3/TASK_1 退场):表单定义的<b>正主是表单中心</b>,办事大厅不再维护第二套设计器,
|
||||||
* 左组件库 / 中多分组画布 / 右字段属性;预览复用 DynamicForm 渲染器。
|
* 也不再复制一套渲染器——两套房子的 schema 方言迟早分叉,而分叉的代价是线上事项填不了表。
|
||||||
* 产出 schema 严格对齐后端 FormValidator 契约(groups/fields + 白名单属性)。
|
*
|
||||||
|
* 路由刻意保留(导航与既有入口都指着 `/svc/form/designer`),直接 404 会让管理员以为系统坏了;
|
||||||
|
* 这里只回答两件事:**去哪儿做**(表单中心管理页)与**做完怎么接上**(回事项编辑页从目录里选)。
|
||||||
|
*
|
||||||
|
* 「表单管理」列表仍在:存量本地表单的历史绑定还要看得到、能清理,只是新建/发布不再在这里发生
|
||||||
|
* (那两个写入口已在后端明确退出,会原样返回业务错)。
|
||||||
*/
|
*/
|
||||||
import { computed, onMounted, ref } from "vue";
|
import { computed } from "vue";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
import { cloneDeep } from "lodash";
|
|
||||||
import { Message, Modal } from "view-ui-plus";
|
|
||||||
import { svcFlowApi, svcFormApi } from "@/api/module/svc";
|
|
||||||
import { useSite } from "@/utils/site";
|
|
||||||
import ComponentLibrary from "@/components/designer/ComponentLibrary.vue";
|
|
||||||
import DesignerCanvas from "@/components/designer/DesignerCanvas.vue";
|
|
||||||
import FieldPropPanel from "@/components/designer/FieldPropPanel.vue";
|
|
||||||
import DynamicForm from "@/components/DynamicForm.vue";
|
|
||||||
import type { DesignField, DesignGroup } from "@/types/designer";
|
|
||||||
import { KEY_PATTERN } from "@/types/designer";
|
|
||||||
import {
|
|
||||||
collectUsedKeys,
|
|
||||||
countFields,
|
|
||||||
defaultField,
|
|
||||||
genUid,
|
|
||||||
makeNextKey,
|
|
||||||
pickAllowed,
|
|
||||||
restoreGroups,
|
|
||||||
} from "@/utils/designer/schema";
|
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
|
||||||
const { currentSiteCode, loadSites } = useSite();
|
|
||||||
|
|
||||||
const formId = ref<number | null>(null);
|
/** 旧设计器带过来的表单 id:只做提示,不再驱动任何编辑 */
|
||||||
const formStatus = ref(0);
|
const legacyId = computed(() => String(route.query.id ?? ""));
|
||||||
const formVersion = ref(1);
|
|
||||||
const code = ref("");
|
|
||||||
const name = ref("");
|
|
||||||
/** 表单级流程绑定(2026-09-20):用本表单的事项未自身绑流时回退用它 */
|
|
||||||
const flowGuid = ref("");
|
|
||||||
const flows = ref<{ flowGuid: string; name: string; version: number }[]>([]);
|
|
||||||
const flowDisabled = ref(false);
|
|
||||||
const flowDisabledReason = ref("");
|
|
||||||
const groups = ref<DesignGroup[]>([]);
|
|
||||||
const selected = ref<{ gi: number; fi: number } | null>(null);
|
|
||||||
const saving = ref(false);
|
|
||||||
const previewModal = ref(false);
|
|
||||||
const previewData = ref<Record<string, any>>({});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 装载的是已发布/停用版本:行不可变,保存必须不传 id,由后端按 code 克隆出 version+1 草稿
|
* 中心表单管理页。生产各子应用同网关、按 context-path 分发(表单中心是 `/form-center/front/`),
|
||||||
* (传 id 会被 ServiceFormService.save 直接拒:「该版本已发布不可修改」)。
|
* 所以用当前 origin 拼得出可用链接;本地各应用分端口跑,这条会落到本应用自己的 dev server,
|
||||||
|
* 需要在本地另开 form-center front(vite dev)后按那边的地址访问——不做多源硬编码配置,
|
||||||
|
* 因为这一份"跳去哪"的权威本来就是部署侧的服务目录。
|
||||||
*/
|
*/
|
||||||
const loadedPublished = computed(() => formId.value !== null && formStatus.value !== 0);
|
const centerUrl = computed(() => `${window.location.origin}/form-center/front/#/admin/forms`);
|
||||||
|
|
||||||
/** key 自动生成器(基于当前占用集合每次重建,保证新增/复制去重) */
|
function openCenter() {
|
||||||
const nextKey = computed(() => makeNextKey(collectUsedKeys(groups.value)));
|
window.open(centerUrl.value, "_blank", "noopener");
|
||||||
/** 全表单重复的 key */
|
|
||||||
const duplicateKeys = computed(() => {
|
|
||||||
const counts = new Map<string, number>();
|
|
||||||
for (const g of groups.value) {
|
|
||||||
for (const f of g.fields) counts.set(f.key, (counts.get(f.key) || 0) + 1);
|
|
||||||
}
|
}
|
||||||
return new Set([...counts.entries()].filter(([, c]) => c > 1).map(([k]) => k));
|
|
||||||
});
|
|
||||||
const selectedField = computed<DesignField | null>(() => {
|
|
||||||
const s = selected.value;
|
|
||||||
if (!s) return null;
|
|
||||||
const g = groups.value[s.gi];
|
|
||||||
return g?.fields[s.fi] ?? null;
|
|
||||||
});
|
|
||||||
const keyFormatOk = computed(() => {
|
|
||||||
for (const g of groups.value) {
|
|
||||||
for (const f of g.fields) {
|
|
||||||
if (!f.key || !KEY_PATTERN.test(f.key)) return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
function buildSchemaJson(): string {
|
|
||||||
return JSON.stringify({
|
|
||||||
groups: groups.value.map((g) => ({
|
|
||||||
title: g.title || undefined,
|
|
||||||
fields: g.fields.map((f) => pickAllowed(f)),
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 预览用的 schema(与保存一致) */
|
|
||||||
const previewSchemaJson = computed(() => buildSchemaJson());
|
|
||||||
|
|
||||||
function select(gi: number, fi: number) {
|
|
||||||
const g = groups.value[gi];
|
|
||||||
if (!g || !g.fields[fi]) {
|
|
||||||
selected.value = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
selected.value = { gi, fi };
|
|
||||||
}
|
|
||||||
|
|
||||||
function addField(seed: { type: string; name: string }) {
|
|
||||||
if (!groups.value.length) {
|
|
||||||
groups.value.push({ _uid: genUid(), title: "基本信息", fields: [] });
|
|
||||||
}
|
|
||||||
const gi = selected.value?.gi ?? groups.value.length - 1;
|
|
||||||
const f = defaultField(seed.type, nextKey.value, seed.name);
|
|
||||||
groups.value[gi].fields.push(f);
|
|
||||||
select(gi, groups.value[gi].fields.length - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function copyField(gi: number, fi: number) {
|
|
||||||
const src = groups.value[gi]?.fields[fi];
|
|
||||||
if (!src) return;
|
|
||||||
const copy: DesignField = cloneDeep(src);
|
|
||||||
copy._uid = genUid();
|
|
||||||
copy.key = nextKey.value(src.type);
|
|
||||||
groups.value[gi].fields.splice(fi + 1, 0, copy);
|
|
||||||
select(gi, fi + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeField(gi: number, fi: number) {
|
|
||||||
groups.value[gi].fields.splice(fi, 1);
|
|
||||||
const s = selected.value;
|
|
||||||
if (s && s.gi === gi) {
|
|
||||||
if (groups.value[gi].fields.length) select(gi, Math.min(s.fi, groups.value[gi].fields.length - 1));
|
|
||||||
else selected.value = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addGroup() {
|
|
||||||
groups.value.push({ _uid: genUid(), title: `分组${groups.value.length + 1}`, fields: [] });
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeGroup(gi: number) {
|
|
||||||
const g = groups.value[gi];
|
|
||||||
if (!g) return;
|
|
||||||
const content = (g.fields || []).length
|
|
||||||
? `分组「${g.title || "未命名"}」内还有 ${g.fields.length} 个字段,删除后字段将一并移除。`
|
|
||||||
: "确定删除该分组吗?";
|
|
||||||
Modal.confirm({
|
|
||||||
title: "删除分组确认",
|
|
||||||
content,
|
|
||||||
onOk: () => {
|
|
||||||
groups.value.splice(gi, 1);
|
|
||||||
const s = selected.value;
|
|
||||||
if (!s) return;
|
|
||||||
if (s.gi === gi) selected.value = null;
|
|
||||||
else if (s.gi > gi) selected.value = { ...s, gi: s.gi - 1 };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function openPreview() {
|
|
||||||
if (!countFields(groups.value)) {
|
|
||||||
Message.info("请先添加字段");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!keyFormatOk.value || duplicateKeys.value.size) {
|
|
||||||
Message.error("字段 key 非法或重复,请检查右侧属性面板");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
previewData.value = {};
|
|
||||||
previewModal.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateBeforeSave(): string | null {
|
|
||||||
const c = code.value.trim();
|
|
||||||
if (!c) return "请输入表单编码";
|
|
||||||
if (!/^[A-Za-z0-9_-]{2,64}$/.test(c)) return "表单编码仅限字母/数字/下划线/中划线,2~64 位";
|
|
||||||
const n = name.value.trim();
|
|
||||||
if (!n) return "请输入表单名称";
|
|
||||||
if (n.length > 128) return "表单名称过长(≤128 字)";
|
|
||||||
if (!countFields(groups.value)) return "请至少添加一个字段";
|
|
||||||
const noLabel = groups.value.flatMap((g) => g.fields).find((f) => !f.label || !f.label.trim());
|
|
||||||
if (noLabel) return `字段「${noLabel.key}」缺少标题(服务端同样必填)`;
|
|
||||||
for (const g of groups.value) {
|
|
||||||
if ((g.title || "").length > 64) return `分组标题过长(≤64):${g.title}`;
|
|
||||||
}
|
|
||||||
if (duplicateKeys.value.size) return "存在重复的字段 key,请修改";
|
|
||||||
if (!keyFormatOk.value) return "存在非法字段 key(仅限字母/数字/下划线,1~64 位)";
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 流程图预览(与事项编辑同款:本应用后端换免签 token 出图) */
|
|
||||||
function openFlowDiagram() {
|
|
||||||
if (!currentSiteCode.value || !flowGuid.value) {
|
|
||||||
Message.warning("请先选择流程并确认当前站点");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
window.open(svcFlowApi.diagramUrl(currentSiteCode.value, flowGuid.value, "dept"), "_blank");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadFlows() {
|
|
||||||
if (!currentSiteCode.value) return;
|
|
||||||
try {
|
|
||||||
flows.value = ((await svcFlowApi.list(currentSiteCode.value).send()) as any[]) || [];
|
|
||||||
} catch (e: any) {
|
|
||||||
flowDisabled.value = true;
|
|
||||||
const msg = String(e?.message || e || "");
|
|
||||||
flowDisabledReason.value = msg.includes("流程中心未接入")
|
|
||||||
? "流程中心未接入,表单暂不能绑定流程"
|
|
||||||
: `流程列表获取失败:${msg}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function save() {
|
|
||||||
if (!currentSiteCode.value) {
|
|
||||||
Message.error("站点上下文缺失,请从表单列表进入设计器");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const err = validateBeforeSave();
|
|
||||||
if (err) {
|
|
||||||
Message.error(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
saving.value = true;
|
|
||||||
try {
|
|
||||||
await svcFormApi
|
|
||||||
.save(currentSiteCode.value, {
|
|
||||||
id: loadedPublished.value ? undefined : formId.value ?? undefined,
|
|
||||||
code: code.value.trim(),
|
|
||||||
name: name.value.trim(),
|
|
||||||
schemaJson: buildSchemaJson(),
|
|
||||||
flowGuid: flowGuid.value || undefined,
|
|
||||||
})
|
|
||||||
.send();
|
|
||||||
Message.success(
|
|
||||||
loadedPublished.value
|
|
||||||
? `已保存为草稿 v${formVersion.value + 1}。新结构需发布后才对发起页生效`
|
|
||||||
: "已保存为草稿。下一步:在表单列表发布,并在「办事事项」中绑定使用"
|
|
||||||
);
|
|
||||||
router.push("/svc/form");
|
|
||||||
} catch (e: any) {
|
|
||||||
Message.error(e?.message || e || "保存失败");
|
|
||||||
} finally {
|
|
||||||
saving.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function load() {
|
|
||||||
if (!formId.value) return;
|
|
||||||
try {
|
|
||||||
const vo: any = await svcFormApi.get(currentSiteCode.value, formId.value).send();
|
|
||||||
code.value = vo.code;
|
|
||||||
name.value = vo.name;
|
|
||||||
formStatus.value = vo.status ?? 0;
|
|
||||||
formVersion.value = vo.version ?? 1;
|
|
||||||
flowGuid.value = vo.flowGuid || "";
|
|
||||||
let parsed: any = {};
|
|
||||||
try {
|
|
||||||
parsed = JSON.parse(vo.schemaJson);
|
|
||||||
} catch {
|
|
||||||
parsed = {};
|
|
||||||
}
|
|
||||||
groups.value = restoreGroups(parsed.groups || []);
|
|
||||||
if (!groups.value.length) {
|
|
||||||
groups.value.push({ _uid: genUid(), title: "基本信息", fields: [] });
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
Message.error(e?.message || e || "表单加载失败");
|
|
||||||
router.push("/svc/form");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await loadSites();
|
|
||||||
loadFlows();
|
|
||||||
const idRaw = route.query.id;
|
|
||||||
formId.value = idRaw !== undefined && idRaw !== null ? Number(idRaw) : null;
|
|
||||||
if (formId.value && !Number.isNaN(formId.value)) {
|
|
||||||
await load();
|
|
||||||
} else {
|
|
||||||
formId.value = null;
|
|
||||||
groups.value = [{ _uid: genUid(), title: "基本信息", fields: [] }];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="designer-page">
|
<div class="page-wrapper">
|
||||||
<div class="designer-head">
|
<div class="main-container">
|
||||||
<div class="head-left">
|
<Alert type="warning" show-icon banner class="notice">
|
||||||
<Button icon="ios-arrow-back" @click="router.push('/svc/form')">返回列表</Button>
|
表单设计已迁到<b>表单中心</b>。本页不再新建或修改表单定义,事项只需绑定中心里<b>已发布的某个版本</b>。
|
||||||
<Input v-model="name" placeholder="请输入表单名称" style="width: 240px" />
|
</Alert>
|
||||||
<Input
|
|
||||||
v-model="code"
|
<div class="steps">
|
||||||
:disabled="!!formId"
|
<div class="step">
|
||||||
placeholder="表单编码(字母/数字/下划线/中划线)"
|
<span class="no">1</span>
|
||||||
style="width: 220px"
|
<div>
|
||||||
/>
|
<div class="title">在表单中心设计并发布</div>
|
||||||
<Select v-model="flowGuid" clearable filterable :disabled="flowDisabled"
|
<div class="desc">
|
||||||
placeholder="绑定流程(可空,事项未绑时回退)" style="width: 240px">
|
进入中心的管理页建表、设计字段、发布;如果这张表要给办事大厅用,
|
||||||
<Option v-for="f in flows" :key="f.flowGuid" :value="f.flowGuid">
|
还需要在中心的「应用设置 · 表单授权」里把读权授给本应用,否则事项里选不到它。
|
||||||
{{ f.name }}(v{{ f.version }})
|
</div>
|
||||||
</Option>
|
</div>
|
||||||
</Select>
|
</div>
|
||||||
<Button v-if="flowGuid" size="small" @click="openFlowDiagram">流程图</Button>
|
<div class="step">
|
||||||
<span v-if="flowDisabled" style="color: #999; font-size: 12px">
|
<span class="no">2</span>
|
||||||
{{ flowDisabledReason }}
|
<div>
|
||||||
</span>
|
<div class="title">回到事项编辑页选择表单</div>
|
||||||
|
<div class="desc">
|
||||||
|
「服务事项 · 编辑」的<b>办理表单</b>已从表单中心目录里选,选中即成对记下
|
||||||
|
表单 uuid 与当时发布版本号,保存时由服务端向中心核验;本地表单编码不再接受新绑定。
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="head-right">
|
|
||||||
<Tag v-if="loadedPublished" color="success">
|
|
||||||
已发布 v{{ formVersion }}(保存将生成新草稿版本)
|
|
||||||
</Tag>
|
|
||||||
<Button @click="openPreview">预览</Button>
|
|
||||||
<Button type="primary" :loading="saving" @click="save">保存</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="designer-body">
|
<div v-if="legacyId" class="tip">
|
||||||
<ComponentLibrary :next-key="nextKey" @add="addField" />
|
你是从「表单管理」里点“编辑”进来的(本地表单 id={{ legacyId }})。这张历史表仍可在列表里查看与清理,
|
||||||
<DesignerCanvas
|
但要改结构请去表单中心重新建一张,再回事项里换绑。
|
||||||
:groups="groups"
|
|
||||||
:selected="selected"
|
|
||||||
:next-key="nextKey"
|
|
||||||
@select="select"
|
|
||||||
@copy="copyField"
|
|
||||||
@remove="removeField"
|
|
||||||
@add="addField"
|
|
||||||
@add-group="addGroup"
|
|
||||||
@remove-group="removeGroup"
|
|
||||||
/>
|
|
||||||
<FieldPropPanel :field="selectedField" :duplicate-keys="duplicateKeys" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Modal v-model="previewModal" width="720" title="表单预览(发起页渲染效果)">
|
<div class="actions">
|
||||||
<DynamicForm v-if="previewModal" :schema-json="previewSchemaJson" v-model:form-data="previewData" />
|
<Button type="primary" @click="openCenter">打开表单中心管理页</Button>
|
||||||
<template #footer>
|
<span class="tip">链接:{{ centerUrl }}</span>
|
||||||
<Button @click="previewModal = false">关闭</Button>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
</Modal>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.designer-page {
|
.notice {
|
||||||
display: flex;
|
margin-bottom: 16px;
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
background: #f2f4f8;
|
|
||||||
}
|
}
|
||||||
.designer-head {
|
|
||||||
|
.steps .step {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px dashed #e8eaec;
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps .no {
|
||||||
|
flex: none;
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
line-height: 22px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #2d8cf0;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps .title {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.steps .desc {
|
||||||
|
color: #515a6e;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
gap: 12px;
|
||||||
padding: 10px 16px;
|
margin-top: 16px;
|
||||||
background: #fff;
|
flex-wrap: wrap;
|
||||||
border-bottom: 1px solid #e8eaec;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
}
|
||||||
.head-left,
|
|
||||||
.head-right {
|
.tip {
|
||||||
display: flex;
|
color: #808695;
|
||||||
align-items: center;
|
font-size: 12px;
|
||||||
gap: 8px;
|
margin-top: 12px;
|
||||||
}
|
|
||||||
.designer-body {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
min-height: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
svcFormApi,
|
svcFormApi,
|
||||||
svcItemApi,
|
svcItemApi,
|
||||||
} from "@/api/module/svc";
|
} from "@/api/module/svc";
|
||||||
import type { SvcDeptVo, SvcFormVo, SvcItemAttachmentVo, SvcItemSaveRequest } from "@/types/svc";
|
import type { FcFormCatalogItem, SvcDeptVo, SvcItemAttachmentVo, SvcItemSaveRequest } from "@/types/svc";
|
||||||
import { useSite } from "@/utils/site";
|
import { useSite } from "@/utils/site";
|
||||||
import RichTextEditor from "@/components/RichTextEditor.vue";
|
import RichTextEditor from "@/components/RichTextEditor.vue";
|
||||||
|
|
||||||
@@ -43,7 +43,10 @@ const form = ref<SvcItemSaveRequest>({
|
|||||||
const objects = ref<{ dictKey: string; dictName: string }[]>([]);
|
const objects = ref<{ dictKey: string; dictName: string }[]>([]);
|
||||||
const categories = ref<{ dictKey: string; dictName: string }[]>([]);
|
const categories = ref<{ dictKey: string; dictName: string }[]>([]);
|
||||||
const depts = ref<SvcDeptVo[]>([]);
|
const depts = ref<SvcDeptVo[]>([]);
|
||||||
const forms = ref<SvcFormVo[]>([]);
|
/** 表单中心目录(v3/TASK_1:办理表单只能从这里选;本地表单定义已停写) */
|
||||||
|
const fcForms = ref<FcFormCatalogItem[]>([]);
|
||||||
|
const fcFormsDisabled = ref(false);
|
||||||
|
const fcFormsReason = ref("");
|
||||||
const flows = ref<{ flowGuid: string; name: string; version: number }[]>([]);
|
const flows = ref<{ flowGuid: string; name: string; version: number }[]>([]);
|
||||||
const flowDisabled = ref(false);
|
const flowDisabled = ref(false);
|
||||||
const flowDisabledReason = ref("");
|
const flowDisabledReason = ref("");
|
||||||
@@ -53,11 +56,22 @@ const uploadingCover = ref(false);
|
|||||||
const editing = computed(() => !!route.query.id);
|
const editing = computed(() => !!route.query.id);
|
||||||
const pageTitle = computed(() => (editing.value ? "编辑事项" : "新建事项"));
|
const pageTitle = computed(() => (editing.value ? "编辑事项" : "新建事项"));
|
||||||
|
|
||||||
/** 选中表单的最新版本是否为草稿/停用(草稿可先绑,但发起页要等发布才生效) */
|
/**
|
||||||
const selectedFormDraft = computed(() => {
|
* 选中即成对存 uuid + 中心当时的发布版(v3 §4:只保存服务器验证过的坐标,不接受手填版本)。
|
||||||
if (!form.value.formCode) return false;
|
* 清空=两列一起清——后端要求成对,留一个没人认领的版本号没有意义。
|
||||||
const f = forms.value.find((x) => x.code === form.value.formCode);
|
*/
|
||||||
return !!f && f.status !== 1;
|
function onFcFormChange(uuid?: string) {
|
||||||
|
const hit = uuid ? fcForms.value.find((f) => f.formUuid === uuid) : undefined;
|
||||||
|
form.value.fcFormUuid = hit ? hit.formUuid : undefined;
|
||||||
|
form.value.fcFormVersion = hit ? hit.version : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 绑定回显:存的是当时核验过的版本,中心又发新版时页面要点出来(不自动改绑) */
|
||||||
|
const fcBinding = computed(() => {
|
||||||
|
if (!form.value.fcFormUuid) return "";
|
||||||
|
const newest = fcForms.value.find((f) => f.formUuid === form.value.fcFormUuid)?.version;
|
||||||
|
const base = `已绑定 ${form.value.fcFormUuid} v${form.value.fcFormVersion}`;
|
||||||
|
return newest && newest !== form.value.fcFormVersion ? `${base}(中心最新已发布 v${newest},需换版请重新选择)` : base;
|
||||||
});
|
});
|
||||||
|
|
||||||
/** 部门树扁平化(缩进展示,承袭 CMS flattenDepts) */
|
/** 部门树扁平化(缩进展示,承袭 CMS flattenDepts) */
|
||||||
@@ -84,7 +98,20 @@ async function loadAll() {
|
|||||||
objects.value = ((await svcDictApi.list(siteCode, "object").send()) as any[]) || [];
|
objects.value = ((await svcDictApi.list(siteCode, "object").send()) as any[]) || [];
|
||||||
categories.value = ((await svcDictApi.list(siteCode, "category").send()) as any[]) || [];
|
categories.value = ((await svcDictApi.list(siteCode, "category").send()) as any[]) || [];
|
||||||
depts.value = (await svcDictApi.deptTree(siteCode).send()) || [];
|
depts.value = (await svcDictApi.deptTree(siteCode).send()) || [];
|
||||||
forms.value = ((await svcFormApi.list(siteCode).send()) as SvcFormVo[]) || [];
|
// 表单中心目录(同源代理):授权没配/中心不可达时降级为"选不了",不阻塞事项其余字段的编辑
|
||||||
|
try {
|
||||||
|
const page = await svcFormApi.centerForms(siteCode, { page: 1, pageSize: 100 }).send();
|
||||||
|
fcForms.value = page?.items || [];
|
||||||
|
fcFormsDisabled.value = false;
|
||||||
|
fcFormsReason.value = "";
|
||||||
|
} catch (e: any) {
|
||||||
|
fcForms.value = [];
|
||||||
|
fcFormsDisabled.value = true;
|
||||||
|
const msg = String(e?.message || e || "");
|
||||||
|
fcFormsReason.value = msg.includes("不可达") || msg.includes("返回空")
|
||||||
|
? msg
|
||||||
|
: `表单中心目录获取失败:${msg}`;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
flows.value = ((await svcFlowApi.list(siteCode).send()) as any[]) || [];
|
flows.value = ((await svcFlowApi.list(siteCode).send()) as any[]) || [];
|
||||||
flowDisabled.value = false;
|
flowDisabled.value = false;
|
||||||
@@ -312,15 +339,22 @@ onMounted(loadAll);
|
|||||||
</Col>
|
</Col>
|
||||||
<Col span="12">
|
<Col span="12">
|
||||||
<FormItem label="办理表单">
|
<FormItem label="办理表单">
|
||||||
<Select v-model="form.formCode" clearable placeholder="该事项发起时填写的自定义表单">
|
<Select :value="form.fcFormUuid" clearable placeholder="从表单中心选择已发布表单"
|
||||||
<Option v-for="f in forms" :key="f.code" :value="f.code">
|
:disabled="fcFormsDisabled" @on-change="onFcFormChange">
|
||||||
{{ f.name }}{{ f.status !== 1 ? `(${f.status === 2 ? "已停用" : "草稿"})` : "" }}
|
<Option v-for="f in fcForms" :key="f.formUuid" :value="f.formUuid">
|
||||||
|
{{ f.name }}({{ f.appCode }}/{{ f.code }} · v{{ f.version }})
|
||||||
</Option>
|
</Option>
|
||||||
</Select>
|
</Select>
|
||||||
<div v-if="selectedFormDraft" class="tip" style="color: #ed4014">
|
<div v-if="fcFormsDisabled" class="tip" style="color: #ed4014">
|
||||||
该表单尚未发布,发布后才能在发起页实际填写;请到「表单管理」发布。
|
{{ fcFormsReason || "表单中心目录不可达" }}
|
||||||
|
</div>
|
||||||
|
<div v-else-if="fcBinding" class="tip">{{ fcBinding }}</div>
|
||||||
|
<div v-else class="tip">
|
||||||
|
目录只列中心已发布、且已授权给本应用的表单;保存时服务端会再向中心核验一次坐标
|
||||||
|
</div>
|
||||||
|
<div v-if="form.formCode" class="tip" style="color: #ed4014">
|
||||||
|
历史本地表单「{{ form.formCode }}」只读保留(本地表单定义已停用,改绑请选表单中心的表)
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="tip">发起页按表单编码取最新已发布版本渲染</div>
|
|
||||||
</FormItem>
|
</FormItem>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span="24">
|
<Col span="24">
|
||||||
|
|||||||
Reference in New Issue
Block a user