shinnosuke
2026-09-18 c2f0ec3b3891bc177bdc58c2a111ab9c4fea911d
rencaika_merchant/services/agreement.uts
@@ -1,4 +1,4 @@
import { getApiEndpoint } from '../config/api.uts'
import { getApiBaseUrl, getApiEndpoint } from '../config/api.uts'
import { HttpError, request } from './http.uts'
export type MerchantAgreement = {
@@ -9,6 +9,37 @@
   state: string
   message: string
   contentHtml: string
}
/**
 * 后台富文本落库时图片/链接存的是相对路径(`/uploads/xxx.png`)。小程序 rich-text 不认相对路径,
 * 直接渲染就是破图(只显示 alt 文字),所以这里统一补成接口域名下的绝对地址。
 *
 * 顺带处理两件后台内容与小程序渲染能力对不齐的事:
 * 1. 去掉 script/style、内联事件属性和 `javascript:` 协议——后台内容不该在小程序里执行脚本;
 * 2. `<s>/<strike>/<u>` 换成 rich-text 支持的 `<del>/<ins>`,并给 `<img>` 补内联样式——
 *    小程序的 rich-text 不继承页面样式,后台图片按原图尺寸会把卡片撑破。
 *
 * 规则与人才端 `rencaika/services/business.uts` 的 sanitizeRichText 保持一致,两端表现不要分叉。
 */
export const sanitizeAgreementHtml = (html: string): string => {
   if (html == null) return ''
   const base = getApiBaseUrl().replace(/\/+$/, '')
   const content = ('' + html)
      .replace(/<script[\s\S]*?<\/script>/gi, '')
      .replace(/<style[\s\S]*?<\/style>/gi, '')
      .replace(/\son[a-z]+\s*=\s*(['"]).*?\1/gi, '')
      .replace(/\s(href|src)\s*=\s*(['"])\s*javascript:[^'"]*\2/gi, '')
   if (base == '') return content
   return content
      .replace(/<\s*(\/?)\s*s(?=[\s>/])([^>]*)>/gi, '<$1del$2>')
      .replace(/<\s*(\/?)\s*strike(?=[\s>/])([^>]*)>/gi, '<$1del$2>')
      .replace(/<\s*(\/?)\s*u(?=[\s>/])([^>]*)>/gi, '<$1ins$2>')
      .replace(/(src|href)=(["'])\/\//gi, '$1=$2https://')
      .replace(/(\s(?:src|href)\s*=\s*(['"]))\/+/gi, '$1' + base + '/')
      .replace(/(\s(?:src|href)\s*=\s*(['"]))(?!https?:|\/\/|\/|data:|javascript:)/gi, '$1' + base + '/')
      .replace(/(<img\b[^>]*?\sstyle=(["']))([^"']*)(\2)/gi, '$1$3max-width:100%;height:auto;display:block;margin:10px auto;$4')
      .replace(/<img\b(?![^>]*\sstyle=)/gi, '<img style="max-width:100%;height:auto;display:block;margin:10px auto;"')
}
export const loadMerchantAgreement = (
@@ -22,5 +53,9 @@
      skipAuth: true,
      skipStoreContext: true,
      skipUnauthorizedRedirect: true
   }, onSuccess, onError)
   }, (agreement: MerchantAgreement) => {
      // 页面拿到的是可直接喂给 rich-text 的内容,不必各自再处理一遍相对路径与标签差异。
      if (agreement != null) agreement.contentHtml = sanitizeAgreementHtml(agreement.contentHtml)
      onSuccess(agreement)
   }, onError)
}