cc12458 10 месяцев назад
Родитель
Сommit
8d88220336

+ 27 - 0
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",
@@ -4408,6 +4417,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",
@@ -11181,6 +11195,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 +11970,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


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

@@ -0,0 +1,543 @@
+/* 基础样式 */
+: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;
+    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 .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;
+}

+ 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:8040`;
+    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>

BIN
public/chat/image/bg_dialog@2x.png


BIN
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


BIN
public/chat/webfonts/fa-solid-900.ttf


BIN
public/chat/webfonts/fa-solid-900.woff2


+ 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>

+ 20 - 0
src/components/Header.vue

@@ -10,6 +10,16 @@
         中医智能
         <br />辅助诊疗系统
       </div>
+      <div>
+        <AIModal url="/chat/ai.html">
+          <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">
                 <el-menu-item v-for="(item,index) in navlist" :key="index" :index="item.id">{{item.title}}
@@ -127,8 +137,10 @@ 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
   },
@@ -386,6 +398,14 @@ export default {
 }
 
 .top-nav {
+  svg.ai {
+    cursor: pointer;
+    &:hover {
+      path {
+        fill: #f56c6c;
+      }
+    }
+  }
   background: #5386f6;
   padding: 0 24px 0 0;
   // padding: 0px;

+ 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

Некоторые файлы не были показаны из-за большого количества измененных файлов