浏览代码

Merge branch 'project/demo' into develop

将(演示)项目相关配置合并
cc12458 5 月之前
父节点
当前提交
40b0417155

+ 40 - 5
package-lock.json

@@ -1792,6 +1792,15 @@
       "integrity": "sha1-trQKdiVCnSvXwigd26YB7QXcfxo=",
       "dev": true
     },
+    "@vxe-ui/core": {
+      "version": "3.2.11",
+      "resolved": "https://registry.npmjs.org/@vxe-ui/core/-/core-3.2.11.tgz",
+      "integrity": "sha512-P7nQFBIs070CWJjlqbs2Cu5tSxMKhacSbkd7XUZGPajjetlCpqsqg9cX4OaWMzTcSmNlh/+ZMTrqLo+ATxkMhQ==",
+      "requires": {
+        "dom-zindex": "^1.0.6",
+        "xe-utils": "^3.7.8"
+      }
+    },
     "@webassemblyjs/ast": {
       "version": "1.9.0",
       "resolved": "https://registry.npmmirror.com/@webassemblyjs/ast/download/@webassemblyjs/ast-1.9.0.tgz?cache=0&sync_timestamp=1610041305745&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40webassemblyjs%2Fast%2Fdownload%2F%40webassemblyjs%2Fast-1.9.0.tgz",
@@ -2318,11 +2327,18 @@
       "dev": true
     },
     "axios": {
-      "version": "0.21.1",
-      "resolved": "https://r.cnpmjs.org/axios/download/axios-0.21.1.tgz",
-      "integrity": "sha1-IlY0gZYvTWvemnbVFu8OXTwJsrg=",
+      "version": "0.22.0",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-0.22.0.tgz",
+      "integrity": "sha512-Z0U3uhqQeg1oNcihswf4ZD57O3NrR1+ZXhxaROaWpDmsDTx7T2HNBV2ulBtie2hwJptu8UvgnJoK+BIqdzh/1w==",
       "requires": {
-        "follow-redirects": "^1.10.0"
+        "follow-redirects": "^1.14.4"
+      },
+      "dependencies": {
+        "follow-redirects": {
+          "version": "1.15.11",
+          "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+          "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="
+        }
       }
     },
     "babel-helper-vue-jsx-merge-props": {
@@ -4408,6 +4424,11 @@
         }
       }
     },
+    "dom-zindex": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/dom-zindex/-/dom-zindex-1.0.6.tgz",
+      "integrity": "sha512-FKWIhiU96bi3xpP9ewRMgANsoVmMUBnMnmpCT6dPMZOunVYJQmJhSRruoI0XSPoHeIif3kyEuiHbFrOJwEJaEA=="
+    },
     "domain-browser": {
       "version": "1.2.0",
       "resolved": "https://registry.npmmirror.com/domain-browser/download/domain-browser-1.2.0.tgz",
@@ -5186,7 +5207,8 @@
     "follow-redirects": {
       "version": "1.14.1",
       "resolved": "https://registry.npmmirror.com/follow-redirects/download/follow-redirects-1.14.1.tgz",
-      "integrity": "sha1-2RFN7Qoc/dM04WTmZirQK/2R/0M="
+      "integrity": "sha1-2RFN7Qoc/dM04WTmZirQK/2R/0M=",
+      "dev": true
     },
     "for-in": {
       "version": "1.0.2",
@@ -11181,6 +11203,14 @@
       "resolved": "https://registry.npmmirror.com/vuex/download/vuex-3.6.2.tgz",
       "integrity": "sha1-I2vAhqhww655lG8QfxbeWdWJXnE="
     },
+    "vxe-pc-ui": {
+      "version": "3.8.20",
+      "resolved": "https://registry.npmjs.org/vxe-pc-ui/-/vxe-pc-ui-3.8.20.tgz",
+      "integrity": "sha512-7U7mOf76DJSUNvDuMaDdMKmnwHfyrKjj38BQ5nbW2HPiMnPCX6zwdUDn6T4GCKh1IteZetoJY9ytn0U880+vpA==",
+      "requires": {
+        "@vxe-ui/core": "^3.2.11"
+      }
+    },
     "watchpack": {
       "version": "1.7.5",
       "resolved": "https://registry.npmmirror.com/watchpack/download/watchpack-1.7.5.tgz",
@@ -11948,6 +11978,11 @@
         "async-limiter": "~1.0.0"
       }
     },
