xsh_1997 6 dias atrás
pai
commit
e29006a00e

+ 30 - 0
ruoyi-ui-app/package-a/ai-assistant/index.vue

@@ -1678,6 +1678,36 @@ export default {
1678 1678
   max-width: 100%;
1679 1679
   overflow: visible;
1680 1680
   box-sizing: border-box;
1681
+  font-size: 28rpx;
1682
+  line-height: 1.55;
1683
+  -webkit-text-size-adjust: 100%;
1684
+  text-size-adjust: 100%;
1685
+}
1686
+
1687
+/* iOS rich-text 会放大 h1/h2(2em);兜底压回正文大小 */
1688
+.bubble__rich ::v-deep ._h1,
1689
+.bubble__rich ::v-deep ._h2,
1690
+.bubble__rich ::v-deep ._h3,
1691
+.bubble__rich ::v-deep ._h4,
1692
+.bubble__rich ::v-deep ._h5,
1693
+.bubble__rich ::v-deep ._h6 {
1694
+  font-size: 14px !important;
1695
+  font-weight: 600;
1696
+  line-height: 1.35;
1697
+  margin: 8px 0 6px;
1698
+}
1699
+
1700
+.bubble__rich ::v-deep ._root,
1701
+.bubble__rich ::v-deep ._p,
1702
+.bubble__rich ::v-deep ._div {
1703
+  font-size: 14px;
1704
+  line-height: 1.55;
1705
+  -webkit-text-size-adjust: 100%;
1706
+  text-size-adjust: 100%;
1707
+}
1708
+
1709
+.bubble__rich ::v-deep ._big {
1710
+  font-size: 14px !important;
1681 1711
 }
1682 1712
 
