聊天室更新markdown

This commit is contained in:
2025-12-25 12:33:12 +08:00
parent 41bc41cfcd
commit 78db3fc9e4
9 changed files with 578 additions and 35 deletions

View File

@@ -1784,6 +1784,12 @@
"resolved": "packages/shared", "resolved": "packages/shared",
"link": true "link": true
}, },
"node_modules/@stomp/stompjs": {
"version": "7.2.1",
"resolved": "https://registry.npmmirror.com/@stomp/stompjs/-/stompjs-7.2.1.tgz",
"integrity": "sha512-DLd/WeicnHS5SsWWSk3x6/pcivqchNaEvg9UEGVqAcfYEBVmS9D6980ckXjTtfpXLjdLDsd96M7IuX4w7nzq5g==",
"license": "Apache-2.0"
},
"node_modules/@types/estree": { "node_modules/@types/estree": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz",
@@ -1816,6 +1822,13 @@
"undici-types": "~6.21.0" "undici-types": "~6.21.0"
} }
}, },
"node_modules/@types/sockjs-client": {
"version": "1.5.4",
"resolved": "https://registry.npmmirror.com/@types/sockjs-client/-/sockjs-client-1.5.4.tgz",
"integrity": "sha512-zk+uFZeWyvJ5ZFkLIwoGA/DfJ+pYzcZ8eH4H/EILCm2OBZyHH6Hkdna1/UWL/CFruh5wj6ES7g75SvUB0VsH5w==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/web-bluetooth": { "node_modules/@types/web-bluetooth": {
"version": "0.0.20", "version": "0.0.20",
"resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
@@ -1834,6 +1847,12 @@
"resolved": "packages/workcase", "resolved": "packages/workcase",
"link": true "link": true
}, },
"node_modules/@vant/weapp": {
"version": "1.11.7",
"resolved": "https://registry.npmmirror.com/@vant/weapp/-/weapp-1.11.7.tgz",
"integrity": "sha512-Rwn9BBnb4kHSV4XmvBicwtd42J+amEUfnFDcXJsGNPNX4a9c/DoT6YLsm4X1wB2+sQbdiQsbFBLAvGRBxCkD8g==",
"license": "MIT"
},
"node_modules/@vitejs/plugin-vue": { "node_modules/@vitejs/plugin-vue": {
"version": "5.2.4", "version": "5.2.4",
"resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
@@ -2363,9 +2382,9 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001759", "version": "1.0.30001761",
"resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001759.tgz", "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001761.tgz",
"integrity": "sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==", "integrity": "sha512-JF9ptu1vP2coz98+5051jZ4PwQgd2ni8A+gYSN7EA7dPKIMf0pDlSUxhdmVOaV3/fYK5uWBkgSXJaRLr4+3A6g==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -2925,6 +2944,15 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/eventsource": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/eventsource/-/eventsource-2.0.2.tgz",
"integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==",
"license": "MIT",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/express": { "node_modules/express": {
"version": "4.22.1", "version": "4.22.1",
"resolved": "https://registry.npmmirror.com/express/-/express-4.22.1.tgz", "resolved": "https://registry.npmmirror.com/express/-/express-4.22.1.tgz",
@@ -2986,6 +3014,18 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/faye-websocket": {
"version": "0.11.4",
"resolved": "https://registry.npmmirror.com/faye-websocket/-/faye-websocket-0.11.4.tgz",
"integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==",
"license": "Apache-2.0",
"dependencies": {
"websocket-driver": ">=0.5.1"
},
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/fdir": { "node_modules/fdir": {
"version": "6.5.0", "version": "6.5.0",
"resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz",
@@ -3322,6 +3362,12 @@
"url": "https://opencollective.com/express" "url": "https://opencollective.com/express"
} }
}, },
"node_modules/http-parser-js": {
"version": "0.5.10",
"resolved": "https://registry.npmmirror.com/http-parser-js/-/http-parser-js-0.5.10.tgz",
"integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==",
"license": "MIT"
},
"node_modules/iconv-lite": { "node_modules/iconv-lite": {
"version": "0.4.24", "version": "0.4.24",
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz", "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz",
@@ -3934,6 +3980,12 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/querystringify": {
"version": "2.2.0",
"resolved": "https://registry.npmmirror.com/querystringify/-/querystringify-2.2.0.tgz",
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
"license": "MIT"
},
"node_modules/range-parser": { "node_modules/range-parser": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz", "resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz",
@@ -3982,6 +4034,12 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
"license": "MIT"
},
"node_modules/resolve": { "node_modules/resolve": {
"version": "1.22.11", "version": "1.22.11",
"resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.11.tgz", "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.11.tgz",
@@ -4824,6 +4882,34 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/sockjs-client": {
"version": "1.6.1",
"resolved": "https://registry.npmmirror.com/sockjs-client/-/sockjs-client-1.6.1.tgz",
"integrity": "sha512-2g0tjOR+fRs0amxENLi/q5TiJTqY+WXFOzb5UwXndlK6TO3U/mirZznpx6w34HVMoc3g7cY24yC/ZMIYnDlfkw==",
"license": "MIT",
"dependencies": {
"debug": "^3.2.7",
"eventsource": "^2.0.2",
"faye-websocket": "^0.11.4",
"inherits": "^2.0.4",
"url-parse": "^1.5.10"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://tidelift.com/funding/github/npm/sockjs-client"
}
},
"node_modules/sockjs-client/node_modules/debug": {
"version": "3.2.7",
"resolved": "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.1"
}
},
"node_modules/source-map-js": { "node_modules/source-map-js": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -4952,6 +5038,10 @@
"node": ">=16.0.0" "node": ">=16.0.0"
} }
}, },
"node_modules/taihao-service-miniprogram": {
"resolved": "packages/wechat_demo",
"link": true
},
"node_modules/tinyglobby": { "node_modules/tinyglobby": {
"version": "0.2.15", "version": "0.2.15",
"resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz", "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz",
@@ -5089,6 +5179,16 @@
"browserslist": ">= 4.21.0" "browserslist": ">= 4.21.0"
} }
}, },
"node_modules/url-parse": {
"version": "1.5.10",
"resolved": "https://registry.npmmirror.com/url-parse/-/url-parse-1.5.10.tgz",
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
"license": "MIT",
"dependencies": {
"querystringify": "^2.1.1",
"requires-port": "^1.0.0"
}
},
"node_modules/utils-merge": { "node_modules/utils-merge": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmmirror.com/utils-merge/-/utils-merge-1.0.1.tgz", "resolved": "https://registry.npmmirror.com/utils-merge/-/utils-merge-1.0.1.tgz",
@@ -5275,6 +5375,29 @@
"typescript": ">=5.0.0" "typescript": ">=5.0.0"
} }
}, },
"node_modules/websocket-driver": {
"version": "0.7.4",
"resolved": "https://registry.npmmirror.com/websocket-driver/-/websocket-driver-0.7.4.tgz",
"integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==",
"license": "Apache-2.0",
"dependencies": {
"http-parser-js": ">=0.5.1",
"safe-buffer": ">=5.1.0",
"websocket-extensions": ">=0.1.1"
},
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/websocket-extensions": {
"version": "0.1.4",
"resolved": "https://registry.npmmirror.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz",
"integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
@@ -5291,6 +5414,10 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/workcase_wechat": {
"resolved": "packages/workcase_wechat",
"link": true
},
"node_modules/wrap-ansi": { "node_modules/wrap-ansi": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
@@ -5423,11 +5550,13 @@
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.3.2", "@element-plus/icons-vue": "^2.3.2",
"@stomp/stompjs": "^7.2.1",
"cors": "^2.8.5", "cors": "^2.8.5",
"element-plus": "^2.12.0", "element-plus": "^2.12.0",
"express": "^4.18.2", "express": "^4.18.2",
"lucide-vue-next": "^0.561.0", "lucide-vue-next": "^0.561.0",
"ofetch": "^1.4.1", "ofetch": "^1.4.1",
"sockjs-client": "^1.6.1",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-router": "^4.5.0" "vue-router": "^4.5.0"
}, },
@@ -6899,22 +7028,32 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"packages/wechat_demo": {
"name": "taihao-service-miniprogram",
"version": "1.0.0",
"dependencies": {
"@vant/weapp": "^1.11.7"
}
},
"packages/workcase": { "packages/workcase": {
"name": "@urbanlifeline/workcase", "name": "@urbanlifeline/workcase",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.3.2", "@element-plus/icons-vue": "^2.3.2",
"@stomp/stompjs": "^7.2.1",
"@vueuse/core": "^11.3.0", "@vueuse/core": "^11.3.0",
"axios": "^1.7.9", "axios": "^1.7.9",
"element-plus": "^2.8.6", "element-plus": "^2.8.6",
"lucide-vue-next": "^0.561.0", "lucide-vue-next": "^0.561.0",
"pinia": "^2.2.8", "pinia": "^2.2.8",
"sockjs-client": "^1.6.1",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-router": "^4.5.0" "vue-router": "^4.5.0"
}, },
"devDependencies": { "devDependencies": {
"@module-federation/vite": "^1.9.3", "@module-federation/vite": "^1.9.3",
"@types/node": "^22.0.0", "@types/node": "^22.0.0",
"@types/sockjs-client": "^1.5.4",
"@vitejs/plugin-vue": "^5.2.1", "@vitejs/plugin-vue": "^5.2.1",
"@vitejs/plugin-vue-jsx": "^4.1.1", "@vitejs/plugin-vue-jsx": "^4.1.1",
"typescript": "^5.7.2", "typescript": "^5.7.2",
@@ -6922,18 +7061,6 @@
"vue-tsc": "^2.2.0" "vue-tsc": "^2.2.0"
} }
}, },
"packages/workcase_wechat": { "packages/workcase_wechat": {}
"name": "workcase-wechat",
"version": "1.0.0",
"extraneous": true,
"dependencies": {
"vue": "^3.0.0"
},
"devDependencies": {
"@dcloudio/uni-cli-shared": "latest",
"@dcloudio/vite-plugin-uni": "latest",
"vite": "latest"
}
}
} }
} }

View File

@@ -345,3 +345,86 @@ $brand-color-hover: #004488;
} }
} }
} }
// ==================== Markdown样式 ====================
.message-bubble {
// 粗体
strong {
font-weight: 600;
color: inherit;
}
// 斜体
em {
font-style: italic;
}
// 行内代码
.inline-code {
background: rgba(0, 0, 0, 0.05);
padding: 2px 6px;
border-radius: 4px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 13px;
color: #e53e3e;
}
// 代码块
.code-block {
background: rgba(0, 0, 0, 0.05);
padding: 12px;
border-radius: 8px;
overflow-x: auto;
margin: 8px 0;
code {
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 13px;
line-height: 1.5;
color: #334155;
}
}
// 链接
.md-link {
color: $brand-color;
text-decoration: underline;
&:hover {
color: $brand-color-hover;
}
}
// 标题
.md-h1 {
font-size: 20px;
font-weight: 700;
margin: 12px 0 8px;
color: inherit;
}
.md-h2 {
font-size: 18px;
font-weight: 600;
margin: 10px 0 6px;
color: inherit;
}
.md-h3 {
font-size: 16px;
font-weight: 600;
margin: 8px 0 4px;
color: inherit;
}
// 列表
.md-ul, .md-ol {
margin: 8px 0;
padding-left: 20px;
}
.md-li {
margin: 4px 0;
line-height: 1.6;
}
}

View File

@@ -40,7 +40,10 @@
<!-- 消息内容 --> <!-- 消息内容 -->
<div class="message-content-wrapper"> <div class="message-content-wrapper">
<div class="message-bubble"> <div class="message-bubble">
<p class="message-text">{{ message.content }}</p> <div
class="message-text"
v-html="renderMarkdown(message.content || '')"
></div>
<!-- 文件列表 --> <!-- 文件列表 -->
<div v-if="message.files && message.files.length > 0" class="message-files"> <div v-if="message.files && message.files.length > 0" class="message-files">
@@ -243,6 +246,51 @@ const formatTime = (time?: string) => {
return date.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' }) return date.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' })
} }
// Markdown渲染函数
const renderMarkdown = (text: string): string => {
if (!text) return ''
// 转义HTML标签
let html = text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
// 处理代码块(```语法)
html = html.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => {
return `<pre class="code-block"><code class="language-${lang || 'text'}">${code.trim()}</code></pre>`
})
// 处理行内代码(`语法)
html = html.replace(/`([^`]+)`/g, '<code class="inline-code">$1</code>')
// 处理粗体(**语法)
html = html.replace(/\*\*([^\*]+)\*\*/g, '<strong>$1</strong>')
// 处理斜体(*语法)
html = html.replace(/\*([^\*]+)\*/g, '<em>$1</em>')
// 处理链接([text](url)语法)
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" class="md-link">$1</a>')
// 处理标题(# ## ###等)
html = html.replace(/^### (.+)$/gm, '<h3 class="md-h3">$1</h3>')
html = html.replace(/^## (.+)$/gm, '<h2 class="md-h2">$1</h2>')
html = html.replace(/^# (.+)$/gm, '<h1 class="md-h1">$1</h1>')
// 处理无序列表(- 或 * 开头)
html = html.replace(/^[*-] (.+)$/gm, '<li class="md-li">$1</li>')
html = html.replace(/(<li class="md-li">.*<\/li>)/s, '<ul class="md-ul">$1</ul>')
// 处理有序列表(数字. 开头)
html = html.replace(/^\d+\. (.+)$/gm, '<li class="md-li">$1</li>')
// 处理换行
html = html.replace(/\n/g, '<br>')
return html
}
// 暴露方法给父组件 // 暴露方法给父组件
defineExpose({ defineExpose({
scrollToBottom scrollToBottom

View File

@@ -361,7 +361,6 @@ $brand-color-hover: #004488;
// ==================== 消息列表 ==================== // ==================== 消息列表 ====================
.messages-list { .messages-list {
max-width: 768px;
margin: 0 auto; margin: 0 auto;
padding: 24px 16px; padding: 24px 16px;
@@ -485,7 +484,6 @@ $brand-color-hover: #004488;
background: #fff; background: #fff;
.quick-bar-inner { .quick-bar-inner {
max-width: 768px;
margin: 0 auto; margin: 0 auto;
display: flex; display: flex;
gap: 8px; gap: 8px;
@@ -528,7 +526,6 @@ $brand-color-hover: #004488;
background: #f8fafc; background: #f8fafc;
.input-wrapper { .input-wrapper {
max-width: 768px;
margin: 0 auto; margin: 0 auto;
} }
@@ -616,3 +613,107 @@ $brand-color-hover: #004488;
margin: 12px 0 0; margin: 12px 0 0;
} }
} }
// ==================== Markdown样式 ====================
.message-bubble {
// 粗体
strong {
font-weight: 600;
color: inherit;
}
// 斜体
em {
font-style: italic;
}
// 行内代码
.inline-code {
background: rgba(0, 0, 0, 0.05);
padding: 2px 6px;
border-radius: 4px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 13px;
color: #e53e3e;
}
// 代码块
.code-block {
background: rgba(0, 0, 0, 0.05);
padding: 12px;
border-radius: 8px;
overflow-x: auto;
margin: 8px 0;
code {
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 13px;
line-height: 1.5;
color: #334155;
}
}
// 链接
.md-link {
color: $brand-color;
text-decoration: underline;
&:hover {
color: $brand-color-hover;
}
}
// 标题
.md-h1 {
font-size: 20px;
font-weight: 700;
margin: 12px 0 8px;
color: inherit;
}
.md-h2 {
font-size: 18px;
font-weight: 600;
margin: 10px 0 6px;
color: inherit;
}
.md-h3 {
font-size: 16px;
font-weight: 600;
margin: 8px 0 4px;
color: inherit;
}
// 列表
.md-ul, .md-ol {
margin: 8px 0;
padding-left: 20px;
}
.md-li {
margin: 4px 0;
line-height: 1.6;
}
}
// 用户消息中的markdown样式白色背景
.message-bubble.user {
.inline-code {
background: rgba(255, 255, 255, 0.2);
color: #fff;
}
.code-block {
background: rgba(255, 255, 255, 0.15);
code {
color: #fff;
}
}
.md-link {
color: #fff;
text-decoration: underline;
}
}

View File

@@ -59,7 +59,7 @@
> >
<MessageCircle :size="16" class="conv-icon" /> <MessageCircle :size="16" class="conv-icon" />
<div class="conv-info"> <div class="conv-info">
<div class="conv-title">{{ conv.title || '新对话' }}</div> <div class="conv-title">{{ getPlainTextPreview(conv.title || '新对话') }}</div>
<div class="conv-time">{{ formatTime(conv.createTime) }}</div> <div class="conv-time">{{ formatTime(conv.createTime) }}</div>
</div> </div>
<button <button
@@ -124,10 +124,13 @@
<!-- 消息内容 --> <!-- 消息内容 -->
<div class="message-bubble" :class="msg.role"> <div class="message-bubble" :class="msg.role">
<p class="message-text"> <div
{{ msg.text }} v-if="msg.text"
<span v-if="isStreaming && msg.role === 'assistant' && messages.indexOf(msg) === messages.length - 1" class="typing-cursor">|</span> class="message-text"
</p> v-html="msg.role === 'assistant' ? renderMarkdown(msg.text) : msg.text"
>
</div>
<span v-if="isStreaming && msg.role === 'assistant' && messages.indexOf(msg) === messages.length - 1" class="typing-cursor">|</span>
<div v-if="!msg.text && isStreaming && msg.role === 'assistant'" class="loading-dots"> <div v-if="!msg.text && isStreaming && msg.role === 'assistant'" class="loading-dots">
<span></span><span></span><span></span> <span></span><span></span><span></span>
</div> </div>
@@ -528,6 +531,83 @@ const formatTime = (time?: string | number): string => {
return date.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' }) return date.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' })
} }
// 去除markdown语法并截取前10个字符用于历史对话列表预览
const getPlainTextPreview = (text: string): string => {
if (!text) return ''
// 去除markdown语法
let plainText = text
// 去除代码块
.replace(/```[\s\S]*?```/g, '[代码]')
// 去除行内代码
.replace(/`([^`]+)`/g, '$1')
// 去除粗体
.replace(/\*\*([^\*]+)\*\*/g, '$1')
// 去除斜体
.replace(/\*([^\*]+)\*/g, '$1')
// 去除链接
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
// 去除标题标记
.replace(/^#{1,6}\s+/gm, '')
// 去除列表标记
.replace(/^[*-]\s+/gm, '')
// 去除多余的空白字符
.replace(/\s+/g, ' ')
.trim()
// 截取前10个字符
if (plainText.length > 10) {
return plainText.substring(0, 10) + '...'
}
return plainText
}
// Markdown渲染函数
const renderMarkdown = (text: string): string => {
if (!text) return ''
// 转义HTML标签
let html = text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
// 处理代码块(```语法)
html = html.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => {
return `<pre class="code-block"><code class="language-${lang || 'text'}">${code.trim()}</code></pre>`
})
// 处理行内代码(`语法)
html = html.replace(/`([^`]+)`/g, '<code class="inline-code">$1</code>')
// 处理粗体(**语法)
html = html.replace(/\*\*([^\*]+)\*\*/g, '<strong>$1</strong>')
// 处理斜体(*语法)
html = html.replace(/\*([^\*]+)\*/g, '<em>$1</em>')
// 处理链接([text](url)语法)
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" class="md-link">$1</a>')
// 处理标题(# ## ###等)
html = html.replace(/^### (.+)$/gm, '<h3 class="md-h3">$1</h3>')
html = html.replace(/^## (.+)$/gm, '<h2 class="md-h2">$1</h2>')
html = html.replace(/^# (.+)$/gm, '<h1 class="md-h1">$1</h1>')
// 处理无序列表(- 或 * 开头)
html = html.replace(/^[*-] (.+)$/gm, '<li class="md-li">$1</li>')
html = html.replace(/(<li class="md-li">.*<\/li>)/s, '<ul class="md-ul">$1</ul>')
// 处理有序列表(数字. 开头)
html = html.replace(/^\d+\. (.+)$/gm, '<li class="md-li">$1</li>')
// 处理换行
html = html.replace(/\n/g, '<br>')
return html
}
// 处理快捷命令 // 处理快捷命令
const handleQuickCommand = (query: string) => { const handleQuickCommand = (query: string) => {
inputText.value = query inputText.value = query

View File

@@ -66,7 +66,7 @@
</view> </view>
<view class="message-content"> <view class="message-content">
<view class="bubble other-bubble"> <view class="bubble other-bubble">
<text class="message-text">{{ msg.content }}</text> <rich-text :nodes="renderMarkdown(msg.content || '')" class="message-rich-text"></rich-text>
</view> </view>
<text class="message-time">{{ formatTime(msg.sendTime) }}</text> <text class="message-time">{{ formatTime(msg.sendTime) }}</text>
</view> </view>
@@ -396,6 +396,42 @@ function formatTime(time?: string): string {
return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}` return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
} }
// Markdown渲染函数返回富文本HTML
function renderMarkdown(text: string): string {
if (!text) return ''
// 转义HTML特殊字符
let html = text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
// 处理粗体(**语法)
html = html.replace(/\*\*([^\*]+)\*\*/g, '<strong>$1</strong>')
// 处理斜体(*语法,但要避免和粗体冲突)
html = html.replace(/(?<!\*)\*([^\*]+)\*(?!\*)/g, '<em>$1</em>')
// 处理行内代码(`语法)
html = html.replace(/`([^`]+)`/g, '<code style="background-color:#f5f5f5;padding:2px 6px;border-radius:3px;font-family:monospace;color:#e53e3e;">$1</code>')
// 处理链接([text](url)语法)
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" style="color:#0055AA;text-decoration:underline;">$1</a>')
// 处理标题(# ## ###等)
html = html.replace(/^### (.+)$/gm, '<div style="font-size:16px;font-weight:600;margin:8px 0 4px;">$1</div>')
html = html.replace(/^## (.+)$/gm, '<div style="font-size:18px;font-weight:600;margin:10px 0 6px;">$1</div>')
html = html.replace(/^# (.+)$/gm, '<div style="font-size:20px;font-weight:700;margin:12px 0 8px;">$1</div>')
// 处理无序列表(- 或 * 开头)
html = html.replace(/^[*-] (.+)$/gm, '<div style="margin-left:16px;">• $1</div>')
// 处理换行
html = html.replace(/\n/g, '<br/>')
return html
}
// 发送消息 // 发送消息
async function sendMessage() { async function sendMessage() {
const text = inputText.value.trim() const text = inputText.value.trim()

View File

@@ -24,7 +24,7 @@
<text class="room-time">{{ formatTime(room.lastMessageTime) }}</text> <text class="room-time">{{ formatTime(room.lastMessageTime) }}</text>
</view> </view>
<view class="room-footer"> <view class="room-footer">
<text class="last-message">{{ room.lastMessage || '暂无消息' }}</text> <text class="last-message">{{ getPlainTextPreview(room.lastMessage) }}</text>
<view class="unread-badge" v-if="room.unreadCount && room.unreadCount > 0"> <view class="unread-badge" v-if="room.unreadCount && room.unreadCount > 0">
<text class="badge-text">{{ room.unreadCount > 99 ? '99+' : room.unreadCount }}</text> <text class="badge-text">{{ room.unreadCount > 99 ? '99+' : room.unreadCount }}</text>
</view> </view>
@@ -148,6 +148,38 @@ function formatTime(time?: string): string {
return `${date.getMonth() + 1}/${date.getDate()}` return `${date.getMonth() + 1}/${date.getDate()}`
} }
// 去除markdown语法并截取前10个字符
function getPlainTextPreview(text?: string): string {
if (!text) return '暂无消息'
// 去除markdown语法
let plainText = text
// 去除代码块
.replace(/```[\s\S]*?```/g, '[代码]')
// 去除行内代码
.replace(/`([^`]+)`/g, '$1')
// 去除粗体
.replace(/\*\*([^\*]+)\*\*/g, '$1')
// 去除斜体
.replace(/\*([^\*]+)\*/g, '$1')
// 去除链接
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
// 去除标题标记
.replace(/^#{1,6}\s+/gm, '')
// 去除列表标记
.replace(/^[*-]\s+/gm, '')
// 去除多余的空白字符
.replace(/\s+/g, ' ')
.trim()
// 截取前10个字符
if (plainText.length > 10) {
return plainText.substring(0, 10) + '...'
}
return plainText
}
// 获取状态样式类 // 获取状态样式类
function getStatusClass(status?: string): string { function getStatusClass(status?: string): string {
switch (status) { switch (status) {

View File

@@ -71,7 +71,7 @@
<view class="typing-dot"></view> <view class="typing-dot"></view>
<view class="typing-dot"></view> <view class="typing-dot"></view>
</view> </view>
<text class="message-text" v-else>{{item.content}}</text> <rich-text v-else :nodes="renderMarkdown(item.content)" class="message-rich-text"></rich-text>
</view> </view>
</view> </view>
<text class="message-time">{{item.time}}</text> <text class="message-time">{{item.time}}</text>
@@ -503,6 +503,42 @@
await callAIChat(question) await callAIChat(question)
} }
// Markdown渲染函数返回富文本节点
function renderMarkdown(text : string) : string {
if (!text) return ''
// 转义HTML特殊字符
let html = text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
// 处理粗体(**语法)
html = html.replace(/\*\*([^\*]+)\*\*/g, '<strong>$1</strong>')
// 处理斜体(*语法,但要避免和粗体冲突)
html = html.replace(/(?<!\*)\*([^\*]+)\*(?!\*)/g, '<em>$1</em>')
// 处理行内代码(`语法)
html = html.replace(/`([^`]+)`/g, '<code style="background-color:#f5f5f5;padding:2px 6px;border-radius:3px;font-family:monospace;color:#e53e3e;">$1</code>')
// 处理链接([text](url)语法)
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" style="color:#0055AA;text-decoration:underline;">$1</a>')
// 处理标题(# ## ###等)
html = html.replace(/^### (.+)$/gm, '<div style="font-size:16px;font-weight:600;margin:8px 0 4px;">$1</div>')
html = html.replace(/^## (.+)$/gm, '<div style="font-size:18px;font-weight:600;margin:10px 0 6px;">$1</div>')
html = html.replace(/^# (.+)$/gm, '<div style="font-size:20px;font-weight:700;margin:12px 0 8px;">$1</div>')
// 处理无序列表(- 或 * 开头)
html = html.replace(/^[*-] (.+)$/gm, '<div style="margin-left:16px;">• $1</div>')
// 处理换行
html = html.replace(/\n/g, '<br/>')
return html
}
// 显示上传选项 // 显示上传选项
function showUploadOptions() { function showUploadOptions() {
uni.showActionSheet({ uni.showActionSheet({

View File

@@ -2,7 +2,7 @@
"libVersion": "3.12.1", "libVersion": "3.12.1",
"projectname": "workcase_wechat", "projectname": "workcase_wechat",
"setting": { "setting": {
"urlCheck": true, "urlCheck": false,
"coverView": true, "coverView": true,
"lazyloadPlaceholderEnable": false, "lazyloadPlaceholderEnable": false,
"skylineRenderEnable": false, "skylineRenderEnable": false,