+    "xe-utils": {
+      "version": "3.7.8",
+      "resolved": "https://registry.npmjs.org/xe-utils/-/xe-utils-3.7.8.tgz",
+      "integrity": "sha512-V/k6B/ASYir6yLYhp62DnM17po9u1N9mou/rn4if5WoFCsAO49JpCiVpkDpwCv4zxGfWmhWgzmz4FytWF+pDVw=="
+    },
     "xtend": {
       "version": "4.0.2",
       "resolved": "https://registry.npmmirror.com/xtend/download/xtend-4.0.2.tgz",

+ 2 - 1
package.json

@@ -18,7 +18,8 @@
     "vue-print-nb": "^1.7.4",
     "vue-router": "^3.2.0",
     "vuescroll": "^4.17.3",
-    "vuex": "^3.4.0"
+    "vuex": "^3.4.0",
+    "vxe-pc-ui": "^3.8.20"
   },
   "devDependencies": {
     "@vue/cli-plugin-babel": "^4.5.0",

+ 258 - 0
public/chat/ai.html

@@ -0,0 +1,258 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"/>
+    <title>灵兰助医</title>
+    <link rel="stylesheet" href="./css/bootstrap.min.css">
+    <link rel="stylesheet" href="./css/all.min.css">
+    <link rel="stylesheet" href="./css/github.min.css">
+    <link rel="stylesheet" href="./css/styles.css">
+    <link rel="stylesheet" href="./css/chat.css">
+</head>
+<body>
+<div class="container-fluid not-bg vh-100 d-flex flex-column">
+    <!-- 顶部标题栏 -->
+    <header class="p-2" style="min-height: 44px; text-align: center; font-size: 18px; font-weight: 700;">
+        <!--中医AI助手-->
+    </header>
+
+    <!-- 聊天主区域 -->
+    <main class="flex-grow-1 d-flex flex-column overflow-hidden" id="chat-container">
+        <div class="flex-grow-1 overflow-auto px-2 py-3" id="chat-messages">
+            <!-- 初始系统消息 -->
+            <div class="message system-message mb-3">
+                <div class="avatar">
+                    <img src="./image/robot.png" alt="">
+                </div>
+                <div class="content">
+                    <div class="card">
+                        <div class="card-body">
+                            <p>您好!我是您的中医AI助手,可以帮助您:</p>
+                            <ul>
+                                <li>分析症状</li>
+                                <li>辨别证型</li>
+                                <li>推荐治疗方案</li>
+                            </ul>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <!-- 输入区域 -->
+        <div class="px-3 py-2 border-top">
+            <div class="input-group input-group-sm">
+                <textarea
+                        id="message-input"
+                        class="form-control"
+                        placeholder="请输入..."
+                        rows="1"
+                        enterkeyhint="send"
+                ></textarea>
+                <button id="send-btn" class="btn btn-primary btn-sm">
+                    <i class="fas fa-paper-plane"></i>
+                </button>
+            </div>
+        </div>
+    </main>
+</div>
+
+<!-- 隐藏字段 -->
+<input type="hidden" id="session-id" value="{{ session_id }}">
+
+<!-- JavaScript 依赖 -->
+<script src="./js/bootstrap.bundle.min.js"></script>
+<script src="./js/marked.min.js"></script>
+<script src="./js/highlight.min.js"></script>
+<script>document.addEventListener('gesturestart', function (event) {
+    event.preventDefault();
+}, false);</script>
+
+<!-- 主应用脚本 -->
+<script>
+    const searchParams = new URLSearchParams(window.location.search);
+    const host = `https://dev.hzliuzhi.com:62006`;
+    document.addEventListener('DOMContentLoaded', function () {
+        if (searchParams.has('hide_title')) document.querySelector('.container-fluid header').style.display = 'none';
+        else document.querySelector('.container-fluid header').innerHTML = document.title;
+        // 获取DOM元素
+        const chatMessages = document.getElementById('chat-messages');
+        const messageInput = document.getElementById('message-input');
+        const sendBtn = document.getElementById('send-btn');
+        let sessionId = document.getElementById('session-id').value;
+        if (sessionId.replace(/\s/g, '') === `{{session_id}}`) document.querySelector(`#session-id`).value = sessionId = searchParams.get('session_id');
+
+        let eventSource = null;
+
+        // 初始化Marked和Highlight.js
+        marked.setOptions({
+            breaks: true,
+            highlight: function (code, language) {
+                const validLanguage = hljs.getLanguage(language) ? language : 'plaintext';
+                return hljs.highlight(validLanguage, code).value;
+            }
+        });
+
+        // 发送消息
+        function sendMessage() {
+            const message = messageInput.value.trim();
+            if (!message) return;
+
+            // 添加用户消息
+            addMessage('user', message);
+            messageInput.value = '';
+
+            // 添加AI思考状态
+            const aiMessageElement = addMessage('assistant', '', true);
+
+            // 关闭之前的连接
+            if (eventSource) {
+                eventSource.close();
+                eventSource = null;
+            }
+
+            let aiResponse = '';
+            let responseElement = null;
+
+            // 使用fetch进行流式请求
+            fetch(`${host}/tcm_chat/chat/tcm`, {
+                method: 'POST',
+                headers: {
+                    'Content-Type': 'application/json',
+                },
+                body: JSON.stringify({
+                    session_id: sessionId,
+                    message: message
+                })
+            })
+                .then(response => {
+                    if (!response.ok) {
+                        throw new Error(`HTTP error! status: ${response.status}`);
+                    }
+
+                    const reader = response.body.getReader();
+                    const decoder = new TextDecoder();
+
+                    function readStream() {
+                        return reader.read().then(({done, value}) => {
+                            if (done) {
+                                return;
+                            }
+                            const chunk = decoder.decode(value);
+                            const lines = chunk.split('\n');
+
+                            for (const line of lines) {
+                                if (line.startsWith('think: ')) { // 思考内容
+                                    try {
+                                        const data = JSON.parse(line.slice(7));
+                                        if (data.content) {
+                                            console.log('流信号 思考内容', data.content)
+                                            // 这里没实现呢,需要你参考openai的实现@xiong
+                                            // 进入思考状态
+                                        }
+                                    } catch (e) {
+                                        console.error('解析响应数据失败:', e);
+                                    }
+                                } else if (line.startsWith('data: ')) { // 返回内容
+                                    try {
+                                        const data = JSON.parse(line.slice(6));
+                                        if (data.error) {
+                                            // 处理错误
+                                            if (!responseElement) {
+                                                aiMessageElement.innerHTML = `<div class="alert alert-danger">${data.error}</div>`;
+                                            }
+                                            return;
+                                        }
+
+                                        if (data.content) {
+                                            console.log('流信号', data.content)
+                                            aiResponse += data.content;
+                                            if (!responseElement) {
+                                                // 移除思考状态
+                                                aiMessageElement.innerHTML = '';
+                                                responseElement = aiMessageElement;
+                                            }
+                                            updateMessageContent(responseElement, aiResponse);
+                                        }
+                                    } catch (e) {
+                                        console.error('解析响应数据失败:', e);
+                                    }
+                                }
+                            }
+                            // 继续读取流
+                            return readStream();
+                        });
+                    }
+
+                    return readStream();
+                })
+                .catch(error => {
+                    console.error('请求失败:', error);
+                    if (!responseElement) {
+                        aiMessageElement.innerHTML = '<div class="alert alert-danger">连接错误,请重试</div>';
+                    }
+                });
+        }
+
+        // 添加消息到聊天界面
+        function addMessage(role, content, isThinking = false) {
+            const messageDiv = document.createElement('div');
+            messageDiv.className = `message ${role}-message mb-3`;
+
+            const avatar = document.createElement('div');
+            avatar.className = 'avatar';
+            avatar.innerHTML = role === 'user' ?
+                '<i class="fas fa-user"></i>' :
+                '<img src="./image/robot.png" alt="">';
+
+            const contentDiv = document.createElement('div');
+            contentDiv.className = 'content';
+
+            if (isThinking) {
+                contentDiv.innerHTML = `
+                        <div class="thinking-container">
+                            <div class="thinking-indicator">
+                                <span></span><span></span><span></span>
+                            </div>
+                            <div class="thinking-text">思考中...</div>
+                        </div>
+                    `;
+            } else {
+                contentDiv.innerHTML = marked.parse(content);
+                hljs.highlightAll();
+            }
+
+            messageDiv.appendChild(avatar);
+            messageDiv.appendChild(contentDiv);
+            chatMessages.appendChild(messageDiv);
+
+            // 滚动到底部
+            // chatMessages.scrollTop = chatMessages.scrollHeight;
+            messageDiv.scrollIntoView({ behavior: 'smooth', block: 'end', inline: 'start' });
+
+            return contentDiv;
+        }
+
+        // 更新消息内容
+        function updateMessageContent(element, content) {
+            element.innerHTML = marked.parse(content);
+            hljs.highlightAll();
+        }
+
+        // 事件监听
+        sendBtn.addEventListener('click', sendMessage);
+
+        messageInput.addEventListener('keypress', function (e) {
+            if (e.key === 'Enter' && !e.shiftKey) {
+                e.preventDefault();
+                sendMessage();
+            }
+        });
+
+        // 初始聚焦输入框
+        messageInput.focus();
+    });
+</script>
+</body>
+</html>

文件差异内容过多而无法显示
+ 5 - 0
public/chat/css/all.min.css


文件差异内容过多而无法显示
+ 4 - 0
public/chat/css/bootstrap.min.css


+ 547 - 0
public/chat/css/chat.css

@@ -0,0 +1,547 @@
+/* 基础样式 */
+:root {
+    --primary-color: #1a73e8;
+    --secondary-color: #f8f9fa;
+    --user-bubble: #7e9bed;
+    --assistant-bubble: #fff;
+    --system-bubble: #fff;
+}
+
+body {
+    background-color: #f5f7fb;
+    color: #202124;
+    font-family: 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+}
+
+/* 聊天消息样式 */
+#chat-messages {
+    padding: 20px;
+    display: flex;
+    flex-direction: column;
+    gap: 16px;
+}
+
+.message {
+    display: flex;
+    width: calc(100% - 4px);
+    max-width: 90%;
+    align-self: flex-start;
+    padding: 0.5rem 0;
+}
+
+.message.user-message {
+    align-self: flex-end;
+    flex-direction: row-reverse;
+}
+
+.message .avatar {
+    width: 40px;
+    height: 40px;
+    border-radius: 50%;
+    background-color: #e8eaed;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    flex-shrink: 0;
+    overflow: hidden;
+
+    > img {
+        width: 100%;
+        height: 100%;
+        object-fit: cover;
+    }
+}
+
+.message.user-message .avatar {
+    background-color: var(--primary-color);
+    color: white;
+    margin-left: 12px;
+}
+.message.robot-message .avatar {
+    margin-right: 12px;
+}
+
+.message .content {
+    padding: 12px 16px;
+    border-radius: 12px;
+    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
+    max-width: calc(100% - 64px);
+    overflow-wrap: break-word;
+}
+
+.user-message .content {
+    color: #fff;
+    background-color: var(--user-bubble);
+    border-top-right-radius: 0;
+}
+
+.user-message .content p {
+    margin-top: 0;
+    margin-bottom: 0;
+}
+
+.assistant-message .avatar {
+    margin-right: 12px;
+}
+
+.assistant-message .content {
+    background-color: var(--assistant-bubble);
+    border-top-left-radius: 0;
+}
+
+.assistant-message .content p {
+    margin-top: 0;
+    margin-bottom: 0;
+}
+
+.system-message .avatar {
+    margin-right: 12px;
+}
+
+.system-message .content {
+    background-color: var(--system-bubble);
+    border-radius: 8px;
+    max-width: 100%;
+}
+
+.system-message .content p {
+    margin-top: 0;
+    margin-bottom: 0;
+}
+
+/* Markdown 内容样式 */
+.markdown-body {
+    font-size: 16px;
+    line-height: 1.6;
+}
+
+.markdown-body h1, .markdown-body h2, .markdown-body h3 {
+    margin-top: 1.5em;
+    margin-bottom: 0.5em;
+    font-weight: 600;
+}
+
+.markdown-body p {
+    margin-bottom: 1em;
+}
+
+.markdown-body ul, .markdown-body ol {
+    margin-bottom: 1em;
+    padding-left: 2em;
+}
+
+.markdown-body code {
+    background-color: rgba(175, 184, 193, 0.2);
+    border-radius: 6px;
+    padding: 0.2em 0.4em;
+    font-size: 85%;
+}
+
+.markdown-body pre {
+    background-color: #f6f8fa;
+    border-radius: 6px;
+    padding: 16px;
+    overflow: auto;
+    margin: 1em 0;
+}
+
+/* 思考指示器 */
+.thinking-container {
+    display: flex;
+    align-items: center;
+    padding: 8px;
+}
+
+.thinking-indicator {
+    display: flex;
+    margin-right: 10px;
+}
+
+.thinking-indicator span {
+    width: 8px;
+    height: 8px;
+    background-color: #5f6368;
+    border-radius: 50%;
+    display: inline-block;
+    margin: 0 3px;
+    animation: bounce 1.4s infinite ease-in-out both;
+}
+
+.thinking-indicator span:nth-child(1) {
+    animation-delay: -0.32s;
+}
+
+.thinking-indicator span:nth-child(2) {
+    animation-delay: -0.16s;
+}
+
+@keyframes bounce {
+    0%, 80%, 100% {
+        transform: scale(0);
+    }
+    40% {
+        transform: scale(1.0);
+    }
+}
+
+.thinking-text {
+    color: #5f6368;
+    font-size: 14px;
+}
+
+/* 结果卡片 */
+.result-card {
+    border-top: 1px solid #e0e0e0;
+    background: white;
+    border-radius: 8px;
+    overflow: hidden;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+    margin-top: 1rem;
+}
+
+.result-card .card-header {
+    background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
+    border-bottom: 1px solid #dee2e6;
+    padding: 8px 16px;
+}
+
+.result-card .card-header h6 {
+    color: #495057;
+    font-weight: 600;
+    font-size: 0.9rem;
+}
+
+.result-card .nav-tabs {
+    border-bottom: 1px solid #dee2e6;
+    background-color: #f8f9fa;
+}
+
+.result-card .nav-tabs .nav-link {
+    border: none;
+    color: #6c757d;
+    font-weight: 500;
+    padding: 8px 16px;
+    border-radius: 0;
+    transition: all 0.3s ease;
+    font-size: 0.85rem;
+}
+
+.result-card .nav-tabs .nav-link:hover {
+    color: var(--primary-color);
+    background-color: #e9ecef;
+}
+
+.result-card .nav-tabs .nav-link.active {
+    color: var(--primary-color);
+    background-color: white;
+    border-bottom: 2px solid var(--primary-color);
+}
+
+.result-card .tab-content {
+    background-color: white;
+}
+
+.result-card .tab-pane {
+    padding: 12px;
+}
+
+/* 科室和医生卡片样式 */
+.department-results .card,
+.doctor-results .card {
+    border: 1px solid #e9ecef;
+    border-radius: 8px;
+    transition: all 0.3s ease;
+    margin-bottom: 12px;
+}
+
+.department-results .card:hover,
+.doctor-results .card:hover {
+    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+    transform: translateY(-2px);
+}
+
+.department-results .card-title,
+.doctor-results .card-title {
+    color: #212529;
+    font-weight: 600;
+    margin-bottom: 8px;
+    font-size: 1rem;
+}
+
+.department-results .card-text,
+.doctor-results .card-text {
+    color: #6c757d;
+    line-height: 1.4;
+    margin-bottom: 12px;
+    font-size: 0.875rem;
+}
+
+/* 科室卡片特殊样式 */
+.dept-card .card-body {
+    padding: 0.75rem !important;
+}
+
+.dept-card .avatar-sm {
+    width: 32px !important;
+    height: 32px !important;
+    background: linear-gradient(135deg, #007bff 0%, #0056b3 100%) !important;
+}
+
+.dept-card .avatar-sm i {
+    font-size: 16px;
+    color: white !important;
+}
+
+/* 医生卡片特殊样式 */
+.doctor-card .card-body {
+    padding: 0.75rem !important;
+}
+
+.doctor-card .avatar-md {
+    width: 45px !important;
+    height: 45px !important;
+    background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%) !important;
+    border: 2px solid #e3f2fd;
+}
+
+.doctor-card .avatar-md i {
+    color: var(--primary-color) !important;
+    font-size: 18px;
+}
+
+/* 徽章样式优化 */
+.badge {
+    font-weight: 500;
+    padding: 4px 8px;
+    border-radius: 16px;
+    font-size: 0.75rem;
+}
+
+.badge.bg-primary {
+    background-color: var(--primary-color) !important;
+}
+
+.badge.bg-light {
+    background-color: #f8f9fa !important;
+    color: #495057 !important;
+    border: 1px solid #dee2e6;
+}
+
+/* 按钮样式优化 */
+.btn-sm {
+    padding: 3px 5px;
+    font-size: 0.875rem;
+    border-radius: 10px;
+}
+
+.btn-outline-primary {
+    border-color: var(--primary-color);
+    color: var(--primary-color);
+    transition: all 0.3s ease;
+    font-size: 0.8rem;
+    padding: 4px 12px;
+}
+
+.btn-outline-primary:hover {
+    background-color: var(--primary-color);
+    border-color: var(--primary-color);
+    transform: translateY(-1px);
+}
+
+.btn-outline-secondary {
+    border-color: #6c757d;
+    color: #6c757d;
+    transition: all 0.3s ease;
+}
+
+.btn-outline-secondary:hover {
+    background-color: #6c757d;
+    border-color: #6c757d;
+    color: white;
+}
+
+/* 结果卡片容器 */
+#result-cards-container {
+    background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
+    border-radius: 12px;
+    padding: 20px;
+    margin: 20px;
+    border: 1px solid #dee2e6;
+}
+
+/* 加载状态样式 */
+.spinner-border.text-primary {
+    color: var(--primary-color) !important;
+}
+
+/* 响应式调整 */
+@media (max-width: 768px) {
+    .message {
+        max-width: 95%;
+    }
+
+    .message .avatar {
+    }
+
+    .message .content {
+        padding: 8px 12px;
+        max-width: calc(100% - 4px);
+    }
+
+    .result-card {
+        margin-top: 1rem;
+        border-radius: 6px;
+    }
+
+    .result-card .card-header {
+        padding: 6px 12px;
+    }
+
+    .result-card .card-header h6 {
+        font-size: 0.85rem;
+    }
+
+    .result-card .nav-tabs .nav-link {
+        padding: 6px 12px;
+        font-size: 0.8rem;
+    }
+
+    .result-card .tab-pane {
+        padding: 8px;
+    }
+
+    #result-cards-container {
+        margin: 15px;
+        padding: 15px;
+    }
+
+    .dept-card .card-body,
+    .doctor-card .card-body {
+        padding: 0.5rem !important;
+    }
+
+    .dept-card .avatar-sm,
+    .doctor-card .avatar-md {
+        width: 35px !important;
+        height: 35px !important;
+    }
+
+    .dept-card .avatar-sm i {
+        font-size: 14px;
+    }
+
+    .doctor-card .avatar-md i {
+        font-size: 16px;
+    }
+
+    .badge {
+        font-size: 0.7rem;
+        padding: 3px 6px;
+    }
+
+    .btn-sm {
+        padding: 0.25rem 0.5rem;
+        font-size: 0.8rem;
+    }
+
+    .card-title {
+        font-size: 0.9rem !important;
+    }
+
+    .card-text {
+        font-size: 0.8rem !important;
+        line-height: 1.4;
+    }
+}
+
+/* 医生卡片交互样式 */
+.doctor-card {
+    cursor: pointer;
+    transition: all 0.3s ease;
+    border: 2px solid transparent;
+    border-radius: 8px;
+}
+
+.doctor-card:hover {
+    border-color: var(--primary-color);
+    box-shadow: 0 6px 16px rgba(26, 115, 232, 0.15);
+    transform: translateY(-3px);
+}
+
+.doctor-card:active {
+    transform: translateY(-1px);
+}
+
+/* 模态框样式 */
+.modal-content {
+    border-radius: 12px;
+    border: none;
+    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
+}
+
+.modal-header {
+    background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
+    border-bottom: 1px solid #dee2e6;
+    border-radius: 12px 12px 0 0;
+}
+
+.modal-title {
+    color: var(--primary-color);
+    font-weight: 600;
+}
+
+.avatar-lg {
+    background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%) !important;
+    border: 3px solid #e3f2fd;
+}
+
+.avatar-lg i {
+    color: var(--primary-color) !important;
+}
+
+/* 标签页切换动画 */
+.tab-pane.fade {
+    transition: opacity 0.3s ease;
+}
+
+.tab-pane.fade.show {
+    opacity: 1;
+}
+
+/* 加载状态优化 */
+.spinner-border {
+    width: 2rem;
+    height: 2rem;
+}
+
+/* 医生卡片内容布局优化 */
+.doctor-card .card-body {
+    padding: 1rem;
+}
+
+.doctor-card .card-title {
+    font-size: 1.1rem;
+    font-weight: 600;
+    color: #212529;
+    margin-bottom: 0.5rem;
+}
+
+.doctor-card .card-text {
+    font-size: 0.9rem;
+    line-height: 1.4;
+}
+
+.doctor-card .badge {
+    font-size: 0.75rem;
+    padding: 0.4rem 0.6rem;
+}
+
+/* 预约按钮样式 */
+.btn-outline-primary:focus {
+    box-shadow: 0 0 0 0.2rem rgba(26, 115, 232, 0.25);
+}
+
+/* 结果卡片容器阴影效果 */
+#result-cards-container {
+    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
+}

+ 10 - 0
public/chat/css/github.min.css

@@ -0,0 +1,10 @@
+pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
+  Theme: GitHub
+  Description: Light theme as seen on github.com
+  Author: github.com
+  Maintainer: @Hirse
+  Updated: 2021-05-15
+
+  Outdated base version: https://github.com/primer/github-syntax-light
+  Current colors taken from GitHub's CSS
+*/.hljs{color:#24292e;background:#fff}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#d73a49}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#6f42c1}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#005cc5}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#032f62}.hljs-built_in,.hljs-symbol{color:#e36209}.hljs-code,.hljs-comment,.hljs-formula{color:#6a737d}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#22863a}.hljs-subst{color:#24292e}.hljs-section{color:#005cc5;font-weight:700}.hljs-bullet{color:#735c0f}.hljs-emphasis{color:#24292e;font-style:italic}.hljs-strong{color:#24292e;font-weight:700}.hljs-addition{color:#22863a;background-color:#f0fff4}.hljs-deletion{color:#b31d28;background-color:#ffeef0}

+ 87 - 0
public/chat/css/styles.css

@@ -0,0 +1,87 @@
+/* 整体布局 */
+.container-fluid {
+    max-width: 1200px;
+    margin: 0 auto;
+    padding: 0 0 1rem  0;
+    background-color: transparent;
+}
+.container-fluid:not(.not-bg) {
+    background-image: url("../image/bg_dialog@2x.png");
+    background-size: 100% 100%;
+    background-repeat: no-repeat;
+    background-color: #f5f6f7;
+    background-position-y: -24px;
+}
+
+/* 输入区域 */
+#message-input {
+    resize: none;
+    border: 1px solid #dadce0;
+    border-radius: 0.75rem;
+    padding: 0.5rem 0.75rem;
+    background: white;
+    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
+    line-height: 2; /* 标准行高 */
+    min-height: calc(1.5em + 1rem + 2px); /* 自动高度计算 */
+    display: flex;
+    align-items: center; /* 垂直居中 */
+
+}
+
+#message-input:focus {
+    outline: none;
+    border-color: var(--primary-color);
+    box-shadow: 0 0 0 2px rgba(26, 115, 232, 0.2);
+}
+
+#send-btn {
+    border-radius: 0.75rem;
+    margin-left: 0.5rem;
+    padding: 0.5rem 1.2rem;
+}
+
+/* 卡片样式 */
+.card {
+    border: none;
+    border-radius: 12px;
+    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+}
+
+.card-header {
+    font-weight: 600;
+    border-bottom: 1px solid rgba(0, 0, 0, 0.05);
+}
+
+/* 标签页样式 */
+.nav-tabs .nav-link {
+    border: none;
+    border-radius: 0;
+    padding: 12px 20px;
+    color: #5f6368;
+    font-weight: 500;
+}
+
+.nav-tabs .nav-link.active {
+    color: var(--primary-color);
+    border-bottom: 3px solid var(--primary-color);
+    background: transparent;
+}
+
+/* 滚动条美化 */
+::-webkit-scrollbar {
+    width: 8px;
+}
+
+::-webkit-scrollbar-track {
+    background: #f1f1f1;
+    border-radius: 4px;
+}
+
+::-webkit-scrollbar-thumb {
+    background: #c1c1c1;
+    border-radius: 4px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+    background: #a8a8a8;
+}

+ 357 - 0
public/chat/e.html

@@ -0,0 +1,357 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"/>
+    <title>萧e生大模型</title>
+    <link rel="stylesheet" href="./css/bootstrap.min.css">
+    <link rel="stylesheet" href="./css/all.min.css">
+    <link rel="stylesheet" href="./css/github.min.css">
+    <link rel="stylesheet" href="./css/styles.css">
+    <link rel="stylesheet" href="./css/chat.css">
+</head>
+<body>
+<div class="container-fluid not-bg vh-100 d-flex flex-column">
+    <!-- 顶部标题栏 -->
+    <header class="p-2" style="min-height: 44px; text-align: center; font-size: 18px; font-weight: 700;">
+        <!--中医AI助手-->
+    </header>
+
+    <!-- 聊天主区域 -->
+    <main class="flex-grow-1 d-flex flex-column overflow-hidden" id="chat-container">
+        <div class="flex-grow-1 overflow-auto px-2 py-3" id="chat-messages"></div>
+
+        <!-- 输入区域 -->
+        <div class="px-3 py-2 border-top">
+            <div class="input-group input-group-sm">
+                <textarea
+                        id="message-input"
+                        class="form-control"
+                        placeholder="请输入..."
+                        rows="1"
+                        enterkeyhint="send"
+                ></textarea>
+                <button id="send-btn" class="btn btn-primary btn-sm">
+                    <i class="fas fa-paper-plane"></i>
+                </button>
+            </div>
+        </div>
+    </main>
+</div>
+
+<!-- 隐藏字段 -->
+<input type="hidden" id="session-id" value="{{ session_id }}">
+
+<!-- JavaScript 依赖 -->
+<script src="./js/bootstrap.bundle.min.js"></script>
+<script src="./js/marked.min.js"></script>
+<script src="./js/highlight.min.js"></script>
+<script>document.addEventListener('gesturestart', function (event) {
+    event.preventDefault();
+}, false);</script>
+
+<!-- 主应用脚本 -->
+<script>
+const u1 = `
+**杨红,男,38岁**
+<br>
+1. **主诉**:脘腹痞胀1年余,去年10月以来多次出血(黑便)
+1. **现病史**:脘腹痞胀1年余,去年10月以来多次出血(黑便),平素饮酒量多,形体丰腴。
+1. **既往史**:有嗜酸粒细胞增多症病史,常服用强的松。
+1. **中医四诊**:脘腹痞胀,右腹隐痛,大便溏薄,易出汗,舌质紫暗,苔薄白,脉沉细。
+1. **辅助检查**:胃镜示“慢性浅表性胃炎伴糜烂”。
+`
+const r1 = `
+根据您描述的情况,患者目前最可能的原因是与消化系统相关,肝脾不调,气血瘀滞导致便血。以下是具体分析:
+
+<br>
+
+### 辨证分析:
+1. 患者形体肥胖属湿盛体质,且平素酒,更易蕴生湿热,湿热灼伤络脉,血溢脉外则为瘀血。
+1. 湿邪壅滞,脾失健运,故大便溏薄。证属肝脾不调,气血瘀滞。
+
+<br>
+
+### 推荐方案:
+治以理气活血,健脾和胃,化瘀通络。常用药:当归、赤芍药、五灵脂、延胡索,另吞服三七粉1~2g
+
+<br>
+
+#### 中药方剂:
+1. **痛泻要方加减** ([国医徐景藩](#)) <a href="javascript:void(0):">转方</a>
+   - 组成:焦白术10g,白芍药15g,炒防风10g,煨木香6g,鸡内金10g,佛手10g,三棱10g,葛根花68,泽泻15g,牡丹皮10g,黄连2g,补骨脂6g,石斛10g,炙甘草3g,焦山楂、神曲各15g,青皮、陈皮各6g。
+   - 煎服方法:每日1剂,水煎服。
+   - **另**:三七粉1g,研末冲服,每日2次。
+   <span></span>
+
+2. **柴胡疏肝散(疏肝理气)** <a href="">转方</a>
+   - 组成:柴胡10g,陈皮12g,川芎10g,香附12g,枳壳10g,芍药12g,甘草6g
+   - 作用:疏肝解郁,行气止痛,缓解右腹隐痛及气滞症状。
+   <span></span>
+
+3. **丹参饮(活血化瘀)** <a href="">转方</a>
+   - 组成:丹参15g,檀香10g(后下),砂仁6g(后下),木香10g
+   - 作用:活血化瘀,改善舌质紫暗及血瘀状态。
+   <span></span>
+
+<br>
+
+#### 适宜技术:
+1. **针刺疗法**:<a href="javascript:void(0):">转方</a>
+   - 【取穴】足三里(健脾和胃)、中脘(调理脾胃)、脾俞(健脾益气)、三阴交(调和肝脾)。
+   - 【操作方法】直刺进针,得气后,每5min行针(平补平泻法)一次,留针 30 min 后出针。
+   - 【疗程说明】针刺、刮痧,可任选一种或两种均选,针刺每周干预2次,连续治疗8次为一疗程;刮痧每周干预1次,连续治疗4次为1疗程。
+   <span></span>
+
+2. **艾灸**:<a href="javascript:void(0):">转方</a>
+   选用神阙、关元、气海,温阳散寒,增强脾阳。距皮肤2~3cm处进行熏烤,根据患者的热感情况调整合适的距离,当患者感觉温热舒适时固定不动,每穴灸10~15分钟,以局部皮肤出现潮红为度。
+
+3. **推拿**:<a href="javascript:void(0):">转方</a>
+   腹部顺时针按摩,配合按压足三里、合谷穴,每日10分钟,促进气血运行。
+
+4. **足浴**:
+   - 银杏叶100g,槐花40g,菊花30g,丹参20g。将以上药物同入药罐中,清水浸泡30分钟,加水2000mL煎汤,煮沸20分钟后去渣取汁,将汁倒入足浴器中,先熏蒸再足浴,每晚1次。20天为一疗程。
+   - 当归50g,牛膝20g,干姜20g,桂枝10g,桑枝10g。将以上药物同入药罐中,清水浸泡30分钟,加水2000mL煎汤,煮沸20分钟后去渣取汁,将汁倒入足浴器中,先熏蒸再足浴,每晚1次。20天为一疗程。
+   <span></span>
+
+5. **茶饮**:
+   - **芎归茶**
+      <span></span>
+      ![芎归茶](./image/c1.png)
+      <span></span>
+      - 【成分】:川芎5克,当归2克。
+      - 【用法】:将川芎、当归放入砂锅中,加适量水煎煮30分钟,去渣取汁。每日1剂,代茶饮用。
+      - 【功效】:活血祛瘀。
+      - 【禁忌】:阴虚火旺的人不宜饮用此茶。
+      <span></span>
+
+   - **通脉花果茶**
+      <span></span>
+      ![通脉花果茶](./image/c2.png)
+      <span></span>
+      - 【原料】:山楂15g,玫瑰花10g,月季花10g,红花5g。
+      - 【制作】:将山楂、玫瑰花、月季花和红花用水冲净,放入保温杯中,倒入滚开的热水,盖上盖子拧紧。然后将保温杯上下颠倒几次,使水充分地浸泡药材。静置20分钟后可以饮用。此药茶可以反复冲泡至味淡。
+      - 【效用】:活血化瘀,理气消食。适合血瘀质兼见面部黄褐斑、情志不遂者饮用。
+      <span></span>
+
+<br>
+
+### 生活调护建议:
+- 饮食清淡,忌辛辣油腻,避免饮酒,可适量食用山药、莲子等健脾食物。
+- 保持规律作息,避免熬夜,适当运动以助气血流通。
+- 建议进一步检查肠镜及血液指标,排除其他潜在疾病。
+
+<br>
+
+### 注意事项:
+- 中医治疗需结合个体情况调整,以上方案需在专业医师指导下使用。
+- 长期服用激素需警惕副作用(如骨质疏松、免疫力下降),建议定期复查。
+- 若出现头晕、乏力、心悸等症状,需警惕气血两虚或出血加重,及时就医。
+
+<br>
+
+**以上建议旨在辅助调理,具体治疗需结合临床实际调整。**
+`
+</script>
+<script>
+    window.addEventListener('message', e => {
+        const messageInput = document.getElementById('message-input');
+        if (e.data && e.data.type === 'input') messageInput.value = e.data.value
+    }, false);
+    function getValue() {
+        const messageInput = document.getElementById('message-input');
+        return messageInput.value;
+    }
+</script>
+<script>
+    const searchParams = new URLSearchParams(window.location.search);
+    const host = `https://dev.hzliuzhi.com:62006`;
+    document.addEventListener('DOMContentLoaded', function () {
+        if (searchParams.has('hide_title')) document.querySelector('.container-fluid header').style.display = 'none';
+        else document.querySelector('.container-fluid header').innerHTML = document.title;
+        // 获取DOM元素
+        const chatMessages = document.getElementById('chat-messages');
+        const messageInput = document.getElementById('message-input');
+        const sendBtn = document.getElementById('send-btn');
+        let sessionId = document.getElementById('session-id').value;
+        if (sessionId.replace(/\s/g, '') === `{{session_id}}`) document.querySelector(`#session-id`).value = sessionId = searchParams.get('session_id');
+
+        let eventSource = null;
+
+        // 初始化Marked和Highlight.js
+        marked.setOptions({
+            breaks: true,
+            highlight: function (code, language) {
+                const validLanguage = hljs.getLanguage(language) ? language : 'plaintext';
+                return hljs.highlight(validLanguage, code).value;
+            }
+        });
+
+        // 发送消息
+        function sendMessage() {
+            const message = messageInput.value.trim();
+            if (!message) return;
+
+            // 添加用户消息
+            addMessage('user', message);
+            messageInput.value = '';
+
+            // 添加AI思考状态
+            const aiMessageElement = addMessage('assistant', '', true);
+
+            // 关闭之前的连接
+            if (eventSource) {
+                eventSource.close();
+                eventSource = null;
+            }
+
+            let aiResponse = '';
+            let responseElement = null;
+
+            // 使用fetch进行流式请求
+            fetch(`${host}/tcm_chat/chat/tcm`, {
+                method: 'POST',
+                headers: {
+                    'Content-Type': 'application/json',
+                },
+                body: JSON.stringify({
+                    session_id: sessionId,
+                    message: message
+                })
+            })
+                .then(response => {
+                    if (!response.ok) {
+                        throw new Error(`HTTP error! status: ${response.status}`);
+                    }
+
+                    const reader = response.body.getReader();
+                    const decoder = new TextDecoder();
+
+                    function readStream() {
+                        return reader.read().then(({done, value}) => {
+                            if (done) {
+                                return;
+                            }
+                            const chunk = decoder.decode(value);
+                            const lines = chunk.split('\n');
+
+                            for (const line of lines) {
+                                if (line.startsWith('think: ')) { // 思考内容
+                                    try {
+                                        const data = JSON.parse(line.slice(7));
+                                        if (data.content) {
+                                            console.log('流信号 思考内容', data.content)
+                                            // 这里没实现呢,需要你参考openai的实现@xiong
+                                            // 进入思考状态
+                                        }
+                                    } catch (e) {
+                                        console.error('解析响应数据失败:', e);
+                                    }
+                                } else if (line.startsWith('data: ')) { // 返回内容
+                                    try {
+                                        const data = JSON.parse(line.slice(6));
+                                        if (data.error) {
+                                            // 处理错误
+                                            if (!responseElement) {
+                                                aiMessageElement.innerHTML = `<div class="alert alert-danger">${data.error}</div>`;
+                                            }
+                                            return;
+                                        }
+
+                                        if (data.content) {
+                                            console.log('流信号', data.content)
+                                            aiResponse += data.content;
+                                            if (!responseElement) {
+                                                // 移除思考状态
+                                                aiMessageElement.innerHTML = '';
+                                                responseElement = aiMessageElement;
+                                            }
+                                            updateMessageContent(responseElement, aiResponse);
+                                        }
+                                    } catch (e) {
+                                        console.error('解析响应数据失败:', e);
+                                    }
+                                }
+                            }
+                            // 继续读取流
+                            return readStream();
+                        });
+                    }
+
+                    return readStream();
+                })
+                .catch(error => {
+                    console.error('请求失败:', error);
+                    if (!responseElement) {
+                        aiMessageElement.innerHTML = '<div class="alert alert-danger">连接错误,请重试</div>';
+                    }
+                });
+        }
+
+        // 添加消息到聊天界面
+        function addMessage(role, content, isThinking = false) {
+            const messageDiv = document.createElement('div');
+            messageDiv.className = `message ${role}-message mb-3`;
+
+            const avatar = document.createElement('div');
+            avatar.className = 'avatar';
+            avatar.innerHTML = role === 'user' ?
+                '<i class="fas fa-user"></i>' :
+                '<img src="./image/robot.png" alt="">';
+
+            const contentDiv = document.createElement('div');
+            contentDiv.className = 'content';
+
+            if (isThinking) {
+                contentDiv.innerHTML = `
+                        <div class="thinking-container">
+                            <div class="thinking-indicator">
+                                <span></span><span></span><span></span>
+                            </div>
+                            <div class="thinking-text">思考中...</div>
+                        </div>
+                    `;
+            } else {
+                contentDiv.innerHTML = marked.parse(content);
+                hljs.highlightAll();
+            }
+
+            messageDiv.appendChild(avatar);
+            messageDiv.appendChild(contentDiv);
+            chatMessages.appendChild(messageDiv);
+
+            // 滚动到底部
+            // chatMessages.scrollTop = chatMessages.scrollHeight;
+            messageDiv.scrollIntoView({ behavior: 'smooth', block: 'end', inline: 'start' });
+
+            return contentDiv;
+        }
+
+        // 更新消息内容
+        function updateMessageContent(element, content) {
+            element.innerHTML = marked.parse(content);
+            hljs.highlightAll();
+        }
+
+        // 事件监听
+        sendBtn.addEventListener('click', sendMessage);
+
+        messageInput.addEventListener('keypress', function (e) {
+            if (e.key === 'Enter' && !e.shiftKey) {
+                e.preventDefault();
+                sendMessage();
+            }
+        });
+
+        // 初始聚焦输入框
+        messageInput.focus();
+
+        setTimeout(() => {
+            addMessage('user', u1);
+            const el = addMessage('robot', '', true);
+            setTimeout(() => { updateMessageContent(el, r1) }, 3000);
+        }, 1000)
+    });
+</script>
+</body>
+</html>

+ 589 - 0
public/chat/guide.html

@@ -0,0 +1,589 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"/>
+    <title>智能导诊</title>
+    <link rel="stylesheet" href="./css/bootstrap.min.css">
+    <link rel="stylesheet" href="./css/all.min.css">
+    <link rel="stylesheet" href="./css/github.min.css">
+    <link rel="stylesheet" href="./css/styles.css">
+    <link rel="stylesheet" href="./css/chat.css">
+</head>
+<body>
+<div class="container-fluid vh-100 d-flex flex-column">
+    <!-- 顶部标题栏 -->
+    <header class="p-2" style="min-height: 44px; text-align: center; font-size: 18px; font-weight: 700;">
+        <!--智能导诊-->
+    </header>
+
+    <!-- 聊天主区域 -->
+    <main class="flex-grow-1 d-flex flex-column overflow-hidden" id="chat-container">
+        <div class="flex-grow-1 overflow-auto px-2 py-3" id="chat-messages">
+            <!-- 初始系统消息 -->
+            <div class="message system-message" style="margin-top: 24px;" >
+                <div class="avatar">
+                    <img src="./image/robot.png" alt="">
+                </div>
+                <div class="content">
+                    <div class="card">
+                        <div class="card-header bg-light">
+                            医疗导诊助手
+                        </div>
+                        <div class="card-body">
+                            <p>您好!我是医院智能导诊助手,可以帮助您:</p>
+                            <ul>
+                                <li>根据症状推荐合适的科室</li>
+                                <li>查找相关领域的专家医生</li>
+                                <li>提供初步的医疗建议</li>
+                            </ul>
+                            <p>请描述您的症状,我会为您提供专业的导诊服务。</p>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <!-- 输入区域 -->
+        <div class="px-3 py-2 border-top">
+            <div class="input-group input-group-sm">
+                <textarea
+                        id="message-input"
+                        class="form-control"
+                        placeholder="请描述您的症状..."
+                        rows="1"
+                        enterkeyhint="send"
+                ></textarea>
+                <button id="send-btn" class="btn btn-primary btn-sm">
+                    <i class="fas fa-paper-plane"></i>
+                </button>
+            </div>
+        </div>
+    </main>
+</div>
+
+<!-- 隐藏字段 -->
+<input type="hidden" id="session-id" value="{{ session_id }}">
+
+<!-- JavaScript 依赖 -->
+<script src="./js/bootstrap.bundle.min.js"></script>
+<script src="./js/marked.min.js"></script>
+<script src="./js/highlight.min.js"></script>
+<script>document.addEventListener('gesturestart', function (event) {
+    event.preventDefault();
+}, false);</script>
+
+<!-- 主应用脚本 -->
+<script>
+    const searchParams = new URLSearchParams(window.location.search);
+    const host = `https://dev.hzliuzhi.com:62006`;
+    document.addEventListener('DOMContentLoaded', function () {
+        if (searchParams.has('hide_title')) document.querySelector('.container-fluid header').style.display = 'none';
+        else document.querySelector('.container-fluid header').innerHTML = document.title;
+        // 获取DOM元素
+        const chatMessages = document.getElementById('chat-messages');
+        const messageInput = document.getElementById('message-input');
+        const sendBtn = document.getElementById('send-btn');
+        let sessionId = document.getElementById('session-id').value;
+        if (sessionId.replace(/\s/g, '') === `{{session_id}}`) document.querySelector(`#session-id`).value = sessionId = searchParams.get('session_id');
+
+
+        let eventSource = null;
+
+        // 初始化Marked和Highlight.js
+        marked.setOptions({
+            breaks: true,
+            highlight: function (code, language) {
+                const validLanguage = hljs.getLanguage(language) ? language : 'plaintext';
+                return hljs.highlight(validLanguage, code).value;
+            }
+        });
+
+        // 发送消息
+        function sendMessage() {
+            const message = messageInput.value.trim();
+            if (!message) return;
+
+            // 添加用户消息
+            addMessage('user', message);
+            messageInput.value = '';
+
+            // 添加AI思考状态
+            const aiMessageElement = addMessage('assistant', '', true);
+
+            // 关闭之前的连接
+            if (eventSource) {
+                eventSource.close();
+                eventSource = null;
+            }
+
+            let aiResponse = '';
+            let responseElement = null;
+
+            // 使用fetch进行流式请求
+            fetch(`${host}/hospital_guide/chat/stream`, {
+                method: 'POST',
+                headers: {
+                    'Content-Type': 'application/json',
+                },
+                body: JSON.stringify({
+                    session_id: sessionId,
+                    message: message
+                })
+            })
+                .then(response => {
+                    if (!response.ok) {
+                        throw new Error(`HTTP error! status: ${response.status}`);
+                    }
+
+                    const reader = response.body.getReader();
+                    const decoder = new TextDecoder();
+
+                    function readStream() {
+                        return reader.read().then(({done, value}) => {
+                            if (done) {
+                                return;
+                            }
+
+                            const chunk = decoder.decode(value);
+                            const lines = chunk.split('\n');
+
+                            for (const line of lines) {
+                                if (line.startsWith('data: ')) {
+                                    try {
+                                        const data = JSON.parse(line.slice(6));
+
+                                        if (data.error) {
+                                            // 处理错误
+                                            if (!responseElement) {
+                                                aiMessageElement.innerHTML = `<div class="alert alert-danger">${data.error}</div>`;
+                                            }
+                                            return;
+                                        }
+
+                                        if (data.end) {
+                                            console.log('结束信号')
+                                            // 显示结果卡片
+                                            showResultCard(aiResponse).then(() => {
+                                                aiMessageElement.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'start' });
+                                            });
+                                            return;
+                                        }
+
+                                        if (data.content) {
+                                            console.log('流信号', data.content)
+                                            aiResponse += data.content;
+
+                                            if (!responseElement) {
+                                                // 移除思考状态
+                                                aiMessageElement.innerHTML = '';
+                                                responseElement = aiMessageElement;
+                                            }
+
+                                            updateMessageContent(responseElement, aiResponse);
+                                            aiMessageElement.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'start' });
+                                        }
+                                    } catch (e) {
+                                        console.error('解析响应数据失败:', e);
+                                    }
+                                }
+                            }
+
+                            // 继续读取流
+                            return readStream();
+                        });
+                    }
+
+                    return readStream();
+                })
+                .catch(error => {
+                    console.error('请求失败:', error);
+                    if (!responseElement) {
+                        aiMessageElement.innerHTML = '<div class="alert alert-danger">连接错误,请重试</div>';
+                    }
+                });
+        }
+
+        // 添加消息到聊天界面
+        function addMessage(role, content, isThinking = false) {
+            const messageDiv = document.createElement('div');
+            messageDiv.className = `message ${role}-message`;
+
+            const avatar = document.createElement('div');
+            avatar.className = 'avatar';
+            avatar.innerHTML = role === 'user' ?
+                '<i class="fas fa-user"></i>' :
+                '<img src="./image/robot.png" alt="">';
+
+            const contentDiv = document.createElement('div');
+            contentDiv.className = 'content';
+
+            if (isThinking) {
+                contentDiv.innerHTML = `
+                        <div class="thinking-container">
+                            <div class="thinking-indicator">
+                                <span></span><span></span><span></span>
+                            </div>
+                            <div class="thinking-text">思考中...</div>
+                        </div>
+                    `;
+            } else {
+                contentDiv.innerHTML = marked.parse(content);
+                hljs.highlightAll();
+            }
+
+            messageDiv.appendChild(avatar);
+            messageDiv.appendChild(contentDiv);
+            chatMessages.appendChild(messageDiv);
+
+            // 滚动到底部
+            // chatMessages.scrollTop = chatMessages.scrollHeight;
+            messageDiv.scrollIntoView({ behavior: 'smooth', block: 'end', inline: 'start' });
+
+            return contentDiv;
+        }
+
+        // 更新消息内容
+        function updateMessageContent(element, content) {
+            element.innerHTML = marked.parse(content);
+            hljs.highlightAll();
+        }
+
+        // 显示结果卡片
+        function showResultCard(symptoms) {
+            // 创建新的结果卡片
+            const resultCardId = 'result-card-' + Date.now();
+            const resultCardHtml = `
+                <div class="result-card mt-3" id="${resultCardId}">
+                    <div class="card shadow-sm">
+                        <div class="card-body p-0">
+                            <ul class="nav" role="tablist">
+                                <li class="nav-item" role="presentation">
+                                    <button class="nav-link active" data-bs-toggle="tab" 
+                                            data-bs-target="#dept-${resultCardId}" type="button" role="tab" aria-controls="dept-${resultCardId}" aria-selected="true">
+                                        <i class="fas fa-clinic-medical me-1"></i> 推荐科室
+                                    </button>
+                                </li>
+                            </ul>
+                            <div class="tab-content">
+                                <div class="tab-pane fade show active" id="dept-${resultCardId}" role="tabpanel">
+                                    <div class="department-results p-2" data-loaded="false">
+                                        <div class="text-center py-3">
+                                            <div class="spinner-border text-primary" role="status">
+                                                <span class="visually-hidden">加载中...</span>
+                                            </div>
+                                            <p class="mt-2 text-muted">正在查询相关科室...</p>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="card shadow-sm">
+                        <div class="card-body p-0">
+                            <ul class="nav" role="tablist">
+                                <li class="nav-item" role="presentation">
+                                    <button class="nav-link active" data-bs-toggle="tab"
+                                            data-bs-target="#doctor-${resultCardId}" type="button" role="tab" aria-controls="doctor-${resultCardId}" aria-selected="false">
+                                        <i class="fas fa-user-md me-1"></i> 推荐医生
+                                    </button>
+                                </li>
+                            </ul>
+                            <div class="tab-content">
+                                <div class="tab-pane fade show active" id="doctor-${resultCardId}" role="tabpanel">
+                                    <div class="doctor-results p-2" data-loaded="false">
+                                        <div class="text-center py-3">
+                                            <div class="spinner-border text-primary" role="status">
+                                                <span class="visually-hidden">加载中...</span>
+                                            </div>
+                                            <p class="mt-2 text-muted">正在查询相关医生...</p>
+                                        </div>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            `;
+
+            // 将结果卡片添加到当前AI消息中
+            const currentAiMessage = document.querySelector('.assistant-message:last-child .content');
+            if (currentAiMessage) {
+                currentAiMessage.insertAdjacentHTML('beforeend', resultCardHtml);
+            }
+
+            // 加载科室数据
+            return Promise.all([
+                loadDepartments(symptoms, resultCardId),
+                loadDoctors(symptoms, resultCardId),
+            ]);
+
+            // 设置医生标签页点击事件 - 只在第一次点击时加载数据
+            const doctorTab = document.querySelector(`[data-bs-target="#doctor-${resultCardId}"]`);
+            if (doctorTab) {
+                doctorTab.addEventListener('click', function () {
+                    // 检查是否已经加载过医生数据
+                    const doctorContainer = document.querySelector(`#doctor-${resultCardId} .doctor-results`);
+                    if (doctorContainer && doctorContainer.dataset.loaded !== 'true') {
+                        // 显示加载状态
+                        doctorContainer.innerHTML = `
+                            <div class="text-center py-3">
+                                <div class="spinner-border text-primary" role="status">
+                                    <span class="visually-hidden">加载中...</span>
+                                </div>
+                                <p class="mt-2 text-muted">正在查询相关医生...</p>
+                            </div>
+                        `;
+
+                        loadDoctors(symptoms, resultCardId);
+                        // 标记为已加载
+                        doctorContainer.dataset.loaded = 'true';
+                    }
+                });
+            }
+
+            // 滚动到底部显示结果
+            setTimeout(() => {
+                chatMessages.scrollTop = chatMessages.scrollHeight;
+            }, 100);
+        }
+
+        // 加载科室数据
+        function loadDepartments(symptoms, resultCardId) {
+            const container = document.querySelector(`#dept-${resultCardId} .department-results`);
+            if (!container) {
+                console.error('找不到科室结果容器');
+                return;
+            }
+            if (container.dataset.loaded === 'true') return;
+
+            // 显示加载状态
+            container.innerHTML = `
+                    <div class="text-center py-3">
+                        <div class="spinner-border text-primary" role="status">
+                            <span class="visually-hidden">加载中...</span>
+                        </div>
+                        <p class="mt-2 text-muted">正在查询相关科室...</p>
+                    </div>`;
+
+            return fetch(`${host}/hospital_guide/api/search/departments`, {
+                method: 'POST',
+                headers: {
+                    'Content-Type': 'application/json'
+                },
+                body: JSON.stringify({
+                    query: symptoms,
+                    session_id: sessionId
+                })
+            })
+                .then(response => response.json())
+                .then(data => {
+                    if (data.results && data.results.length > 0) {
+                        let html = '';
+                        data.results.forEach(dept => {
+                            // 截断描述文本,超过50字显示省略号
+                            const description = dept.description.length > 50
+                                ? dept.description.substring(0, 50) + '...'
+                                : dept.description;
+
+                            html += `
+                            <div class="card mb-2 dept-card">
+                                <div class="card-body p-2">
+                                    <div class="d-flex align-items-center mb-2">
+                                        <div class="flex-shrink-0">
+                                            <div class="avatar-sm bg-primary rounded-circle d-flex align-items-center justify-content-center" >
+                                                <i class="fas fa-clinic-medical fa-sm text-white"></i>
+                                            </div>
+                                        </div>
+                                        <div class="flex-grow-1 ms-2">
+                                            <h6 class="card-title mb-0">${escapeHtml(dept.name)}</h6>
+                                        </div>
+                                        <div class="d-flex align-items-center">
+                                            <button class="btn btn-sm btn-outline-primary">
+                                                <i class="fas fa-info-circle me-1"></i> 详情
+                                            </button>
+                                        </div>
+                                    </div>
+                                    <p class="card-text small text-muted mb-0" title="${escapeHtml(dept.description)}">${escapeHtml(description)}</p>
+                                </div>
+                            </div>`;
+                        });
+                        container.innerHTML = html;
+                        // 标记为已加载
+                        container.dataset.loaded = 'true';
+
+                    } else {
+                        container.innerHTML = '<div class="alert alert-warning">未找到相关科室</div>';
+                        container.dataset.loaded = 'true';
+                    }
+                })
+                .catch(error => {
+                    console.error('加载科室失败:', error);
+                    container.innerHTML = '<div class="alert alert-danger">加载科室失败</div>';
+                    container.dataset.loaded = 'true';
+                });
+        }
+
+        // 加载医生数据
+        function loadDoctors(symptoms, resultCardId) {
+            const container = document.querySelector(`#doctor-${resultCardId} .doctor-results`);
+            if (!container) {
+                console.error('找不到医生结果容器');
+                return;
+            }
+            if (container.dataset.loaded === 'true') return;
+
+            // 显示加载状态
+            container.innerHTML = `
+                    <div class="text-center py-3">
+                        <div class="spinner-border text-primary" role="status">
+                            <span class="visually-hidden">加载中...</span>
+                        </div>
+                        <p class="mt-2 text-muted">正在查询相关医生...</p>
+                    </div>`;
+
+            return fetch(`${host}/hospital_guide/api/search/doctors`, {
+                method: 'POST',
+                headers: {
+                    'Content-Type': 'application/json'
+                },
+                body: JSON.stringify({
+                    query: symptoms,
+                    session_id: sessionId
+                })
+            })
+                .then(response => response.json())
+                .then(data => {
+                    if (data.results && data.results.length > 0) {
+                        let html = '';
+                        data.results.forEach(doctor => {
+                            // 截断描述文本,超过40字显示省略号
+                            const description = doctor.description && doctor.description.length > 40
+                                ? doctor.description.substring(0, 40) + '...'
+                                : (doctor.description || '暂无描述');
+
+                            html += `
+                            <div class="card mb-2 doctor-card" onclick="showDoctorDetail('${escapeHtml(doctor.name || '未知医生')}', '${escapeHtml(doctor.description || '')}', ${Math.round((doctor.score || 0) * 100)})">
+                                <div class="card-body p-2">
+                                    <div class="d-flex align-items-center mb-2">
+                                        <div class="flex-shrink-0">
+                                            <div class="avatar-md bg-light rounded-circle d-flex align-items-center justify-content-center" >
+                                                <i class="fas fa-user-md fa-lg text-primary"></i>
+                                            </div>
+                                        </div>
+                                        <div class="flex-grow-1 ms-2">
+                                            <h6 class="card-title mb-0" >${escapeHtml(doctor.name || '未知医生')}</h6>
+                                        </div>
+                                        <div class="d-flex align-items-center">
+                                            <span class="badge bg-light text-dark me-2" >${escapeHtml(doctor.department || '未知科室')}</span>
+                                            <button class="btn btn-sm btn-outline-primary" onclick="event.stopPropagation(); showDoctorDetail('${escapeHtml(doctor.name || '未知医生')}', '${escapeHtml(doctor.description || '')}', ${Math.round((doctor.score || 0) * 100)})">
+                                                <i class="fas fa-calendar-check me-1"></i> 预约
+                                            </button>
+                                        </div>
+                                    </div>
+                                    <p class="card-text small text-muted mb-0" title="${escapeHtml(doctor.description || '')}" >${escapeHtml(description)}</p>
+                                </div>
+                            </div>`;
+                        });
+                        container.innerHTML = html;
+                        // 标记为已加载
+                        container.dataset.loaded = 'true';
+
+                    } else {
+                        container.innerHTML = '<div class="alert alert-warning">未找到相关医生</div>';
+                        container.dataset.loaded = 'true';
+                    }
+                })
+                .catch(error => {
+                    console.error('加载医生失败:', error);
+                    container.innerHTML = '<div class="alert alert-danger">加载医生失败</div>';
+                    container.dataset.loaded = 'true';
+                });
+        }
+
+
+        // HTML转义函数
+        function escapeHtml(unsafe) {
+            if (!unsafe) return '';
+            return unsafe
+                .replace(/&/g, "&amp;")
+                .replace(/</g, "&lt;")
+                .replace(/>/g, "&gt;")
+                .replace(/"/g, "&quot;")
+                .replace(/'/g, "&#039;");
+        }
+
+        // 显示医生详情
+        function showDoctorDetail(doctorName, description, matchScore) {
+            const modalHtml = `
+                <div class="modal fade" id="doctorDetailModal" tabindex="-1" aria-labelledby="doctorDetailModalLabel" aria-hidden="true">
+                    <div class="modal-dialog modal-lg">
+                        <div class="modal-content">
+                            <div class="modal-header">
+                                <h5 class="modal-title" id="doctorDetailModalLabel">
+                                    <i class="fas fa-user-md me-2"></i>${doctorName}
+                                </h5>
+                                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+                            </div>
+                            <div class="modal-body">
+                                <div class="row">
+                                    <div class="col-md-4 text-center">
+                                        <div class="avatar-lg bg-light rounded-circle d-flex align-items-center justify-content-center mx-auto mb-3" >
+                                            <i class="fas fa-user-md fa-3x text-primary"></i>
+                                        </div>
+                                        <h6 class="text-primary">${doctorName}</h6>
+                                        <span class="badge bg-primary">匹配度: ${matchScore}%</span>
+                                    </div>
+                                    <div class="col-md-8">
+                                        <h6>专业描述</h6>
+                                        <p class="text-muted">${description}</p>
+                                        <hr>
+                                        <h6>联系方式</h6>
+                                        <p class="text-muted">请通过医院前台或预约系统联系</p>
+                                        <hr>
+                                        <h6>出诊时间</h6>
+                                        <p class="text-muted">具体出诊时间请咨询医院</p>
+                                    </div>
+                                </div>
+                            </div>
+                            <div class="modal-footer">
+                                <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
+                                <button type="button" class="btn btn-primary">
+                                    <i class="fas fa-calendar-check me-1"></i> 预约挂号
+                                </button>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            `;
+
+            // 移除已存在的模态框
+            const existingModal = document.getElementById('doctorDetailModal');
+            if (existingModal) {
+                existingModal.remove();
+            }
+
+            // 添加新的模态框
+            document.body.insertAdjacentHTML('beforeend', modalHtml);
+
+            // 显示模态框
+            const modal = new bootstrap.Modal(document.getElementById('doctorDetailModal'));
+            modal.show();
+        }
+
+        // 事件监听
+        sendBtn.addEventListener('click', sendMessage);
+
+        messageInput.addEventListener('keypress', function (e) {
+            if (e.key === 'Enter' && !e.shiftKey) {
+                e.preventDefault();
+                sendMessage();
+            }
+        });
+
+        // 初始聚焦输入框
+        messageInput.focus();
+    });
+</script>
+</body>
+</html>

二进制
public/chat/image/bg_dialog@2x.png


二进制
public/chat/image/c1.png


二进制
public/chat/image/c2.png


二进制
public/chat/image/robot.png


文件差异内容过多而无法显示
+ 5 - 0
public/chat/js/bootstrap.bundle.min.js


文件差异内容过多而无法显示
+ 312 - 0
public/chat/js/highlight.min.js


文件差异内容过多而无法显示
+ 11 - 0
public/chat/js/marked.min.js


二进制
public/chat/webfonts/fa-solid-900.ttf


二进制
public/chat/webfonts/fa-solid-900.woff2


+ 19 - 0
public/config.js

@@ -0,0 +1,19 @@
+window.SIX_config = {
+  BASE_URL: window.BASE_URL,
+  title: '中医智能辅助诊疗系统',
+  // icon: 'favicon.ico',
+  // logo: '',
+  AI_URL: window.BASE_URL + 'chat/ai.html',
+  AI_MODEL_URL: window.BASE_URL + 'chat/e.html',
+  SST_TOKEN_URL: '[get]https://dev.hzliuzhi.com:62006/tcm_chat/asr/token',
+  NLP_GET_URL: '[post]https://dev.hzliuzhi.com:62006/tcm_chat/json/record',
+
+  unifyPrescription: '医派协定方',
+}
+document.addEventListener('DOMContentLoaded', function () {
+  document.title = window.SIX_config.title;
+  if (SIX_config.icon) {
+    const icon =  document.querySelector('link[rel="icon"]');
+    icon.href = window.SIX_config.icon
+  }
+});

+ 19 - 0
public/config.xs.js

@@ -0,0 +1,19 @@
+window.SIX_config = {
+  BASE_URL: window.BASE_URL,
+  title: '萧山区中医智能辅助诊疗系统',
+  logo: window.BASE_URL + 'xs/logo.jpg',
+  icon: window.BASE_URL + 'xs/favicon.ico',
+  // AI_URL: '',
+  // AI_MODEL_URL: '',
+  // SST_TOKEN_URL: '',
+  // NLP_GET_URL: '',
+
+  unifyPrescription: '萧然医派协定方',
+}
+document.addEventListener('DOMContentLoaded', function () {
+  document.title = window.SIX_config.title;
+  if (SIX_config.icon) {
+    const icon =  document.querySelector('link[rel="icon"]');
+    icon.href = window.SIX_config.icon
+  }
+});

二进制
public/favicon.ico


+ 3 - 1
public/index.html

@@ -9,13 +9,15 @@
   <!-- <title>
     <%= htmlWebpackPlugin.options.title %>
   </title> -->
-  <title>萧山区中医智能辅助诊疗系统</title>
+  <title>中医智能辅助诊疗系统</title>
   <style>
     * {
       padding: 0;
       margin: 0;
     }
   </style>
+  <script>window.BASE_URL="<%= BASE_URL %>"</script>
+  <script src="<%= BASE_URL %>config.js?"></script>
 </head>
 
 <body>

二进制
public/xs/favicon.ico


二进制
public/xs/logo.jpg


+ 51 - 0
src/api/diagnosis.js

@@ -1,6 +1,7 @@
 import request from '@/utils/request.js'
 import {fromAnalysisModel} from "@/model/tongue-analysis.model";
 import {removeRedundantSemicolons} from '@/api/hack';
+import {generateUUID} from '@/utils';
 
 // 获取就诊病人信息
 export function getPatiiensMsg(data) {
@@ -20,6 +21,56 @@ export function getEmrShowMsg(data) {
     })
 };
 
+// 获取自然语义分析 中医电子病历回显信息
+export function getNLP_TemplateResult(text) {
+    const [_, method, url ] = (window.SIX_config.NLP_GET_URL || '').match(/^(?:\[(\w*)])?\s*(.*)?/) || [];
+    return request({
+        url: url || `https://dev.hzliuzhi.com:62006/tcm_chat/json/record`,
+        method: method || 'post',
+        data: {
+            message: text,
+        },
+        meta: { ignoreResponse: true },
+    }).then(res => {
+        if (res.code === 200) {
+            const {fourmedicine: value, ...data} = res.data;
+            return {
+                ...data,
+                echoData: value ? typeof value === 'string' ? value : JSON.stringify(value) : '{}',
+            };
+        }
+        throw new Error(res.message);
+    });
+}
+
+export function getASR_Token() {
+    try {
+        let token = localStorage.getItem('ASR_TOKEN');
+        if (token) {
+            const proof = JSON.parse(token);
+            if (Math.round(Date.now() / 1000) < proof.expireTime) return proof;
+        }
+    } catch (e) {}
+    const [_, method, url ] = (window.SIX_config.SST_TOKEN_URL || '').match(/^(?:\[(\w*)])?\s*(.*)?/) || [];
+    return request({
+        url: url || `https://dev.hzliuzhi.com:62006/tcm_chat/asr/token`,
+        method: method || 'get',
+        meta: { ignoreResponse: true },
+    }).then(res => {
+        if (res.code === 200) {
+            if (!res.data.appKey) res.data.appKey = `zF5fGsgjoAXnL7F3`;
+            localStorage.setItem('ASR_TOKEN', JSON.stringify(res.data));
+            return res.data;
+        }
+        localStorage.removeItem('ASR_TOKEN');
+        throw new Error(res.message);
+    });
+}
+
+export function connectASR_WebSocket(proof) {
+    return new WebSocket(`wss://nls-gateway.cn-shanghai.aliyuncs.com/ws/v1?token=${proof.token}`);
+}
+
 
 // 提交诊断页面数据
 export function addDiagnosisData(data) {

+ 111 - 0
src/components/AIModal.vue

@@ -0,0 +1,111 @@
+<script>
+export default {
+  name: 'AIModal',
+  props: {
+    url: {
+      type: String,
+      default: '',
+    },
+    immediate: {
+      type: Boolean,
+      default: false,
+    },
+    id: String,
+    title: String,
+  },
+  computed: {
+    realUrl() {
+      if (this.url.startsWith(`http`)) return this.url;
+      const url = this.url.startsWith('/') ? this.url.slice(1) : this.url;
+      return location.href.includes(`index.html`)
+          ? location.href.replace(location.search, '').replace(location.hash, '').split('index.html')[0] + url
+          : location.origin + '/' + url;
+    },
+    realSrc() {
+      if (!this.sessionId) this.sessionId = Date.now() + Array.from({length: 16}, () => Math.floor(Math.random() * 16).toString(16)).join('');
+      const [url, search] = this.realUrl.split('?');
+      const params = new URLSearchParams(search);
+      if (!params.has('session_id')) params.append('session_id', this.sessionId);
+      if (!this.hideTitle) params.delete('hide_title');
+      else if (!params.has('hide_title')) params.append('hide_title', true);
+      return `${url}?${params.toString()}`;
+    },
+  },
+  data() {
+    return {
+      modalLoaded: false,
+      modalProps: {
+        show: false,
+        type: 'modal',
+        remember: true,
+        position: {
+          top: 0,
+          left: 0,
+        },
+        padding: false,
+        showMinimize: true,
+        resize: true,
+        mask: false, lockView: false, lockScroll: false,
+        showClose: true, escClosable: true,
+
+        loading: true,
+        title: ' ',
+        width: Math.min(430, window.innerWidth * 0.6),
+        height: Math.min(932, window.innerHeight * 0.8),
+      },
+
+      sessionId: '',
+      hideTitle: true,
+    };
+  },
+  methods: {
+    handle() {
+      if (this.modalProps.show && this.$refs.modal.isMinimized) this.$refs.modal.revert();
+      else {
+        if (!this.sessionId) this.modalProps.loading = true;
+        this.modalProps.show = true;
+        setTimeout(() => { try { this.$refs.kiosk.contentDocument.querySelector('#message-input').focus(); } catch (e) {} }, 1000);
+      }
+    },
+    load() {
+      const iframe = this.$refs.kiosk;
+      this.modalProps.loading = false;
+      this.modalProps.title = iframe.contentDocument.title;
+      try { this.$refs.kiosk.contentDocument.querySelector('#message-input').focus(); } catch (e) {}
+    },
+    onClose({type, $event}) {
+      if (!$event) $event = {};
+      if (type === 'close' && !$event.shiftKey) this.sessionId = '';
+    },
+  },
+  mounted() {
+    this.modalProps.showClose = !this.immediate;
+    if (this.immediate) {
+      this.handle();
+      setTimeout(() => {
+        this.$refs.modal.minimize();
+        this.modalLoaded = true;
+      }, 2000);
+    } else this.modalLoaded = true;
+  },
+};
+</script>
+
+<template>
+  <div>
+    <slot :handle="handle" :title="modalProps.title" :open="modalProps.show">
+      <div v-if="!modalProps.show" @click="handle()">{{ title || modalProps.title }}</div>
+    </slot>
+    <vxe-modal :class="{initialize: immediate && !modalLoaded}" ref="modal" v-bind="modalProps"
+               v-model="modalProps.show" :id="id" :esc-closable="modalProps.escClosable" @close="onClose">
+      <iframe ref="kiosk" :src="realSrc" style="display: block;width: 100%;height: 100%;border: 0;"
+              @load="load"></iframe>
+    </vxe-modal>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.vxe-modal--wrapper.initialize {
+  display: none;
+}
+</style>

+ 139 - 0
src/components/AiEModal.vue

@@ -0,0 +1,139 @@
+<script>
+import SoundRecorder from '@/components/SoundRecorder.vue';
+
+export default {
+  name: 'AiEModal',
+  components: {SoundRecorder},
+  props: {
+    url: {
+      type: String,
+      default: '',
+    },
+    immediate: {
+      type: Boolean,
+      default: false,
+    },
+    id: String,
+    title: String,
+  },
+  computed: {
+    realUrl() {
+      if (this.url.startsWith(`http`)) return this.url;
+      const url = this.url.startsWith('/') ? this.url.slice(1) : this.url;
+      return location.href.includes(`index.html`)
+          ? location.href.replace(location.search, '').replace(location.hash, '').split('index.html')[0] + url
+          : location.origin + '/' + url;
+    },
+    realSrc() {
+      if (!this.sessionId) this.sessionId = Date.now() + Array.from({length: 16}, () => Math.floor(Math.random() * 16).toString(16)).join('');
+      const [url, search] = this.realUrl.split('?');
+      const params = new URLSearchParams(search);
+      if (!params.has('session_id')) params.append('session_id', this.sessionId);
+      if (!this.hideTitle) params.delete('hide_title');
+      else if (!params.has('hide_title')) params.append('hide_title', true);
+      return `${url}?${params.toString()}`;
+    },
+  },
+  data() {
+    return {
+      modalLoaded: false,
+      modalProps: {
+        show: false,
+        type: 'modal',
+        remember: true,
+        position: {
+          top: 0,
+          left: 0,
+        },
+        padding: false,
+        showMinimize: true,
+        resize: true,
+        mask: false, lockView: false, lockScroll: false,
+        showClose: true, escClosable: true,
+
+        loading: true,
+        title: ' ',
+        width: Math.max(430, window.innerWidth * 0.6),
+        height: Math.max(932, window.innerHeight * 0.8),
+      },
+
+      sessionId: '',
+      hideTitle: true,
+
+      aiText: '',
+      aiTextBackups: '',
+    };
+  },
+  methods: {
+    handle() {
+      if (this.modalProps.show && this.$refs.modal.isMinimized) this.$refs.modal.revert();
+      else {
+        if (!this.sessionId) this.modalProps.loading = true;
+        this.modalProps.show = true;
+        setTimeout(() => { try { this.$refs.kiosk.contentDocument.querySelector('#message-input').focus(); } catch (e) {} }, 1000);
+      }
+    },
+    load() {
+      const iframe = this.$refs.kiosk;
+      this.modalProps.loading = false;
+      this.modalProps.title = this.title || iframe.contentDocument.title;
+      try { this.$refs.kiosk.contentDocument.querySelector('#message-input').focus(); } catch (e) {}
+    },
+    onClose({type, $event}) {
+      if (!$event) $event = {};
+      if (type === 'close' && !$event.shiftKey) this.sessionId = '';
+    },
+
+    begin() {
+      try {
+        this.aiText = '';
+        this.aiTextBackups = this.$refs.kiosk.contentWindow.getValue();
+      } catch (e) { this.aiTextBackups = this.aiText ; }
+    },
+    change(value) {
+      this.aiText = this.aiTextBackups + value;
+      this.$refs.kiosk.contentWindow.postMessage({type: 'input', value: this.aiText });
+    },
+  },
+  mounted() {
+    this.modalProps.showClose = !this.immediate;
+    if (this.immediate) {
+      this.handle();
+      setTimeout(() => {
+        this.$refs.modal.minimize();
+        this.modalLoaded = true;
+      }, 2000);
+    } else this.modalLoaded = true;
+  },
+};
+</script>
+
+<template>
+  <div>
+    <slot :handle="handle" :title="modalProps.title" :open="modalProps.show">
+      <div v-if="!modalProps.show" @click="handle()">{{ title || modalProps.title }}</div>
+    </slot>
+    <vxe-modal :class="{initialize: immediate && !modalLoaded}" ref="modal" v-bind="modalProps"
+               v-model="modalProps.show" :id="id" :esc-closable="modalProps.escClosable" @close="onClose">
+      <iframe ref="kiosk" :src="realSrc" style="display: block;width: 100%;height: 100%;border: 0;"
+              @load="load"></iframe>
+      <div class="float-wrapper" v-if="!modalProps.loading && modalProps.show">
+        <SoundRecorder @begin="begin()" @change="change($event)" @finish="change($event)"/>
+      </div>
+    </vxe-modal>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.vxe-modal--wrapper.initialize {
+  display: none;
+}
+
+.float-wrapper {
+  $size: 36px;
+  position: absolute;
+  bottom: (16 + 8px) + (46 - $size) / 2 + 5px;
+  right: (16px) + (55px + 8px) + 12px;
+  z-index: 11;
+}
+</style>

+ 33 - 5
src/components/Header.vue

@@ -2,13 +2,22 @@
   <div>
     <div class="top-nav flex-vertical-between">
       <div class="title flex-center" v-if="innerWidth>768">
-        <img src="../assets/logo-new.jpg" alt />
-        萧山区中医智能辅助诊疗系统
+        <img v-if="logo" :src="logo" alt />
+        {{title}}
       </div>
       <div class="title flex-center" v-else>
-        <img src="../assets/logo-new.jpg" alt />
-        萧山区中医智能
-        <br />辅助诊疗系统
+        <img v-if="logo" :src="logo" alt />
+        {{titleSplit[0]}}<br />{{titleSplit[1]}}
+      </div>
+      <div v-if="ai_url">
+        <AIModal :url="ai_url">
+          <template #default="{ handle }">
+            <svg @click="handle()" class="ai" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32">
+              <path d="M501.824 32C303.552 32 141.504 176.992 141.504 357.76c0 23.712 2.816 47.104 8.32 69.856l-51.008 114.208a32 32 0 0 0 24.704 44.736c54.272 7.744 76.672 31.168 76.672 77.312v111.552a64 64 0 0 0 64 64h20.704a64 64 0 0 1 64 64V960a32 32 0 0 0 32 32h345.6a32 32 0 0 0 0-64h-313.6v-24.608a128 128 0 0 0-128-128h-20.736v-111.552c0-65.664-32.192-110.688-91.2-131.136l39.872-89.28a31.968 31.968 0 0 0 1.568-21.792 233.088 233.088 0 0 1-8.896-63.904c0-143.712 131.936-261.76 296.32-261.76s296.32 118.016 296.32 261.76a32 32 0 0 0 64 0C862.144 176.992 700.064 32 501.824 32zM904 448a32 32 0 0 0-32 32v360a32 32 0 0 0 64 0V480a32 32 0 0 0-32-32z" fill="#ffffff"></path>
+              <path d="M673.888 466.656c-11.744-25.568-48.416-24.64-58.816 1.536l-132.8 333.76a32 32 0 0 0 59.488 23.68l32.608-81.92c0.576 0.032 1.088 0.32 1.664 0.32h154.848l38.176 83.104a31.968 31.968 0 1 0 58.144-26.72l-153.312-333.76zM599.68 680l47.264-118.72 54.528 118.72H599.68z" fill="#ffffff"></path>
+            </svg>
+          </template>
+        </AIModal>
       </div>
       <!-- <el-menu :default-active="active" class="el-menu-demo" mode="horizontal" @select="handleSelect"
                 background-color="#5386F6" text-color="#fff" active-text-color="#fff" active-background-color="#fff">
@@ -127,13 +136,18 @@ import { getPatiensBasisM, getRecordDetail } from "@/api/diagnosis";
 import { getPatiensVisitList } from "@/api/patients.js";
 import pageProup from "@/components/Propup";
 import outpatientRecord from "@/components/ui/outpatientRecords";
+import AIModal from '@/components/AIModal.vue';
 export default {
   components: {
+    AIModal,
     pageProup,
     outpatientRecord
   },
   data() {
     return {
+      title: window.SIX_config.title,
+      logo: window.SIX_config.logo,
+      ai_url: window.SIX_config.AI_URL,
       activeName: "",
       active: "0",
       // 修改一级目录名称时 要同步修改 router 里面的 值
@@ -358,6 +372,12 @@ export default {
     }
   },
   computed: {
+    titleSplit() {
+      const suffix = `辅助诊疗系统`
+      const values = this.title.split(suffix);
+      values[1] = values.length === 2 ? suffix + values[1] : '';
+      return values;
+    },
     ...mapGetters(["getuserinfo", "getPatiensInfo", "getActiveID", "getActive"])
   }
 };
@@ -386,6 +406,14 @@ export default {
 }
 
 .top-nav {
+  svg.ai {
+    cursor: pointer;
+    &:hover {
+      path {
+        fill: #f56c6c;
+      }
+    }
+  }
   background: #5386f6;
   padding: 0 24px 0 0;
   // padding: 0px;

+ 275 - 0
src/components/SoundRecorder.vue

@@ -0,0 +1,275 @@
+<script>
+import {connectASR_WebSocket, getASR_Token} from '@/api/diagnosis';
+import {generateUUID} from '@/utils';
+
+let websocket;
+let dispose = () => {};
+
+let onAudioConnect = () => {};
+let onAudioCleanup = () => {};
+let onSocketCleanup = () => {};
+
+export default {
+  name: 'SoundRecorder',
+  props: {
+    enabled: {
+      type: Boolean,
+      default: false,
+    },
+  },
+  data() {
+    return {
+      taskId: '',
+
+      recording: false,
+      loading: false,
+      proof: {token: '', appKey: ''},
+    };
+  },
+  beforeMount() {
+    dispose = (event) => {
+      switch (event.type) {
+        case 'open':
+          return this.startTranscription(this.taskId);
+        case 'close': {
+          websocket = null;
+          this.recording = false;
+          this.loading = false;
+          break;
+        }
+        case 'message':
+          return this.analysis(JSON.parse(event.data));
+        case 'audioprocess':
+          return this.send(event.inputBuffer.getChannelData(0));
+      }
+      console.log(event, 'log:');
+    };
+  },
+  beforeDestroy() {
+    this.stop();
+    this.disconnect();
+  },
+  watch: {
+    enabled(value) {
+      if (!value) this.stop();
+    },
+  },
+  methods: {
+    handle() {
+      if (this.loading) return;
+      try { if (this.recording) this.stop(); else this.start(); } catch (e) {
+        this.$message.warning(`出错了,请重试`);
+        this.disconnect();
+        console.log('[sr-ars]', e);
+      }
+    },
+    async connect() {
+      if (!this.proof || !this.proof.token) this.proof = await getASR_Token();
+      websocket = await connectASR_WebSocket(this.proof);
+      websocket.addEventListener('open', dispose);
+      websocket.addEventListener('close', dispose);
+      websocket.addEventListener('message', dispose);
+      websocket.addEventListener('error', dispose);
+      onSocketCleanup = () => {
+        websocket.removeEventListener('open', dispose);
+        websocket.removeEventListener('close', dispose);
+        websocket.removeEventListener('message', dispose);
+        websocket.removeEventListener('error', dispose);
+        onSocketCleanup = () => {};
+      };
+    },
+    async disconnect() {
+      this.loading = false;
+      this.recording = false;
+      onSocketCleanup();
+    },
+    async start() {
+      if (onAudioCleanup) onAudioCleanup();
+
+      this.loading = true;
+      this.taskId = generateUUID();
+
+      const stream = await navigator.mediaDevices.getUserMedia({audio: true});
+      const context = new window.AudioContext({sampleRate: 16000});
+      const input = context.createMediaStreamSource(stream);
+      const processor = context.createScriptProcessor(2048, 1, 1);
+      processor.addEventListener('audioprocess', dispose);
+
+      onAudioCleanup = () => {
+        this.loading = false;
+        processor.removeEventListener('audioprocess', dispose);
+        processor.disconnect();
+        input.disconnect();
+        stream.getTracks().forEach(track => track.stop());
+        context.close();
+        onAudioCleanup = () => {};
+      };
+
+      onAudioConnect = () => {
+        this.loading = false;
+        input.connect(processor);
+        processor.connect(context.destination);
+        onAudioConnect = () => {};
+      };
+
+      if (!websocket || websocket.readyState !== WebSocket.OPEN) await this.connect();
+      else if (this.recording) onAudioConnect();
+    },
+    async stop() {
+      if (!websocket || websocket.readyState !== WebSocket.OPEN) return;
+      websocket.send(JSON.stringify({
+        header: {
+          appkey: this.proof.appKey,
+          namespace: 'SpeechTranscriber',
+          name: 'StopTranscription',
+          task_id: this.taskId,
+          message_id: generateUUID(),
+        },
+      }));
+    },
+
+    startTranscription(taskId = generateUUID()) {
+      if (!websocket || websocket.readyState !== WebSocket.OPEN) return;
+      websocket.send(JSON.stringify({
+        header: {
+          appkey: this.proof.appKey,
+          namespace: 'SpeechTranscriber',
+          name: 'StartTranscription',
+          task_id: taskId,
+          message_id: generateUUID(),
+        },
+        payload: {
+          'format': 'pcm',
+          'sample_rate': 16000,
+          'enable_intermediate_result': true,
+          'enable_punctuation_prediction': true,
+          'enable_inverse_text_normalization': true,
+        },
+      }));
+    },
+    send(data) {
+      if (!websocket || websocket.readyState !== WebSocket.OPEN) return;
+      const inputData16 = new Int16Array(data.length);
+      for (let i = 0; i < data.length; ++i) {
+        inputData16[i] = Math.max(-1, Math.min(1, data[i])) * 0x7FFF; // PCM 16-bit
+      }
+      websocket.send(inputData16.buffer);
+      console.log('[sr-ars] 发送音频数据块');
+    },
+    analysis(message) {
+      switch (message.header.name) {
+        case 'TranscriptionStarted': {
+          this.recording = true;
+          onAudioConnect();
+          break;
+        }
+        case 'TranscriptionCompleted': {
+          this.recording = false;
+          onAudioCleanup();
+          break;
+        }
+        case 'SentenceBegin': {
+          const {index, time} = message.payload;
+          console.log(`[sr-ars]:message: 一句话开始:[${index}(${time} - )]`);
+          this.$emit('begin');
+          break;
+        }
+        case 'SentenceEnd': {
+          const {index, time, result, begin_time} = message.payload;
+          console.log(`[sr-ars]:message: 一句话结束:${result} [${index}(${begin_time} - ${time})]`);
+          this.$emit('finish', result);
+          break;
+        }
+        case 'TranscriptionResultChanged': {
+          const {index, time, result} = message.payload;
+          console.log(`[sr-ars]:message: 识别结果变化:${result} [${index}(${time})]`);
+          this.$emit('change', result);
+          break;
+        }
+        default: {
+          console.log('[sr-ars]:message', message);
+        }
+      }
+    },
+  },
+};
+</script>
+
+<template>
+  <div style="z-index: 11;">
+    <slot>
+      <div class="sr-wrapper" :class="{recording, loading}" @click="handle()">
+        <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+
+          <circle v-if="loading" cx="12" cy="12" r="5" fill="none" stroke="white" stroke-width="1"></circle>
+          <rect v-if="!loading" x="9" y="6" :width="recording? 10 : 6" height="10" :rx="recording ? 5 : 3"
+                fill="white"/>
+          <path v-if="!loading"
+                d="M6.75 12.75V13C6.75 15.8995 9.1005 18.25 12 18.25C14.8995 18.25 17.25 15.8995 17.25 13V12.75"
+                stroke="white" stroke-width="1.5" stroke-linecap="round"/>
+        </svg>
+      </div>
+    </slot>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.sr-wrapper {
+  position: relative;
+  width: 24px;
+  height: 24px;
+  background: #FF3359;
+  border-radius: 50%;
+  transform: scale(1.5);
+  cursor: pointer;
+  box-shadow: 0 4px 50px #FF3359;
+
+  &.recording {
+    svg {
+      rect {
+        transform: translate(-2px, 1px);
+        animation: recording 2s infinite 0.1s;
+      }
+
+      path {
+        transform: scale(0.7);
+        opacity: 0;
+      }
+
+    }
+  }
+
+  &.loading {
+    cursor: not-allowed;
+
+    svg {
+      animation: loading-rotate 2s linear infinite;
+    }
+  }
+
+  svg {
+    rect {
+      stroke-width: 0;
+      stroke: white;
+      transition: all 0.2s ease;
+      transform-origin: center;
+    }
+
+    circle {
+      stroke-linecap: round;
+      animation: loading-dash 1.5s ease-in-out infinite;
+    }
+
+    path {
+      transition: all 0.2s ease;
+      transform-origin: center;
+    }
+  }
+
+  @keyframes recording {
+    50% {
+      stroke-width: 2;
+    }
+  }
+}
+</style>

+ 8 - 0
src/main.js

@@ -22,6 +22,14 @@ import echarts from 'echarts'
 Vue.prototype.$echarts = echarts
 Vue.prototype.$router1 = router
 
+import { VxeUI, VxeModal } from 'vxe-pc-ui'
+import 'vxe-pc-ui/es/style.css'
+// import 'vxe-pc-ui/es/icon/style.css'
+// import 'vxe-pc-ui/es/modal/style.css'
+import zhCN from 'vxe-pc-ui/lib/language/zh-CN'
+Vue.use(VxeModal);
+VxeUI.setI18n('zh-CN', zhCN)
+VxeUI.setLanguage('zh-CN')
 
 
 Vue.config.productionTip = false

+ 4 - 0
src/utils/index.js

@@ -32,3 +32,7 @@ export function deepClone (source) {
   })
   return targetObj
 }
+
+export function generateUUID() {
+  return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c => (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)).replace(/-/g, '');
+}

+ 2 - 1
src/utils/request.js

@@ -54,7 +54,8 @@ service.interceptors.response.use(
 
       return res
     }
-
+    const meta = response.config['meta'] || {};
+    if (meta['ignoreResponse']) return res;
 
     if (response.headers['access-control-expose-headers']) {
       let str = response.headers['access-control-expose-headers'].split(';')

+ 2 - 1
src/views/Home.vue

@@ -2,7 +2,7 @@
   <div class="home">
     <div class="top"></div>
     <div class="flex-center flex-column login-body">
-      <h2>萧山区中医智能辅助诊疗系统</h2>
+      <h2>{{title}}</h2>
       <div class="login-form">
         <!-- <div class="flex-center">
           <img src="../assets/logo.png" alt="" class="logo" />
@@ -46,6 +46,7 @@ export default {
   components: {},
   data() {
     return {
+      title: window.SIX_config.title,
       name: "",
       pas: "",
       code: "",

+ 73 - 1
src/views/diagnosis/Emr.vue

@@ -4,6 +4,7 @@
       <span>{{temItem.templateName}}</span>
       <div class="button">
         <!--<el-button type="success" size="mini" @click="handleHisMsg()">回传HIS(JS通知)</el-button>-->
+        <el-button v-if="load_SST" type="primary" size="mini" @click="showAiDialog = true">自然语言处理</el-button>
         <el-button type="primary" size="mini" @click="handleHis()">回传HIS</el-button>
         <el-button type="primary" size="mini" @click="submit()">保存病历</el-button>
       </div>
@@ -44,10 +45,29 @@
         </div>
       </div>
     </el-dialog>
+
+    <el-dialog v-if="load_SST" class="ai-dialog-wrapper" title="自然语言处理" :visible.sync="showAiDialog" center :close-on-click-modal="false" :before-close="closeAiDialog">
+      <div style="position: relative; height: 100%; overflow: hidden;">
+        <el-input
+            type="textarea" :readonly="aiLoading"
+            :autosize="{ minRows: 4, maxRows: 8}"
+            placeholder="请描述患者的病情"
+            v-model="aiText">
+        </el-input>
+        <SoundRecorder style="position: absolute; right: 12px; bottom: 12px;"
+                       :enabled="showAiDialog"
+                       @begin="aiTextBackups = aiText;" @change="aiText = aiTextBackups + $event;" @finish="aiText = aiTextBackups + $event;"
+        />
+      </div>
+      <div slot="footer" class="dialog-footer">
+        <el-button :disabled="aiLoading" @click="closeAiDialog()">取 消</el-button>
+        <el-button type="primary" :loading="aiLoading" :disabled="!load_NLP" @click="loadAi()">确 定</el-button>
+      </div>
+    </el-dialog>
   </div>
 </template>
 <script>
-import { addEMR, getEmrShowMsg, editEMR, backToHis } from "@/api/diagnosis.js";
+import { addEMR, getEmrShowMsg, editEMR, backToHis, getNLP_TemplateResult } from "@/api/diagnosis.js";
 
 import { getTemplateList, getLastTemId, saveLastTemId } from "@/api/emr";
 import { mapGetters, mapMutations } from "vuex";
@@ -55,14 +75,22 @@ import uploadFile from "@/components/UploadFile.vue";
 import PriviewEdit from "../business/components/PriviewEdit.vue";
 
 import {formatPicture, toPicture} from "@/utils/picture";
+import SoundRecorder from '@/components/SoundRecorder.vue';
 
 export default {
   components: {
+    SoundRecorder,
     uploadFile,
     PriviewEdit
   },
   data() {
     return {
+      load_SST: window.SIX_config.SST_TOKEN_URL,
+      load_NLP: window.SIX_config.NLP_GET_URL,
+      showAiDialog: false,
+      aiLoading: false,
+      aiText: '',
+      aiTextBackups: '',
       isSkip: true,
       isEdit: false, // 是否修改
       activeId: "",
@@ -143,6 +171,30 @@ export default {
     }
   },
   methods: {
+    closeAiDialog(done) {
+      if (this.aiLoading) return;
+      if (done) done();
+      this.showAiDialog = false;
+      this.aiText = '';
+    },
+    async loadAi() {
+      if (this.aiText && this.aiText.trim()) {
+        this.aiLoading = true;
+        try {
+          const result = await getNLP_TemplateResult(this.aiText.trim());
+          if (result) {
+            this.showMsgToTem(result);
+            this.aiText = ``;
+          } else throw { message: `` }
+        } catch (e) {
+          if (e.message) this.$message.error(e.message);
+          this.aiLoading = false;
+          return;
+        }
+      }
+      this.aiLoading = false;
+      this.closeAiDialog();
+    },
     // 关闭预览弹窗
     handleClose() {
       this.dialogVisible = false;
@@ -1184,4 +1236,24 @@ export default {
     height: 76vh;
   }
 }
+</style>
+<style lang="scss" scoped>
+.ai-dialog-wrapper {
+  ::v-deep {
+    .el-dialog, .el-dialog__body {
+      height: auto;
+      overflow: hidden;
+    }
+    .el-dialog__body {
+      max-height: 50vh;
+    }
+  }
+  .el-textarea, ::v-deep textarea {
+    max-height: 45vh;
+  }
+  ::v-deep textarea {
+    padding-bottom: 24px;
+  }
+}
+
 </style>

+ 13 - 3
src/views/diagnosis/Prescribing.vue

@@ -311,7 +311,12 @@
         </div>
         <!-- 右侧按钮 -->
         <div class="flex-vertical-center-r flex-wrap">
-          <el-button v-if="showPrec" size="mini" @click="openUnifyPrescPrec" type="primary">萧然医派协定方</el-button>
+          <AiEModal v-if="AI_MODEL_URL" :url="AI_MODEL_URL" title="e生大模型">
+            <template #default="{ handle }">
+              <el-button size="mini" style="margin-right: 10px;" @click="handle" type="primary">e生大模型</el-button>
+            </template>
+          </AiEModal>
+          <el-button v-if="showPrec && unifyPrescriptionTitle" size="mini" @click="openUnifyPrescPrec" type="primary">{{unifyPrescriptionTitle}}</el-button>
           <el-button size="mini" key="tongue" :loading="tongueAndFaceLoading" :disabled="!tongueAndFaceAnalysis" @click="openTongueAndFaceAnalysis" type="primary">舌面诊影像</el-button>
           <el-button
             size="mini"
@@ -1061,11 +1066,11 @@
       </div>
     </Popup>
 
-    <!-- 查询萧然医派协定方 页面768 -->
+    <!-- 查询医派协定方 页面768 -->
     <Popup
       :showDialog="showUnifyPresc"
       @cancle="showUnifyPresc=false"
-      title="查询萧然医派协定方"
+      title="查询医派协定方"
       :showBtns="false"
       width="78%"
       distanceTop="5vh"
@@ -1183,10 +1188,12 @@ import prescribing from "../../utils/minix/prescribing";
 
 import {formatPicture} from "@/utils/picture";
 import {CC_Dosage2Basis} from '@/utils/medicine';
+import AiEModal from '@/components/AiEModal.vue';
 
 export default {
   mixins: [prescribing],
   components: {
+    AiEModal,
     Popup,
     chineseMedicine,
     medicineChinese,
@@ -1203,6 +1210,9 @@ export default {
   },
   data() {
     return {
+      AI_MODEL_URL: window.SIX_config.AI_MODEL_URL,
+      unifyPrescriptionTitle: window.SIX_config.unifyPrescription,
+
       tongueAndFaceLoading: false, // 舌面象加载
       tongueAndFaceAnalysis: null, // 舌面象数据
       showTongueAnalysis: false, // 舌象分析弹窗

部分文件因为文件数量过多而无法显示