1683 1713
 .bubble--user .bubble__rich {

+ 90 - 31
ruoyi-ui-app/utils/chatMarkdown.js

@@ -1,7 +1,20 @@
1 1
 /**
2
- * AI 聊天气泡:将 Markdown 转为 mp-html 可渲染的 HTML(默认 uni_modules 包不含 markdown 插件)
2
+ * AI 聊天气泡:将 Markdown 转为 mp-html 可渲染的 HTML
3
+ * iOS App 的 rich-text 对 rpx、h1~h6(2em) 支持差,统一用 px + p 标签
3 4
  */
4 5
 
6
+const FONT = {
7
+  body: 14,
8
+  bodySm: 12,
9
+  h1: 15,
10
+  h2: 14.5,
11
+  h3: 14
12
+}
13
+
14
+function px(n) {
15
+  return `${n}px`
16
+}
17
+
5 18
 function escapeHtml(s) {
6 19
   return String(s)
7 20
     .replace(/&/g, '&')
@@ -15,6 +28,24 @@ function looksLikeHtml(s) {
15 28
   return /<\/?[a-z][\s\S]*>/i.test(s)
16 29
 }
17 30
 
31
+/** iOS rich-text:h1~h6 改为 p+px,去掉 font/big 等会放大的标签 */
32
+function normalizeAiHtml(html) {
33
+  let s = String(html)
34
+  const headingPx = [FONT.h1, FONT.h2, FONT.h3, FONT.h3, FONT.h3, FONT.h3]
35
+  for (let level = 1; level <= 6; level += 1) {
36
+    const size = headingPx[level - 1]
37
+    const re = new RegExp(`<h${level}(\\s[^>]*)?>([\\s\\S]*?)</h${level}>`, 'gi')
38
+    s = s.replace(
39
+      re,
40
+      (_, _attrs, content) =>
41
+        `<p style="margin:8px 0 6px;font-size:${px(size)};font-weight:600;line-height:1.35;word-break:break-word;">${content}</p>`
42
+    )
43
+  }
44
+  s = s.replace(/<font\b[^>]*>/gi, '<span>').replace(/<\/font>/gi, '</span>')
45
+  s = s.replace(/<big\b[^>]*>/gi, '<span>').replace(/<\/big>/gi, '</span>')
46
+  return s
47
+}
48
+
18 49
 /**
19 50
  * @param {string} text 原始 Markdown 或纯文本
20 51
  * @returns {string} HTML
@@ -25,7 +56,7 @@ export function markdownToHtml(text) {
25 56
   }
26 57
   const raw = String(text)
27 58
   if (looksLikeHtml(raw)) {
28
-    return raw
59
+    return normalizeAiHtml(raw)
29 60
   }
30 61
 
31 62
   const blocks = []
@@ -51,7 +82,14 @@ export function markdownToHtml(text) {
51 82
 
52 83
 function formatCodeBlock(code) {
53 84
   const body = escapeHtml(code.replace(/^\n|\n$/g, ''))
54
-  return `<pre style="margin:8px 0;padding:10px 12px;border-radius:8px;background:#f6f8fa;overflow-x:auto;font-size:24rpx;line-height:1.45"><code>${body}</code></pre>`
85
+  return `<pre style="margin:8px 0;padding:10px 12px;border-radius:8px;background:#f6f8fa;overflow-x:auto;font-size:${px(
86
+    FONT.bodySm
87
+  )};line-height:1.45"><code>${body}</code></pre>`
88
+}
89
+
90
+function headingStyle(level) {
91
+  const size = level === 1 ? FONT.h1 : level === 2 ? FONT.h2 : FONT.h3
92
+  return `margin:8px 0 6px;font-size:${px(size)};font-weight:600;line-height:1.35;word-break:break-word;`
55 93
 }
56 94
 
57 95
 function formatMdBlock(text) {
@@ -71,10 +109,7 @@ function formatMdBlock(text) {
71 109
     if (/^#{1,3}\s+/.test(trimmed)) {
72 110
       const level = trimmed.match(/^(#+)/)[1].length
73 111
       const content = inlineFormat(escapeHtml(trimmed.replace(/^#+\s+/, '')))
74
-      const size = level === 1 ? '32rpx' : level === 2 ? '30rpx' : '28rpx'
75
-      out.push(
76
-        `<div style="margin:12px 0 8px;font-size:${size};font-weight:600;line-height:1.35">${content}</div>`
77
-      )
112
+      out.push(`<p style="${headingStyle(level)}">${content}</p>`)
78 113
       i += 1
79 114
       continue
80 115
     }
@@ -86,8 +121,8 @@ function formatMdBlock(text) {
86 121
         i += 1
87 122
       }
88 123
       out.push(
89
-        `<ul style="margin:6px 0 12px;padding-left:1.25em">${items
90
-          .map((li) => `<li style="margin:4px 0;line-height:1.5">${li}</li>`)
124
+        `<ul style="margin:6px 0 12px;padding-left:1.25em;font-size:${px(FONT.body)}">${items
125
+          .map((li) => `<li style="margin:4px 0;line-height:1.5;font-size:${px(FONT.body)}">${li}</li>`)
91 126
           .join('')}</ul>`
92 127
       )
93 128
       continue
@@ -100,8 +135,8 @@ function formatMdBlock(text) {
100 135
         i += 1
101 136
       }
102 137
       out.push(
103
-        `<ol style="margin:6px 0 12px;padding-left:1.25em">${items
104
-          .map((li) => `<li style="margin:4px 0;line-height:1.5">${li}</li>`)
138
+        `<ol style="margin:6px 0 12px;padding-left:1.25em;font-size:${px(FONT.body)}">${items
139
+          .map((li) => `<li style="margin:4px 0;line-height:1.5;font-size:${px(FONT.body)}">${li}</li>`)
105 140
           .join('')}</ol>`
106 141
       )
107 142
       continue
@@ -115,7 +150,9 @@ function formatMdBlock(text) {
115 150
       }
116 151
       const body = quoteLines.map((l) => inlineFormat(escapeHtml(l))).join('<br/>')
117 152
       out.push(
118
-        `<blockquote style="margin:8px 0;padding:6px 12px;border-left:4px solid #22c55e;background:rgba(255,255,255,0.55)">${body}</blockquote>`
153
+        `<blockquote style="margin:8px 0;padding:6px 12px;border-left:4px solid #22c55e;background:rgba(255,255,255,0.55);font-size:${px(
154
+          FONT.body
155
+        )}">${body}</blockquote>`
119 156
       )
120 157
       continue
121 158
     }
@@ -126,35 +163,57 @@ function formatMdBlock(text) {
126 163
       i += 1
127 164
     }
128 165
     const body = inlineFormat(escapeHtml(para.join('\n'))).replace(/\n/g, '<br/>')
129
-    out.push(`<p style="margin:0 0 10px;line-height:1.55;word-break:break-word">${body}</p>`)
166
+    out.push(
167
+      `<p style="margin:0 0 10px;line-height:1.55;word-break:break-word;font-size:${px(FONT.body)}">${body}</p>`
168
+    )
130 169
   }
131 170
 
132
-  return out.join('') || `<p style="margin:0;line-height:1.55">${inlineFormat(escapeHtml(text))}</p>`
171
+  return (
172
+    out.join('') ||
173
+    `<p style="margin:0;line-height:1.55;font-size:${px(FONT.body)}">${inlineFormat(escapeHtml(text))}</p>`
174
+  )
133 175
 }
134 176
 
135 177
 function inlineFormat(s) {
136 178
   return s
137
-    .replace(/`([^`]+)`/g, '<code style="padding:2px 6px;border-radius:4px;background:rgba(0,0,0,0.06);font-size:24rpx">$1</code>')
179
+    .replace(
180
+      /`([^`]+)`/g,
181
+      `<code style="padding:2px 6px;border-radius:4px;background:rgba(0,0,0,0.06);font-size:${px(FONT.bodySm)}">$1</code>`
182
+    )
138 183
     .replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
139 184
     .replace(/\*([^*]+)\*/g, '<em>$1</em>')
140 185
 }
141 186
 
142
-/** mp-html tag-style 默认样式(对象形式,见插件文档) */
187
+const bodyStyle = `font-size:${px(FONT.body)};line-height:1.55;word-break:break-word;`
188
+
189
+/** mp-html tag-style(统一 px,避免 iOS rich-text 忽略 rpx) */
143 190
 export const MP_HTML_TAG_STYLE = {
144
-  p: 'margin:0 0 10px;line-height:1.55;word-break:break-word;',
145
-  div: 'word-break:break-word;',
146
-  ul: 'margin:6px 0 12px;padding-left:1.25em;',
147
-  ol: 'margin:6px 0 12px;padding-left:1.25em;',
148
-  li: 'margin:4px 0;line-height:1.5;',
149
-  pre: 'margin:8px 0;padding:10px 12px;border-radius:8px;background:#f6f8fa;overflow-x:auto;',
150
-  code: 'font-family:monospace;font-size:24rpx;',
151
-  blockquote:
152
-    'margin:8px 0;padding:6px 12px;border-left:4px solid #22c55e;background:rgba(255,255,255,0.55);',
153
-  a: 'color:#16a34a;text-decoration:underline;',
154
-  table: 'border-collapse:collapse;margin:8px 0;font-size:24rpx;max-width:100%;',
155
-  th: 'border:1px solid #ddd4c8;padding:6px 10px;background:#f0ebe3;',
156
-  td: 'border:1px solid #ddd4c8;padding:6px 10px;'
191
+  p: `margin:0 0 10px;${bodyStyle}`,
192
+  div: bodyStyle,
193
+  h1: `display:block;margin:8px 0 6px;font-size:${px(FONT.h1)};font-weight:600;line-height:1.35;`,
194
+  h2: `display:block;margin:8px 0 6px;font-size:${px(FONT.h2)};font-weight:600;line-height:1.35;`,
195
+  h3: `display:block;margin:8px 0 6px;font-size:${px(FONT.h3)};font-weight:600;line-height:1.35;`,
196
+  h4: `display:block;margin:8px 0 4px;font-size:${px(FONT.h3)};font-weight:600;line-height:1.35;`,
197
+  h5: `display:block;margin:6px 0 4px;font-size:${px(FONT.h3)};font-weight:600;line-height:1.35;`,
198
+  h6: `display:block;margin:6px 0 4px;font-size:${px(FONT.h3)};font-weight:600;line-height:1.35;`,
199
+  ul: `margin:6px 0 12px;padding-left:1.25em;font-size:${px(FONT.body)};`,
200
+  ol: `margin:6px 0 12px;padding-left:1.25em;font-size:${px(FONT.body)};`,
201
+  li: `margin:4px 0;line-height:1.5;font-size:${px(FONT.body)};`,
202
+  pre: `margin:8px 0;padding:10px 12px;border-radius:8px;background:#f6f8fa;overflow-x:auto;font-size:${px(FONT.bodySm)};`,
203
+  code: `font-family:monospace;font-size:${px(FONT.bodySm)};`,
204
+  blockquote: `margin:8px 0;padding:6px 12px;border-left:4px solid #22c55e;background:rgba(255,255,255,0.55);font-size:${px(
205
+    FONT.body
206
+  )};`,
207
+  a: `color:#16a34a;text-decoration:underline;font-size:${px(FONT.body)};`,
208
+  strong: 'font-weight:600;',
209
+  em: 'font-style:italic;',
210
+  big: `display:inline;font-size:${px(FONT.body)};`,
211
+  small: `display:inline;font-size:${px(FONT.bodySm)};`,
212
+  table: `border-collapse:collapse;margin:8px 0;font-size:${px(FONT.bodySm)};max-width:100%;`,
213
+  th: `border:1px solid #ddd4c8;padding:6px 10px;background:#f0ebe3;font-size:${px(FONT.bodySm)};`,
214
+  td: `border:1px solid #ddd4c8;padding:6px 10px;font-size:${px(FONT.bodySm)};`
157 215
 }
158 216
 
159
-export const MP_HTML_CONTAINER_STYLE =
160
-  'display:block;width:100%;min-width:0;overflow:visible;line-height:1.55;font-size:28rpx;color:#4a4542;word-break:break-word;'
217
+export const MP_HTML_CONTAINER_STYLE = `display:block;width:100%;min-width:0;overflow:visible;line-height:1.55;font-size:${px(
218
+  FONT.body
219
+)};color:#4a4542;word-break:break-word;-webkit-text-size-adjust:100%;text-size-adjust:100%;`