| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359 |
- <template>
- <view class="deepseek-chat-container">
- <scroll-view class="chat-messages" scroll-y :scroll-into-view="scrollIntoView" v-show="pestMessage">
- <view class="message-content">
- <view class="message-input" style="padding:0">{{ pestMessage }}</view>
- <view v-if="isloading" class="loading-hint">AI 正在分析中…</view>
- <rich-text class="message-text markdown-output" :nodes="outputHtml"></rich-text>
- <view class="message-input footer">内容由大模型生成,仅供参考,相关风险需自行承担。</view>
- </view>
- <view id="chatBottomAnchor"></view>
- </scroll-view>
- <view v-if="!pestMessage" style="height:100%">
-
- </view>
- </view>
- </template>
- <script>
- import { createSSEConnection } from './sse.js';
- import { renderMarkdown } from './markdownRenderer.js';
- export default {
- name: 'DeepSeekChat',
- props: {
- pestMessage: {
- type: String,
- default: ''
- },
- },
- data() {
- return {
- text: '',
- sseConnection: null,
- result: '',
- thinking: false,
- activeNames: ['1'],
- thinkHtml: '',
- isloading: true,
- mdParser: null,
- mdRawText: '', // 累积的原始 markdown 文本
- outputHtml: '', // renderMarkdown 渲染后的 HTML,交给 rich-text 展示
- scrollIntoView: '', // scroll-view 滚动到底部锚点用
- };
- },
- watch: {
- text: {
- handler() {
- this.$nextTick(() => {
- this.scrollToBottom();
- });
- },
- deep: true
- },
- pestMessage(newVal) {
- if (newVal) {
- this.connectSSE(newVal);
- } else {
- this.isloading = false;
- }
- }
- },
- methods: {
- initParser() {
- this.mdRawText = '';
- this.outputHtml = '';
- },
- connectSSE(message) {
- this.initParser();
- this.text = '';
- this.thinkHtml = '';
- let thinkContent = '';
- let isCollecting = false;
- const sseUrl = `https://ai.nyzhwlw.com/analysis?message=${message}`;
- this.sseConnection = createSSEConnection(sseUrl);
- this.sseConnection.onMessage((data) => {
- this.isloading = false;
- if (data === 'yfkj@2025' || data === '[DONE]') {
- return;
- }
- // 处理 <think>...</think> 思考块:模板里不展示思考内容,
- // 且原代码用到的 mdParser 未初始化(为 null)会抛错,导致 isCollecting 一直为 true、
- // 后续正文也再不会渲染。这里整体丢弃思考块,只保留 </think> 之后可能跟着的正文。
- if (isCollecting || data.indexOf('<think>') > -1) {
- if (data.indexOf('<think>') > -1) {
- isCollecting = true;
- this.thinking = true;
- }
- thinkContent += data;
- const endIdx = thinkContent.indexOf('</think>');
- if (endIdx === -1) {
- return; // 仍在思考块内,等待结束标记
- }
- // 思考块结束,取出其后的正文继续走正常渲染
- data = thinkContent.slice(endIdx + '</think>'.length);
- thinkContent = '';
- isCollecting = false;
- this.thinking = false;
- if (!data) {
- return;
- }
- }
- // 累积正文并渲染为 markdown,用 rich-text 展示
- this.mdRawText += data;
- this.outputHtml = renderMarkdown(this.mdRawText);
- this.scrollToBottom();
- });
- this.sseConnection.onError((error) => {
- console.error('SSE Error:', error);
- this.disconnectSSE();
- });
- },
- collapseHtml() {
- let result = '';
- return result;
- },
- async renderHtml(result) {
- this.text = result;
- // 触发浏览器重绘
- await new Promise((resolve) => setTimeout(resolve, 20));
- },
- disconnectSSE() {
- if (this.sseConnection) {
- this.sseConnection.close();
- this.sseConnection = null;
- this.isloading = false;
- }
- },
- formatTime(date) {
- return date.toLocaleTimeString([], {
- hour: '2-digit',
- minute: '2-digit'
- });
- },
- scrollToBottom() {
- // 小程序没有 DOM,不能用 $refs.scrollTop;改用 scroll-view 的 scroll-into-view 滚到底部锚点
- this.scrollIntoView = '';
- this.$nextTick(() => {
- this.scrollIntoView = 'chatBottomAnchor';
- });
- }
- },
- mounted() {
- this.scrollToBottom();
- if (this.pestMessage) {
- this.connectSSE(this.pestMessage);
- } else {
- this.isloading = false;
- }
- },
- beforeDestroy() {
- // 组件卸载时断开连接(uni-app 默认 Vue2,用 beforeDestroy)
- this.disconnectSSE();
- }
- };
- </script>
- <style scoped lang="less">
- .el-collapse {
- background: #f6f6f6;
- color: #a4aab2;
- border-radius: 8rpx;
- }
- ::v-deep .el-collapse-item__header {
- padding: 0 16rpx;
- height: 72rpx;
- background: #f6f6f6 !important;
- border-radius: 8rpx;
- }
- ::v-deep .el-collapse-item__wrap {
- background: #f6f6f6 !important;
- color: #a4aab2 !important;
- border-radius: 8rpx;
- }
- ::v-deep .el-collapse-item__content {
- color: #a4aab2 !important;
- border-radius: 8rpx;
- }
- .deep-thinking {
- padding: 0 52rpx;
- border-radius: 8rpx;
- }
- .deepseek-chat-container {
- position: relative;
- display: flex;
- flex-direction: column;
- height: calc(100vh - 640rpx);
- // max-width: 1086px;
- margin: 0 auto;
- border-radius: 8rpx;
- // box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
- overflow: hidden;
- font-family: 'Segoe UI', Arial, sans-serif;
- // background-color: #f9f9f9;
- }
- .chat-messages {
- flex: 1;
- overflow-y: auto;
- height: 100%;
- // background-color: #ffffff;
- }
- .message {
- display: flex;
- margin-bottom: 24rpx;
- }
- .message-content {
- max-width: 100%;
- }
- .loading-hint {
- padding: 16rpx;
- color: #999;
- font-size: 26rpx;
- }
- .message-text {
- padding: 16rpx;
- border-radius: 18rpx;
- line-height: 1.4;
- word-wrap: break-word;
- padding-bottom: 60rpx;
- }
- .message.bot .message-text {
- background-color: #fff;
- border-top-left-radius: 8rpx;
- color: #5c5c5c;
- }
- .message.user .message-text {
- color: white;
- border-top-right-radius: 8rpx;
- }
- .message-time {
- font-size: 22rpx;
- color: #777;
- margin-top: 8rpx;
- text-align: right;
- }
- .message.user .message-time {
- text-align: left;
- }
- .typing-indicator {
- display: flex;
- padding: 12rpx 30rpx;
- background-color: #f6f6f6;
- border-radius: 18rpx;
- border-top-left-radius: 8rpx;
- }
- .typing-indicator span {
- width: 16rpx;
- height: 16rpx;
- margin: 0 2rpx;
- background-color: #999;
- border-radius: 50%;
- display: inline-block;
- animation: bounce 1.4s infinite ease-in-out both;
- }
- .typing-indicator span:nth-child(1) {
- animation-delay: -0.32s;
- }
- .typing-indicator span:nth-child(2) {
- animation-delay: -0.16s;
- }
- @keyframes bounce {
- 0%,
- 80%,
- 100% {
- transform: scale(0);
- }
- 40% {
- transform: scale(1);
- }
- }
- /* Markdown样式 - 使用深度选择器 */
- ::v-deep .markdown-output {
- /* 标题样式 */
- h1, h2, h3, h4, h5, h6 {
- margin-top: 32rpx;
- margin-bottom: 16rpx;
- font-weight: 600;
- line-height: 1.25;
- }
- h1 { font-size: 2em; }
- h2 { font-size: 1.5em; }
- h3 { font-size: 1.25em; }
- h4 { font-size: 1em; }
- h5 { font-size: 0.875em; }
- h6 { font-size: 0.85em; }
- /* 列表样式 */
- ul, ol {
- padding-left: 2em;
- margin: 16rpx 0;
- list-style-type: disc;
- }
- ol {
- list-style-type: decimal;
- }
- li {
- margin: 8rpx 0;
- line-height: 1.5;
- }
- /* 粗体斜体 */
- strong {
- font-weight: 600;
- }
- em {
- font-style: italic;
- }
- /* 代码块 */
- pre {
- background-color: #f6f8fa;
- padding: 32rex;
- border-radius: 12rpx;
- overflow-x: auto;
- margin: 16rpx 0;
- }
- code {
- background-color: #f6f8fa;
- padding: 4rpx 8rpx;
- border-radius: 6rpx;
- font-family: monospace;
- }
- /* 段落 */
- p {
- margin: 16rpx 0;
- line-height: 1.5;
- }
- }
- .message-input {
- padding: 20rpx 30rpx;
- border-radius: 8rpx;
- background-color: #f6f6f6;
- &.footer {
- position: fixed;
- bottom: 20rpx;
- left: 0;
- right: 0rpx;
- margin-top: 40rpx;
- font-size: 24rpx;
- color: #999;
- text-align: center;
- }
- }
- </style>
|