Files
form-center-front/src/components/designer/PreviewModal.vue
T
chenxing 79c98e4280
build / build (push) Failing after 32s
feat(p89+p90): 复制表单链接 + 设计器整表预览
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 通过。
2026-10-15 23:42:25 +08:00

181 lines
6.4 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.
<script setup lang="ts">
/**
* 设计器整表预览(P90):按填报视角全屏渲染当前画布内容(**含未保存改动**)。
*
* 口径对齐:
* - 控件件与填报页同一批(fields/index.ts 的 fieldWidget),值通道 update:modelValue——
* 「预览过了保存被拒」不允许出现两套渲染口径;
* - 选项解析与 DesignerCanvas.resolvedOptions 同规则:inline 数组直用,字典引用按
* 可选字典展开(字典不在可选域给显式降级文案,不静默空白);
* - 必填红星/占位/约束提示与 Fill.vue 同位;值放本地容器可试填交互,纯前端、不提交。
* - 已发布版本行只读态(locked)也可预览——看效果不需要写权限。
*/
import { reactive, ref, watch } from "vue";
import { Modal } from "view-ui-plus";
import { fieldWidget } from "@/components/fields";
import type { DesignField, DesignGroup, FieldOption } from "@/types/formSchema";
import { dictRefCode, isDictRef } from "@/types/formSchema";
import type { DictView } from "@/types/form";
const props = defineProps<{ groups: DesignGroup[]; dicts: DictView[] }>();
const visible = ref(false);
/** 试填值容器(key → 值),打开时清空——预览每次都是「新填一遍」的视角 */
const previewValues = reactive<Record<string, unknown>>({});
/** 记录各字段累计字符数之类的展示需求没有;错误态也不做(预览不做校验拦截) */
watch(visible, (v) => {
if (v) {
for (const k of Object.keys(previewValues)) delete previewValues[k];
}
});
function open() {
visible.value = true;
}
defineExpose({ open });
/** 选项解析(与 DesignerCanvas.resolvedOptions 同规则,此处独立成函数供模板用) */
function resolvedOptions(field: DesignField): FieldOption[] {
const options = field.options as FieldOption[] | { source?: string } | undefined;
if (!options) return [];
if (Array.isArray(options)) return options;
if (isDictRef(options)) {
const code = dictRefCode(options) || "";
const dict = (props.dicts || []).find((d) => d.code === code);
return dict ? dict.items.map((i) => ({ label: i.label, value: i.value })) : [];
}
return [];
}
function widgetOf(field: DesignField) {
return fieldWidget(field.type, "pc");
}
/** 字典引用且字典不在可选域(与画布同判定,给显式降级而非空下拉) */
function dictMissing(field: DesignField): boolean {
const code = dictRefCode(field.options as never);
if (!code) return false;
return !(props.dicts || []).some((d) => d.code === code);
}
function setValue(key: string, val: unknown) {
previewValues[key] = val;
}
</script>
<template>
<Modal v-model="visible" fullscreen title="填报预览(当前画布内容,含未保存改动)" :footer-hide="true">
<div class="pv-body">
<div class="pv-card">
<Alert show-icon type="info" class="pv-note">
这是填报者视角的整表预览:字段按真实控件渲染、可试填;约束提示与必填标记同填报页。
预览不提交、不校验,也不代表已保存。
</Alert>
<Alert v-if="!groups.length" show-icon type="warning">
画布还没有任何分组/字段。回到设计器从左侧控件栏添加。
</Alert>
<template v-else>
<div v-for="group in groups" :key="group._uid" class="pv-group">
<h3 v-if="group.label" class="pv-group-label">{{ group.label }}</h3>
<Alert v-if="!group.fields.length" show-icon type="warning" class="pv-empty-group">
分组「{{ group.label || group.key }}」还没有字段
</Alert>
<div v-for="field in group.fields" :key="field._uid" class="pv-item">
<label class="pv-label">
{{ field.label }}
<span v-if="field.required" class="pv-required">*</span>
<span class="pv-key">{{ field.key }}</span>
</label>
<div class="pv-control">
<template v-if="widgetOf(field)">
<component :is="widgetOf(field)" :field="field"
:options="resolvedOptions(field)" :model-value="previewValues[field.key]"
:readonly="false"
@update:model-value="(val: unknown) => setValue(field.key, val)" />
<div v-if="dictMissing(field)" class="pv-dict-missing">
字典 dict:{{ dictRefCode(field.options as never) }} 不在可选域,发布前请确认字典已建且可用
</div>
</template>
<span v-else class="pv-unknown">[未知类型 {{ field.type }},无渲染控件]</span>
<div v-if="field.tip" class="pv-tip">{{ field.tip }}</div>
</div>
</div>
</div>
</template>
</div>
</div>
</Modal>
</template>
<style scoped>
.pv-body {
height: 100%;
overflow: auto;
background: #f5f7f9;
padding: 16px 0 32px;
}
.pv-card {
max-width: 800px;
margin: 0 auto;
background: #fff;
border-radius: 4px;
padding: 24px;
}
.pv-note {
margin-bottom: 20px;
}
.pv-group {
margin-bottom: 24px;
}
.pv-group-label {
font-size: 16px;
font-weight: 600;
color: #17233d;
border-bottom: 1px solid #e8eaec;
padding-bottom: 8px;
margin-bottom: 16px;
}
.pv-empty-group {
margin-bottom: 8px;
}
.pv-item {
margin-bottom: 16px;
}
.pv-label {
display: block;
margin-bottom: 8px;
font-size: 14px;
color: #515a6e;
}
.pv-required {
color: #ed4014;
margin-left: 2px;
}
.pv-key {
color: #c5c8ce;
font-size: 12px;
margin-left: 8px;
font-family: monospace;
}
.pv-control {
position: relative;
}
.pv-dict-missing {
margin-top: 4px;
font-size: 12px;
color: #ff9900;
}
.pv-unknown {
color: #ed4014;
font-size: 12px;
}
.pv-tip {
margin-top: 4px;
font-size: 12px;
color: #808695;
}
</style>