a322b2eed4
中标公告区域两块改动:
- 公告正文改按富文本渲染。一站式推的是富文本编辑器出的 HTML,原来当纯文本渲染
(whitespace-pre-line),会把 <p>、 、style="…" 原样显示在页面上。
判定(HTML / 纯文本)、净化、纯文本换行抽到 app/utils/richText.ts,
与 hjc-h5 的 src/utils/richText.js 同一套口径。
- 公告附件:渲染后端下发的 bulletinFiles(后端已解析成 {name,url} 并拼好完整
下载地址,含 URL 编码),复用 HjcTenderFiles 组件。公告附件是公开资料
(不判登录 / 资质 / 购买),与标书附件(付费内容、仅已支付订单下发)是两条线,
所以这里不做权限判断。
- types/tender.ts:补 bulletinFiles 字段。
验证:npm run build 通过
77 lines
3.5 KiB
TypeScript
77 lines
3.5 KiB
TypeScript
/**
|
||
* 「公告正文」富文本处理(hjc-web)。
|
||
*
|
||
* **必须与 `hjc-h5/src/utils/richText.js` 保持一致**——两端是不同仓库、不同技术栈
|
||
* (Nuxt4 + TS / uni-app + JS),代码无法真正共享,只能靠"两处同一套判定"来对齐。
|
||
* 改这里的时候,另一边也要改。
|
||
*
|
||
* 为什么需要它:一站式平台推来的公告正文(`content` → `bulletin_content`)是**富文本编辑器
|
||
* 出的 HTML**,后端原样落库(见 `HjcBizServiceImpl#upsertFromPush`),而标书详情页此前把它
|
||
* 当纯文本渲染(`whitespace-pre-line`)——页面上会直接露出 `<p>`、` `、`style="..."`
|
||
* 这些标记。
|
||
*
|
||
* 两条路都要留着:
|
||
* - 是 HTML → 净化后交给 `RichText` 组件走 `v-html`;
|
||
* - 不是 HTML(推送历史数据 / 后台手录)→ 按纯文本转义、换行转 `<br>`,保住改造前
|
||
* `whitespace-pre-line` 的观感,不让这次改动把纯文本那条路弄塌。
|
||
*/
|
||
|
||
/**
|
||
* 是否「看起来是 HTML」。
|
||
*
|
||
* 判据只有一条:出现 `<字母…>` 这样的标签。纯文本里几乎不会自然出现它
|
||
* (正文写「工期 < 30 天」也不会命中——`<` 后面必须紧跟字母),
|
||
* 所以不做更聪明的解析:正文格式只有「编辑器 HTML」和「纯文本」两种。
|
||
*/
|
||
const HTML_TAG_RE = /<\/?[a-z][a-z0-9-]*(\s[^<>]*)?\/?>/i
|
||
|
||
/** 整段丢掉的标签(连内容一起):能跑脚本、能引外部资源、能改整页样式的一律不留 */
|
||
const DROP_TAGS = ['script', 'style', 'iframe', 'frame', 'frameset', 'object', 'embed', 'link', 'meta', 'base', 'form']
|
||
|
||
/** 事件属性:`v-html` 不执行 `<script>`,但**会**执行 `onerror` / `onload` 这类事件属性 */
|
||
const EVENT_ATTR_RE = /\son[a-z-]+\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi
|
||
|
||
/** `href` / `src` 里的伪协议(`data:` 只挡 text/html,`data:image/*` 是合法图片) */
|
||
const JS_URL_RE = /\s(href|src)\s*=\s*(["'])\s*(?:javascript|vbscript|data:text\/html)[^"']*\2/gi
|
||
|
||
/**
|
||
* 净化 HTML。
|
||
*
|
||
* 内容来自**站外**(一站式平台,虽然带 appKey/sign 鉴权),而这条链路是 `v-html`,
|
||
* 摘掉脚本与事件属性是这一层该负的责任(`RichText` 组件里那条正则只挡 `<script>`)。
|
||
*/
|
||
function sanitize(html: string): string {
|
||
let out = html
|
||
for (const tag of DROP_TAGS) {
|
||
out = out.replace(new RegExp(`<${tag}\\b[^>]*>[\\s\\S]*?<\\/${tag}\\s*>`, 'gi'), '')
|
||
out = out.replace(new RegExp(`<\\/?${tag}\\b[^>]*>`, 'gi'), '')
|
||
}
|
||
return out.replace(EVENT_ATTR_RE, '').replace(JS_URL_RE, ' $1="#"')
|
||
}
|
||
|
||
/**
|
||
* 纯文本 → 可直接塞进 HTML 的文本。
|
||
*
|
||
* 只转义 `<` `>`;**已成的实体不动**(` `、`&` 保持原样,否则会二次转义成
|
||
* `&nbsp;` 在页面上露出来),只有裸 `&` 才补成 `&`。换行转 `<br>`,
|
||
* 顶替原来的 `whitespace-pre-line`。
|
||
*/
|
||
function escapeText(text: string): string {
|
||
return text
|
||
.replace(/&(?![a-z][a-z0-9]*;|#\d+;|#x[0-9a-f]+;)/gi, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/\r\n|\r|\n/g, '<br />')
|
||
}
|
||
|
||
/**
|
||
* 公告正文 → 可交给 `RichText` 渲染的 HTML。
|
||
*
|
||
* 空内容返回空串,由调用方决定兜底文案(不在这里塞「暂无」——那是页面的事)。
|
||
*/
|
||
export function toRichTextHtml(raw?: string | null): string {
|
||
const content = (raw || '').trim()
|
||
if (!content) return ''
|
||
return HTML_TAG_RE.test(content) ? sanitize(content) : escapeText(content)
|
||
}
|