Ver código fonte

田老师demo2两个,加入免登录页跳转两个功能demo页

liyanbo 1 mês atrás
pai
commit
fa2f8a3a1e

+ 54 - 0
src/api/qsAiQuestions.js

@@ -0,0 +1,54 @@
+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 流式问答。
+ * 请求体严格使用 qsAi 新接口约定的 conversationId、content、useContext、attachments 四个字段。
+ */
+export async function sendQsAiChatMessageStream(
+  conversationId,
+  content,
+  _contentAnswer,
+  ctrl,
+  useContext,
+  onMessage,
+  onError,
+  onClose,
+  _playAudio,
+  attachments = []
+) {
+  const token = localStorage.getItem('token')
+  const requestAttachments = attachments.map(({ url, name, type, contentType }) => ({
+    url,
+    name,
+    contentType: contentType || type || 'application/octet-stream'
+  }))
+
+  return fetchEventSource(`${import.meta.env.VITE_BASE_URL}/bjdxWeb/qsAi/dialogue-send-stream`, {
+    method: 'post',
+    headers: {
+      'Content-Type': 'application/json',
+      ...(token ? { Authorization: `Bearer ${token}` } : {})
+    },
+    openWhenHidden: true,
+    body: JSON.stringify({
+      conversationId,
+      content,
+      useContext,
+      attachments: requestAttachments
+    }),
+    onmessage: onMessage,
+    onerror: onError,
+    onclose: onClose,
+    signal: ctrl.signal
+  })
+}

+ 21 - 5
src/router/index.js

@@ -1,4 +1,4 @@
-import {createRouter, createWebHistory} from 'vue-router'
+import {createRouter, createWebHistory} from 'vue-router'
 import {CONFIG, refreshRoleRoute} from "@/utils/roleUtils.js";
 import {Message} from "@/utils/message/Message.js";
 import {refreshAllDictData} from "@/utils/dictUtils.js";
@@ -20,6 +20,7 @@ const routes = [
   // 免登录
   { path: '/quick-login', component: () => import('../views/QuickLogin.vue') },
   { path: '/promotion-login', component: () => import('../views/PromotionLogin.vue') },
+  { path: '/login-qsfl', component: () => import('../views/qsfl/LoginQsfl.vue') },
   // //【AI实验课】登录
   // { path: '/ai-login', component: () => import('../views/AiCourseLogin.vue') },
   // //【blockly编程课】免租户登录
@@ -183,6 +184,17 @@ const routes = [
     component: () => import('../views/programming/Interface.vue')
   },
 
+  // ==========【强生菲林 - AI智能问答】
+  {
+    path: '/ai-qa',
+    component: () => import('../views/qsfl/aiQA.vue')
+  },
+  {
+    path: '/ai-qa-new',
+    component: () => import('../views/qsfl/aiQANew.vue')
+  },
+
+
   // 【AI实验课】首页
   // 实验室主题
   {
@@ -290,8 +302,9 @@ const router = createRouter({
 
 // 导航守卫
 router.beforeEach(async (to, from, next) => {
-  // 注册页面始终允许访问,不需要登录状态
-  if ( ['/register-login',
+  // ======= 免登录白名单(新增的页面在此注册即可免登录访问)=======
+  if ( ['/login-qsfl',
+    '/register-login',
     '/reg',
     '/login-mobile',
     '/quick-login',
@@ -309,7 +322,7 @@ router.beforeEach(async (to, from, next) => {
 
   // 如果未登录且不是允许访问的页面,重定向到登录页
   if (!isLoggedIn && !allowedPages.includes(to.path)) {
-    next('/login')
+    next(['/ai-qa', '/ai-qa-new'].includes(to.path) ? '/login-qsfl' : '/login')
     return
   }else if(to.path  === '/sub-jump') {
     next('/sub-jump')
@@ -349,7 +362,10 @@ router.beforeEach(async (to, from, next) => {
   const hasManagementPermission = true // 管理界面默认允许所有登录用户访问
 
   // 检查目标路由是否在允许的范围内
-  if ((to.path === managementRoutes.home || managementRoutes.children.includes(to.path)) && hasManagementPermission) {
+  if (['/ai-qa', '/ai-qa-new'].includes(to.path)) {
+    // 强生菲林问答页要求已登录,但不受课程菜单角色权限限制。
+    isAllowed = true
+  } else 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

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

@@ -0,0 +1,62 @@
+<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',
+  redirectPath: '/ai-qa'
+})
+
+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>

+ 18 - 0
src/views/qsfl/aiQA.vue

@@ -0,0 +1,18 @@
+<template>
+  <AiQAChat
+    :config="qaConfig"
+    :create-dialogue-api="CreateDialogue"
+    :send-chat-stream-api="sendChatMessageStream"
+  />
+</template>
+
+<script setup>
+import AiQAChat from './components/AiQAChat.vue'
+import { CreateDialogue, sendChatMessageStream } from '@/api/questions.js'
+
+const qaConfig = Object.freeze({
+  roleId: 258,
+  enableFileUpload: false,
+  suggestions: ['开始陪练', '你都知道什么?', '我需要知识点....']
+})
+</script>

+ 24 - 0
src/views/qsfl/aiQANew.vue

@@ -0,0 +1,24 @@
+<template>
+  <AiQAChat
+    :config="qaConfig"
+    :create-dialogue-api="createQsAiDialogue"
+    :send-chat-stream-api="sendQsAiChatMessageStream"
+  />
+</template>
+
+<script setup>
+import AiQAChat from './components/AiQAChat.vue'
+import { createQsAiDialogue, sendQsAiChatMessageStream } from '@/api/qsAiQuestions.js'
+
+const qaConfig = Object.freeze({
+  roleId: 259,
+  enableFileUpload: true,
+  maxFiles: 3,
+  maxFileSizeMB: 10,
+  maxTotalFileSizeMB: 15,
+  maxInputLength: 1000,
+  uploadHint: '支持图片、PDF、Word、Excel 文件;单次最多上传 3 个附件,单个文件不超过 10 MB,附件总大小不超过 15 MB。文档内容过长时,系统将仅分析前 20,000 个字符。',
+  inputHint: '单次提问最多输入 1,000 个字符。',
+  suggestions: ['上传资料后提问', '帮我分析附件', '总结文档重点']
+})
+</script>

Diferenças do arquivo suprimidas por serem muito extensas
+ 669 - 0
src/views/qsfl/components/AiQAChat.vue


Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff