fix(document): 统一编辑器与文书纸的排版和字体口径
This commit is contained in:
@@ -153,6 +153,17 @@ ClinicalLayout(实际导航和页面路由由授权菜单资源树提供)
|
||||
|
||||
### 编辑区与签署态的呈现差异
|
||||
|
||||
**版式两边是同一份**:编辑区的编辑面(`EditorWangPane`)与签署态的纸(`DocumentPaper`)都挂 `utils/document-paper.ts` 里的 `doc-paper` 类 —— 段落、标题、列表、表格、字号行距字体的规则全项目只有这一条来源,编辑时看到的换行位置就是签出来、下载到的位置。两件事要留意:
|
||||
|
||||
- WangEditor 自带一套权重更高的正文默认(`p{margin:15px 0}`、`td/th` 的 `line-height:1.5` 等),会把那份排版盖掉。它整包被压进 `styles/editor-vendor.css` 的 cascade layer —— 未分层的规则优先于分层规则,因此不必写 `!important` 也不复述 vendor 的数值。**换编辑器引擎时这里要一并换成新引擎的样式入口**;
|
||||
- 纸在窄容器里不缩窄:`.doc-paper-stage`(加在纸桌/纸舞台上)让纸保持 A4 设计宽 820px、由容器横向滚动。纸一被压窄,页边距与版心跟着变小,换行位置就和编辑器、和导出的 PDF 对不上了。
|
||||
|
||||
**文书字体**(后台「文书字体设置」里配置的默认字体)两边也是同一个:编辑区与纸都加载默认文书字体并挂 `.managed-font`,后台配了仿宋/楷体时不会出现「编辑区雅黑、签出来仿宋」。
|
||||
|
||||
**表格宽度同样只认渲染端的 CSS**(`.doc-paper table { width: 100% }`)。这里有个坑:WangEditor 导出表格时会写成 `<table style="width: auto;">`,而内联样式优先级高于样式表 —— 结果编辑器里表格撑满版心(编辑器 DOM 上那是**属性**,值为 `auto` 时浏览器直接忽略),预览 / 签署里却按内容收缩。渲染管线(`buildSigningDocumentHtml`)会先调 `utils/rich-text.ts` 的 `unlockTableLayout` 把这层内联锁解开,与粘贴 Word 表格时的处理是同一份规则。
|
||||
|
||||
**列宽拖拽只在本轮编辑会话内有效**:WangEditor 把列宽存在 `colgroup` 上,而它导出的 HTML 里没有 `colgroup`,保存后重开会回到「按内容分配列宽」——这也正是预览 / 签署里的样子。
|
||||
|
||||
**编辑区**(`utils/signing-editor-marks.ts`)把 `{…}` 与 `□` 渲染成带类型角标的胶囊,让科室一眼分出「正文」和「待签署的内容」。这是**只读投影**:靠 wangEditor 的 `decorate` 把文本切成叶子再挂样式,`editor.getHtml()` 输出一字未变,保存与签署链路的口径不受影响。
|
||||
|
||||
**签署态**(`utils/signing-document.ts`)把占位符就地换成可交互控件。其中**签名位**(`kind=signature`)在任务处于可签署状态时渲染成「✍ 点击签名」的可点击入口:点一下就打开签字板,签完的图就地回显到该签名位,只回填这一个字段,**不会结束签署任务**——任务级的「签署完成」仍由主签名位(`{患者签名}`)通过「手写板签署 / 线上签署」触发。
|
||||
|
||||
@@ -361,11 +361,9 @@ defineExpose({ getDocumentElement })
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.managed-font,
|
||||
.managed-font :deep(*) {
|
||||
font-family: var(--document-font) !important;
|
||||
font-synthesis: none;
|
||||
}
|
||||
/* 文书字体(.managed-font / --document-font)在 utils/document-paper.ts 的
|
||||
DOCUMENT_PAPER_CSS 里 —— 编辑器编辑面也要换上同一份字体,所以它是全局规则,
|
||||
不是这张纸私有的。 */
|
||||
|
||||
/* 基础排版(p / h2 / h3 / table / th / td / img)在 utils/document-paper.ts 的
|
||||
DOCUMENT_PAPER_CSS 里,运行期注入 head —— 那里是唯一一份。
|
||||
|
||||
@@ -58,7 +58,14 @@ const props = withDefaults(
|
||||
* - `stage`:深灰底、居中 A4,用于「在线预览(PDF)」(原型 #mdPdfView)
|
||||
*/
|
||||
stage?: 'plain' | 'stage'
|
||||
/** 弹窗宽度(px) */
|
||||
/**
|
||||
* 弹窗宽度(px)。
|
||||
*
|
||||
* 默认 880 不是随手取的:纸钉在 A4 设计宽 820px(.doc-paper-stage),
|
||||
* 纸舞台左右各留 18px 内边距 → 856px 刚好放下。窄于它虽然靠横向滚动也能看全,
|
||||
* 但一进来就出现横向滚动条会让人以为版式坏了。深灰舞台(stage)的左右内边距是 24px,
|
||||
* 所以那一档要 960(见 SignedDocumentDialog)。
|
||||
*/
|
||||
width?: number
|
||||
/**
|
||||
* 是否叠在另一个弹窗之上(发起签署弹窗里的文档预览)。
|
||||
@@ -84,7 +91,7 @@ const props = withDefaults(
|
||||
note: '',
|
||||
emptyText: '该文书暂无正文内容。',
|
||||
stage: 'plain',
|
||||
width: 720,
|
||||
width: 880,
|
||||
stacked: false,
|
||||
},
|
||||
)
|
||||
@@ -159,7 +166,10 @@ defineExpose({ getDocumentElement })
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="preview-body" :class="{ 'preview-body--stage': stage === 'stage' }">
|
||||
<div
|
||||
class="preview-body doc-paper-stage"
|
||||
:class="{ 'preview-body--stage': stage === 'stage' }"
|
||||
>
|
||||
<p v-if="note && !loading && !error" class="preview-note">{{ note }}</p>
|
||||
<div v-if="loading" class="preview-state">正在加载文书内容…</div>
|
||||
<div v-else-if="error" class="preview-state preview-state--error">{{ error }}</div>
|
||||
@@ -335,7 +345,10 @@ defineExpose({ getDocumentElement })
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 唯一滚动区:纸在自己的框里滚,头尾常驻 */
|
||||
/* 唯一滚动区:纸在自己的框里滚,头尾常驻。
|
||||
`doc-paper-stage`(见 utils/document-paper.ts)把纸钉在 A4 设计宽 820px:
|
||||
框比纸窄时横向滚动,而不是把纸压窄 —— 版心一变,换行位置就和编辑器/PDF 不同了。
|
||||
`overflow-y: auto` 已隐含 overflow-x 为 auto,横向滚动条按需出现,不必另写。 */
|
||||
.preview-body {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
|
||||
@@ -110,8 +110,12 @@ defineExpose({ getDocumentElement })
|
||||
|
||||
<!-- 纸桌:灰底 + 内滚,纸(一页 A4)居中摆在上面。
|
||||
纸不能直接当 flex 子项 —— `flex: 1` 会把高度交给布局,
|
||||
`aspect-ratio` 在主轴上不生效,A4 比例就没了 -->
|
||||
<div class="sign-doc-body">
|
||||
`aspect-ratio` 在主轴上不生效,A4 比例就没了。
|
||||
`doc-paper-stage`:纸钉在 A4 设计宽(820px),这一栏比纸窄时横向滚动,
|
||||
不把纸压窄 —— 压窄会连版心一起变,换行位置就与编辑器/预览/PDF 对不上,
|
||||
而「工作台里看到的」必须就是「签出来、下载到的那一份」(规则见
|
||||
utils/document-paper.ts 的 .doc-paper-stage)。 -->
|
||||
<div class="sign-doc-body doc-paper-stage">
|
||||
<DocumentPaper
|
||||
ref="paperRef"
|
||||
:document-font="task.documentFont ?? null"
|
||||
|
||||
@@ -13,8 +13,9 @@ import { ensureDocumentPaperStyle } from './utils/document-paper'
|
||||
* 文书纸的基础排版 + A4 规格变量(`--doc-paper-*`)在启动时就注入。
|
||||
*
|
||||
* DocumentPaper 自己在 setup 期也会幂等注入一次,但**编辑器**的编辑面不是
|
||||
* `DocumentPaper`(是 WangEditor 的 `[data-slate-editor]`),它也要吃同一套页边距 ——
|
||||
* 只在组件里注入的话,先进编辑器(还没挂载过任何 DocumentPaper)会拿不到变量。
|
||||
* `DocumentPaper`(是 EditorWangPane 的 `.pane-content`)—— 它同样挂着 `doc-paper`,
|
||||
* 要吃的正是这一份排版与页边距。只在组件里注入的话,先进编辑器
|
||||
* (还没挂载过任何 DocumentPaper)会先看到一帧没有排版的正文。
|
||||
*/
|
||||
ensureDocumentPaperStyle()
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* WangEditor 自带样式的**降级层**(cascade layer)。
|
||||
*
|
||||
* 为什么需要:编辑器的编辑面要和签署预览、签署工作台、导出的 PDF 用**同一份正文排版**
|
||||
* —— 就是 `utils/document-paper.ts` 里以 `.doc-paper` 开头那一套,编辑器面也挂了
|
||||
* `doc-paper` 类(见 EditorWangPane)。
|
||||
*
|
||||
* 但 WangEditor 自己对正文有一套默认值,而且选择器权重比我们高:
|
||||
*
|
||||
* .w-e-text-container [data-slate-editor] p { margin: 15px 0 } ← (0,2,1)
|
||||
* .w-e-text-container [data-slate-editor] h1…h5 { margin: 20px 0 } ← (0,2,1)
|
||||
* .w-e-text-container li, p, td, th { line-height: 1.5 } ← (0,1,1)
|
||||
* .w-e-text-container * { margin: 0; padding: 0 } ← (0,1,0)
|
||||
*
|
||||
* 而 `.doc-paper p { margin: 0 0 6px }` 只有 (0,1,1),比不过它 —— 结果就是
|
||||
* 「编辑时段落间距 15px、纸上是 6px」,同一份文书在编辑区与预览里换行都不一样。
|
||||
*
|
||||
* 解法不是在那份排版里堆 `!important`:那会让「唯一一份排版」变成谁也别想覆盖的
|
||||
* 暴力规则,以后想为某个特定页面调一处间距都没有退路。改用层叠层——
|
||||
* **未分层的规则一律优先于分层规则,与选择器权重无关**,所以只需要把整包 vendor
|
||||
* 样式压进最低优先级的层,我们那份排版就自然生效,而且不必在这里复述 vendor 的
|
||||
* 任何数值(复述一遍就等于又开了一份副本,正是要避免的事)。
|
||||
*
|
||||
* 影响面:本应用只有文书编辑器用 WangEditor,且我们的规则都是精确选择器,
|
||||
* 只会在确实要覆盖的(正文 p / h / li / table / td / th)那些属性上生效;
|
||||
* 工具栏、悬浮菜单、表格操作手柄等没有对应规则的地方仍按 vendor 自己的样式渲染。
|
||||
*
|
||||
* ⚠️ 换成别的编辑器引擎时,这里要跟着换成那个引擎的样式入口
|
||||
* (EditorWangPane 的注释里列了换引擎要一并处理的三件事)。
|
||||
*/
|
||||
@import '@wangeditor/editor/dist/css/style.css' layer(vendor-editor);
|
||||
@@ -13,10 +13,15 @@
|
||||
* 仍然保留「运行期注入」而不是普通 css 文件,是因为注入的时机可控
|
||||
* (setup 期,避免首帧"纸没排版"的闪烁),且规则文本同时供导出 PDF 之类的场景复用。
|
||||
*
|
||||
* 选择器一律以 `.doc-paper` 开头,挂在容器 div 上。
|
||||
* 选择器一律以 `.doc-paper` 开头。这个类有两个「根」:
|
||||
* ① DocumentPaper 渲染的纸(`.doc-paper__body` 的父级);
|
||||
* ② **编辑器的编辑面**(EditorWangPane 的 `.pane-content`,即 WangEditor 的
|
||||
* `.w-e-text-container`)—— 编辑时看到的版式必须就是签出来的版式。
|
||||
* 注意 WangEditor 自带一套正文默认(`p{margin:15px 0}`、`td/th` 的 padding…)
|
||||
* 而且权重更高,所以它整包被压进 styles/editor-vendor.css 的 cascade layer,
|
||||
* 这一份排版才盖得住(理由在那个文件里)。
|
||||
* 例外只有一处:A4 规格的 `--doc-paper-*` 变量放在 `:root`(见下),
|
||||
* 因为编辑器里的编辑面不是 `.doc-paper`(是 WangEditor 的 `[data-slate-editor]`),
|
||||
* 它也要吃同一套页边距。
|
||||
* 因为变量要在纸壳与编辑面上都能取到。
|
||||
*
|
||||
* 注意:填写项胶囊 / 选项 ☑☐ / 系统页眉的样式**不在这里**,
|
||||
* 它们与「签署态」语义强相关,统一在 styles/signing-document.css。
|
||||
@@ -48,6 +53,17 @@ export const A4_CONTENT_WIDTH_MM = A4_PAPER_MM.width - A4_PAPER_MM.marginX * 2
|
||||
/** 屏幕上的纸宽上限(CSS px)。A4 在 96dpi 下约 794px,取 820 让正文更舒展 */
|
||||
export const A4_PAPER_MAX_WIDTH_PX = 820
|
||||
|
||||
/**
|
||||
* 纸宽 = 上限时「一页 A4」的高度(px)= 820 × 297/210。
|
||||
*
|
||||
* 两个用途:① 上面 `--doc-paper-min-h` 的第一个分支;② 调用方把纸**钉在设计宽**
|
||||
* (视口不够宽时宁可口横向滚动,也不把纸压窄)时必须改用这个定值 ——
|
||||
* 那时纸宽 ≠ 纸壳宽,`cqw` 那个分支会按更窄的纸壳算,得出的高度比一页 A4 矮。
|
||||
*/
|
||||
export const A4_PAPER_FULL_HEIGHT_PX = Math.round(
|
||||
A4_PAPER_MAX_WIDTH_PX * (A4_PAPER_MM.height / A4_PAPER_MM.width),
|
||||
)
|
||||
|
||||
/** 纸宽百分比(% padding 四边都以包含块宽度为基准,所以上下也用纸宽算) */
|
||||
const pct = (mm: number) => `${((mm / A4_PAPER_MM.width) * 100).toFixed(2)}%`
|
||||
|
||||
@@ -79,9 +95,10 @@ export const DOCUMENT_PAPER_CSS = `
|
||||
⚠️ 本段在模板字符串里,注释内**不要写反引号**,会提前结束字符串。 */
|
||||
:root {
|
||||
--doc-paper-max-w: ${A4_PAPER_MAX_WIDTH_PX}px;
|
||||
--doc-paper-full-h: ${A4_PAPER_FULL_HEIGHT_PX}px;
|
||||
--doc-paper-pad-x: clamp(20px, ${pct(A4_PAPER_MM.marginX)}, ${A4_PAPER_PAD_X_PX}px);
|
||||
--doc-paper-pad-y: clamp(16px, ${pct(A4_PAPER_MM.marginY)}, ${A4_PAPER_PAD_Y_PX}px);
|
||||
/* A4 的**最小高度**:纸宽封顶 ${A4_PAPER_MAX_WIDTH_PX} 时就是 ${Math.round(A4_PAPER_MAX_WIDTH_PX * (A4_PAPER_MM.height / A4_PAPER_MM.width))}px(= 纸宽 × 297/210);
|
||||
/* A4 的**最小高度**:纸宽封顶 ${A4_PAPER_MAX_WIDTH_PX} 时就是 ${A4_PAPER_FULL_HEIGHT_PX}px(= 纸宽 × 297/210,即上面的 --doc-paper-full-h);
|
||||
容器比它窄时纸也会跟着窄,那时按容器宽 × 297/210 算(cqw = 容器宽的 1%)。
|
||||
两个分支在「容器 == 纸宽」处正好接上,所以 min() 挑哪个都对。
|
||||
|
||||
@@ -91,9 +108,11 @@ export const DOCUMENT_PAPER_CSS = `
|
||||
min-height 只设下限,正文多了纸一定长高(编辑器的纸一直是这么写的,没出过问题)。
|
||||
|
||||
⚠️ cqw 相对**最近的容器查询祖先**解析 —— 纸的父级必须带
|
||||
container-type: inline-size(probe-a4-paper.cjs 会逐处断言纵向不溢出)。 */
|
||||
container-type: inline-size(probe-a4-paper.cjs 会逐处断言纵向不溢出)。
|
||||
调用方若把纸**钉在**设计宽(纸宽 ≠ 纸壳宽),必须改取 --doc-paper-full-h:
|
||||
那时 cqw 会按更窄的纸壳算,得出来的高度比一页 A4 矮。 */
|
||||
--doc-paper-min-h: min(
|
||||
calc(var(--doc-paper-max-w) * ${(A4_PAPER_MM.height / A4_PAPER_MM.width).toFixed(6)}),
|
||||
var(--doc-paper-full-h),
|
||||
${((A4_PAPER_MM.height / A4_PAPER_MM.width) * 100).toFixed(2)}cqw
|
||||
);
|
||||
}
|
||||
@@ -113,6 +132,28 @@ export const DOCUMENT_PAPER_CSS = `
|
||||
padding: var(--doc-paper-pad-y) var(--doc-paper-pad-x);
|
||||
}
|
||||
|
||||
/* ============ 「钉在设计宽」的纸舞台 ============
|
||||
把 .doc-paper-stage 加在**滚动容器**上(工作台的 .sign-doc-body、
|
||||
预览弹窗的 .preview-body):容器比纸窄时,纸保持 A4 设计宽、由容器横向滚动,
|
||||
而不是把纸压窄。
|
||||
|
||||
为什么必须钉住:纸一被压窄,页边距(clamp 的百分比分支)与版心跟着变小,
|
||||
同一句话的换行位置就与编辑器、与导出的 PDF 对不上 —— 而"预览看到的"必须就是
|
||||
"签署时、下载到的"那一份。钉住之后纸宽恒为 ${A4_PAPER_MAX_WIDTH_PX}px、
|
||||
版心恒为 ${A4_CONTENT_WIDTH_PX}px;窄窗口下被裁掉的只是右侧那圈页边距
|
||||
(横向滚动能看回来),版心仍在视野内。
|
||||
|
||||
两个配套的定值不能省:
|
||||
- padding:% padding 以**包含块**宽度为基准,纸不再等于包含块时,
|
||||
clamp 的百分比分支会算出偏小的页边距(这正是文件顶部解释过的那件事);
|
||||
- min-height:cqw 同样相对包含块,会算出比一页 A4 矮的高度。 */
|
||||
.doc-paper-stage .doc-paper--a4 {
|
||||
width: var(--doc-paper-max-w);
|
||||
max-width: none;
|
||||
min-height: var(--doc-paper-full-h);
|
||||
padding: ${A4_PAPER_PAD_Y_PX}px ${A4_PAPER_PAD_X_PX}px;
|
||||
}
|
||||
|
||||
.doc-paper {
|
||||
color: #1c2b33;
|
||||
/* 与原型一致:**无衬线微软雅黑**,不是宋体。
|
||||
@@ -131,8 +172,13 @@ export const DOCUMENT_PAPER_CSS = `
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* line-height: inherit 是给**编辑器面**加的(纸这侧本来就继承 .doc-paper 的 1.85,
|
||||
写不写都一样):WangEditor 对正文里的 p / li / td / th 写死了 line-height: 1.5,
|
||||
不给这几处显式 inherit,编辑区的行距就比纸紧一档,同一段话占的行数都不一样。
|
||||
写 inherit 而不是复述 1.85:行距仍然只有 .doc-paper 一处定义。 */
|
||||
.doc-paper p {
|
||||
margin: 0 0 6px;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
.doc-paper h1,
|
||||
@@ -148,7 +194,11 @@ export const DOCUMENT_PAPER_CSS = `
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.doc-paper h4 {
|
||||
/* h5 与 h4 同档:不给它一条规则的话两边都不是我们定的口径 ——
|
||||
纸上吃浏览器默认(0.83em、1.67em 外边距),编辑器里吃 WangEditor 的 20px 外边距。
|
||||
索性一并定下来,免得「五级标题」在编辑区与纸上是两种字号。 */
|
||||
.doc-paper h4,
|
||||
.doc-paper h5 {
|
||||
margin: 10px 0 4px;
|
||||
font-size: 13.5px;
|
||||
}
|
||||
@@ -161,6 +211,18 @@ export const DOCUMENT_PAPER_CSS = `
|
||||
|
||||
.doc-paper li {
|
||||
margin: 0 0 4px;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
/* WangEditor 给每张表格套了一层 .table-container(虚线框 + 10px 内边距,
|
||||
见 @wangeditor/editor 的默认样式)——那是编辑器自己的拾取框,不是文书的一部分:
|
||||
纸上没有它,导出的 PDF 里也没有。不压平的话,同一张表在编辑区看着带一圈虚线、
|
||||
进了预览就消失。只压这层的框与留白,选中高亮(box-shadow)保留。 */
|
||||
.doc-paper .table-container {
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* 表格按内容自适应列宽并允许换行:绝对列宽在窄容器里会把整表裁掉 */
|
||||
@@ -180,6 +242,9 @@ export const DOCUMENT_PAPER_CSS = `
|
||||
border: 1px solid #52646e;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
/* 与 p / li 同一个理由:WangEditor 在单元格上也写死了 1.5,
|
||||
而纸上单元格继承 table 的 1.6(表格要挤一点才放得下) */
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
.doc-paper th {
|
||||
@@ -200,6 +265,21 @@ export const DOCUMENT_PAPER_CSS = `
|
||||
.doc-paper > .sign-doc-header:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* ============ 文书字体(后台可配置) ============
|
||||
谁的根上挂了 .managed-font,谁就换上 --document-font(由调用方挂内联变量)。
|
||||
纸(DocumentPaper)与编辑器编辑面(EditorWangPane)都挂它 ——
|
||||
「编辑时看到的字体」必须就是签出来的字体,否则同一份文书在两边排版不一样。
|
||||
|
||||
!important 在这里是**必须**的,不是偷懒:字体是最不该被别处改写的一项,
|
||||
而编辑器侧第三方的字体声明(WangEditor 的字体栈)与浏览器的默认字体
|
||||
都会在正文上生效,不给 !important 就会随优先级/加载顺序漂移。
|
||||
font-synthesis: none 让没有粗体字重的字体不做伪粗(与 .doc-paper 的正文一致)。 */
|
||||
.managed-font,
|
||||
.managed-font * {
|
||||
font-family: var(--document-font) !important;
|
||||
font-synthesis: none;
|
||||
}
|
||||
`
|
||||
|
||||
const STYLE_TAG_ID = 'document-paper-base-style'
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
*
|
||||
* 本模块只解除这些「布局锁定」,表格结构(行列、合并、边框、加粗、对齐)全部保留,
|
||||
* 宽度改由各渲染端的 CSS 统一控制,避免同一份文书在不同容器里表现不一致。
|
||||
*
|
||||
* 解除动作本身是 `unlockTableLayout`,粘贴近来要用,**渲染出去也要用**:
|
||||
* 编辑器序列化出来的表格自带 `style="width: auto"` 这类内联锁,只堵粘贴这一个口
|
||||
* 挡不住它(详见该函数的注释)。
|
||||
*/
|
||||
|
||||
const COMMENTS = /<!--[\s\S]*?-->/g
|
||||
@@ -49,6 +53,43 @@ function sanitizeStyleAttribute(style: string): string {
|
||||
.join('; ')
|
||||
}
|
||||
|
||||
/**
|
||||
* 解除表格上的「布局锁定」:绝对宽度、固定行高、固定布局、不换行、内容裁剪。
|
||||
* 宽度交回渲染端 CSS(`.doc-paper table { width: 100% }` 那一套)。
|
||||
*
|
||||
* **两个边界都要做,少一个就会出现"编辑器里与渲染出来不一样"**:
|
||||
*
|
||||
* - **粘贴时**(本模块的 normalizePastedHtml):Word / WPS 的表格带 pt/px 绝对宽度、
|
||||
* `nowrap` 与 `overflow: hidden`;
|
||||
* - **渲染时**(utils/signing-document 的 buildSigningDocumentHtml):WangEditor 的表格
|
||||
* 序列化会把宽度写成**内联样式** —— 它导出的每张表都是
|
||||
* `<table style="width: auto;">`(见 @wangeditor/editor 的 table elemToHtml)。
|
||||
* 而内联样式优先级高于 `.doc-paper table { width: 100% }`,于是同一张表
|
||||
* 在编辑器里撑满版心(编辑器 DOM 上那个 width 是**属性**、值为 `auto` 时浏览器直接忽略),
|
||||
* 到预览 / 签署就变成"按内容收缩",宽度两边对不上。渲染端统一按 CSS 排版才是唯一口径。
|
||||
*
|
||||
* 就地修改传入的文档片段,无返回值。
|
||||
*/
|
||||
export function unlockTableLayout(root: ParentNode): void {
|
||||
root.querySelectorAll('table, col, colgroup, td, th').forEach((element) => {
|
||||
LAYOUT_LOCK_ATTRIBUTES.forEach((name) => element.removeAttribute(name))
|
||||
|
||||
const style = element.getAttribute('style')
|
||||
|
||||
if (!style) {
|
||||
return
|
||||
}
|
||||
|
||||
const sanitized = sanitizeStyleAttribute(style)
|
||||
|
||||
if (sanitized) {
|
||||
element.setAttribute('style', sanitized)
|
||||
} else {
|
||||
element.removeAttribute('style')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 归一化 Word / WPS 粘贴的富文本 HTML。
|
||||
* 入参为空或解析失败时原样返回,不会抛错。
|
||||
@@ -77,23 +118,7 @@ export function normalizePastedHtml(html: string): string {
|
||||
})
|
||||
|
||||
// 表格、列定义与单元格:解除绝对尺寸与不换行约束,宽度交由渲染端 CSS 决定
|
||||
root.querySelectorAll('table, col, colgroup, td, th').forEach((element) => {
|
||||
LAYOUT_LOCK_ATTRIBUTES.forEach((name) => element.removeAttribute(name))
|
||||
|
||||
const style = element.getAttribute('style')
|
||||
|
||||
if (!style) {
|
||||
return
|
||||
}
|
||||
|
||||
const sanitized = sanitizeStyleAttribute(style)
|
||||
|
||||
if (sanitized) {
|
||||
element.setAttribute('style', sanitized)
|
||||
} else {
|
||||
element.removeAttribute('style')
|
||||
}
|
||||
})
|
||||
unlockTableLayout(root)
|
||||
|
||||
return root.innerHTML
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import type {
|
||||
SigningFieldValue,
|
||||
} from '@/api/workbench/types'
|
||||
|
||||
import { unlockTableLayout } from './rich-text'
|
||||
import {
|
||||
collectStructuredFieldIds,
|
||||
FIELD_ELEMENT_SELECTOR,
|
||||
@@ -783,6 +784,12 @@ export function buildSigningDocumentHtml(
|
||||
return contentHtml
|
||||
}
|
||||
|
||||
// 表格的「宽度锁」在渲染这一侧也要解掉,且必须在渲染填写项之前:
|
||||
// 编辑器导出的每张表都带 style="width: auto"(WangEditor 的序列化口径),
|
||||
// 它是**内联样式**,会盖掉 `.doc-paper table { width: 100% }` —— 结果就是
|
||||
// 「编辑器里撑满版心、预览/签署里按内容收缩」。详见 utils/rich-text 的 unlockTableLayout。
|
||||
unlockTableLayout(root)
|
||||
|
||||
// 已填写的取值优先于系统快照:签署后被固化的填写项要保持原样回显
|
||||
const answers: SigningAnswers = {
|
||||
...resolveAnswersFromSystem(data),
|
||||
|
||||
@@ -1,11 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import '@wangeditor/editor/dist/css/style.css'
|
||||
/**
|
||||
* 文书编辑器(WangEditor 引擎)。
|
||||
*
|
||||
* **编辑面就是一张「纸」**:编辑器根节点同时挂 `pane-content`(纸壳里的白纸)与
|
||||
* `doc-paper`(utils/document-paper.ts 里那份唯一排版)—— 编辑时看到的段落间距、
|
||||
* 表格内边距、字号字体,与签署预览、签署工作台、导出的 PDF 完全同一份来源。
|
||||
*
|
||||
* 为什么还要绕一层 styles/editor-vendor.css:WangEditor 自带一套权重更高的正文默认
|
||||
* (`p{margin:15px 0}` 等),会把那份排版盖掉。它整包被压进最低优先级的 cascade layer,
|
||||
* 于是 `.doc-paper p` 这类规则不必写 !important 也能生效。细节见那个文件。
|
||||
*/
|
||||
import '@/styles/editor-vendor.css'
|
||||
|
||||
import { onBeforeUnmount, shallowRef } from 'vue'
|
||||
import { onBeforeUnmount, ref, shallowRef } from 'vue'
|
||||
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
||||
import { DomEditor } from '@wangeditor/editor'
|
||||
import type { IDomEditor, IEditorConfig, IToolbarConfig } from '@wangeditor/editor'
|
||||
|
||||
import { loadDocumentFont } from '../../../../../../../packages/document-fonts'
|
||||
import { getDefaultDocumentFont } from '@/api/management/document-fonts'
|
||||
import type { SigningFieldDef } from '@/api/workbench/types'
|
||||
import { normalizePastedHtml } from '@/utils/rich-text'
|
||||
import { decorateSigningMarkers, registerSigningEditorMarks } from '@/utils/signing-editor-marks'
|
||||
@@ -40,6 +53,34 @@ const editorRef = shallowRef<IDomEditor>()
|
||||
const editableRef = shallowRef<Element | null>(null)
|
||||
const editorMode = 'default'
|
||||
|
||||
/**
|
||||
* 文书字体(后台「文书字体设置」里配置的默认字体)。
|
||||
*
|
||||
* 加载口径与 DocumentPaper 完全一致:**同一个默认字体**。少了这一道,
|
||||
* 后台配了仿宋/楷体时会出现「编辑器里是微软雅黑、签出来是仿宋」——
|
||||
* 同一份文书两副长相,而且换行位置也会跟着变。
|
||||
*
|
||||
* 用 ref 而不是直接等:字体是网络请求,加载完再挂 .managed-font,
|
||||
* 加载失败就退回全局字体(编辑不受影响,只是字体与签署态不一致)。
|
||||
* 不做「转圈等字体」:编辑器要能立刻编辑,不能因为一个字体把人挡在门外。
|
||||
*/
|
||||
const fontFamily = ref('')
|
||||
|
||||
async function prepareFont() {
|
||||
try {
|
||||
const font = await getDefaultDocumentFont()
|
||||
const family = await loadDocumentFont(font, import.meta.env.VITE_API_BASE_URL || '/api')
|
||||
|
||||
if (family) {
|
||||
fontFamily.value = family
|
||||
}
|
||||
} catch {
|
||||
fontFamily.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
void prepareFont()
|
||||
|
||||
/**
|
||||
* 用户是否真的把光标放进过正文。
|
||||
*
|
||||
@@ -258,11 +299,17 @@ onBeforeUnmount(() => {
|
||||
`.pane-desk` 提供灰底与唯一滚动条;
|
||||
`.pane-sheet` 负责限宽居中 —— % 页边距以**包含块**宽度为基准,
|
||||
限宽必须落在这里,落到纸上就会按更宽的桌面算(页边距偏大);
|
||||
`.pane-content` 才是白纸本身(A4 比例 + 页边距)。 -->
|
||||
`.pane-content` 才是白纸本身(A4 比例 + 页边距)。
|
||||
`doc-paper` 是**唯一那份正文排版**的根类(utils/document-paper.ts):
|
||||
白纸壳与编辑面各自挂一次,段落 / 标题 / 表格 / 字体的口径因此只有一份。
|
||||
WangEditor 自己的 DOM(`.w-e-text-container`、`[data-slate-editor]`)会挂在
|
||||
这层下面,所以那份排版是**继承**进去的,不是另抄一份。 -->
|
||||
<div class="pane-desk">
|
||||
<div class="pane-sheet">
|
||||
<Editor
|
||||
class="pane-content"
|
||||
class="pane-content doc-paper"
|
||||
:class="{ 'managed-font': Boolean(fontFamily) }"
|
||||
:style="{ '--document-font': fontFamily }"
|
||||
:model-value="props.modelValue"
|
||||
:default-config="editorConfig"
|
||||
:mode="editorMode"
|
||||
@@ -328,9 +375,13 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
/* 空文档提示绝对定位于纸的内层容器,WangEditor 自带一点偏移(实测右下各偏 ~10/17px),
|
||||
这里拉回版心原点,否则空文档时提示文字不与正文左边界对齐 */
|
||||
这里拉回版心原点,否则空文档时提示文字不与正文左边界对齐。
|
||||
`color: inherit` 同理:WangEditor 在这个容器上写死了自己的正文色(#333),
|
||||
而纸的正文色是 #1c2b33 —— 与字号字体一样,正文色也只该有纸那一份口径。
|
||||
这里用 inherit 透传、不复述颜色值,颜色仍然只有 .doc-paper 一处定义。 */
|
||||
.pane-content :deep(.w-e-text-container) {
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.pane-content :deep(.w-e-text-placeholder) {
|
||||
@@ -342,27 +393,11 @@ onBeforeUnmount(() => {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 粘贴的 Word 表格:解除绝对列宽、允许单元格换行,
|
||||
超出容器时横向滚动而不是被裁切 */
|
||||
.pane-content :deep(table) {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
table-layout: auto;
|
||||
}
|
||||
|
||||
.pane-content :deep(th),
|
||||
.pane-content :deep(td) {
|
||||
padding: 4px 6px;
|
||||
vertical-align: top;
|
||||
border: 1px solid #52646e;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.pane-content :deep(th p),
|
||||
.pane-content :deep(td p) {
|
||||
margin: 0;
|
||||
}
|
||||
/* 表格(width / border-collapse / table-layout / 边框色 / 单元格内边距 / 换行策略)
|
||||
不再在这里写第二份 —— 编辑面挂着 doc-paper,吃的是 utils/document-paper.ts 里
|
||||
唯一那一份。这里曾经各写一套(4px 6px / vertical-align: top),
|
||||
结果「编辑时看到的表格」与「预览、签署时看到的表格」不是同一张。
|
||||
粘进来的 Word 表格的绝对列宽由 utils/rich-text.ts 的 normalizePastedHtml 解除。 */
|
||||
|
||||
/* ---- 占位符 / 勾选框胶囊 ----
|
||||
配色与形状照原型 V10.2 的 .docfld(见 styles/signing-document.css 里签署态的同一套色):
|
||||
|
||||
Reference in New Issue
Block a user