Browse Source

强生:demo1问答和陪练拆分成两个

liyanbo 1 tháng trước cách đây
mục cha
commit
0fac092c18

+ 7 - 1
src/router/index.js

@@ -21,6 +21,7 @@ const routes = [
   { path: '/quick-login', component: () => import('../views/QuickLogin.vue') },
   { path: '/quick-login', component: () => import('../views/QuickLogin.vue') },
   { path: '/promotion-login', component: () => import('../views/PromotionLogin.vue') },
   { path: '/promotion-login', component: () => import('../views/PromotionLogin.vue') },
   { path: '/qsfl-demo1', component: () => import('../views/qsfl/LoginQsfl.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-demo2', component: () => import('../views/qsfl/LoginQsfl2.vue') },
   { path: '/qsfl-demo3', component: () => import('../views/qsfl/LoginQsfl3.vue') },
   { path: '/qsfl-demo3', component: () => import('../views/qsfl/LoginQsfl3.vue') },
   // //【AI实验课】登录
   // //【AI实验课】登录
@@ -191,6 +192,10 @@ const routes = [
     path: '/qs-demo1',
     path: '/qs-demo1',
     component: () => import('../views/qsfl/qsflDemo1.vue')
     component: () => import('../views/qsfl/qsflDemo1.vue')
   },
   },
+  {
+    path: '/qs-demo1-2',
+    component: () => import('../views/qsfl/qsflDemo1-2.vue')
+  },
   {
   {
     path: '/qs-demo2',
     path: '/qs-demo2',
     component: () => import('../views/qsfl/qsflDemo2.vue')
     component: () => import('../views/qsfl/qsflDemo2.vue')
@@ -329,6 +334,7 @@ router.beforeEach(async (to, from, next) => {
   // 如果未登录且不是允许访问的页面,重定向到登录页
   // 如果未登录且不是允许访问的页面,重定向到登录页
   if (!isLoggedIn && !allowedPages.includes(to.path)) {
   if (!isLoggedIn && !allowedPages.includes(to.path)) {
     if (to.path === '/qs-demo1')next('/qsfl-demo1')
     if (to.path === '/qs-demo1')next('/qsfl-demo1')
+    if (to.path === '/qs-demo1-2')next('/qsfl-demo1-2')
     if (to.path === '/qs-demo2')next('/qsfl-demo2')
     if (to.path === '/qs-demo2')next('/qsfl-demo2')
     if (to.path === '/qs-demo3')next('/qsfl-demo3')
     if (to.path === '/qs-demo3')next('/qsfl-demo3')
     next('/login')
     next('/login')
@@ -371,7 +377,7 @@ router.beforeEach(async (to, from, next) => {
   const hasManagementPermission = true // 管理界面默认允许所有登录用户访问
   const hasManagementPermission = true // 管理界面默认允许所有登录用户访问
 
 
   // 检查目标路由是否在允许的范围内
   // 检查目标路由是否在允许的范围内
-  if (['/qs-demo1', '/qs-demo2', '/qs-demo3'].includes(to.path)) {
+  if (['/qs-demo1','/qs-demo1-2', '/qs-demo2', '/qs-demo3'].includes(to.path)) {
     // 强生菲林问答页要求已登录,但不受课程菜单角色权限限制。
     // 强生菲林问答页要求已登录,但不受课程菜单角色权限限制。
     isAllowed = true
     isAllowed = true
   } else if ((to.path === managementRoutes.home || managementRoutes.children.includes(to.path)) && hasManagementPermission) {
   } else if ((to.path === managementRoutes.home || managementRoutes.children.includes(to.path)) && hasManagementPermission) {

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

@@ -0,0 +1,63 @@
+<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>

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 840 - 0
src/views/qsfl/components/AiQAChat1-2.vue


+ 5 - 4
src/views/qsfl/components/AiQAChat.vue → src/views/qsfl/components/AiQAChat1.vue

@@ -41,8 +41,8 @@ const DEFAULT_AI_QA_CONFIG = Object.freeze({
 })
 })
 
 
 const VBP_CITATION_RESOURCE = Object.freeze({
 const VBP_CITATION_RESOURCE = Object.freeze({
-  documentTitle: 'VBP执行期的生存法则逐字稿',
-  documentUrl: `/qsfl/${encodeURIComponent('《VBP执行期的生存法则》逐字稿.pdf')}`,
+  documentTitle: 'VBP执行期的生存法则文字版',
+  documentUrl: `/qsfl/${encodeURIComponent('《VBP执行期的生存法则》文字版.pdf')}`,
   videoTitle: 'VBP执行期的生存法则相关视频',
   videoTitle: 'VBP执行期的生存法则相关视频',
   videoUrl: `/qsfl/${encodeURIComponent('VBP执行期的生存法则相关视频.mp4')}`
   videoUrl: `/qsfl/${encodeURIComponent('VBP执行期的生存法则相关视频.mp4')}`
 })
 })
@@ -437,6 +437,7 @@ watch(isAudioSpeaking, async (speaking) => {
   if (!video) return
   if (!video) return
   if (speaking) {
   if (speaking) {
     video.currentTime = 0
     video.currentTime = 0
+    video.playbackRate = 1.5
     video.play().catch((error) => console.warn('数字人视频播放失败', error))
     video.play().catch((error) => console.warn('数字人视频播放失败', error))
   } else {
   } else {
     resetDigitalHumanVideo()
     resetDigitalHumanVideo()
@@ -725,8 +726,8 @@ onUnmounted(() => {
               muted
               muted
               playsinline
               playsinline
               preload="auto"
               preload="auto"
+              :loop="isAudioSpeaking"
               @loadedmetadata="resetDigitalHumanVideo"
               @loadedmetadata="resetDigitalHumanVideo"
-              @ended="resetDigitalHumanVideo"
             ></video>
             ></video>
           </div>
           </div>
           <div class="digital-human-status"><i></i>{{ isAudioSpeaking ? '正在为您播报' : '在线为您服务' }}</div>
           <div class="digital-human-status"><i></i>{{ isAudioSpeaking ? '正在为您播报' : '在线为您服务' }}</div>
@@ -738,7 +739,7 @@ onUnmounted(() => {
             <p class="welcome-copy">向非临 AI 助教描述您的问题,获得及时的智能解答。</p>
             <p class="welcome-copy">向非临 AI 助教描述您的问题,获得及时的智能解答。</p>
             <div class="initial-conversation-actions">
             <div class="initial-conversation-actions">
               <button class="initial-conversation-button" type="button" :disabled="isCreatingConversation" @click="createInitialConversation('qa')"><el-icon><Plus /></el-icon>智能问答</button>
               <button class="initial-conversation-button" type="button" :disabled="isCreatingConversation" @click="createInitialConversation('qa')"><el-icon><Plus /></el-icon>智能问答</button>
-              <button class="initial-conversation-button" type="button" :disabled="isCreatingConversation" @click="createInitialConversation('practice')"><el-icon><Plus /></el-icon>智能陪练</button>
+<!--              <button class="initial-conversation-button" type="button" :disabled="isCreatingConversation" @click="createInitialConversation('practice')"><el-icon><Plus /></el-icon>智能陪练</button>-->
             </div>
             </div>
           </div>
           </div>
 
 

+ 9 - 6
src/views/qsfl/components/AiQAChat2.vue

@@ -39,8 +39,8 @@ const DEFAULT_AI_QA_CONFIG = Object.freeze({
 })
 })
 
 
 const VBP_CITATION_RESOURCE = Object.freeze({
 const VBP_CITATION_RESOURCE = Object.freeze({
-  documentTitle: 'VBP执行期的生存法则逐字稿',
-  documentUrl: `/qsfl/${encodeURIComponent('《VBP执行期的生存法则》逐字稿.pdf')}`,
+  documentTitle: 'VBP执行期的生存法则文字版',
+  documentUrl: `/qsfl/${encodeURIComponent('《VBP执行期的生存法则》文字版.pdf')}`,
   videoTitle: 'VBP执行期的生存法则相关视频',
   videoTitle: 'VBP执行期的生存法则相关视频',
   videoUrl: `/qsfl/${encodeURIComponent('VBP执行期的生存法则相关视频.mp4')}`
   videoUrl: `/qsfl/${encodeURIComponent('VBP执行期的生存法则相关视频.mp4')}`
 })
 })
@@ -432,6 +432,7 @@ watch(isAudioSpeaking, async (speaking) => {
   if (!video) return
   if (!video) return
   if (speaking) {
   if (speaking) {
     video.currentTime = 0
     video.currentTime = 0
+    video.playbackRate = 1.5
     video.play().catch((error) => console.warn('数字人视频播放失败', error))
     video.play().catch((error) => console.warn('数字人视频播放失败', error))
   } else {
   } else {
     resetDigitalHumanVideo()
     resetDigitalHumanVideo()
@@ -478,7 +479,7 @@ const copyMessageContent = async (content, event) => {
   }
   }
 }
 }
 
 
-const getRequestContent = (content, script) => script ? `补充评分规则:\n${script}\n\n学员拜访记录:\n${content}` : content
+const getRequestContent = (content, script) => script ? `${content}\n 补充评分规则:\n${script}` : content
 const selectFile = () => fileInputRef.value?.click()
 const selectFile = () => fileInputRef.value?.click()
 const formatSize = (size) => size < 1024 * 1024 ? `${Math.max(1, Math.round(size / 1024))} KB` : `${(size / 1024 / 1024).toFixed(1)} MB`
 const formatSize = (size) => size < 1024 * 1024 ? `${Math.max(1, Math.round(size / 1024))} KB` : `${(size / 1024 / 1024).toFixed(1)} MB`
 const getFileExtension = (fileName) => fileName.split('.').pop()?.toLowerCase() || ''
 const getFileExtension = (fileName) => fileName.split('.').pop()?.toLowerCase() || ''
@@ -534,6 +535,8 @@ const sendMessage = async () => {
   typewriterFullText = ''
   typewriterFullText = ''
   typewriterIndex = 0
   typewriterIndex = 0
   try {
   try {
+    console.log(getRequestContent(content, script))
+    debugger
     await props.sendChatStreamApi(id, getRequestContent(content, script), null, abortController.value, AI_QA_CONFIG.enableContext, async (event) => {
     await props.sendChatStreamApi(id, getRequestContent(content, script), null, abortController.value, AI_QA_CONFIG.enableContext, async (event) => {
       try {
       try {
         const result = JSON.parse(event.data)
         const result = JSON.parse(event.data)
@@ -719,8 +722,8 @@ onUnmounted(() => {
               muted
               muted
               playsinline
               playsinline
               preload="auto"
               preload="auto"
+              :loop="isAudioSpeaking"
               @loadedmetadata="handleDigitalHumanVideoReady"
               @loadedmetadata="handleDigitalHumanVideoReady"
-              @ended="resetDigitalHumanVideo"
             ></video>
             ></video>
           </div>
           </div>
           <div class="digital-human-status"><i></i>{{ isAudioSpeaking ? '正在为您播报' : '在线为您服务' }}</div>
           <div class="digital-human-status"><i></i>{{ isAudioSpeaking ? '正在为您播报' : '在线为您服务' }}</div>
@@ -751,7 +754,7 @@ onUnmounted(() => {
                     :citation-ids="message.segments?.map((item) => item.id) || []"
                     :citation-ids="message.segments?.map((item) => item.id) || []"
                     @citation-click="openCitationById(message, $event)"
                     @citation-click="openCitationById(message, $event)"
                   />
                   />
-<!--                  <div v-if="message.segments?.length" class="citation-list">
+                  <div v-if="message.segments?.length" class="citation-list">
                     <button class="citation-summary" type="button" @click="toggleCitationList(message)">
                     <button class="citation-summary" type="button" @click="toggleCitationList(message)">
                       <span class="citation-summary-name">《{{ message.segments[0]?.documentName || '知识库文档' }}》</span>
                       <span class="citation-summary-name">《{{ message.segments[0]?.documentName || '知识库文档' }}》</span>
                       <span class="citation-summary-toggle">{{ getCitationSummaryAction(message) }}</span>
                       <span class="citation-summary-toggle">{{ getCitationSummaryAction(message) }}</span>
@@ -769,7 +772,7 @@ onUnmounted(() => {
                         </div>
                         </div>
                       </div>
                       </div>
                     </div>
                     </div>
-                  </div>-->
+                  </div>
                 </template>
                 </template>
                 <p v-else class="user-content">{{ message.content }}</p>
                 <p v-else class="user-content">{{ message.content }}</p>
                 <div v-if="message.attachments?.length" class="attachment-list"><div v-for="file in message.attachments" :key="file.id" class="attachment-card"><el-icon><Paperclip /></el-icon><span class="attachment-name">{{ file.name }}</span><small>{{ formatSize(file.size) }}</small></div></div>
                 <div v-if="message.attachments?.length" class="attachment-list"><div v-for="file in message.attachments" :key="file.id" class="attachment-card"><el-icon><Paperclip /></el-icon><span class="attachment-name">{{ file.name }}</span><small>{{ formatSize(file.size) }}</small></div></div>

+ 18 - 0
src/views/qsfl/qsflDemo1-2.vue

@@ -0,0 +1,18 @@
+<template>
+  <AiQAChat12
+    :config="qaConfig"
+    :create-dialogue-api="CreateDialogue"
+    :send-chat-stream-api="sendChatMessageStream"
+  />
+</template>
+
+<script setup>
+import AiQAChat12 from './components/AiQAChat1-2.vue'
+import { CreateDialogue, sendChatMessageStream } from '@/api/questions.js'
+
+const qaConfig = Object.freeze({
+  roleId: 266,
+  enableFileUpload: false,
+  suggestions: []
+})
+</script>

+ 1 - 1
src/views/qsfl/qsflDemo1.vue

@@ -7,7 +7,7 @@
 </template>
 </template>
 
 
 <script setup>
 <script setup>
-import AiQAChat from './components/AiQAChat.vue'
+import AiQAChat from './components/AiQAChat1.vue'
 import { CreateDialogue, sendChatMessageStream } from '@/api/questions.js'
 import { CreateDialogue, sendChatMessageStream } from '@/api/questions.js'
 
 
 const qaConfig = Object.freeze({
 const qaConfig = Object.freeze({

+ 1 - 1
src/views/qsfl/qsflDemo2.vue

@@ -23,6 +23,6 @@ const qaConfig = Object.freeze({
   maxInputLength: 1000,
   maxInputLength: 1000,
   uploadHint: '支持图片、PDF、Word、Excel 文件;单次最多上传 3 个附件,单个文件不超过 10 MB,附件总大小不超过 15 MB。文档内容过长时,系统将仅分析前 20,000 个字符。',
   uploadHint: '支持图片、PDF、Word、Excel 文件;单次最多上传 3 个附件,单个文件不超过 10 MB,附件总大小不超过 15 MB。文档内容过长时,系统将仅分析前 20,000 个字符。',
   inputHint: '请输入或粘贴学员拜访记录文字稿,最多 1,000 个字符。',
   inputHint: '请输入或粘贴学员拜访记录文字稿,最多 1,000 个字符。',
-  suggestions: ['请根据标准评分规则对以下学员拜访记录进行评分']
+  suggestions: ['请对附件中学员拜访记录进行评分:']
 })
 })
 </script>
 </script>

+ 1 - 1
src/views/qsfl/qsflDemo3.vue

@@ -23,6 +23,6 @@ const qaConfig = Object.freeze({
   maxInputLength: 1000,
   maxInputLength: 1000,
   uploadHint: '支持图片、PDF、Word、Excel 文件;单次最多上传 3 个附件,单个文件不超过 10 MB,附件总大小不超过 15 MB。文档内容过长时,系统将仅分析前 20,000 个字符。',
   uploadHint: '支持图片、PDF、Word、Excel 文件;单次最多上传 3 个附件,单个文件不超过 10 MB,附件总大小不超过 15 MB。文档内容过长时,系统将仅分析前 20,000 个字符。',
   inputHint: '请输入或粘贴学员拜访记录文字稿,最多 1,000 个字符。',
   inputHint: '请输入或粘贴学员拜访记录文字稿,最多 1,000 个字符。',
-  suggestions: ['请根据标准评分规则对以下学员拜访记录进行评分']
+  suggestions: ['请对附件中学员拜访记录进行评分:']
 })
 })
 </script>
 </script>

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác