Selaa lähdekoodia

人工智能通识课主分支归档,移除测试demo

liyanbo 3 viikkoa sitten
vanhempi
sitoutus
502ad98873

+ 0 - 1
src/App.vue

@@ -33,7 +33,6 @@ const loginRoutes = new Set([
   '/login-mobile',
   '/quick-login',
   '/promotion-login',
-  '/login-qsfl',
   '/register-login',
   '/ai-login',
   '/blockly-login'

+ 0 - 52
src/api/qsAiQuestions.js

@@ -1,52 +0,0 @@
-import axios from '@/utils/request'
-import { fetchEventSource } from '@microsoft/fetch-event-source'
-
-/** 创建强生菲林 AI 问答会话。新接口不要求角色参数,默认发送空对象。 */
-export function createQsAiDialogue(data = {}) {
-  return axios({
-    url: 'bjdxWeb/qsAi/create-dialogue',
-    method: 'post',
-    data
-  })
-}
-
-/**
- * 强生菲林 AI 流式问答。
- * 无附件时使用 JSON 请求;有附件时使用 multipart/form-data,其中 request 为 JSON 请求体、files 为本地文件。
- */
-export async function sendQsAiChatMessageStream(
-  conversationId,
-  content,
-  _contentAnswer,
-  ctrl,
-  useContext,
-  onMessage,
-  onError,
-  onClose,
-  playAudio = false,
-  attachments = []
-) {
-  const token = localStorage.getItem('token')
-  const request = { conversationId, content, useContext, playAudio }
-  const files = attachments.map((attachment) => attachment.file || attachment).filter((file) => file instanceof File)
-  const hasFiles = files.length > 0
-  const formData = new FormData()
-  if (hasFiles) {
-    formData.append('request', new Blob([JSON.stringify(request)], { type: 'application/json' }))
-    files.forEach((file) => formData.append('files', file, file.name))
-  }
-
-  return fetchEventSource(`${import.meta.env.VITE_BASE_URL}/bjdxWeb/qsAi/dialogue-send-stream`, {
-    method: 'post',
-    headers: {
-      ...(hasFiles ? {} : { 'Content-Type': 'application/json' }),
-      ...(token ? { Authorization: `Bearer ${token}` } : {})
-    },
-    openWhenHidden: true,
-    body: hasFiles ? formData : JSON.stringify(request),
-    onmessage: onMessage,
-    onerror: onError,
-    onclose: onClose,
-    signal: ctrl.signal
-  })
-}

BIN
src/assets/images/ai-qa/starry-sky.png


+ 2 - 28
src/router/index.js

@@ -20,10 +20,6 @@ const routes = [
   // 免登录
   { path: '/quick-login', component: () => import('../views/QuickLogin.vue') },
   { path: '/promotion-login', component: () => import('../views/PromotionLogin.vue') },
-  { path: '/qsfl-demo1', component: () => import('../views/qsfl/LoginQsfl.vue') },
-  { path: '/qsfl-demo1-2', component: () => import('../views/qsfl/LoginQsfl1-2.vue') },
-  { path: '/qsfl-demo2', component: () => import('../views/qsfl/LoginQsfl2.vue') },
-  { path: '/qsfl-demo3', component: () => import('../views/qsfl/LoginQsfl3.vue') },
   // //【AI实验课】登录
   // { path: '/ai-login', component: () => import('../views/AiCourseLogin.vue') },
   // //【blockly编程课】免租户登录
@@ -192,21 +188,6 @@ const routes = [
     component: () => import('../views/programming/Interface.vue')
   },
 
-  // ==========【强生菲林 - AI智能问答】
-  {
-    path: '/qs-demo1',
-    component: () => import('../views/qsfl/qsflDemo1.vue')
-  },
-  {
-    path: '/qs-demo2',
-    component: () => import('../views/qsfl/qsflDemo2.vue')
-  },
-  {
-    path: '/qs-demo3',
-    component: () => import('../views/qsfl/qsflDemo3.vue')
-  },
-
-
   // 【AI实验课】首页
   // 实验室主题
   {
@@ -316,8 +297,7 @@ const router = createRouter({
 // 导航守卫
 router.beforeEach(async (to, from, next) => {
   // ======= 免登录白名单(新增的页面在此注册即可免登录访问)=======
-  if ( ['/qsfl-demo1','/qsfl-demo2','/qsfl-demo3',
-    '/register-login',
+  if ( ['/register-login',
     '/reg',
     '/login-mobile',
     '/quick-login',
@@ -335,9 +315,6 @@ router.beforeEach(async (to, from, next) => {
 
   // 如果未登录且不是允许访问的页面,重定向到登录页
   if (!isLoggedIn && !allowedPages.includes(to.path)) {
-    if (to.path === '/qs-demo1')next('/qsfl-demo1')
-    if (to.path === '/qs-demo2')next('/qsfl-demo2')
-    if (to.path === '/qs-demo3')next('/qsfl-demo3')
     next('/login')
     return
   }else if(to.path  === '/sub-jump') {
@@ -378,10 +355,7 @@ router.beforeEach(async (to, from, next) => {
   const hasManagementPermission = true // 管理界面默认允许所有登录用户访问
 
   // 检查目标路由是否在允许的范围内
-  if (['/qs-demo1','/qs-demo1-2', '/qs-demo2', '/qs-demo3'].includes(to.path)) {
-    // 强生菲林问答页要求已登录,但不受课程菜单角色权限限制。
-    isAllowed = true
-  } else if ((to.path === managementRoutes.home || managementRoutes.children.includes(to.path)) && hasManagementPermission) {
+  if ((to.path === managementRoutes.home || managementRoutes.children.includes(to.path)) && hasManagementPermission) {
     isAllowed = true
   } else if ((to.path === homeRoutes.home || homeRoutes.children.includes(to.path)) && hasHomePermission) {
     isAllowed = true

+ 0 - 62
src/views/qsfl/LoginQsfl.vue

@@ -1,62 +0,0 @@
-<template>
-  <main class="qsfl-login-page">
-    <div class="login-glow glow-blue"></div>
-    <div class="login-glow glow-red"></div>
-    <section class="login-card" aria-live="polite">
-      <span class="login-orbit orbit-one"></span>
-      <span class="login-orbit orbit-two"></span>
-      <div class="login-mark">AI</div>
-      <h1>非临 AI 助教</h1>
-      <p>{{ statusText }}</p>
-      <div v-if="isLoading" class="loading-line"><i></i><i></i><i></i></div>
-      <button v-else type="button" @click="startLogin">重新进入</button>
-    </section>
-  </main>
-</template>
-
-<script setup>
-import { onMounted, ref } from 'vue'
-import { useRouter } from 'vue-router'
-import { autoLogin } from '@/utils/loginUtils.js'
-
-// 强生菲林 AI 问答专用的固定登录配置。
-const QSFL_LOGIN_CONFIG = Object.freeze({
-  tenantName: import.meta.env.VITE_APP_TITLE,
-  username: 'test',
-  password: 'test@2026',
-  redirectPath: '/qs-demo1'
-})
-
-const router = useRouter()
-const isLoading = ref(false)
-const statusText = ref('正在验证访问权限…')
-
-const startLogin = async () => {
-  if (isLoading.value) return
-  isLoading.value = true
-  statusText.value = '正在安全校验…'
-  const success = await autoLogin(
-    QSFL_LOGIN_CONFIG.tenantName,
-    QSFL_LOGIN_CONFIG.username,
-    QSFL_LOGIN_CONFIG.password,
-    router,
-    QSFL_LOGIN_CONFIG.redirectPath
-  )
-  if (!success) {
-    statusText.value = '校验失败,请稍后重试'
-    isLoading.value = false
-  }
-}
-
-onMounted(startLogin)
-</script>
-
-<style scoped>
-.qsfl-login-page { position: fixed; inset: 0; display: grid; place-items: center; overflow: hidden; color: #edf4ff; background: radial-gradient(circle at 50% 20%, #183c79, #071a41 52%, #020a1d 100%); }
-.login-glow { position: absolute; pointer-events: none; filter: blur(35px); }.glow-blue { width: 45vw; height: 45vw; top: -28vw; right: -8vw; border-radius: 50%; background: rgba(87, 150, 255, .29); }.glow-red { width: 42vw; height: 60vw; bottom: -40vw; left: -17vw; background: rgba(214, 35, 57, .38); transform: rotate(-35deg); }
-.login-card { position: relative; display: flex; width: min(360px, calc(100vw - 48px)); min-height: 260px; align-items: center; flex-direction: column; justify-content: center; overflow: hidden; border: 1px solid rgba(190, 215, 255, .25); border-radius: 22px; background: rgba(7, 25, 60, .65); box-shadow: 0 28px 80px rgba(0, 0, 0, .32), inset 0 1px rgba(255, 255, 255, .12); backdrop-filter: blur(18px); }
-.login-mark { position: relative; z-index: 1; display: grid; width: 58px; height: 58px; margin-bottom: 19px; place-items: center; border: 1px solid rgba(225, 236, 255, .5); border-radius: 17px; background: linear-gradient(135deg, #e33a4a, #3f82dc); box-shadow: 0 10px 28px rgba(58, 128, 225, .3); font-size: 21px; font-weight: 700; letter-spacing: .06em; }
-h1, p, button { position: relative; z-index: 1; }h1 { margin: 0; font-size: 24px; font-weight: 600; letter-spacing: .03em; }p { margin: 11px 0 0; color: rgba(222, 234, 255, .7); font-size: 14px; }button { margin-top: 20px; padding: 8px 15px; border: 1px solid rgba(201, 221, 255, .32); border-radius: 8px; color: #eff5ff; background: rgba(255,255,255,.08); cursor: pointer; }
-.loading-line { position: relative; z-index: 1; display: flex; gap: 6px; margin-top: 24px; }.loading-line i { width: 6px; height: 6px; border-radius: 50%; background: #a6c9ff; animation: loading-dot 1s ease-in-out infinite; }.loading-line i:nth-child(2) { animation-delay: .15s; }.loading-line i:nth-child(3) { animation-delay: .3s; }.login-orbit { position: absolute; width: 180px; height: 180px; border: 1px solid rgba(127, 173, 250, .15); border-radius: 50%; }.orbit-one { top: -112px; right: -80px; }.orbit-two { bottom: -120px; left: -95px; border-style: dashed; animation: orbit 15s linear infinite; }
-@keyframes loading-dot { 50% { transform: translateY(-5px); opacity: .4; } } @keyframes orbit { to { transform: rotate(360deg); } }
-</style>

+ 0 - 63
src/views/qsfl/LoginQsfl1-2.vue

@@ -1,63 +0,0 @@
-<template>
-  <main class="qsfl-login-page">
-    <div class="login-glow glow-blue"></div>
-    <div class="login-glow glow-red"></div>
-    <section class="login-card" aria-live="polite">
-      <span class="login-orbit orbit-one"></span>
-      <span class="login-orbit orbit-two"></span>
-      <div class="login-mark">AI</div>
-      <h1>非临 AI 助教</h1>
-      <p>{{ statusText }}</p>
-      <div v-if="isLoading" class="loading-line"><i></i><i></i><i></i></div>
-      <button v-else type="button" @click="startLogin">重新进入</button>
-    </section>
-  </main>
-</template>
-
-<script setup>
-import { onMounted, ref } from 'vue'
-import { useRouter } from 'vue-router'
-import { autoLogin } from '@/utils/loginUtils.js'
-
-// 强生菲林 AI 问答专用的固定登录配置。
-const QSFL_LOGIN_CONFIG = Object.freeze({
-  tenantName: import.meta.env.VITE_APP_TITLE,
-  username: 'test',
-  password: 'test@2026',
-  redirectPath: '/qs-demo1-2'
-})
-
-
-const router = useRouter()
-const isLoading = ref(false)
-const statusText = ref('正在验证访问权限…')
-
-const startLogin = async () => {
-  if (isLoading.value) return
-  isLoading.value = true
-  statusText.value = '正在安全校验…'
-  const success = await autoLogin(
-    QSFL_LOGIN_CONFIG.tenantName,
-    QSFL_LOGIN_CONFIG.username,
-    QSFL_LOGIN_CONFIG.password,
-    router,
-    QSFL_LOGIN_CONFIG.redirectPath
-  )
-  if (!success) {
-    statusText.value = '校验失败,请稍后重试'
-    isLoading.value = false
-  }
-}
-
-onMounted(startLogin)
-</script>
-
-<style scoped>
-.qsfl-login-page { position: fixed; inset: 0; display: grid; place-items: center; overflow: hidden; color: #edf4ff; background: radial-gradient(circle at 50% 20%, #183c79, #071a41 52%, #020a1d 100%); }
-.login-glow { position: absolute; pointer-events: none; filter: blur(35px); }.glow-blue { width: 45vw; height: 45vw; top: -28vw; right: -8vw; border-radius: 50%; background: rgba(87, 150, 255, .29); }.glow-red { width: 42vw; height: 60vw; bottom: -40vw; left: -17vw; background: rgba(214, 35, 57, .38); transform: rotate(-35deg); }
-.login-card { position: relative; display: flex; width: min(360px, calc(100vw - 48px)); min-height: 260px; align-items: center; flex-direction: column; justify-content: center; overflow: hidden; border: 1px solid rgba(190, 215, 255, .25); border-radius: 22px; background: rgba(7, 25, 60, .65); box-shadow: 0 28px 80px rgba(0, 0, 0, .32), inset 0 1px rgba(255, 255, 255, .12); backdrop-filter: blur(18px); }
-.login-mark { position: relative; z-index: 1; display: grid; width: 58px; height: 58px; margin-bottom: 19px; place-items: center; border: 1px solid rgba(225, 236, 255, .5); border-radius: 17px; background: linear-gradient(135deg, #e33a4a, #3f82dc); box-shadow: 0 10px 28px rgba(58, 128, 225, .3); font-size: 21px; font-weight: 700; letter-spacing: .06em; }
-h1, p, button { position: relative; z-index: 1; }h1 { margin: 0; font-size: 24px; font-weight: 600; letter-spacing: .03em; }p { margin: 11px 0 0; color: rgba(222, 234, 255, .7); font-size: 14px; }button { margin-top: 20px; padding: 8px 15px; border: 1px solid rgba(201, 221, 255, .32); border-radius: 8px; color: #eff5ff; background: rgba(255,255,255,.08); cursor: pointer; }
-.loading-line { position: relative; z-index: 1; display: flex; gap: 6px; margin-top: 24px; }.loading-line i { width: 6px; height: 6px; border-radius: 50%; background: #a6c9ff; animation: loading-dot 1s ease-in-out infinite; }.loading-line i:nth-child(2) { animation-delay: .15s; }.loading-line i:nth-child(3) { animation-delay: .3s; }.login-orbit { position: absolute; width: 180px; height: 180px; border: 1px solid rgba(127, 173, 250, .15); border-radius: 50%; }.orbit-one { top: -112px; right: -80px; }.orbit-two { bottom: -120px; left: -95px; border-style: dashed; animation: orbit 15s linear infinite; }
-@keyframes loading-dot { 50% { transform: translateY(-5px); opacity: .4; } } @keyframes orbit { to { transform: rotate(360deg); } }
-</style>

+ 0 - 62
src/views/qsfl/LoginQsfl2.vue

@@ -1,62 +0,0 @@
-<template>
-  <main class="qsfl-login-page">
-    <div class="login-glow glow-blue"></div>
-    <div class="login-glow glow-red"></div>
-    <section class="login-card" aria-live="polite">
-      <span class="login-orbit orbit-one"></span>
-      <span class="login-orbit orbit-two"></span>
-      <div class="login-mark">AI</div>
-      <h1>非临 AI 助教</h1>
-      <p>{{ statusText }}</p>
-      <div v-if="isLoading" class="loading-line"><i></i><i></i><i></i></div>
-      <button v-else type="button" @click="startLogin">重新进入</button>
-    </section>
-  </main>
-</template>
-
-<script setup>
-import { onMounted, ref } from 'vue'
-import { useRouter } from 'vue-router'
-import { autoLogin } from '@/utils/loginUtils.js'
-
-// 强生菲林 AI 问答专用的固定登录配置。
-const QSFL_LOGIN_CONFIG = Object.freeze({
-  tenantName: import.meta.env.VITE_APP_TITLE,
-  username: 'test',
-  password: 'test@2026',
-  redirectPath: '/qs-demo2'
-})
-
-const router = useRouter()
-const isLoading = ref(false)
-const statusText = ref('正在验证访问权限…')
-
-const startLogin = async () => {
-  if (isLoading.value) return
-  isLoading.value = true
-  statusText.value = '正在安全校验…'
-  const success = await autoLogin(
-      QSFL_LOGIN_CONFIG.tenantName,
-      QSFL_LOGIN_CONFIG.username,
-      QSFL_LOGIN_CONFIG.password,
-      router,
-      QSFL_LOGIN_CONFIG.redirectPath
-  )
-  if (!success) {
-    statusText.value = '校验失败,请稍后重试'
-    isLoading.value = false
-  }
-}
-
-onMounted(startLogin)
-</script>
-
-<style scoped>
-.qsfl-login-page { position: fixed; inset: 0; display: grid; place-items: center; overflow: hidden; color: #edf4ff; background: radial-gradient(circle at 50% 20%, #183c79, #071a41 52%, #020a1d 100%); }
-.login-glow { position: absolute; pointer-events: none; filter: blur(35px); }.glow-blue { width: 45vw; height: 45vw; top: -28vw; right: -8vw; border-radius: 50%; background: rgba(87, 150, 255, .29); }.glow-red { width: 42vw; height: 60vw; bottom: -40vw; left: -17vw; background: rgba(214, 35, 57, .38); transform: rotate(-35deg); }
-.login-card { position: relative; display: flex; width: min(360px, calc(100vw - 48px)); min-height: 260px; align-items: center; flex-direction: column; justify-content: center; overflow: hidden; border: 1px solid rgba(190, 215, 255, .25); border-radius: 22px; background: rgba(7, 25, 60, .65); box-shadow: 0 28px 80px rgba(0, 0, 0, .32), inset 0 1px rgba(255, 255, 255, .12); backdrop-filter: blur(18px); }
-.login-mark { position: relative; z-index: 1; display: grid; width: 58px; height: 58px; margin-bottom: 19px; place-items: center; border: 1px solid rgba(225, 236, 255, .5); border-radius: 17px; background: linear-gradient(135deg, #e33a4a, #3f82dc); box-shadow: 0 10px 28px rgba(58, 128, 225, .3); font-size: 21px; font-weight: 700; letter-spacing: .06em; }
-h1, p, button { position: relative; z-index: 1; }h1 { margin: 0; font-size: 24px; font-weight: 600; letter-spacing: .03em; }p { margin: 11px 0 0; color: rgba(222, 234, 255, .7); font-size: 14px; }button { margin-top: 20px; padding: 8px 15px; border: 1px solid rgba(201, 221, 255, .32); border-radius: 8px; color: #eff5ff; background: rgba(255,255,255,.08); cursor: pointer; }
-.loading-line { position: relative; z-index: 1; display: flex; gap: 6px; margin-top: 24px; }.loading-line i { width: 6px; height: 6px; border-radius: 50%; background: #a6c9ff; animation: loading-dot 1s ease-in-out infinite; }.loading-line i:nth-child(2) { animation-delay: .15s; }.loading-line i:nth-child(3) { animation-delay: .3s; }.login-orbit { position: absolute; width: 180px; height: 180px; border: 1px solid rgba(127, 173, 250, .15); border-radius: 50%; }.orbit-one { top: -112px; right: -80px; }.orbit-two { bottom: -120px; left: -95px; border-style: dashed; animation: orbit 15s linear infinite; }
-@keyframes loading-dot { 50% { transform: translateY(-5px); opacity: .4; } } @keyframes orbit { to { transform: rotate(360deg); } }
-</style>

+ 0 - 62
src/views/qsfl/LoginQsfl3.vue

@@ -1,62 +0,0 @@
-<template>
-  <main class="qsfl-login-page">
-    <div class="login-glow glow-blue"></div>
-    <div class="login-glow glow-red"></div>
-    <section class="login-card" aria-live="polite">
-      <span class="login-orbit orbit-one"></span>
-      <span class="login-orbit orbit-two"></span>
-      <div class="login-mark">AI</div>
-      <h1>非临 AI 助教</h1>
-      <p>{{ statusText }}</p>
-      <div v-if="isLoading" class="loading-line"><i></i><i></i><i></i></div>
-      <button v-else type="button" @click="startLogin">重新进入</button>
-    </section>
-  </main>
-</template>
-
-<script setup>
-import { onMounted, ref } from 'vue'
-import { useRouter } from 'vue-router'
-import { autoLogin } from '@/utils/loginUtils.js'
-
-// 强生菲林 AI 问答专用的固定登录配置。
-const QSFL_LOGIN_CONFIG = Object.freeze({
-  tenantName: import.meta.env.VITE_APP_TITLE,
-  username: 'test',
-  password: 'test@2026',
-  redirectPath: '/qs-demo3'
-})
-
-const router = useRouter()
-const isLoading = ref(false)
-const statusText = ref('正在验证访问权限…')
-
-const startLogin = async () => {
-  if (isLoading.value) return
-  isLoading.value = true
-  statusText.value = '正在安全校验…'
-  const success = await autoLogin(
-      QSFL_LOGIN_CONFIG.tenantName,
-      QSFL_LOGIN_CONFIG.username,
-      QSFL_LOGIN_CONFIG.password,
-      router,
-      QSFL_LOGIN_CONFIG.redirectPath
-  )
-  if (!success) {
-    statusText.value = '校验失败,请稍后重试'
-    isLoading.value = false
-  }
-}
-
-onMounted(startLogin)
-</script>
-
-<style scoped>
-.qsfl-login-page { position: fixed; inset: 0; display: grid; place-items: center; overflow: hidden; color: #edf4ff; background: radial-gradient(circle at 50% 20%, #183c79, #071a41 52%, #020a1d 100%); }
-.login-glow { position: absolute; pointer-events: none; filter: blur(35px); }.glow-blue { width: 45vw; height: 45vw; top: -28vw; right: -8vw; border-radius: 50%; background: rgba(87, 150, 255, .29); }.glow-red { width: 42vw; height: 60vw; bottom: -40vw; left: -17vw; background: rgba(214, 35, 57, .38); transform: rotate(-35deg); }
-.login-card { position: relative; display: flex; width: min(360px, calc(100vw - 48px)); min-height: 260px; align-items: center; flex-direction: column; justify-content: center; overflow: hidden; border: 1px solid rgba(190, 215, 255, .25); border-radius: 22px; background: rgba(7, 25, 60, .65); box-shadow: 0 28px 80px rgba(0, 0, 0, .32), inset 0 1px rgba(255, 255, 255, .12); backdrop-filter: blur(18px); }
-.login-mark { position: relative; z-index: 1; display: grid; width: 58px; height: 58px; margin-bottom: 19px; place-items: center; border: 1px solid rgba(225, 236, 255, .5); border-radius: 17px; background: linear-gradient(135deg, #e33a4a, #3f82dc); box-shadow: 0 10px 28px rgba(58, 128, 225, .3); font-size: 21px; font-weight: 700; letter-spacing: .06em; }
-h1, p, button { position: relative; z-index: 1; }h1 { margin: 0; font-size: 24px; font-weight: 600; letter-spacing: .03em; }p { margin: 11px 0 0; color: rgba(222, 234, 255, .7); font-size: 14px; }button { margin-top: 20px; padding: 8px 15px; border: 1px solid rgba(201, 221, 255, .32); border-radius: 8px; color: #eff5ff; background: rgba(255,255,255,.08); cursor: pointer; }
-.loading-line { position: relative; z-index: 1; display: flex; gap: 6px; margin-top: 24px; }.loading-line i { width: 6px; height: 6px; border-radius: 50%; background: #a6c9ff; animation: loading-dot 1s ease-in-out infinite; }.loading-line i:nth-child(2) { animation-delay: .15s; }.loading-line i:nth-child(3) { animation-delay: .3s; }.login-orbit { position: absolute; width: 180px; height: 180px; border: 1px solid rgba(127, 173, 250, .15); border-radius: 50%; }.orbit-one { top: -112px; right: -80px; }.orbit-two { bottom: -120px; left: -95px; border-style: dashed; animation: orbit 15s linear infinite; }
-@keyframes loading-dot { 50% { transform: translateY(-5px); opacity: .4; } } @keyframes orbit { to { transform: rotate(360deg); } }
-</style>

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 881
src/views/qsfl/components/AiQAChat1.vue


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 1107
src/views/qsfl/components/AiQAChat2.vue


+ 0 - 878
src/views/qsfl/components/AiQAMarkdown.vue

@@ -1,878 +0,0 @@
-<template>
-  <div ref="contentRef" class="markdown-view" :class="`markdown-view--${props.theme}`" @click="handleCitationClick">
-    <template v-for="segment in renderedSegments" :key="segment.key">
-      <div v-if="segment.type === 'markdown'" class="markdown-fragment" v-html="segment.html"></div>
-      <section v-else class="markdown-score-radar" :data-score-block-key="segment.blockKey">
-        <div class="markdown-score-radar__header">
-          <div>
-            <strong>{{ segment.data.studentName }} · AI评分雷达图</strong>
-            <small>满分 {{ segment.data.maxScore }} 分</small>
-          </div>
-          <button class="markdown-score-radar__action" type="button" data-copy-exclude @click.stop="openExpertDialog(segment)">
-            {{ hasExpertScores(segment) ? '修改专家评分' : '补充专家评分' }}
-          </button>
-        </div>
-
-        <svg class="markdown-score-radar__chart" viewBox="0 0 420 350" role="img" :aria-label="`${segment.data.studentName}评分雷达图`">
-          <g transform="translate(210 164)">
-            <polygon
-                v-for="level in segment.data.maxScore"
-                :key="`grid-${level}`"
-                class="markdown-score-radar__grid"
-                :points="gridPoints(segment.data.dimensions.length, level / segment.data.maxScore)"
-            />
-            <line
-                v-for="(_, index) in segment.data.dimensions"
-                :key="`axis-${index}`"
-                class="markdown-score-radar__axis"
-                x1="0"
-                y1="0"
-                :x2="polarPoint(index, segment.data.dimensions.length, 1).x"
-                :y2="polarPoint(index, segment.data.dimensions.length, 1).y"
-            />
-            <polygon class="markdown-score-radar__area markdown-score-radar__area--ai" :points="scorePoints(segment, 'ai')" />
-            <polygon v-if="hasExpertScores(segment)" class="markdown-score-radar__area markdown-score-radar__area--expert" :points="scorePoints(segment, 'expert')" />
-            <template v-for="(dimension, index) in segment.data.dimensions" :key="`point-${dimension.key}`">
-              <circle
-                  v-if="dimension.aiScore !== null"
-                  class="markdown-score-radar__point markdown-score-radar__point--ai"
-                  :cx="scorePoint(segment, index, 'ai').x"
-                  :cy="scorePoint(segment, index, 'ai').y"
-                  r="3.5"
-              />
-              <circle
-                  v-if="expertScore(segment, dimension.key) !== null"
-                  class="markdown-score-radar__point markdown-score-radar__point--expert"
-                  :cx="scorePoint(segment, index, 'expert').x"
-                  :cy="scorePoint(segment, index, 'expert').y"
-                  r="3.5"
-              />
-              <text
-                  class="markdown-score-radar__label"
-                  :x="labelPoint(index, segment.data.dimensions.length).x"
-                  :y="labelPoint(index, segment.data.dimensions.length).y"
-                  :text-anchor="labelAnchor(index, segment.data.dimensions.length)"
-                  dominant-baseline="middle"
-              >{{ dimension.shortName }}</text>
-            </template>
-          </g>
-        </svg>
-
-        <div class="markdown-score-radar__legend">
-          <span><i class="markdown-score-radar__legend-dot markdown-score-radar__legend-dot--ai"></i>AI评分</span>
-          <span v-if="hasExpertScores(segment)"><i class="markdown-score-radar__legend-dot markdown-score-radar__legend-dot--expert"></i>专家评分</span>
-        </div>
-
-        <div class="markdown-score-radar__score-list">
-          <div v-for="dimension in segment.data.dimensions" :key="dimension.key">
-            <span>{{ dimension.name }}</span>
-            <b>AI:{{ formatScore(dimension.aiScore) }}</b>
-            <b v-if="hasExpertScores(segment)">专家:{{ formatScore(expertScore(segment, dimension.key)) }}</b>
-          </div>
-        </div>
-      </section>
-    </template>
-  </div>
-
-  <el-dialog v-model="expertDialogVisible" title="补充专家评分" width="min(520px, 92vw)" append-to-body destroy-on-close>
-    <p class="markdown-score-dialog__hint">{{ editingSegment?.data.studentName }} · 评分范围1–5分,可留空表示未评分</p>
-    <div class="markdown-score-dialog__list">
-      <label v-for="dimension in editingSegment?.data.dimensions || []" :key="dimension.key">
-        <span>{{ dimension.name }}</span>
-        <el-select v-model="expertDraft[dimension.key]" clearable placeholder="未评分">
-          <el-option v-for="score in 5" :key="score" :label="`${score}分`" :value="score" />
-        </el-select>
-      </label>
-    </div>
-    <template #footer>
-      <el-button @click="expertDialogVisible = false">取消</el-button>
-      <el-button type="primary" @click="saveExpertScores">确认并生成对比</el-button>
-    </template>
-  </el-dialog>
-</template>
-
-<script setup lang="ts">
-import MarkdownIt from 'markdown-it'
-import 'highlight.js/styles/vs2015.min.css'
-import hljs from 'highlight.js'
-import { computed, reactive, ref } from 'vue'
-
-const props = defineProps({
-  content: {
-    type: String,
-    required: true
-  },
-  citationIds: {
-    type: Array,
-    default: () => []
-  },
-  theme: {
-    type: String,
-    default: 'light',
-    validator: (value: string) => ['light', 'dark'].includes(value)
-  },
-  enableStudentScore: {
-    type: Boolean,
-    default: false
-  },
-  scoreOverrides: {
-    type: Object,
-    default: () => ({})
-  }
-})
-
-const emit = defineEmits(['citation-click', 'student-score-change'])
-
-const contentRef = ref()
-const expertDialogVisible = ref(false)
-const editingSegment = ref<any>(null)
-const expertDraft = reactive<Record<string, number | null>>({})
-
-const SCORE_DIMENSION_KEYS = [
-  'customer_context',
-  'customer_insight',
-  'policy_interpretation',
-  'opportunity_risk',
-  'objection_reframing',
-  'fabetc'
-]
-
-type ScoreDimension = {
-  key: string
-  name: string
-  shortName: string
-  aiScore: number | null
-}
-
-type StudentScoreData = {
-  studentId: string
-  studentName: string
-  maxScore: number
-  dimensions: ScoreDimension[]
-}
-
-type MarkdownSegment = {
-  key: string
-  type: 'markdown'
-  html: string
-}
-
-type RadarSegment = {
-  key: string
-  type: 'radar'
-  blockKey: string
-  data: StudentScoreData
-}
-
-type RenderedSegment = MarkdownSegment | RadarSegment
-
-/**
- * 将表格行按 | 分割,去除首尾空字符串,返回单元格数组
- */
-function parseTableRow(line: string): string[] {
-  const parts = line.split('|')
-  // 去除首尾空元素(因为 | 在开头和结尾)
-  if (parts[0]?.trim() === '') parts.shift()
-  if (parts[parts.length - 1]?.trim() === '') parts.pop()
-  return parts.map(p => p.trim())
-}
-
-/**
- * 检查字符串是否为有效的表格分隔符单元格
- * 标准格式:---, :---, ---:, :---:
- */
-function isSeparatorCell(cell: string): boolean {
-  return /^:?-{2,}:?$/.test(cell)
-}
-
-/**
- * 预处理 Markdown 内容,修复不规范的表格格式
- * 主要修复:分隔符行列数不匹配、格式错误、多余的竖线、空单元格格式问题
- */
-function normalizeMarkdownTables(content: string): string {
-  if (!content) return content
-
-  const lines = content.split('\n')
-  const result: string[] = []
-  let i = 0
-
-  while (i < lines.length) {
-    const line = lines[i]
-    const trimmedLine = line.trim()
-
-    // 检测可能的表格起始行(以 | 开头结尾,且包含多个单元格)
-    if (trimmedLine.startsWith('|') && trimmedLine.endsWith('|') && trimmedLine.includes('|', 1) && i + 1 < lines.length) {
-      const headerCells = parseTableRow(trimmedLine)
-      const expectedCols = headerCells.length
-
-      // 至少需要2列才能构成表格
-      if (expectedCols >= 2) {
-        const nextLineRaw = lines[i + 1]
-        const nextLine = nextLineRaw?.trim() || ''
-
-        // 检查下一行是否看起来像分隔符行(包含大量 | 和 -)
-        const looksLikeSeparator = nextLine.startsWith('|') && nextLine.endsWith('|')
-            && nextLine.includes('-') && /^\|[\s\-:|]+\|$/.test(nextLine)
-
-        // 或尝试宽松检测:包含足够多的 | 和 - 字符
-        const pipeCount = (nextLine.match(/\|/g) || []).length
-        const dashCount = (nextLine.match(/-/g) || []).length
-        const looseSeparatorMatch = pipeCount >= 2 && dashCount >= 3
-
-        if (looksLikeSeparator || looseSeparatorMatch) {
-          // 尝试解析分隔符行的单元格
-          let sepCells = parseTableRow(nextLine)
-
-          // 如果分隔符单元格解析后大部分是有效的分隔符格式,确认是表格
-          const validSepCount = sepCells.filter(c => isSeparatorCell(c) || /^-+$/.test(c.replace(/\s/g, ''))).length
-
-          if (validSepCount >= 1 || (pipeCount >= expectedCols - 1 && dashCount >= expectedCols * 2)) {
-            // 确认是表格,开始修复
-
-            // 修复分隔符行:确保列数与表头一致,每个单元格是 --- 格式
-            const fixedSepCells: string[] = []
-            for (let k = 0; k < expectedCols; k++) {
-              const orig = sepCells[k] || ''
-              // 保留原有的对齐标记(:)
-              let leftAlign = orig.startsWith(':')
-              let rightAlign = orig.endsWith(':')
-              fixedSepCells.push((leftAlign ? ':' : '') + '---' + (rightAlign ? ':' : ''))
-            }
-            const fixedSepLine = '| ' + fixedSepCells.join(' | ') + ' |'
-
-            const tableLines: string[] = []
-            // 修复表头行,确保单元格之间格式正确
-            tableLines.push('| ' + headerCells.join(' | ') + ' |')
-            tableLines.push(fixedSepLine)
-
-            let j = i + 2
-            // 收集并修复数据行
-            while (j < lines.length) {
-              const dataLineRaw = lines[j]
-              const dataLine = dataLineRaw?.trim() || ''
-
-              if (dataLine.startsWith('|') && dataLine.endsWith('|')) {
-                let dataCells = parseTableRow(dataLine)
-
-                // 修复数据行列数:不足补空,多余截断
-                if (dataCells.length < expectedCols) {
-                  while (dataCells.length < expectedCols) dataCells.push('')
-                } else if (dataCells.length > expectedCols) {
-                  dataCells = dataCells.slice(0, expectedCols)
-                }
-
-                tableLines.push('| ' + dataCells.join(' | ') + ' |')
-                j++
-              } else if (dataLine === '') {
-                // 空行可能表示表格结束,但也可能是表格内的换行(保守处理:表格结束)
-                break
-              } else {
-                break
-              }
-            }
-
-            result.push(...tableLines)
-            i = j
-            continue
-          }
-        }
-      }
-    }
-
-    result.push(line)
-    i++
-  }
-
-  return result.join('\n')
-}
-
-// ====== 创建 markdown-it 实例 ======
-const md = new MarkdownIt({
-  html: true,
-  linkify: true,
-  typographer: true,
-  breaks: false,
-  highlight: function (str, lang) {
-    if (lang && hljs.getLanguage(lang)) {
-      try {
-        const copyHtml = `<div id="copy" data-copy='${str}' style="position: absolute; right: 10px; top: 5px; color: #fff;cursor: pointer;">复制</div>`
-        return `<pre style="position: relative;">${copyHtml}<code class="hljs">${hljs.highlight(lang, str, true).value}</code></pre>`
-      } catch (__) {}
-    }
-    return `<pre><code>${escapeHtml(str)}</code></pre>`
-  }
-})
-
-// ====== 关键修复:正确启用表格 ======
-md.enable('table')
-
-md.inline.ruler.before('text', 'citation_mark', (state, silent) => {
-  const match = /^\[S(\d+)]/.exec(state.src.slice(state.pos, state.posMax))
-  const citationIds = Array.isArray(state.env.citationIds) ? state.env.citationIds : []
-  if (!match || !citationIds.includes(Number(match[1]))) return false
-  if (silent) return true
-
-  const token = state.push('citation_mark', '', 0)
-  token.meta = { id: Number(match[1]) }
-  token.content = match[0]
-  state.pos += match[0].length
-  return true
-})
-
-md.renderer.rules.citation_mark = (tokens, idx) => {
-  const id = tokens[idx].meta.id
-  return `<button class="citation-mark" type="button" data-citation-id="${id}">${tokens[idx].content}</button>`
-}
-
-function handleCitationClick(event: MouseEvent) {
-  const target = event.target as HTMLElement | null
-  const mark = target?.closest<HTMLElement>('[data-citation-id]')
-  const segmentId = Number(mark?.dataset.citationId)
-  if (Number.isFinite(segmentId)) emit('citation-click', segmentId)
-}
-
-function escapeHtml(text: string): string {
-  const map: Record<string, string> = {
-    '&': '&amp;',
-    '<': '&lt;',
-    '>': '&gt;',
-    '"': '&quot;',
-    "'": '&#039;'
-  }
-  return text.replace(/[&<>"']/g, function(m) { return map[m] })
-}
-
-// ====== 覆盖表格渲染规则 ======
-const defaultTableOpen = md.renderer.rules.table_open
-const defaultTableClose = md.renderer.rules.table_close
-
-md.renderer.rules.table_open = function(tokens, idx, options, env, self) {
-  return '<div class="markdown-table-wrap"><table>'
-}
-
-md.renderer.rules.table_close = function(tokens, idx, options, env, self) {
-  return '</table></div>'
-}
-
-// ====== 确保 tbody 正确渲染 ======
-md.renderer.rules.tbody_open = function(tokens, idx, options, env, self) {
-  return '<tbody>'
-}
-
-md.renderer.rules.tbody_close = function(tokens, idx, options, env, self) {
-  return '</tbody>'
-}
-
-function normalizeStudentScoreData(value: unknown): StudentScoreData | null {
-  if (!value || typeof value !== 'object') return null
-  const raw = value as Record<string, any>
-  const maxScore = Number(raw.maxScore)
-  if (!raw.studentName || !Number.isInteger(maxScore) || maxScore !== 5 || !Array.isArray(raw.dimensions)) return null
-  if (raw.dimensions.length !== SCORE_DIMENSION_KEYS.length) return null
-
-  const dimensions: ScoreDimension[] = []
-  for (let index = 0; index < SCORE_DIMENSION_KEYS.length; index++) {
-    const item = raw.dimensions[index]
-    if (!item || item.key !== SCORE_DIMENSION_KEYS[index] || !item.name || !item.shortName) return null
-    const aiScore = item.aiScore === null ? null : Number(item.aiScore)
-    if (aiScore !== null && (!Number.isInteger(aiScore) || aiScore < 1 || aiScore > maxScore)) return null
-    dimensions.push({
-      key: String(item.key),
-      name: String(item.name),
-      shortName: String(item.shortName),
-      aiScore
-    })
-  }
-
-  return {
-    studentId: String(raw.studentId || ''),
-    studentName: String(raw.studentName),
-    maxScore,
-    dimensions
-  }
-}
-
-function renderMarkdown(markdown: string): string {
-  if (!markdown) return ''
-  const normalizedContent = normalizeMarkdownTables(markdown)
-  return md.render(normalizedContent, { citationIds: props.citationIds })
-}
-
-const renderedSegments = computed<any[]>(() => {
-  if (!props.content) return []
-  if (!props.enableStudentScore) {
-    return [{ key: 'markdown-0', type: 'markdown', html: renderMarkdown(props.content) }]
-  }
-
-  const segments: RenderedSegment[] = []
-  const pattern = /```student-score-json\s*\r?\n([\s\S]*?)\r?\n```/g
-  let cursor = 0
-  let blockIndex = 0
-  let match: RegExpExecArray | null
-
-  try {
-    while ((match = pattern.exec(props.content)) !== null) {
-      const before = props.content.slice(cursor, match.index)
-      if (before) segments.push({ key: `markdown-${blockIndex}`, type: 'markdown', html: renderMarkdown(before) })
-
-      let data: StudentScoreData | null = null
-      try {
-        data = normalizeStudentScoreData(JSON.parse(match[1]))
-      } catch (_) {
-        data = null
-      }
-
-      if (data) {
-        const identity = data.studentId || data.studentName || `student-${blockIndex + 1}`
-        const blockKey = `${identity}-${blockIndex}`
-        segments.push({ key: `radar-${blockKey}`, type: 'radar', blockKey, data })
-      } else {
-        segments.push({ key: `markdown-invalid-${blockIndex}`, type: 'markdown', html: renderMarkdown(match[0]) })
-      }
-
-      cursor = pattern.lastIndex
-      blockIndex += 1
-    }
-
-    const rest = props.content.slice(cursor)
-    if (rest) segments.push({ key: `markdown-${blockIndex}`, type: 'markdown', html: renderMarkdown(rest) })
-    return segments
-  } catch (error) {
-    console.error('Markdown 渲染失败:', error)
-    return [{ key: 'markdown-error', type: 'markdown', html: escapeHtml(props.content) }]
-  }
-})
-
-const RADAR_RADIUS = 104
-const LABEL_RADIUS = 132
-
-function polarPoint(index: number, count: number, ratio: number, radius = RADAR_RADIUS) {
-  const angle = -Math.PI / 2 + (Math.PI * 2 * index) / count
-  return {
-    x: Number((Math.cos(angle) * radius * ratio).toFixed(2)),
-    y: Number((Math.sin(angle) * radius * ratio).toFixed(2))
-  }
-}
-
-function gridPoints(count: number, ratio: number) {
-  return Array.from({ length: count }, (_, index) => {
-    const point = polarPoint(index, count, ratio)
-    return `${point.x},${point.y}`
-  }).join(' ')
-}
-
-function getExpertScoreMap(segment: RadarSegment): Record<string, number | null> {
-  const value = props.scoreOverrides?.[segment.blockKey]
-  return value && typeof value === 'object' ? value : {}
-}
-
-function expertScore(segment: RadarSegment, key: string): number | null {
-  const value = getExpertScoreMap(segment)[key]
-  return Number.isInteger(value) && Number(value) >= 1 && Number(value) <= segment.data.maxScore ? Number(value) : null
-}
-
-function hasExpertScores(segment: RadarSegment) {
-  return segment.data.dimensions.some((dimension) => expertScore(segment, dimension.key) !== null)
-}
-
-function scorePoint(segment: RadarSegment, index: number, source: 'ai' | 'expert') {
-  const dimension = segment.data.dimensions[index]
-  const score = source === 'ai' ? dimension.aiScore : expertScore(segment, dimension.key)
-  return polarPoint(index, segment.data.dimensions.length, (score ?? 0) / segment.data.maxScore)
-}
-
-function scorePoints(segment: RadarSegment, source: 'ai' | 'expert') {
-  return segment.data.dimensions.map((_, index) => {
-    const point = scorePoint(segment, index, source)
-    return `${point.x},${point.y}`
-  }).join(' ')
-}
-
-function labelPoint(index: number, count: number) {
-  return polarPoint(index, count, 1, LABEL_RADIUS)
-}
-
-function labelAnchor(index: number, count: number) {
-  const x = labelPoint(index, count).x
-  if (Math.abs(x) < 8) return 'middle'
-  return x > 0 ? 'start' : 'end'
-}
-
-function formatScore(score: number | null) {
-  return score === null ? '未评分' : `${score}分`
-}
-
-function openExpertDialog(segment: RadarSegment) {
-  editingSegment.value = segment
-  Object.keys(expertDraft).forEach((key) => delete expertDraft[key])
-  segment.data.dimensions.forEach((dimension) => {
-    expertDraft[dimension.key] = expertScore(segment, dimension.key)
-  })
-  expertDialogVisible.value = true
-}
-
-function saveExpertScores() {
-  const segment = editingSegment.value as RadarSegment | null
-  if (!segment) return
-  const scores: Record<string, number | null> = {}
-  segment.data.dimensions.forEach((dimension) => {
-    const value = expertDraft[dimension.key]
-    scores[dimension.key] = Number.isInteger(value) ? Number(value) : null
-  })
-  emit('student-score-change', {
-    blockKey: segment.blockKey,
-    studentId: segment.data.studentId,
-    studentName: segment.data.studentName,
-    scores
-  })
-  expertDialogVisible.value = false
-}
-</script>
-
-<style lang="scss">
-.markdown-view {
-  --md-text: #000;
-  --md-heading: #000;
-  --md-table-wrap-bg: rgba(15, 23, 42, .02);
-  --md-table-border: rgba(15, 23, 42, .18);
-  --md-table-head-bg: #edf3fa;
-  --md-table-head-text: #000;
-  --md-table-cell-text: #000;
-  --md-table-row-even: rgba(15, 23, 42, .035);
-  --md-table-row-hover: rgba(74, 137, 231, .1);
-  --md-scroll-track: rgba(15, 23, 42, .05);
-  --md-scroll-thumb: rgba(74, 137, 231, .35);
-  --md-scroll-thumb-hover: rgba(74, 137, 231, .55);
-  font-family: 'SourceHanSansCN-Normal';
-  font-weight: 400;
-  letter-spacing: 0em;
-  text-align: left;
-  color: var(--md-text);
-  max-width: 100%;
-
-  &.markdown-view--dark {
-    --md-text: #f0f5ff;
-    --md-heading: #f0f5ff;
-    --md-table-wrap-bg: rgba(255, 255, 255, .02);
-    --md-table-border: rgba(174, 205, 255, .25);
-    --md-table-head-bg: rgba(74, 137, 231, .4);
-    --md-table-head-text: #fff;
-    --md-table-cell-text: #e8f0ff;
-    --md-table-row-even: rgba(255, 255, 255, .03);
-    --md-table-row-hover: rgba(74, 137, 231, .1);
-    --md-scroll-track: rgba(255, 255, 255, .04);
-    --md-scroll-thumb: rgba(74, 137, 231, .35);
-    --md-scroll-thumb-hover: rgba(74, 137, 231, .55);
-  }
-
-  pre {
-    position: relative;
-  }
-
-  pre code.hljs {
-    width: auto;
-  }
-
-  code.hljs {
-    border-radius: 6px;
-    padding-top: 20px;
-    width: auto;
-    @media screen and (min-width: 1536px) {
-      width: 960px;
-    }
-    @media screen and (max-width: 1536px) and (min-width: 1024px) {
-      width: calc(100vw - 400px - 64px - 32px * 2);
-    }
-    @media screen and (max-width: 1024px) and (min-width: 768px) {
-      width: calc(100vw - 32px * 2);
-    }
-    @media screen and (max-width: 768px) {
-      width: calc(100vw - 16px * 2);
-    }
-  }
-
-  p, code.hljs {
-    margin-bottom: 16px;
-  }
-
-  p {
-    margin: 0;
-    margin-bottom: 3px;
-  }
-
-  h1, h2, h3, h4, h5, h6 {
-    color: var(--md-heading);
-    margin: 24px 0 8px;
-    font-weight: 600;
-  }
-
-  h1 { font-size: 22px; line-height: 32px; }
-  h2 { font-size: 20px; line-height: 30px; }
-  h3 { font-size: 18px; line-height: 28px; }
-  h4 { font-size: 16px; line-height: 26px; }
-  h5 { font-size: 16px; line-height: 24px; }
-  h6 { font-size: 16px; line-height: 24px; }
-
-  ul, ol {
-    margin: 0 0 8px 0;
-    padding: 0;
-    font-size: 16px;
-    line-height: 24px;
-    color: var(--md-text);
-  }
-
-  li {
-    margin: 4px 0 0 20px;
-    margin-bottom: 1rem;
-  }
-
-  ol > li {
-    list-style-type: decimal;
-    margin-bottom: 1rem;
-  }
-
-  ul > li {
-    list-style-type: disc;
-    font-size: 16px;
-    line-height: 24px;
-    margin-right: 11px;
-    margin-bottom: 1rem;
-    color: var(--md-text);
-  }
-
-  ol ul, ol ul > li, ul ul, ul ul li {
-    font-size: 16px;
-    list-style: none;
-    margin-left: 6px;
-    margin-bottom: 1rem;
-  }
-
-  ul ul ul, ul ul ul li, ol ol, ol ol > li, ol ul ul, ol ul ul > li, ul ol, ul ol > li {
-    list-style: square;
-  }
-
-  // 表格容器样式
-  .markdown-table-wrap {
-    display: block !important;
-    overflow-x: auto;
-    margin: 16px 0;
-    border-radius: 8px;
-    border: 1px solid var(--md-table-border) !important;
-    -webkit-overflow-scrolling: touch;
-    background: var(--md-table-wrap-bg);
-
-    &::-webkit-scrollbar {
-      height: 6px;
-    }
-    &::-webkit-scrollbar-track {
-      background: var(--md-scroll-track);
-      border-radius: 3px;
-    }
-    &::-webkit-scrollbar-thumb {
-      background: var(--md-scroll-thumb);
-      border-radius: 3px;
-    }
-    &::-webkit-scrollbar-thumb:hover {
-      background: var(--md-scroll-thumb-hover);
-    }
-  }
-
-  // 强制表格元素使用正确的display属性,防止被其他样式覆盖
-  table {
-    display: table !important;
-    width: 100% !important;
-    min-width: 480px !important;
-    border-collapse: collapse !important;
-    border-spacing: 0 !important;
-    margin: 0 !important;
-    font-size: 13px;
-    table-layout: auto;
-  }
-
-  thead {
-    display: table-header-group !important;
-  }
-
-  tbody {
-    display: table-row-group !important;
-  }
-
-  tr {
-    display: table-row !important;
-    page-break-inside: avoid;
-  }
-
-  th {
-    display: table-cell !important;
-    background: var(--md-table-head-bg) !important;
-    color: var(--md-table-head-text) !important;
-    font-weight: 600 !important;
-    padding: 10px 14px !important;
-    border: 1px solid var(--md-table-border) !important;
-    text-align: left !important;
-    white-space: nowrap;
-    position: relative;
-  }
-
-  td {
-    display: table-cell !important;
-    color: var(--md-table-cell-text) !important;
-    padding: 10px 14px !important;
-    border: 1px solid var(--md-table-border) !important;
-    vertical-align: top !important;
-    text-align: left !important;
-    word-break: break-word;
-    line-height: 1.6;
-  }
-
-  // 表格斑马纹效果
-  tr:nth-child(even) td {
-    background: var(--md-table-row-even) !important;
-  }
-
-  tr:nth-child(odd) td {
-    background: transparent !important;
-  }
-
-  // 表格hover效果
-  tr:hover td {
-    background: var(--md-table-row-hover) !important;
-  }
-
-  // 表格内段落样式修正
-  table p, td p, th p {
-    margin: 0 !important;
-    padding: 0 !important;
-  }
-
-  // 防止表格内容被转义成纯文本
-  td, th {
-    white-space: normal;
-  }
-
-  .markdown-score-radar {
-    margin: 16px 0 8px;
-    overflow: hidden;
-    border: 1px solid var(--md-table-border);
-    border-radius: 12px;
-    background: var(--md-table-wrap-bg);
-  }
-
-  .markdown-score-radar__header {
-    display: flex;
-    align-items: center;
-    justify-content: space-between;
-    gap: 12px;
-    padding: 14px 16px;
-    border-bottom: 1px solid var(--md-table-border);
-
-    strong, small { display: block; }
-    strong { color: var(--md-heading); font-size: 15px; }
-    small { margin-top: 2px; color: var(--md-text); opacity: .62; font-size: 11px; }
-  }
-
-  .markdown-score-radar__action {
-    flex: 0 0 auto;
-    padding: 7px 12px;
-    border: 1px solid rgba(91, 152, 237, .7);
-    border-radius: 8px;
-    color: #fff;
-    background: linear-gradient(135deg, #5b98ed, #4a72ca);
-    font: inherit;
-    font-size: 12px;
-    cursor: pointer;
-  }
-
-  .markdown-score-radar__chart {
-    display: block;
-    width: min(100%, 560px);
-    height: auto;
-    margin: 4px auto 0;
-    overflow: visible;
-  }
-
-  .markdown-score-radar__grid,
-  .markdown-score-radar__axis {
-    fill: none;
-    stroke: var(--md-table-border);
-    stroke-width: 1;
-  }
-
-  .markdown-score-radar__area {
-    stroke-width: 2.2;
-    stroke-linejoin: round;
-  }
-
-  .markdown-score-radar__area--ai { fill: rgba(62, 130, 219, .24); stroke: #4f9cff; }
-  .markdown-score-radar__area--expert { fill: rgba(245, 158, 11, .18); stroke: #f59e0b; }
-  .markdown-score-radar__point--ai { fill: #4f9cff; }
-  .markdown-score-radar__point--expert { fill: #f59e0b; }
-  .markdown-score-radar__label { fill: var(--md-text); font-size: 11px; }
-
-  .markdown-score-radar__legend {
-    display: flex;
-    justify-content: center;
-    gap: 20px;
-    margin: -12px 0 12px;
-    color: var(--md-text);
-    font-size: 12px;
-
-    span { display: inline-flex; align-items: center; gap: 6px; }
-  }
-
-  .markdown-score-radar__legend-dot {
-    width: 9px;
-    height: 9px;
-    border-radius: 50%;
-  }
-
-  .markdown-score-radar__legend-dot--ai { background: #4f9cff; }
-  .markdown-score-radar__legend-dot--expert { background: #f59e0b; }
-
-  .markdown-score-radar__score-list {
-    display: grid;
-    grid-template-columns: repeat(2, minmax(0, 1fr));
-    border-top: 1px solid var(--md-table-border);
-
-    > div {
-      display: grid;
-      grid-template-columns: minmax(0, 1fr) auto auto;
-      gap: 8px;
-      padding: 8px 12px;
-      border-right: 1px solid var(--md-table-border);
-      border-bottom: 1px solid var(--md-table-border);
-      color: var(--md-text);
-      font-size: 11px;
-    }
-
-    span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
-    b { font-weight: 500; white-space: nowrap; }
-  }
-
-  @media (max-width: 620px) {
-    .markdown-score-radar__header { align-items: flex-start; flex-direction: column; }
-    .markdown-score-radar__score-list { grid-template-columns: 1fr; }
-    .markdown-score-radar__label { font-size: 9px; }
-  }
-}
-
-.markdown-score-dialog__hint {
-  margin: 0 0 12px;
-  color: #64748b;
-  font-size: 13px;
-}
-
-.markdown-score-dialog__list {
-  display: grid;
-  gap: 10px;
-
-  label {
-    display: grid;
-    grid-template-columns: minmax(0, 1fr) 120px;
-    align-items: center;
-    gap: 12px;
-  }
-
-  span { font-size: 13px; }
-}
-</style>

+ 0 - 21
src/views/qsfl/qsflDemo1.vue

@@ -1,21 +0,0 @@
-<template>
-  <AiQAChat
-    :config="qaConfig"
-    :create-dialogue-api="CreateDialogue"
-    :send-chat-stream-api="sendChatMessageStream"
-  />
-</template>
-
-<script setup>
-import AiQAChat from './components/AiQAChat1.vue'
-import { CreateDialogue, sendChatMessageStream } from '@/api/questions.js'
-
-const qaConfig = Object.freeze({
-  entries: [
-    { key: 'qa', label: '智能问答', roleId: 263 },
-    { key: 'practice', label: '智能陪练', roleId: 266 }
-  ],
-  enableFileUpload: false,
-  suggestions: []
-})
-</script>

+ 0 - 28
src/views/qsfl/qsflDemo2.vue

@@ -1,28 +0,0 @@
-<template>
-  <AiQAChat2
-    :config="qaConfig"
-    :create-dialogue-api="createQsAiDialogue"
-    :send-chat-stream-api="sendQsAiChatMessageStream"
-  />
-</template>
-
-<script setup>
-import AiQAChat2 from './components/AiQAChat2.vue'
-import { createQsAiDialogue, sendQsAiChatMessageStream } from '@/api/qsAiQuestions.js'
-
-const qaConfig = Object.freeze({
-  roleId: 264,
-  enableFileUpload: true,
-  maxFiles: 3,
-  maxFileSizeMB: 10,
-  maxTotalFileSizeMB: 15,
-  invalidFileNamePattern: /[\\/:*?"<>|\s]/,
-  invalidFileNameMessage: '文件名不能包含空格或\\ / : * ? " < > | 等非法字符,请修改后重试',
-  enableReferenceScript: true,
-  referenceScriptMaxLength: 2000,
-  maxInputLength: 1000,
-  uploadHint: '支持图片、PDF、Word、Excel 文件;单次最多上传 3 个附件,单个文件不超过 10 MB,附件总大小不超过 15 MB。文档内容过长时,系统将仅分析前 20,000 个字符。',
-  inputHint: '请输入或粘贴学员拜访记录文字稿,最多 1,000 个字符。',
-  suggestions: ['请对附件中学员拜访记录进行评分。']
-})
-</script>

+ 0 - 28
src/views/qsfl/qsflDemo3.vue

@@ -1,28 +0,0 @@
-<template>
-  <AiQAChat2
-    :config="qaConfig"
-    :create-dialogue-api="createQsAiDialogue"
-    :send-chat-stream-api="sendQsAiChatMessageStream"
-  />
-</template>
-
-<script setup>
-import AiQAChat2 from './components/AiQAChat2.vue'
-import { createQsAiDialogue, sendQsAiChatMessageStream } from '@/api/qsAiQuestions.js'
-
-const qaConfig = Object.freeze({
-  roleId: 265,
-  enableFileUpload: true,
-  maxFiles: 3,
-  maxFileSizeMB: 10,
-  maxTotalFileSizeMB: 15,
-  invalidFileNamePattern: /[\\/:*?"<>|\s]/,
-  invalidFileNameMessage: '文件名不能包含空格或\\ / : * ? " < > | 等非法字符,请修改后重试',
-  enableReferenceScript: true,
-  referenceScriptMaxLength: 2000,
-  maxInputLength: 1000,
-  uploadHint: '支持图片、PDF、Word、Excel 文件;单次最多上传 3 个附件,单个文件不超过 10 MB,附件总大小不超过 15 MB。文档内容过长时,系统将仅分析前 20,000 个字符。',
-  inputHint: '请输入或粘贴学员拜访记录文字稿,最多 1,000 个字符。',
-  suggestions: ['请对附件中学员拜访记录进行评分:']
-})
-</script>

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä