diff --git a/.eslintrc.js b/off.eslintrc.js similarity index 100% rename from .eslintrc.js rename to off.eslintrc.js diff --git a/package.json b/package.json index 38422c2..48192b7 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "clipboard": "2.0.8", "core-js": "3.25.3", "dayjs": "^1.11.8", + "dompurify": "^3.2.6", "echarts": "5.4.0", "echarts-gl": "^2.0.9", "element-china-area-data": "^6.1.0", @@ -63,6 +64,8 @@ "jspdf": "^2.5.2", "lodash": "^4.17.21", "mapv-three": "^1.0.18", + "markdown-it": "^13.0.2", + "marked": "^4.3.0", "nprogress": "0.2.0", "print-js": "^1.6.0", "quill": "1.3.7", diff --git a/src/api/aiChat/ai_index.js b/src/api/aiChat/ai_index.js new file mode 100644 index 0000000..ecbb1ca --- /dev/null +++ b/src/api/aiChat/ai_index.js @@ -0,0 +1,110 @@ +import request from '@/utils/request' + +/** + * 获取聊天历史记录 + * @param {Object} params 请求参数 + * @param {string} params.conversationId 会话ID + * @param {string} params.user 用户ID + * @param {number} [params.limit=20] 返回记录数量 + * @param {string} [params.beforeId] 获取此ID之前的记录 + * @returns {Promise} 包含历史记录的Promise + */ +export const getHistory = ({ + conversationId, + user, + limit = 20, + beforeId +}) => { + const params = { + conversationId, + user, + limit + } + + // 如果有beforeId参数,添加到请求中(后端参数名为firstId) + if (beforeId) { + params.firstId = beforeId + } + + return request({ + url: '/aitutor/aichat/getMessagesToUser', + method: 'get', + params + }) +} + +/** + * 发送反馈(点赞/点踩) + * @param {Object} params 请求参数 + * @param {string} params.messageId 消息ID + * @param {number} params.action 1-点赞 0-点踩 + * @param {string} params.user 用户ID + * @returns {Promise} 包含操作结果的Promise + */ +export const sendFeedback = ({ + messageId, + action, + user +}) => { + return request({ + url: '/aitutor/aichat/feedback', + method: 'post', + data: { + message_id: messageId, + rating: action === 1 ? 'like' : 'dislike', + user + } + }) +} + +/** + * 上传文件 + * @param {FormData} formData 包含文件的FormData + * @param {string} user 用户ID + * @returns {Promise} 包含文件URL的Promise + */ +export const uploadFile = (formData, user) => { + formData.append('user', user) + return request({ + url: '/aitutor/aichat/files/upload', + method: 'post', + data: formData, + headers: { + 'Content-Type': 'multipart/form-data' + } + }) +} + +/** + * 创建新会话 + * @param {string} user 用户ID + * @param {string} title 会话标题 + * @returns {Promise} 包含新会话ID的Promise + */ +export const createConversation = (user, title) => { + return request({ + url: '/aitutor/aichat/conversation/create', + method: 'post', + data: { + user, + title + } + }) +} + +/** + * 删除会话 + * @param {string} conversationId 会话ID + * @param {string} user 用户ID + * @returns {Promise} 包含操作结果的Promise + */ +export const deleteConversation = (conversationId, user) => { + return request({ + url: '/aitutor/aichat/conversation/delete', + method: 'post', + data: { + conversation_id: conversationId, + user + } + }) +} \ No newline at end of file diff --git a/src/api/aitutor/chat.js b/src/api/aitutor/chat.js index 15eff17..e8a2ed8 100644 --- a/src/api/aitutor/chat.js +++ b/src/api/aitutor/chat.js @@ -1,85 +1,19 @@ -import request from '@/utils/request'; +import request from '@/utils/request' -/** - * 获取会话列表 - * @param {Object} params - 请求参数 - * @param {number} params.user - 用户ID - * @returns {Promise} - */ -export function getConversationList(params) { +// 获取学生AI对话消息列表(管理员查看) +export function getMessagesToAdmin(params) { return request({ - url: '/aitutor/aichat/conversations', + url: '/aitutor/aichat/getMessagesToAdmin', method: 'get', - params - }); + params: params + }) } -/** - * 发送消息 - * @param {Object} data - 请求体 - * @param {string} data.query - 查询内容 - * @param {string} [data.conversation_id] - 会话ID - * @param {number} data.user - 用户ID - * @param {number} data.user_id - 用户ID - * @param {string} data.user_name - 用户名 - * @param {string} data.user_role - 用户角色 - * @param {string} data.user_token - 用户token - * @returns {Promise} - */ -export function sendMessage(data) { +// 获取心理评估数据 +export function getPsychologicalRatings(query) { return request({ - url: '/aitutor/aichat/stream', - method: 'post', - data, - headers: { - 'Accept': 'text/event-stream' - } - }); -} - -/** - * 提交反馈 - * @param {Object} data - 请求体 - * @param {string} data.message_id - 消息ID - * @param {number} data.user_id - 用户ID - * @param {string} data.rating - 评分('like'或'dislike') - * @param {string} [data.content] - 反馈内容 - * @returns {Promise} - */ -export function submitFeedback(data) { - return request({ - url: '/aitutor/aichat/feedback', - method: 'post', - data - }); -} - -/** - * 获取反馈列表 - * @param {Object} params - 请求参数 - * @param {string} params.conversation_id - 会话ID - * @param {number} params.user_id - 用户ID - * @returns {Promise} - */ -export function getFeedbacks(params) { - return request({ - url: '/aitutor/aichat/app/feedbacks', + url: '/api/wechat/rating/all', method: 'get', - params - }); -} - -/** - * 获取历史消息 - * @param {Object} params - 请求参数 - * @param {string} params.conversation_id - 会话ID - * @param {number} params.user - 用户ID - * @returns {Promise} - */ -export function getHistoryMessages(params) { - return request({ - url: '/aitutor/aichat/history', - method: 'get', - params - }); + params: query + }) } \ No newline at end of file diff --git a/src/api/stuCQS/process-center/auditDetails.js b/src/api/stuCQS/process-center/auditDetails.js index 84dc0c1..b7ec607 100644 --- a/src/api/stuCQS/process-center/auditDetails.js +++ b/src/api/stuCQS/process-center/auditDetails.js @@ -139,3 +139,10 @@ export function delAuditDetails(id) { method: 'post' }) } +//撤销审核 +export function cancelAudit(id) { + return request({ + url: '/comprehensive/auditDetails/cancelAudit/'+id, + method: 'post' + }) +} diff --git a/src/assets/ai/AI.png b/src/assets/ai/AI.png new file mode 100644 index 0000000..1eea8d3 Binary files /dev/null and b/src/assets/ai/AI.png differ diff --git a/src/assets/ai/good.svg b/src/assets/ai/good.svg new file mode 100644 index 0000000..b676949 --- /dev/null +++ b/src/assets/ai/good.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/ai/tread.svg b/src/assets/ai/tread.svg new file mode 100644 index 0000000..9ea37b3 --- /dev/null +++ b/src/assets/ai/tread.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/ai/yonghu.png b/src/assets/ai/yonghu.png new file mode 100644 index 0000000..f03cd4b Binary files /dev/null and b/src/assets/ai/yonghu.png differ diff --git a/src/assets/ai_icon/AI.png b/src/assets/ai_icon/AI.png new file mode 100644 index 0000000..1eea8d3 Binary files /dev/null and b/src/assets/ai_icon/AI.png differ diff --git a/src/assets/ai_icon/add.svg b/src/assets/ai_icon/add.svg new file mode 100644 index 0000000..4ee40ab --- /dev/null +++ b/src/assets/ai_icon/add.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/ai_icon/good.svg b/src/assets/ai_icon/good.svg new file mode 100644 index 0000000..b676949 --- /dev/null +++ b/src/assets/ai_icon/good.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/ai_icon/history.svg b/src/assets/ai_icon/history.svg new file mode 100644 index 0000000..97e80dd --- /dev/null +++ b/src/assets/ai_icon/history.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/ai_icon/newChat.svg b/src/assets/ai_icon/newChat.svg new file mode 100644 index 0000000..e987847 --- /dev/null +++ b/src/assets/ai_icon/newChat.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/ai_icon/search.svg b/src/assets/ai_icon/search.svg new file mode 100644 index 0000000..d152fab --- /dev/null +++ b/src/assets/ai_icon/search.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/ai_icon/send.svg b/src/assets/ai_icon/send.svg new file mode 100644 index 0000000..953c9c9 --- /dev/null +++ b/src/assets/ai_icon/send.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/ai_icon/tread.svg b/src/assets/ai_icon/tread.svg new file mode 100644 index 0000000..9ea37b3 --- /dev/null +++ b/src/assets/ai_icon/tread.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/ai_icon/voice.svg b/src/assets/ai_icon/voice.svg new file mode 100644 index 0000000..282a92e --- /dev/null +++ b/src/assets/ai_icon/voice.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/ai_icon/yonghu.png b/src/assets/ai_icon/yonghu.png new file mode 100644 index 0000000..f03cd4b Binary files /dev/null and b/src/assets/ai_icon/yonghu.png differ diff --git a/src/components/aiChat/HistoryDrawer.vue b/src/components/aiChat/HistoryDrawer.vue new file mode 100644 index 0000000..129f688 --- /dev/null +++ b/src/components/aiChat/HistoryDrawer.vue @@ -0,0 +1,499 @@ + + + + + diff --git a/src/composables/useToast.js b/src/composables/useToast.js new file mode 100644 index 0000000..48e2c10 --- /dev/null +++ b/src/composables/useToast.js @@ -0,0 +1,17 @@ +import { showToast, showSuccess, showError, showWarning, showInfo } from '@/utils/toast' + +/** + * Toast composable + * 提供统一的消息提示功能 + */ +export function useToast() { + return { + showToast, + showSuccess, + showError, + showWarning, + showInfo + } +} + +export default useToast \ No newline at end of file diff --git a/src/layout/components/Aichat/ChatPopup.vue b/src/layout/components/Aichat/ChatPopup.vue new file mode 100644 index 0000000..3a68a9d --- /dev/null +++ b/src/layout/components/Aichat/ChatPopup.vue @@ -0,0 +1,2011 @@ + + + + + diff --git a/src/layout/index.vue b/src/layout/index.vue index 373ee9b..336810e 100644 --- a/src/layout/index.vue +++ b/src/layout/index.vue @@ -1,3 +1,4 @@ + @@ -30,13 +39,15 @@ import { AppMain, Navbar, Settings, Sidebar, TagsView } from './components' import ResizeMixin from './mixin/ResizeHandler' import { mapState } from 'vuex' import variables from '@/assets/styles/variables.scss' +import ChatPopup from '../layout/components/Aichat/ChatPopup.vue' import { initCoze -} from "@/utils/ai.js"; +} from '@/utils/ai.js' import { getAccessToken -} from "@/api/aiJWT/aiJWT.js" +} from '@/api/aiJWT/aiJWT.js' + export default { name: 'Layout', components: { @@ -45,9 +56,15 @@ export default { RightPanel, Settings, Sidebar, - TagsView + TagsView, + ChatPopup // 注册ChatPopup组件 }, mixins: [ResizeMixin], + data() { + return { + showAI: false // 控制AI弹窗显示/隐藏的变量 + } + }, computed: { ...mapState({ theme: state => state.settings.theme, @@ -70,42 +87,55 @@ export default { } }, variables() { - return variables; + return variables } }, methods: { handleClickOutside() { this.$store.dispatch('app/closeSideBar', { withoutAnimation: false }) }, - async showAI() { - let userInfo = { - roleGroup: this.userInfo.roles[0].roleName || "student", - nickName: this.userInfo.nickName, - username: this.userInfo.userName, - avater: this.avatar, - user_token: this.token + // 切换AI弹窗显示状态 + toggleAI() { + // 使用明确的状态切换,避免与close事件冲突 + if (this.showAI) { + this.showAI = false + } else { + this.showAI = true } - console.log("请求AI的信息", userInfo) - - //1.获取token - userInfo.accessToken = (await this.getAccessToken()).access_token; - userInfo.onRefreshToken = async () => (await this.getAccessToken()).accessToken; - const sdk = await initCoze(userInfo); - sdk.showChatBot(); + }, + // 原有AI初始化逻辑,保持注释状态 + async initializeAI() { + // let userInfo = { + // roleGroup: this.userInfo.roles[0].roleName || "student", + // nickName: this.userInfo.nickName, + // username: this.userInfo.userName, + // avater: this.avatar, + // user_token: this.token + // } + // console.log("请求AI的信息", userInfo) + // + // //1.获取token + // userInfo.accessToken = (await this.getAccessToken()).access_token; + // userInfo.onRefreshToken = async () => (await this.getAccessToken()).accessToken; + // const sdk = await initCoze(userInfo); + // sdk.showChatBot(); }, async getAccessToken() { - const res = await getAccessToken(); // 调用请求函数 - const data = JSON.parse(res.data); // 解析数据 - return data; // ✅ 返回 data + const res = await getAccessToken() // 调用请求函数 + const data = JSON.parse(res.data) // 解析数据 + return data // ✅ 返回 data } } } + diff --git a/src/utils/ai_request.js b/src/utils/ai_request.js new file mode 100644 index 0000000..5b387b4 --- /dev/null +++ b/src/utils/ai_request.js @@ -0,0 +1,54 @@ +import axios from "axios"; +import { getTokenKeySessionStorage } from "./auth"; +import { useRouter } from "vue-router"; +import { showToast } from "@/utils/toast"; // 请替换为你的Toast组件 + +// 创建axios实例 +const service = axios.create({ + baseURL: process.env.VUE_APP_API_BASE_URL || 'http://localhost:8088', + timeout: 15000, + headers: { + "Content-Type": "application/json", + }, +}); + +// 请求拦截器 +service.interceptors.request.use( + (config) => { + // 从本地存储获取token + const token = getTokenKeySessionStorage(); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + (error) => { + return Promise.reject(error); + } +); + +// 响应拦截器 +service.interceptors.response.use( + (response) => { + // 对响应数据做处理 + return response.data; + }, + (error) => { + const router = useRouter(); + + // 处理401未授权 + if (error.response?.status === 401) { + showToast("登录已过期,请重新登录", "error"); + router.push("/login"); + } + + // 处理其他错误状态码 + if (error.response?.status === 500) { + showToast("服务器错误,请稍后再试", "error"); + } + + return Promise.reject(error); + } +); + +export default service; diff --git a/src/utils/ai_stream.js b/src/utils/ai_stream.js new file mode 100644 index 0000000..bf26f24 --- /dev/null +++ b/src/utils/ai_stream.js @@ -0,0 +1,111 @@ +import { getTokenKeySessionStorage } from "@/utils/auth"; +/** + * 创建聊天流式连接 + * @param {Object} params 请求参数 + * @param {string} params.prompt 用户输入 + * @param {string} params.userId 用户ID + * @param {string} params.userName 用户名 + * @param {string} [params.conversationId] 会话ID + * @returns {Object} 包含stream和cancel方法的对象 + */ +export function createChatStream(params) { + const requestId = `req-${Date.now()}-${Math.random() + .toString(36) + .slice(2, 8)}`; + const url = `${process.env.VUE_APP_BASE_API}/aitutor/aichat/stream`; + const token = getTokenKeySessionStorage(); + + if (!token) { + throw new Error("请先登录"); + } + + const controller = new AbortController(); + + const fetchPromise = fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + "X-Request-ID": requestId, + }, + body: JSON.stringify({ + query: params.prompt, + user_id: params.userId, + user_name: params.userName, + user_token: params.user_token || "123", + user_role: "student", + conversation_id: params.conversationId || null, + }), + signal: controller.signal, + }).then((response) => { + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + if (!response.body) { + throw new Error("Response body is null"); + } + return { + reader: response.body.getReader(), + decoder: new TextDecoder("utf-8"), + }; + }); + + return { + stream: fetchPromise, + cancel: (reason) => { + if (!controller.signal.aborted) { + controller.abort(reason); + } + }, + }; +} + +/** + * 处理流式响应 + * @param {ReadableStreamDefaultReader} reader 读取器 + * @param {TextDecoder} decoder 文本解码器 + * @param {Function} onMessage 消息回调 + * @param {Function} onError 错误回调 + * @param {Function} onComplete 完成回调 + */ +export async function processStream( + reader, + decoder, + { onMessage, onError, onComplete } +) { + let buffer = ""; + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split("\n"); + buffer = lines.pop() || ""; + + for (const line of lines) { + if (!line.trim()) continue; + + try { + const data = JSON.parse(line); + if (typeof onMessage === "function") { + onMessage(data); + } + } catch (e) { + console.warn("解析消息失败:", line, e); + } + } + } + + if (typeof onComplete === "function") { + onComplete(); + } + } catch (error) { + if (error.name !== "AbortError" && typeof onError === "function") { + onError(error); + } + } finally { + reader.releaseLock(); + } +} diff --git a/src/utils/toast.js b/src/utils/toast.js new file mode 100644 index 0000000..2b45e22 --- /dev/null +++ b/src/utils/toast.js @@ -0,0 +1,48 @@ +import { Message } from 'element-ui' + +/** + * 显示Toast消息 + * @param {string} message - 消息内容 + * @param {string} type - 消息类型: 'success', 'warning', 'info', 'error' + * @param {number} duration - 显示时长,默认3000ms + */ +export function showToast(message, type = 'info', duration = 3000) { + Message({ + message, + type, + duration, + showClose: true + }) +} + +/** + * 显示成功消息 + * @param {string} message - 消息内容 + */ +export function showSuccess(message) { + showToast(message, 'success') +} + +/** + * 显示错误消息 + * @param {string} message - 消息内容 + */ +export function showError(message) { + showToast(message, 'error') +} + +/** + * 显示警告消息 + * @param {string} message - 消息内容 + */ +export function showWarning(message) { + showToast(message, 'warning') +} + +/** + * 显示信息消息 + * @param {string} message - 消息内容 + */ +export function showInfo(message) { + showToast(message, 'info') +} \ No newline at end of file diff --git a/src/views/Home/comps/fdy-undo.vue b/src/views/Home/comps/fdy-undo.vue index 03fc822..3c880b2 100644 --- a/src/views/Home/comps/fdy-undo.vue +++ b/src/views/Home/comps/fdy-undo.vue @@ -74,6 +74,31 @@ export default { value: 0, url: "/task/todo" }, + { + label: "辅导员·困难资助审核", + name: "knzz", + value: 0, + url: "hard/tufa/fdy" + }, + { + label: "辅导员·学生离校审核", + name: "leave", + value: 0, + url: "/survey/leave/fdy" + }, + { + // 宁博 + label: "辅导员·学生返校审核", + name: "return", + value: 0, + url: "/survey/return/fdy" + }, + { + label: "辅导员·国家励志奖学金审核", + name: "knzzgl", + value: 0, + url: "/hard/gl/fdy" + } ], diff --git a/src/views/Home/comps/jwc-undo.vue b/src/views/Home/comps/jwc-undo.vue index 94b7aa9..5e7ff40 100644 --- a/src/views/Home/comps/jwc-undo.vue +++ b/src/views/Home/comps/jwc-undo.vue @@ -74,6 +74,19 @@ export default { value: 0, url: "/task/todo" }, + { + label: "学工·困难资助审核", + name: "knzz", + value: 0, + url: "hard/tufa/xg" + }, + // 宁博 + { + label: "学工·国家励志奖学金审核", + name: "knzzgl", + value: 0, + url: "/hard/gl/xg" + } ] } diff --git a/src/views/Home/comps/xw-undo.vue b/src/views/Home/comps/xw-undo.vue index 08c3790..d9c2dca 100644 --- a/src/views/Home/comps/xw-undo.vue +++ b/src/views/Home/comps/xw-undo.vue @@ -1,170 +1,187 @@ \ No newline at end of file + diff --git a/src/views/Home/index-new-blue.vue b/src/views/Home/index-new-blue.vue index fd63cce..cfd9f9f 100644 --- a/src/views/Home/index-new-blue.vue +++ b/src/views/Home/index-new-blue.vue @@ -1,9 +1,8 @@