build / build (push) Failing after 32s
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 通过。
181 lines
6.4 KiB
Vue
181 lines
6.4 KiB
Vue
<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>
|