From 79c98e4280407d8210160a60a2bb1e85c9fc4f26 Mon Sep 17 00:00:00 2001 From: chenxing Date: Thu, 15 Oct 2026 23:42:25 +0800 Subject: [PATCH] =?UTF-8?q?feat(p89+p90):=20=E5=A4=8D=E5=88=B6=E8=A1=A8?= =?UTF-8?q?=E5=8D=95=E9=93=BE=E6=8E=A5=20+=20=E8=AE=BE=E8=AE=A1=E5=99=A8?= =?UTF-8?q?=E6=95=B4=E8=A1=A8=E9=A2=84=E8=A7=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P89 复制填报链接(旧系统先例对齐:Cms AttachmentManage 的 copyToClipboard + 文案「链接已复制」;svc-hall 同款 execCommand 兜底): - 新增 utils/fillLink.ts:buildFillLink 从当前地址推导部署根(生产 /form-center/front/、本地 dev=/),hash 路由 query 进 hash(#/f/{code}?appCode=), 中心表单不带 appCode;copyToClipboard=navigator.clipboard 优先 + 临时 textarea+execCommand 兜底,双失败亮链接提示手动复制不静默。 - Forms.vue 行操作「复制链接」文字链(P51 形态,收「更多」逻辑不动); FormDesigner 头部「复制链接」按钮(草稿态可复制,发布后链接即生效)。 P90 设计器整表预览: - 新增 components/designer/PreviewModal.vue:全屏 Modal 按填报视角渲染当前 画布内容(含未保存改动);控件件与填报页同源(fieldWidget pc),选项解析 与 DesignerCanvas.resolvedOptions 同规则(inline 直用/字典引用展开,字典 缺域显式降级);必填红星/占位/约束 tip 同 Fill.vue 位;值容器本地可试填, 不校验不提交;locked 只读态也可用;空分组/未知类型显式空态。 - FormDesigner 头部「预览」按钮。 验证:vue-tsc --noEmit 干净;vite build 通过。 --- src/components/designer/PreviewModal.vue | 180 +++++++++++++++++++++++ src/utils/fillLink.ts | 64 ++++++++ src/views/admin/FormDesigner.vue | 13 ++ src/views/admin/Forms.vue | 2 + 4 files changed, 259 insertions(+) create mode 100644 src/components/designer/PreviewModal.vue create mode 100644 src/utils/fillLink.ts diff --git a/src/components/designer/PreviewModal.vue b/src/components/designer/PreviewModal.vue new file mode 100644 index 0000000..a6f33af --- /dev/null +++ b/src/components/designer/PreviewModal.vue @@ -0,0 +1,180 @@ + + + + + diff --git a/src/utils/fillLink.ts b/src/utils/fillLink.ts new file mode 100644 index 0000000..89c8851 --- /dev/null +++ b/src/utils/fillLink.ts @@ -0,0 +1,64 @@ +/** + * 填报链接构建与复制(P89,参考旧系统先例:Cms AttachmentManage 的「复制链接」 + * 行操作 + Utils.copyToClipboard + 文案「链接已复制」;svc-hall 同款 execCommand 兜底)。 + * + * 链接契约:填报页按 code 寻址(/f/:code,P87 起菜单与链接都解析),产物部署在 + * /form-center/front/,hash 路由的 query 段进 hash(route.query 才读得到)。 + * 这里不写死 contextPath——从当前地址推导(生产=…/form-center/front/index.html, + * 本地 dev=/),两处复制出来的链接都能用;快应用表单追加 ?appCode=(resolve 的 + * 归属域参数,中心表单 form-center 不带)。 + */ +import { Message } from "view-ui-plus"; + +/** 中心默认归属应用(与 FormConfigModal/创建弹窗的默认值同源) */ +const DEFAULT_APP_CODE = "form-center"; + +/** 表单 → 填报链接(同源部署形态自适应,见文件头) */ +export function buildFillLink(form: { code: string; appCode?: string | null }): string { + const base = window.location.origin + window.location.pathname.replace(/index\.html?$/i, ""); + const appCode = String(form.appCode ?? "").trim(); + const query = appCode && appCode !== DEFAULT_APP_CODE ? `?appCode=${encodeURIComponent(appCode)}` : ""; + return `${base}#/f/${encodeURIComponent(form.code)}${query}`; +} + +/** + * 复制文本到剪贴板:navigator.clipboard 优先(安全上下文),失败回退旧系统同款 + * 临时 textarea + execCommand(http 内网环境/旧内核兜底)。返回是否成功。 + */ +export async function copyToClipboard(text: string): Promise { + if (navigator.clipboard && window.isSecureContext) { + try { + await navigator.clipboard.writeText(text); + return true; + } catch { + /* 权限被拒/非焦点等,走兜底 */ + } + } + const ta = document.createElement("textarea"); + ta.value = text; + ta.style.position = "fixed"; + ta.style.opacity = "0"; + document.body.appendChild(ta); + ta.select(); + let ok = false; + try { + ok = document.execCommand("copy"); + } catch { + ok = false; + } finally { + document.body.removeChild(ta); + } + return ok; +} + +/** 行操作/按钮统一入口:构建链接→复制→按旧先例文案提示 */ +export async function copyFillLink(form: { code: string; appCode?: string | null }): Promise { + const link = buildFillLink(form); + const ok = await copyToClipboard(link); + if (ok) { + Message.success("链接已复制"); + } else { + // 兜底也失败(极端环境):把链接亮出来让用户手动复制,绝不静默 + Message.warning({ content: `复制失败,请手动复制:${link}`, duration: 8 }); + } +} diff --git a/src/views/admin/FormDesigner.vue b/src/views/admin/FormDesigner.vue index 5371c89..443db62 100644 --- a/src/views/admin/FormDesigner.vue +++ b/src/views/admin/FormDesigner.vue @@ -20,11 +20,13 @@ import DesignerCanvas from "@/components/designer/DesignerCanvas.vue"; import FieldPropPanel from "@/components/designer/FieldPropPanel.vue"; import FormConfigModal from "@/components/form/FormConfigModal.vue"; import FormAdminModal from "@/components/admin/FormAdminModal.vue"; +import PreviewModal from "@/components/designer/PreviewModal.vue"; import { formApi } from "@/api/module/form"; import { dictApi } from "@/api/module/dict"; import type { DictView, FormView, VersionView } from "@/types/form"; import type { DesignField, DesignGroup } from "@/types/formSchema"; import { MAX_CHANGE_LOG_LEN, MAX_NAME_LEN } from "@/types/formSchema"; +import { copyFillLink } from "@/utils/fillLink"; import { buildSchemaJson, cloneField, @@ -61,6 +63,8 @@ const baseline = ref(""); const configRef = ref | null>(null); /** 单表单管理员弹窗(与设计器的「表单配置」并排,方案 §6.4:与表单列表行操作同一形态) */ const adminRef = ref | null>(null); +/** P90:整表预览弹层(含未保存改动,locked 只读态也可用) */ +const previewRef = ref | null>(null); const publishModal = reactive({ visible: false, changeLog: "" }); const locked = computed(() => viewingPublished.value); @@ -306,6 +310,12 @@ function onConfigSaved(res: FormView) { form.value = res; } +/** P89:复制当前表单的填报链接(草稿态也可复制——链接按 code 寻址,发布后即生效) */ +function onCopyLink() { + if (!form.value) return; + copyFillLink(form.value); +} + async function boot() { if (!uuid.value) { Message.warning("请从表单列表进入设计器"); @@ -348,6 +358,8 @@ onMounted(boot);
+ + @@ -399,6 +411,7 @@ onMounted(boot); +

diff --git a/src/views/admin/Forms.vue b/src/views/admin/Forms.vue index 8eab946..0902584 100644 --- a/src/views/admin/Forms.vue +++ b/src/views/admin/Forms.vue @@ -16,6 +16,7 @@ import { useRouter } from "vue-router"; import { Message, Modal } from "view-ui-plus"; import { formApi } from "@/api/module/form"; import { actionColumn } from "@/utils/tableActions"; +import { copyFillLink } from "@/utils/fillLink"; import { useElementHeight } from "@/utils/hook/useElementHeight"; import FormConfigModal from "@/components/form/FormConfigModal.vue"; import FormAdminModal from "@/components/admin/FormAdminModal.vue"; @@ -105,6 +106,7 @@ const columns = computed(() => [ [ { key: "design", label: "设计", on: (row: FormView) => goDesigner(row) }, { key: "config", label: "配置", on: (row: FormView) => openConfig(row) }, + { key: "copyLink", label: "复制链接", on: (row: FormView) => copyFillLink(row) }, { key: "publish", label: "发布",