diff --git a/api/aiChat/ai_index.js b/api/aiChat/ai_index.js
new file mode 100644
index 0000000..9a813c5
--- /dev/null
+++ b/api/aiChat/ai_index.js
@@ -0,0 +1,38 @@
+// src/api/index.js
+// import request from '@/utils/ai_request.js'
+import request from "../../utils/ai_request";
+
+// 获取历史
+export const getHistory = ({
+ conversationId,
+ user,
+ limit = 20
+}) => {
+ return request({
+ url: '/aitutor/aichat/getMessagesToUser',
+ method: 'get',
+ params: {
+ conversationId,
+ user,
+ limit
+ }
+ // headers: {
+ // Authorization: 'Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6IjBmMTY3NmY2LTgwOGMtNGUwMC04NDJjLWIwNmY1ZTM5NzJlNCJ9.VVc6OwQ-Xn9pxzYbPhlCpvDp6TwESS00gJi9IXUEIbFw4RFACZDmYCYjQ7voTM4fppy9SAMJCWT-L7Uy-K1eqw'
+ // }
+ });
+};
+
+// 点赞/点踩 action: 1 点赞 0 点踩
+export const sendFeedback = ({
+ messageId,
+ action
+}) => {
+ return request({
+ url: '/api/chat/feedback',
+ method: 'post',
+ data: {
+ messageId,
+ action
+ }
+ });
+};
\ No newline at end of file
diff --git a/components/aiChat/HistoryDrawer.vue b/components/aiChat/HistoryDrawer.vue
new file mode 100644
index 0000000..d8bd244
--- /dev/null
+++ b/components/aiChat/HistoryDrawer.vue
@@ -0,0 +1,411 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ group.title }}
+
+
+
+ {{ item.date }}
+ {{ item.time }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ searchKeyword ? '没有找到匹配的记录' : '暂无聊天记录' }}
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/config.js b/config.js
index 06259eb..86209fd 100644
--- a/config.js
+++ b/config.js
@@ -3,7 +3,7 @@ module.exports = {
//baseUrl: 'http://zhxg.gxsdxy.cn/prod_api',
// baseUrl: 'http://172.16.96.111:8085',
// baseUrl: 'http://192.168.211.22:8085',
- baseUrl: 'http://localhost:8085',
+ baseUrl: 'http://localhost:8088',
// 应用信息
appInfo: {
// 应用名称
@@ -28,7 +28,7 @@ module.exports = {
/**
* 开启cas
*/
- casEnable: true,
+ casEnable: false,
/**
* 单点登录url
diff --git a/package.json b/package.json
index dbcd9a3..de63eeb 100644
--- a/package.json
+++ b/package.json
@@ -12,7 +12,18 @@
"@types/uni-app": "^1.4.8"
},
"dependencies": {
+ "@dcloudio/uni-app-plus": "^2.0.2-4070520250711001",
+ "@dcloudio/uni-cli-i18n": "^2.0.2-4070520250711001",
+ "@dcloudio/uni-cli-shared": "^2.0.2-4070520250711001",
+ "@dcloudio/uni-helper-json": "^1.0.13",
+ "@dcloudio/uni-migration": "^2.0.2-4070520250711001",
+ "@dcloudio/uni-stat": "^2.0.2-4070520250711001",
+ "@dcloudio/vue-cli-plugin-uni": "^2.0.2-4070520250711001",
+ "@vue/cli-service": "^5.0.8",
"@wecom/jssdk": "^2.3.1",
+ "axios": "^1.11.0",
+ "dompurify": "^3.2.6",
+ "markdown-it": "^14.1.0",
"weixin-js-sdk": "^1.6.5"
}
}
diff --git a/pages.json b/pages.json
index ccad9a0..21f623a 100644
--- a/pages.json
+++ b/pages.json
@@ -1148,14 +1148,19 @@
}
},
{
- "path" : "pages/sub/StudoEdit",
- "style" :
- {
- "navigationBarTitleText" : "离校留校申请",
+ "path": "pages/sub/StudoEdit",
+ "style": {
+ "navigationBarTitleText": "离校留校申请",
"enablePullDownRefresh": false,
"navigationBarBackgroundColor": "#1890FF",
"navigationBarTextStyle": "white"
}
+ },
+ {
+ "path": "pages/aiChat/ai_index",
+ "style": {
+ "navigationBarTitleText": "AI辅导员"
+ }
}
],
"globalStyle": {
diff --git a/pages/Login/Login.vue b/pages/Login/Login.vue
index 8fea1ee..b3b4b98 100644
--- a/pages/Login/Login.vue
+++ b/pages/Login/Login.vue
@@ -1,3 +1,4 @@
+
@@ -11,14 +12,14 @@
-
+
-
+
柳州市网信办 柳州职业技术学院
-
+
@@ -39,22 +40,22 @@
showPassword: false
};
},
-
+
methods: {
login() {
- let sdata= {
+ let sdata = {
username: this.username,
password: this.password
};
- doLogin(sdata).then(res=>{
+ doLogin(sdata).then(res => {
if (res.code == 200) {
- uni.setStorageSync('token', res.token);
+ uni.setStorageSync('token', res.token);
uni.showToast({
title: "登录成功",
icon: "success"
});
- getInfo().then(res=>{
- uni.setStorageSync("roles",res.roles[0])
+ getInfo().then(res => {
+ uni.setStorageSync("roles", res.roles[0])
})
uni.switchTab({
url: "/pages/index/index"
@@ -65,19 +66,19 @@
uni.showToast({
title: res.msg,
icon: "error"
-
+
})
- } else{
+ } else {
console.log('11');
}
})
-
+
},
togglePassword() {
this.showPassword = !this.showPassword;
},
- }
+ }
};
diff --git a/pages/aiChat/ai_index.vue b/pages/aiChat/ai_index.vue
new file mode 100644
index 0000000..eb85b4b
--- /dev/null
+++ b/pages/aiChat/ai_index.vue
@@ -0,0 +1,457 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 智水AI辅导员
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.content }}
+
+
+
+
+
+
+
+ 引用来源:
+
+
+
+
+ {{ ref.document_name }}
+
+
+
+
+ {{ ref.name }}({{ ref.document_name }})
+ {{ ref.content }}
+
+
+
+
+
+
+ 回答由AI生成
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ AI
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pages/index/index.vue b/pages/index/index.vue
index 8340f89..de6f82d 100644
--- a/pages/index/index.vue
+++ b/pages/index/index.vue
@@ -1,3 +1,4 @@
+
@@ -32,17 +33,17 @@
-
+
-
-
-
-
- AI
+
+
+
+ AI
+
-
-
+
+
@@ -169,7 +170,9 @@
// ]
- log: "log"
+ log: "log",
+ // 新增:AI聊天显示状态,与聊天页同步
+ isAIChatVisible: true
}
},
onLoad(query) {
@@ -184,9 +187,27 @@
this.getQgzxLogExamineTotal()
this.getqgzxMenoyTotal();
}
+
+ // 从本地存储获取AI状态
+ const aiStatus = uni.getStorageSync('aiVisibleStatus');
+ if (aiStatus !== null && aiStatus !== undefined) {
+ this.isAIChatVisible = aiStatus;
+ } else {
+ // 默认显示
+ this.isAIChatVisible = true;
+ uni.setStorageSync('aiVisibleStatus', true);
+ }
},
onShow() {
+ // 检查token是否存在
if (!getToken()) {
+ uni.showToast({
+ title: '请先登录',
+ icon: 'none'
+ });
+ uni.navigateTo({
+ url: '/pages/login/index'
+ });
return;
}
this.getUserInfo();
@@ -194,6 +215,9 @@
this.getUserRouters();
this.getQgzxLogExamineTotal()
this.getqgzxMenoyTotal();
+
+ // 页面显示时同步AI状态
+ this.isAIChatVisible = uni.getStorageSync('aiVisibleStatus') || true;
},
methods: {
getUserRouters() {
@@ -264,21 +288,13 @@
getImgUrl(name) {
return require('../../static/images/workbench/' + name + '.png');
},
- async showAI() {
- let userInfo = {
- roleGroup: uni.getStorageSync("roles"),
- nickName: this.nickName,
- username: this.username,
- avater: this.avater,
- user_token: getToken()
- }
- //1.获取token
- userInfo.accessToken = (await this.getAccessToken()).access_token;
- userInfo.onRefreshToken = async () => (await this.getAccessToken()).accessToken;
- // console.log("请求AI的信息", userInfo)
- const sdk = await initCoze(userInfo);
- sdk.showChatBot();
+
+ toAI() {
+ uni.navigateTo({
+ url: '/pages/aiChat/ai_index'
+ });
},
+
async getAccessToken() {
const res = await getAccessToken(); // 调用请求函数
const data = JSON.parse(res.data); // 解析数据
@@ -387,7 +403,6 @@
}
}
-
// ai悬停
.ai-hover {
position: fixed;
@@ -403,5 +418,24 @@
justify-content: center;
align-items: center;
cursor: pointer;
+ box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
+ transition: transform 0.2s;
+ }
+
+ .ai-hover-content {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ height: 100%;
+ }
+
+ .ai-hover-text {
+ font-size: 36rpx;
+ font-weight: bold;
+ }
+
+ .ai-hover:active {
+ transform: scale(0.95);
}
\ No newline at end of file
diff --git a/static/AI.png b/static/AI.png
new file mode 100644
index 0000000..1eea8d3
Binary files /dev/null and b/static/AI.png differ
diff --git a/static/add.svg b/static/add.svg
new file mode 100644
index 0000000..4ee40ab
--- /dev/null
+++ b/static/add.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/static/good.svg b/static/good.svg
new file mode 100644
index 0000000..b676949
--- /dev/null
+++ b/static/good.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/static/history.svg b/static/history.svg
new file mode 100644
index 0000000..97e80dd
--- /dev/null
+++ b/static/history.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/static/newChat.svg b/static/newChat.svg
new file mode 100644
index 0000000..e987847
--- /dev/null
+++ b/static/newChat.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/static/scss/ai_index.css b/static/scss/ai_index.css
new file mode 100644
index 0000000..b3da404
--- /dev/null
+++ b/static/scss/ai_index.css
@@ -0,0 +1,363 @@
+/* ai_index.css */
+/* ============= 整体容器 ============= */
+.chat-container {
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+ /* 改为最小高度,允许内容撑开 */
+ width: 100%;
+ overflow: hidden;
+ position: relative;
+ background-color: #F5F5F5;
+ padding-top: 10px;
+ /* 为固定导航栏预留空间 */
+ box-sizing: border-box;
+ /* 确保padding不会增加总宽度 */
+}
+
+/* ============= 状态栏占位 ============= */
+.status-bar-placeholder {
+ height: var(--status-bar-height);
+ background-color: #fff;
+}
+
+/* ============= 自定义导航栏(固定在顶部) ============= */
+.custom-nav-bar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ height: 44px;
+ padding: 0 16px;
+ background-color: #fff;
+ box-shadow: 0 1px 5px rgba(0, 0, 0, 0.1);
+ position: fixed;
+ top: 0;
+ left: 0;
+ z-index: 999;
+ border-bottom: 1px solid #eee;
+ box-sizing: border-box;
+}
+
+.nav-title {
+ font-size: 17px;
+ font-weight: 600;
+ color: #333;
+ text-align: center;
+ flex: 1;
+}
+
+.nav-icon {
+ width: 24px;
+ height: 24px;
+}
+
+.nav-left,
+.nav-right {
+ width: 44px;
+ height: 44px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+/* ============= 消息列表(可滚动区域) ============= */
+.message-list {
+ flex: 1;
+ padding: 16px 0 calc(env(safe-area-inset-bottom) + 80px) 0;
+ /* 增加底部内边距 */
+ background-color: #f5f5f5;
+ box-sizing: border-box;
+ overflow-y: auto;
+ /* 确保内容超出时可滚动 */
+ -webkit-overflow-scrolling: touch;
+ /* 增加iOS设备上的滚动流畅度 */
+}
+
+/* ============= 消息项 ============= */
+.message-item {
+ display: flex;
+ align-items: flex-start;
+ margin-bottom: 16px;
+ max-width: 100%;
+}
+
+.user-message {
+ flex-direction: row-reverse;
+ margin-right: 20px;
+}
+
+.ai-message {
+ flex-direction: row;
+ margin-left: 20px;
+}
+
+.avatar {
+ width: 40px;
+ height: 40px;
+ border-radius: 8px;
+ margin: 0 8px;
+}
+
+.message-content {
+ max-width: 70%;
+ padding: 10px 12px;
+ border-radius: 10px;
+ word-wrap: break-word;
+ line-height: 1.5;
+}
+
+.user-message .message-content {
+ background-color: #e1f5fe;
+}
+
+.ai-message .message-content {
+ background-color: #fff;
+ border: 1px solid #eee;
+}
+
+.sent-image {
+ max-width: 200px;
+ max-height: 200px;
+ border-radius: 8px;
+ margin-top: 4px;
+ object-fit: cover;
+}
+
+/* ============= AI 提示区域 ============= */
+.ai-hint {
+ margin-top: 8px;
+ font-size: 12px;
+ color: #666;
+}
+
+.quote-icon {
+ display: block;
+ margin-bottom: 4px;
+ color: #888;
+}
+
+.ai-actions {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.ai-text {
+ color: #999;
+}
+
+.icon-group {
+ display: flex;
+ gap: 12px;
+}
+
+.btn-icon {
+ width: 16px;
+ height: 16px;
+ cursor: pointer;
+}
+
+/* ============= 输入框区域(固定在底部) ============= */
+.input-container {
+ display: flex;
+ align-items: center;
+ padding: 8px 16px;
+ background-color: #fff;
+ border-top: 1px solid #eee;
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ z-index: 10;
+ box-sizing: border-box;
+ padding-bottom: calc(env(safe-area-inset-bottom) + 10px);
+ padding-left: env(safe-area-inset-left);
+ padding-right: env(safe-area-inset-right);
+}
+
+.input-container input {
+ flex: 1;
+ max-width: 90%;
+ /* 缩短输入框 */
+ height: 40px;
+ padding: 0 16px;
+ background-color: #f5f5f5;
+ border-radius: 20px;
+ font-size: 14px;
+ margin: 0 10px;
+ outline: none;
+}
+
+.add-icon,
+.send-icon {
+ width: 24px;
+ height: 24px;
+ cursor: pointer;
+ margin: 0 12px;
+ /* 增加图标之间的间距 */
+}
+
+.toggle-button {
+ margin-top: 10px;
+ color: blue;
+ cursor: pointer;
+ text-decoration: underline;
+}
+
+.reference-details {
+ margin-top: 10px;
+ padding-left: 20px;
+ border-left: 2px solid #ccc;
+}
+
+/* md格式 */
+.markdown-content {
+ line-height: 1.6;
+ font-size: 14px;
+}
+
+.markdown-content h1,
+.markdown-content h2,
+.markdown-content h3 {
+ margin: 1em 0 0.5em;
+}
+
+.markdown-content p {
+ margin: 0.5em 0;
+}
+
+.markdown-content code {
+ background-color: #f3f4f6;
+ padding: 0.2em 0.4em;
+ border-radius: 3px;
+ font-family: monospace;
+}
+
+.markdown-content pre {
+ background-color: #f6f8fa;
+ border-radius: 5px;
+ padding: 10px;
+ overflow-x: auto;
+ margin: 10px 0;
+}
+
+.markdown-content pre code {
+ background-color: transparent;
+ padding: 0;
+}
+
+.markdown-content ul,
+.markdown-content ol {
+ margin-left: 1em;
+ margin-bottom: 0.5em;
+}
+
+.markdown-content blockquote {
+ border-left: 4px solid #d0d7de;
+ color: #57606a;
+ margin: 1em 0;
+ padding-left: 1em;
+}
+
+
+/* 可点击文档名 */
+.doc-name-link {
+ color: #007AFF;
+ text-decoration: underline;
+ margin-right: 16rpx;
+ font-size: 10rpx;
+}
+
+/* 每个引用项容器 */
+.reference-item-wrapper {
+ margin-top: 12rpx;
+}
+
+/* ============= 引用详情 - 自适应字体 ============= */
+
+.reference-details-item {
+ margin-top: 12rpx;
+ padding: 20rpx;
+ background-color: #f9f9f9;
+ border-radius: 8rpx;
+ border: 1rpx solid #e0e0e0;
+ font-size: clamp(12px, 2.8vw, 16px);
+ color: #555;
+ line-height: 1.6;
+}
+
+.reference-meta {
+ font-weight: 500;
+ color: #333;
+ display: block;
+ margin-bottom: 8rpx;
+ font-size: clamp(13px, 3.2vw, 17px);
+}
+
+.reference-content {
+ color: #444;
+ line-height: 1.7;
+}
+
+.doc-name-link {
+ color: #007AFF;
+ text-decoration: underline;
+ margin-right: 16rpx;
+ font-size: clamp(13px, 3vw, 15px);
+ font-weight: 500;
+}
+
+/* AI悬浮按钮样式 - 全局显示 */
+.ai-hover {
+ position: fixed;
+ right: 40rpx;
+ bottom: 120rpx;
+ z-index: 9999;
+ width: 100rpx;
+ height: 100rpx;
+ border-radius: 50%;
+ background-color: #409eff;
+ color: #fff;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
+ cursor: pointer;
+ transition: transform 0.2s;
+}
+
+.ai-hover-content {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ height: 100%;
+}
+
+.ai-hover-text {
+ font-size: 36rpx;
+ font-weight: bold;
+}
+
+.ai-hover:active {
+ transform: scale(0.95);
+}
+
+/* ============= 小屏设备适配 ============= */
+@media (max-width: 600px) {
+ .message-content {
+ max-width: 65%;
+ font-size: 14px;
+ }
+
+ .nav-title {
+ font-size: 15px;
+ }
+
+ .btn-icon {
+ width: 14px;
+ height: 14px;
+ margin-left: 6px;
+ }
+}
\ No newline at end of file
diff --git a/static/search.svg b/static/search.svg
new file mode 100644
index 0000000..d152fab
--- /dev/null
+++ b/static/search.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/static/send.svg b/static/send.svg
new file mode 100644
index 0000000..953c9c9
--- /dev/null
+++ b/static/send.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/static/tread.svg b/static/tread.svg
new file mode 100644
index 0000000..9ea37b3
--- /dev/null
+++ b/static/tread.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/static/voice.svg b/static/voice.svg
new file mode 100644
index 0000000..282a92e
--- /dev/null
+++ b/static/voice.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/static/yonghu.png b/static/yonghu.png
new file mode 100644
index 0000000..690d094
Binary files /dev/null and b/static/yonghu.png differ
diff --git a/utils/ai_request.js b/utils/ai_request.js
new file mode 100644
index 0000000..35b6a55
--- /dev/null
+++ b/utils/ai_request.js
@@ -0,0 +1,43 @@
+// src/utils/ai_request.js
+import axios from 'axios'
+import {
+ getToken
+} from './auth'
+
+const service = axios.create({
+ // baseURL: 'http://localhost:9090/dev-api/aitutor/aichat',
+ baseURL: 'http://localhost:8088/aitutor/aichat',
+ // baseURL: 'http://localhost:8080/aitutor/aichat',
+ timeout: 15000,
+})
+
+// 请求拦截器:统一加 token
+service.interceptors.request.use(
+ config => {
+ const token = getToken()
+ if (token) {
+ config.headers.Authorization = `Bearer ${token}`
+ }
+ return config
+ },
+ error => Promise.reject(error)
+)
+
+// 响应拦截器
+service.interceptors.response.use(
+ res => res.data,
+ err => {
+ if (err.response?.status === 401) {
+ uni.showToast({
+ title: '未登录或 token 失效',
+ icon: 'none'
+ })
+ uni.navigateTo({
+ url: '/pages/login/index'
+ })
+ }
+ return Promise.reject(err)
+ }
+)
+
+export default service
\ No newline at end of file
diff --git a/utils/ai_stream.js b/utils/ai_stream.js
new file mode 100644
index 0000000..e42ab93
--- /dev/null
+++ b/utils/ai_stream.js
@@ -0,0 +1,57 @@
+// src/utils/ai_stream.js (H5 优化版)
+import {
+ getToken
+} from '@/utils/auth';
+
+const BASE_URL = (() => {
+ // #ifdef H5
+ return 'http://localhost:8088';
+ // #endif
+ // #ifndef H5
+ // return 'http://192.168.x.x:8088'; // 换成你的电脑 IP
+ // #endif
+})();
+
+export function createChatStream(params) {
+ const requestId = `req-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
+ const url = `${BASE_URL}/aitutor/aichat/stream`;
+ const token = getToken();
+ if (!token) throw new Error('请先登录');
+
+ const controller = new AbortController();
+
+ const fetchPromise = fetch(url, {
+ method: 'POST',
+ headers: {
+ 'Accept': 'text/event-stream',
+ '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(resp => {
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
+ if (!resp.body) throw new Error('Response body is null');
+ return resp.body;
+ });
+
+ return fetchPromise.then(body => ({
+ stream: Promise.resolve({
+ reader: body.getReader(),
+ decoder: new TextDecoder('utf-8')
+ }),
+ cancel: reason => !controller.signal.aborted && controller.abort(reason)
+ })).catch(err => ({
+ stream: Promise.reject(err),
+ cancel: () => {}
+ }));
+}
\ No newline at end of file
diff --git a/utils/auth.js b/utils/auth.js
index 9a7cc04..04325e5 100644
--- a/utils/auth.js
+++ b/utils/auth.js
@@ -1,3 +1,4 @@
+// auth.js
const TokenKey = 'App-Token'
export function getToken() {