Compare commits

..

23 Commits

Author SHA1 Message Date
yangzhe a4ee711418 fix(chat): 修复消息显示逻辑并添加发送失败提示 2026-03-19 17:29:49 +08:00
yangzhe 28c617a33d fix(消息头像): 修复消息列表中接收者头像显示问题 2026-03-19 17:00:46 +08:00
yangzhe f043ee6fa3 feat(消息处理): 添加sendUserType字段到消息对象 2026-03-19 16:36:40 +08:00
yangzhe af1a29c071 fix(聊天界面): 修正教师和提示消息的显示逻辑 2026-03-19 16:13:33 +08:00
yangzhe a1c9ef82c7 Merge branch 'main' of http://sl.vrgon.com:3000/JiXinHui/YingXingAI 2026-03-19 15:41:17 +08:00
yangzhe a0ea24f8f9 fix: 修复实时客服聊天判断逻辑 2026-03-19 15:41:13 +08:00
JiXinHui e14818a399 Merge branch 'main' of http://sl.vrgon.com:3000/JiXinHui/YingXingAI 2026-03-19 15:36:14 +08:00
JiXinHui a01b8c957a fix:实时通信会话错乱 2026-03-19 15:36:13 +08:00
yangzhe 10f358c40b fix: 修复用户消息显示条件及AI消息ID赋值 2026-03-19 15:26:54 +08:00
yangzhe f7a13b1a9d fix(消息展示): 添加系统提示样式并移除多余的成功提示 2026-03-19 15:17:13 +08:00
yangzhe 8bc9691b2c fix: 移除上拉刷新的currentDMid检查 2026-03-19 14:20:52 +08:00
yangzhe 526a580e14 feat(ui): 优化文件消息的视觉样式和布局 2026-03-19 13:45:51 +08:00
yangzhe 8e9c57a6f9 feat(chat): AI回复支持结构化消息渲染,包括文本、图片和文件 2026-03-19 11:06:30 +08:00
yangzhe 3c4aaa0f77 fix(login): 修复代码错误 2026-03-19 09:07:13 +08:00
JiXinHui 8f8ace89e1 Merge branch 'main' of http://sl.vrgon.com:3000/JiXinHui/YingXingAI 2026-03-18 16:59:22 +08:00
JiXinHui 11c286c717 feat:新增修改密码 2026-03-18 16:59:20 +08:00
yangzhe 227b836d93 refactor(home): 为AI和老师消息内容添加容器包装层
重构消息列表布局,将AI和老师消息的avatar和内容区域分别包裹在独立的容器元素中。
2026-03-18 13:57:41 +08:00
yangzhe 86b3a51206 fix(home): 将消息容器的宽度属性改为最大宽度
避免消息内容过长时超出容器宽度,保持布局的灵活性
2026-03-18 11:21:38 +08:00
JiXinHui 1536637159 Merge branch 'main' of http://sl.vrgon.com:3000/JiXinHui/YingXingAI 2026-03-18 11:08:49 +08:00
JiXinHui ac49885fad fix:人工接口字段修改 去除在线咨询 2026-03-18 11:08:47 +08:00
yangzhe f738c1db94 feat: 优化首页界面并新增通话与结束会话功能
- 新增四个静态图片资源用于界面图标
- 在首页头部添加用户设置入口图标
- 重构电话咨询组件,将单一电话文本改为可点击拨打的卡片式设计
- 在Vuex状态管理中新增update_MsgUser方法用于局部更新聊天用户信息
- 在API配置中添加EndLiveAgentApi接口用于结束人工服务
- 重构首页功能卡片布局,添加描述文本和操作按钮
- 实现点击电话卡片直接拨打电话功能
- 新增handleEndConversation方法处理会话结束逻辑
- 优化热门问题点击逻辑,自动切换到对话模式
- 标记留言板页面为已废弃
2026-03-18 10:51:49 +08:00
yangzhe e8009a845c chore: 更新后端服务端口从8082改为8073 2026-03-16 16:47:41 +08:00
yangzhe 52a1f58f78 feat: 添加一个标记 2026-03-16 16:20:47 +08:00
23 changed files with 1250 additions and 242 deletions

28
App.vue
View File

@ -109,7 +109,7 @@ export default {
window.location.protocol.indexOf("https") === 0 ? "wss" : "ws"; window.location.protocol.indexOf("https") === 0 ? "wss" : "ws";
const Id = const Id =
(this.vuex_user && (this.vuex_user.id || this.vuex_user.Id)) || ""; (this.vuex_user && (this.vuex_user.id || this.vuex_user.Id)) || "";
return `${protocol}://120.55.234.65:8082/api/Dialogue/HandleConnection?Id=${Id}`; // &equipmentType=0 return `${protocol}://120.55.234.65:8073/api/Dialogue/HandleConnection?Id=${Id}`; // &equipmentType=0
}, },
// WebSocket // WebSocket
initWebSocket() { initWebSocket() {
@ -230,8 +230,10 @@ export default {
receiverId: data.ReceiverId, receiverId: data.ReceiverId,
sendDate: data.SendDate, sendDate: data.SendDate,
message: data.Message, message: data.Message,
sendUserType: data.SendUserType, // 0 AI 1 2 3
}; };
const id = data.Id || data.id || Math.random().toString(36).substring(2); const id =
data.Id || data.id || Math.random().toString(36).substring(2);
const msg = { const msg = {
...processData, ...processData,
messageType: 0, messageType: 0,
@ -240,11 +242,22 @@ export default {
}; };
const storeState = (this.$store && this.$store.state) || {}; const storeState = (this.$store && this.$store.state) || {};
const isTransferChat = !!storeState.vuex_isTransferChat; const isTransferChat = !!storeState.vuex_isTransferChat;
const activeDialogueId =
storeState.vuex_msgUser?.dialogueManagementId ||
storeState.vuex_msgUser?.friendId ||
storeState.vuex_msgUser?.id;
const isActiveChat =
activeDialogueId &&
String(activeDialogueId) === String(processData.dialogueManagementId);
if (isTransferChat) { if (isTransferChat) {
this.$store.commit("push_AiLiveAgentMsg", msg); if (isActiveChat) {
this.$store.commit("push_AiLiveAgentMsg", msg);
}
} else { } else {
this.$store.commit("push_Msg", msg); if (isActiveChat) {
this.$store.commit("push_Msg", msg);
}
} }
// / / // / /
@ -391,9 +404,10 @@ uni-page-body {
body { body {
margin: 0; margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", font-family:
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu",
"Microsoft Yahei", sans-serif; "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", "Microsoft Yahei",
sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
// max-width: 1536rpx; // max-width: 1536rpx;

View File

@ -208,6 +208,12 @@ const install = (Vue, vm) => {
// 验证码登录-教师 // 验证码登录-教师
let TeacherLoginByCode = (params = {}) => let TeacherLoginByCode = (params = {}) =>
vm.$u.post("api/Login/PhoneLoginManagementEnd", params); vm.$u.post("api/Login/PhoneLoginManagementEnd", params);
// 忘记密码-获取短信验证码
let RequestForgotPasswordSMSCode = (params = {}) =>
vm.$u.post("api/Login/RequestForgotPasswordSMSCode", params);
// 忘记密码-修改密码
let ForgotPasswordChangePassword = (params = {}) =>
vm.$u.post("api/Login/ForgotPasswordChangePassword", params);
/** 用户-个人中心 */ /** 用户-个人中心 */
// 获取用户信息 // 获取用户信息
@ -251,6 +257,9 @@ const install = (Vue, vm) => {
// 转人工服务 // 转人工服务
let TransferToALiveAgentApi = (params = {}) => let TransferToALiveAgentApi = (params = {}) =>
vm.$u.post("api/Dialogue/TransferToALiveAgent", params); vm.$u.post("api/Dialogue/TransferToALiveAgent", params);
// 结束人工服务
let EndLiveAgentApi = (params = {}) =>
vm.$u.post("api/Dialogue/EndLiveAgent", params);
// 将各个定义的接口名称统一放进对象挂载到vm.$u.api(因为vm就是this也即this.$u.api)下 // 将各个定义的接口名称统一放进对象挂载到vm.$u.api(因为vm就是this也即this.$u.api)下
vm.$u.api = { vm.$u.api = {
@ -309,6 +318,8 @@ const install = (Vue, vm) => {
GetTeacherVerifyCode, GetTeacherVerifyCode,
TeacherLogin, TeacherLogin,
TeacherLoginByCode, TeacherLoginByCode,
RequestForgotPasswordSMSCode,
ForgotPasswordChangePassword,
GetUserApi, GetUserApi,
UpdateUserApi, UpdateUserApi,
GetTeacherListApi, GetTeacherListApi,
@ -322,6 +333,7 @@ const install = (Vue, vm) => {
ReadMessageApi, ReadMessageApi,
DeleteDialogueApi, DeleteDialogueApi,
TransferToALiveAgentApi, TransferToALiveAgentApi,
EndLiveAgentApi,
}; };
}; };

View File

@ -4,8 +4,8 @@ const install = (Vue, vm) => {
Vue.prototype.$u.http.setConfig({ Vue.prototype.$u.http.setConfig({
// baseUrl: 'https://xy.apps.service.zheke.com', // baseUrl: 'https://xy.apps.service.zheke.com',
// imgUrl: 'https://xy.apps.service.zheke.com/', // imgUrl: 'https://xy.apps.service.zheke.com/',
baseUrl: "http://120.55.234.65:8082", baseUrl: "http://120.55.234.65:8073",
imgUrl: "http://120.55.234.65:8082/", imgUrl: "http://120.55.234.65:8073/",
// imgUrl:'http://115.238.47.235:8987/', // imgUrl:'http://115.238.47.235:8987/',
// baseUrl: 'http://115.238.47.235:8993', // baseUrl: 'http://115.238.47.235:8993',
// 如果将此值设置为true拦截回调中将会返回服务端返回的所有数据response而不是response.data // 如果将此值设置为true拦截回调中将会返回服务端返回的所有数据response而不是response.data

View File

@ -10,11 +10,31 @@
> >
<view class="phone-popup"> <view class="phone-popup">
<view class="phone-title">招生电话</view> <view class="phone-title">招生电话</view>
<view class="phone-content">0790-6764666/6765666</view>
<view class="phone-card-list">
<view class="phone-card" @click="makeCall('0790-6764666')">
<view class="icon-wrapper">
<u-icon name="phone-fill" color="#ffffff" size="40"></u-icon>
</view>
<view class="info-wrapper">
<text class="phone-number">0790-6764666</text>
<text class="phone-desc">拨打招生电话1</text>
</view>
</view>
<view class="phone-card" @click="makeCall('0790-6765666')">
<view class="icon-wrapper">
<u-icon name="phone-fill" color="#ffffff" size="40"></u-icon>
</view>
<view class="info-wrapper">
<text class="phone-number">0790-6765666</text>
<text class="phone-desc">拨打招生电话2</text>
</view>
</view>
</view>
<view class="phone-button"> <view class="phone-button">
<u-button class="cancel-button" type="default" @click="closePopup" <view class="cancel-btn" @click="closePopup">取消</view>
>取消</u-button
>
</view> </view>
</view> </view>
</u-popup> </u-popup>
@ -48,6 +68,11 @@ export default {
}, },
}, },
methods: { methods: {
makeCall(phoneNumber) {
uni.makePhoneCall({
phoneNumber,
});
},
closePopup() { closePopup() {
this.showPopup = false; this.showPopup = false;
this.$emit("update:show", false); this.$emit("update:show", false);
@ -66,7 +91,6 @@ export default {
background-size: 630rpx 100rpx; background-size: 630rpx 100rpx;
background-position: -20rpx 0; background-position: -20rpx 0;
.phone-title { .phone-title {
text-align: center; text-align: center;
font-family: DouyinSans; font-family: DouyinSans;
@ -76,19 +100,72 @@ export default {
margin-bottom: 40rpx; margin-bottom: 40rpx;
} }
.phone-content { .phone-card-list {
margin-bottom: 120rpx; padding: 0 48rpx;
text-align: center; margin-bottom: 60rpx;
font-family: PingFang SC;
font-size: 32rpx; .phone-card {
color: #333333; display: flex;
align-items: center;
padding: 30rpx 40rpx;
margin-bottom: 24rpx;
background: #ffffff;
border: 1px solid #f0f0f0;
box-shadow: 0px 4rpx 12rpx rgba(0, 0, 0, 0.05);
border-radius: 16rpx;
&:last-child {
margin-bottom: 0;
}
.icon-wrapper {
width: 80rpx;
height: 80rpx;
background: #5ac799;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
margin-right: 30rpx;
flex-shrink: 0;
}
.info-wrapper {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
flex: 1;
.phone-number {
font-size: 36rpx;
font-weight: bold;
color: #333333;
letter-spacing: 2rpx;
margin-bottom: 10rpx;
}
.phone-desc {
font-size: 24rpx;
color: #999999;
}
}
}
} }
.phone-button { .phone-button {
padding: 0 40rpx; padding: 0 48rpx;
.cancel-button { .cancel-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
text-align: center;
background: #ffffff;
border: 1px solid #e5e5e5;
border-radius: 16rpx; border-radius: 16rpx;
font-size: 32rpx;
color: #333333;
} }
} }
} }

View File

@ -4,15 +4,15 @@
*/ */
export const TAB_BAR_CONFIG = [ export const TAB_BAR_CONFIG = [
{ // {
text: "在线咨询", // text: "在线咨询",
icon: "/static/tabbar/tabbar-icon1.png", // icon: "/static/tabbar/tabbar-icon1.png",
activeIcon: "/static/tabbar/tabbar-icon1-active.png", // activeIcon: "/static/tabbar/tabbar-icon1-active.png",
pagePath: "/pages/consultation/index", // pagePath: "/pages/consultation/index",
// 可选配置 // // 可选配置
badge: "", // 角标文字 // badge: "", // 角标文字
dot: false, // 是否显示小红点 // dot: false, // 是否显示小红点
}, // },
{ {
text: "人工转接", text: "人工转接",
icon: "/static/tabbar/tabbar-icon4.png", icon: "/static/tabbar/tabbar-icon4.png",

View File

@ -9,10 +9,10 @@ const getWebSocketUrl = () => {
// #ifdef H5 // #ifdef H5
// H5 开发环境 // H5 开发环境
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
return 'ws://localhost:8082/ws/chat' return 'ws://localhost:8073/ws/chat'
} }
// H5 生产环境 // H5 生产环境
return 'wss://120.55.234.65:8082/ws/chat' return 'wss://120.55.234.65:8073/ws/chat'
// #endif // #endif
} }

View File

@ -57,6 +57,13 @@
"navigationStyle": "custom" "navigationStyle": "custom"
} }
}, },
{
"path": "pages/my/change-password",
"style": {
"navigationBarTitleText": "修改密码",
"navigationStyle": "custom"
}
},
{ {
"path": "pages/home/admissions/index", "path": "pages/home/admissions/index",
"style": { "style": {
@ -73,14 +80,14 @@
"navigationStyle": "custom" "navigationStyle": "custom"
} }
}, },
{ // {
"path": "pages/consultation/index", // "path": "pages/consultation/index",
"style": { // "style": {
"navigationBarTitleText": "在线咨询", // "navigationBarTitleText": "在线咨询",
"enablePullDownRefresh": false, // "enablePullDownRefresh": false,
"navigationStyle": "custom" // "navigationStyle": "custom"
} // }
}, // },
{ {
"path": "pages/transfer/index", "path": "pages/transfer/index",
"style": { "style": {
@ -134,12 +141,13 @@
"borderStyle": "black", "borderStyle": "black",
"backgroundColor": "#ffffff", "backgroundColor": "#ffffff",
"list": [ "list": [
{ // 线便
"pagePath": "pages/consultation/index", // {
"iconPath": "static/tabbar/icon_home.png", // "pagePath": "pages/consultation/index",
"selectedIconPath": "static/tabbar/icon_home_active.png", // "iconPath": "static/tabbar/icon_home.png",
"text": "在线咨询" // "selectedIconPath": "static/tabbar/icon_home_active.png",
}, // "text": "在线咨询"
// },
{ {
"pagePath": "pages/transfer/index", "pagePath": "pages/transfer/index",
"iconPath": "static/tabbar/icon_message.png", "iconPath": "static/tabbar/icon_message.png",

View File

@ -118,10 +118,18 @@
{{ formatShowTime(message.sendDate) }} {{ formatShowTime(message.sendDate) }}
</view> </view>
<!-- 提示 -->
<!-- message.sendUserType 0 AI 1 学生 2 教师 3 提示目前只有2和3有用 -->
<view class="message-tip" v-if="message.sendUserType === 3">
{{ message.message }}
</view>
<!-- 0 发送消息 --> <!-- 0 发送消息 -->
<view <view
class="message-right" class="message-right"
v-if="message.senderId === vuex_user.Id" v-if="
message.senderId === vuex_user.Id && message.sendUserType === 2
"
:id="'msg-' + message.id" :id="'msg-' + message.id"
> >
<view class="message-content"> <view class="message-content">
@ -137,7 +145,11 @@
<!-- 1 收到消息 --> <!-- 1 收到消息 -->
<view <view
class="message-left" class="message-left"
v-if="message.senderId !== vuex_user.Id" v-if="
message.senderId !== vuex_user.Id &&
message.sendUserType !== 2 &&
message.sendUserType !== 3
"
:id="'msg-' + message.id" :id="'msg-' + message.id"
> >
<image <image
@ -309,7 +321,6 @@ export default {
this.$u.api this.$u.api
.SendMessage_PrivateApi(message) .SendMessage_PrivateApi(message)
.then((res) => { .then((res) => {
console.log(res, "发送消息成功");
if (res.succeed) { if (res.succeed) {
// //
const msgUserData = { const msgUserData = {
@ -331,6 +342,14 @@ export default {
// this.$nextTick(() => { // this.$nextTick(() => {
// this.scrollToBottom(); // this.scrollToBottom();
// }); // });
} else {
//
this.messageValue = "";
//
uni.showToast({
title: res.error || "发送失败",
icon: "none",
});
} }
}) })
.catch((error) => { .catch((error) => {
@ -568,6 +587,12 @@ export default {
color: #999999; color: #999999;
padding: 20rpx; padding: 20rpx;
} }
.message-tip {
text-align: center;
font-size: 24rpx;
color: #999999;
padding-bottom: 40rpx;
}
.message-left, .message-left,
.message-right { .message-right {

View File

@ -2,9 +2,26 @@
<view class="home-container"> <view class="home-container">
<header-bar <header-bar
title="源小新" title="源小新"
:showLeftIcon="false"
leftIcon="list" leftIcon="list"
@leftClick="handleLeftClick" @leftClick="handleLeftClick"
></header-bar> >
<image
slot="left"
class="header-icon"
src="/static/common/images/icon-userSetting.png"
@click="handleSettingClick"
></image>
<!-- 记录干掉了 -->
<!-- <image
slot="right"
class="header-icon"
src="/static/common/images/icon-userRecord.png"
@click="handleRecordClick"
></image> -->
</header-bar>
<!-- 这是新版 -->
<!-- 首页 --> <!-- 首页 -->
<view class="main-content" v-if="!isChat"> <view class="main-content" v-if="!isChat">
@ -59,10 +76,16 @@
:style="{ :style="{
background: item.background, background: item.background,
}" }"
@click="handleFeatureClick(item)"
> >
<image :src="item.icon" class="feature-icon"></image> <image :src="item.icon" class="feature-icon"></image>
<text class="feature-text">{{ item.title }}</text> <view class="feature-text-wrapper">
<text class="feature-title">{{ item.title }}</text>
<text class="feature-tip">{{ item.tip }}</text>
</view>
<view class="feature-action-btn" @click="handleFeatureClick(item)">
<text>去咨询</text>
<u-icon name="arrow-right" size="24" color="#ffffff"></u-icon>
</view>
</view> </view>
</view> </view>
</view> </view>
@ -136,6 +159,12 @@
{{ message.displayTime }} {{ message.displayTime }}
</view> </view>
<!-- 提示 -->
<!-- message.sendUserType 0 AI 1 学生 2 教师 3 提示目前只有2和3有用 -->
<view class="message-tip" v-if="message.sendUserType === 3">
{{ message.message }}
</view>
<!-- 转人工选择卡片独立消息不属于AI回复不展示头像/反馈 --> <!-- 转人工选择卡片独立消息不属于AI回复不展示头像/反馈 -->
<view <view
class="message-transfer" class="message-transfer"
@ -201,9 +230,14 @@
<!-- 用户消息 --> <!-- 用户消息 -->
<!-- 0 用户消息 --> <!-- 0 用户消息 -->
<!-- 排除sendUserType 2 教师 3 提示 -->
<view <view
class="message-right" class="message-right"
v-else-if="message.interactMode === 0" v-else-if="
message.interactMode === 0 &&
message.sendUserType !== 3 &&
message.sendUserType !== 2
"
:id="'msg-' + message.id" :id="'msg-' + message.id"
> >
<view class="message-content"> <view class="message-content">
@ -211,7 +245,7 @@
</view> </view>
<image <image
class="user-avatar" class="user-avatar"
:src="headSculptureUrl" :src="$getHeadImgUrl(vuex_user.HeadSculptureUrl)"
mode="scaleToFill" mode="scaleToFill"
/> />
</view> </view>
@ -225,25 +259,75 @@
" "
:id="'msg-' + message.id" :id="'msg-' + message.id"
> >
<image <view class="message-content-container-AI">
class="ai-avatar" <image
src="/static/common/images/avatar_ai.png" class="ai-avatar"
mode="scaleToFill" src="/static/common/images/avatar_ai.png"
/> mode="scaleToFill"
<view />
class="message-content" <view
:class="{ class="message-content"
'message-content-width': !message.isLoading, :class="{
}" 'message-content-width': !message.isLoading,
> 'message-content-structured':
<!-- 加载动画 --> getStructuredMessageBlocks(message).length,
<view v-if="message.isLoading" class="loading-dots"> }"
<view class="dot"></view> >
<view class="dot"></view> <!-- 加载动画 -->
<view class="dot"></view> <view v-if="message.isLoading" class="loading-dots">
<view class="dot"></view>
<view class="dot"></view>
<view class="dot"></view>
</view>
<view
v-else-if="getStructuredMessageBlocks(message).length"
class="structured-message"
>
<view
v-for="(block, blockIndex) in getStructuredMessageBlocks(
message,
)"
:key="`${message.id || index}-block-${blockIndex}`"
class="structured-item"
:class="`structured-item-${block.type}`"
>
<text
v-if="block.type === 'text'"
class="structured-text"
>
{{ block.value }}
</text>
<image
v-else-if="block.type === 'image'"
class="structured-image"
:src="block.url"
mode="widthFix"
@click="previewMessageImage(block.url)"
></image>
<view
v-else-if="block.type === 'file'"
class="structured-file"
@click="downloadMessageFile(block.url)"
>
<image
class="structured-file-icon"
src="/static/common/images/icon-file.png"
mode="scaleToFill"
></image>
<text class="structured-file-name">{{
block.name
}}</text>
<u-icon
name="download"
size="36"
color="#666666"
class="structured-file-download"
></u-icon>
</view>
</view>
</view>
<markdown-viewer v-else :content="message.message" />
</view> </view>
<!-- 正常消息内容 -->
<markdown-viewer v-else :content="message.message" />
</view> </view>
<!-- 回答反馈点赞/点踩 --> <!-- 回答反馈点赞/点踩 -->
@ -281,29 +365,81 @@
</view> </view>
<!-- 转人工消息 --> <!-- 转人工消息 -->
<!-- 8 人工回复 --> <!-- 8 人工回复 message.interactMode === 8 -->
<!-- 改成 message.sendUserType === 2 -->
<view <view
class="message-left" class="message-left"
v-else-if="message.interactMode === 8" v-else-if="message.sendUserType === 2"
:id="'msg-' + message.id" :id="'msg-' + message.id"
> >
<image <view class="message-content-container-Teacher">
class="ai-avatar" <image
:src="receiverHeadSculptureUrl" class="ai-avatar"
mode="scaleToFill" :src="getReceiverHeadSculptureUrl(message)"
/> mode="scaleToFill"
<view />
class="message-content" <view
:class="{ class="message-content"
'message-content-width': !message.isLoading, :class="{
}" 'message-content-width': !message.isLoading,
> 'message-content-structured':
<view v-if="message.isLoading" class="loading-dots"> getStructuredMessageBlocks(message).length,
<view class="dot"></view> }"
<view class="dot"></view> >
<view class="dot"></view> <view v-if="message.isLoading" class="loading-dots">
<view class="dot"></view>
<view class="dot"></view>
<view class="dot"></view>
</view>
<view
v-else-if="getStructuredMessageBlocks(message).length"
class="structured-message"
>
<view
v-for="(block, blockIndex) in getStructuredMessageBlocks(
message,
)"
:key="`${message.id || index}-teacher-block-${blockIndex}`"
class="structured-item"
:class="`structured-item-${block.type}`"
>
<text
v-if="block.type === 'text'"
class="structured-text"
>
{{ block.value }}
</text>
<image
v-else-if="block.type === 'image'"
class="structured-image"
:src="block.url"
mode="widthFix"
@click="previewMessageImage(block.url)"
></image>
<view
v-else-if="block.type === 'file'"
class="structured-file"
@click="downloadMessageFile(block.url)"
>
<image
class="structured-file-icon"
src="/static/common/images/icon-file.png"
mode="scaleToFill"
></image>
<text class="structured-file-name">{{
block.name
}}</text>
<u-icon
name="download"
size="36"
color="#666666"
class="structured-file-download"
></u-icon>
</view>
</view>
</view>
<markdown-viewer v-else :content="message.message" />
</view> </view>
<markdown-viewer v-else :content="message.message" />
</view> </view>
</view> </view>
</block> </block>
@ -316,7 +452,7 @@
<view class="floating-tabs"> <view class="floating-tabs">
<view <view
class="tab-item" class="tab-item"
v-for="(tab, index) in floatingTabs" v-for="(tab, index) in displayFloatingTabs"
:key="index" :key="index"
@click="handleFeatureClick({ title: tab.title, path: tab.path })" @click="handleFeatureClick({ title: tab.title, path: tab.path })"
> >
@ -422,16 +558,17 @@ export default {
"我什么时候能推知道自己是否被录取?", "我什么时候能推知道自己是否被录取?",
], ],
features: [ features: [
{ // {
title: "在线咨询", // title: "线",
icon: "/static/common/images/icon_admissions.png", // icon: "/static/common/images/icon_admissions.png",
path: "/pages/home/admissions/index", // path: "/pages/home/admissions/index",
background: "linear-gradient(0deg, #F4FBFE 0%, #F4FBFE 100%)", // background: "linear-gradient(0deg, #F4FBFE 0%, #F4FBFE 100%)",
}, // },
{ {
title: "电话咨询", title: "电话咨询",
icon: "/static/common/images/icon_phone.png", tip: "欢迎致电本校招生咨询热线",
background: "linear-gradient(0deg, #F4FBF9 0%, #F4FBF9 100%)", icon: "/static/common/images/icon-phone1.png",
background: "#fff",
}, },
], ],
floatingTabs: [ floatingTabs: [
@ -439,11 +576,11 @@ export default {
title: "首页", title: "首页",
icon: "/static/common/images/icon_home.png", icon: "/static/common/images/icon_home.png",
}, },
{ // {
title: "招生在线", // title: "线",
icon: "/static/common/images/icon_admissions2.png", // icon: "/static/common/images/icon_admissions2.png",
path: "/pages/home/admissions/index", // path: "/pages/home/admissions/index",
}, // },
{ {
title: "转人工", title: "转人工",
icon: "/static/common/images/icon_conversation.png", icon: "/static/common/images/icon_conversation.png",
@ -503,12 +640,6 @@ export default {
}, },
computed: { computed: {
headSculptureUrl() {
return this.$getHeadImgUrl(this.vuex_user.HeadSculptureUrl);
},
receiverHeadSculptureUrl() {
return this.$getHeadImgUrl(this.vuex_msgUser.headSculptureUrl);
},
aiMessageGroups() { aiMessageGroups() {
return Array.isArray(this.vuex_aiMessageGroups) return Array.isArray(this.vuex_aiMessageGroups)
? this.vuex_aiMessageGroups ? this.vuex_aiMessageGroups
@ -516,6 +647,10 @@ export default {
}, },
displayFeatures() { displayFeatures() {
const list = Array.isArray(this.features) ? this.features : []; const list = Array.isArray(this.features) ? this.features : [];
return list;
},
displayFloatingTabs() {
const list = Array.isArray(this.floatingTabs) ? this.floatingTabs : [];
return list.map((item) => { return list.map((item) => {
if (item && item.title === "转人工") { if (item && item.title === "转人工") {
return { return {
@ -527,11 +662,11 @@ export default {
}); });
}, },
isLiveAgentChat() { isLiveAgentChat() {
if (!this.vuex_isTransferChat) return false; return !!(
const current = this.currentDMid || ""; this.vuex_isTransferChat &&
const active = this.vuex_msgUser?.dialogueManagementId &&
this.vuex_msgUser && this.vuex_msgUser.dialogueManagementId; this.vuex_msgUser?.receiverId
return !!(current && active && String(active) === String(current)); );
}, },
}, },
@ -570,12 +705,29 @@ export default {
}, },
methods: { methods: {
//
getReceiverHeadSculptureUrl(message) {
return this.$getHeadImgUrl(message.headSculptureUrl);
},
async handleLeftClick() { async handleLeftClick() {
await this.getChatHistoryList(); await this.getChatHistoryList();
await this.GetDialogueList_User(); await this.GetDialogueList_User();
this.handlePopupShow(); this.handlePopupShow();
}, },
//
handleSettingClick() {
uni.navigateTo({
url: "/pages/home/userSetting/index",
});
},
//
// handleRecordClick() {
// console.log("");
// },
// //
resetChatState({ resetChatState({
conversationId = "", conversationId = "",
@ -658,38 +810,46 @@ export default {
extraOffset: 200, extraOffset: 200,
}); });
}, },
//
handleFeatureClick(item) { handleFeatureClick(item) {
if (item.title === "首页") { const actions = {
this.resetChatState({ isChat: false }); 首页: () => this.resetChatState({ isChat: false }),
return; 转人工: () => this.handleTransferEntryClick(),
} 结束会话: () => this.handleEndConversation(),
if (item.title === "转人工") { 电话咨询: () => {
this.handleTransferEntryClick(); this.advicePhoneShow = true;
return; },
} };
if (item.title === "结束会话") {
this.$store.commit("set_IsTransferChat", false);
return;
}
if (item.title === "电话咨询") { const action = actions[item.title];
this.advicePhoneShow = true; if (action) {
return; action();
} else if (item.path) { } else if (item.path) {
uni.navigateTo({ uni.navigateTo({ url: item.path });
url: item.path,
});
return;
} else { } else {
this.$refs.uToast.show({ this.$refs.uToast.show({ title: "暂未开放", type: "warning" });
title: "暂未开放",
type: "warning",
});
return;
} }
}, },
//
handleEndConversation() {
this.$u.api
.EndLiveAgentApi({
dialogueManagementId: this.vuex_msgUser?.dialogueManagementId,
teacherManagementId: this.vuex_msgUser?.receiverId,
})
.then((res) => {
if (res.succeed) {
this.$store.commit("set_IsTransferChat", false);
this.$u.toast("会话已结束");
} else {
this.$u.toast(res.error || "会话结束失败");
}
});
},
//
createLocalUserTextMessage(messageText) { createLocalUserTextMessage(messageText) {
return { return {
id: Math.random().toString(36).substring(2, 15), id: Math.random().toString(36).substring(2, 15),
@ -704,6 +864,7 @@ export default {
}; };
}, },
//
createTransferCardMessage() { createTransferCardMessage() {
return { return {
id: "transfer_" + Math.random().toString(36).substring(2, 15), id: "transfer_" + Math.random().toString(36).substring(2, 15),
@ -722,6 +883,7 @@ export default {
}; };
}, },
//
handleTransferEntryClick() { handleTransferEntryClick() {
if (!this.isChat) { if (!this.isChat) {
this.resetChatState(); this.resetChatState();
@ -807,22 +969,14 @@ export default {
return; return;
} }
// dialogueManagementId
const dialogueManagementId = const dialogueManagementId =
this.currentDMid || (res && res.data && res.data.dialogueManagementId) || "";
(res && // receiverId 线
res.data && const receiverId = (res && res.data && res.data.receiverId) || "";
(res.data.dialogueManagementId ||
res.data.DialogueManagementId)) || console.log(dialogueManagementId, "dialogueManagementId");
""; console.log(receiverId, "receiverId");
const receiverId =
(res &&
res.data &&
(res.data.receiverId ||
res.data.ReceiverId ||
res.data.liveAgentId ||
res.data.agentId ||
res.data.userId)) ||
"";
if (!this.currentDMid && dialogueManagementId) { if (!this.currentDMid && dialogueManagementId) {
this.currentDMid = dialogueManagementId; this.currentDMid = dialogueManagementId;
@ -835,12 +989,17 @@ export default {
if (dialogueManagementId && receiverId) { if (dialogueManagementId && receiverId) {
this.$store.commit("set_IsTransferChat", true); this.$store.commit("set_IsTransferChat", true);
this.$store.commit("update_MsgUser", {
dialogueManagementId,
receiverId,
});
this.$store.dispatch("selectTeacherChatItem", { this.$store.dispatch("selectTeacherChatItem", {
id: dialogueManagementId, id: dialogueManagementId,
receiverId, receiverId,
navigate: false, navigate: false,
}); });
} }
console.log(this.vuex_msgUser, "vuex_msgUser");
}) })
.catch(() => { .catch(() => {
updateTransferCard({ updateTransferCard({
@ -965,10 +1124,11 @@ export default {
}, },
// //
handleQAClick(item) { async handleQAClick(item) {
// //
if (!this.isChat) { if (!this.isChat) {
this.resetChatState(); // this.resetChatState();
await this.handleStartChat(); // ,
} }
const sendMessage = item.content; const sendMessage = item.content;
@ -1028,6 +1188,7 @@ export default {
if (res.succeed) { if (res.succeed) {
console.log("hotQuestionsDetail.....", res.data); console.log("hotQuestionsDetail.....", res.data);
this.currentDMid = res.data.dmId; this.currentDMid = res.data.dmId;
const messageId = res.data.messageId;
const data = res.data.entityInfo; const data = res.data.entityInfo;
// //
@ -1035,12 +1196,19 @@ export default {
(msg) => !msg.isLoading, (msg) => !msg.isLoading,
); );
const answerTypeLabel = data.detailedExplanation; //
const imageUrl = data.imageUrl; //
const nearbyPaths = data.nearbyPaths; //
const message = `{\r\n "answerTypeLabel": ${JSON.stringify(
answerTypeLabel || "",
)},\r\n "imageUrl": ${JSON.stringify(
imageUrl || "",
)},\r\n "nearbyPaths": ${JSON.stringify(nearbyPaths || "")}\r\n}`;
// AI // AI
const aiMessage = { const aiMessage = {
id: id: messageId || Math.random().toString(36).substring(2, 15),
data.conversationId || message: message,
Math.random().toString(36).substring(2, 15),
message: data.detailedExplanation,
sendDate: "", sendDate: "",
isSend: true, isSend: true,
isRead: false, isRead: false,
@ -1108,6 +1276,107 @@ export default {
this.handleGetConversationDetail(); this.handleGetConversationDetail();
}, },
//
getStructuredMessageBlocks(message) {
const rawMessage =
(message && (message.rawMessage || message.message)) || "";
if (!rawMessage || typeof rawMessage !== "string") return [];
let parsed = null;
try {
parsed = JSON.parse(rawMessage);
} catch (error) {
return [];
}
if (!parsed || typeof parsed !== "object") return [];
const answerTypeLabel = (parsed.answerTypeLabel || "").trim();
const imageUrl = parsed.imageUrl || "";
const nearbyPaths = parsed.nearbyPaths || "";
if (!answerTypeLabel && !imageUrl && !nearbyPaths) return [];
const blocks = [];
if (answerTypeLabel) {
blocks.push({ type: "text", value: answerTypeLabel });
}
this.normalizeResourceList(imageUrl).forEach((item) => {
const url = this.formatMessageResourceUrl(item);
if (url) blocks.push({ type: "image", url });
});
this.normalizeResourceList(nearbyPaths).forEach((item) => {
const url = this.formatMessageResourceUrl(item);
if (!url) return;
blocks.push({
type: "file",
url,
name: this.getFileNameFromPath(item),
});
});
return blocks;
},
normalizeResourceList(value) {
if (!value) return [];
if (Array.isArray(value)) {
return value.map((item) => String(item || "").trim()).filter(Boolean);
}
return String(value)
.split(/[\n,|]/)
.map((item) => item.trim())
.filter(Boolean);
},
formatMessageResourceUrl(path) {
if (!path) return "";
const currentPath = String(path).trim();
if (!currentPath) return "";
if (
currentPath.startsWith("http://") ||
currentPath.startsWith("https://") ||
currentPath.startsWith("blob:")
) {
return currentPath;
}
const cleanPath = currentPath.replace(/\\/g, "/");
const cleanBaseUrl = (this.baseUrl || "")
.replace(/\\/g, "/")
.replace(/\/$/, "");
if (!cleanBaseUrl) return cleanPath;
if (cleanPath.startsWith("/")) return `${cleanBaseUrl}${cleanPath}`;
return `${cleanBaseUrl}/${cleanPath}`;
},
getFileNameFromPath(path) {
const cleanPath = String(path || "")
.replace(/\\/g, "/")
.split("?")[0];
const segments = cleanPath.split("/");
const fileName = segments[segments.length - 1] || "下载文件";
try {
return decodeURIComponent(fileName);
} catch (error) {
return fileName;
}
},
previewMessageImage(url) {
if (!url) return;
uni.previewImage({
current: url,
urls: [url],
});
},
downloadMessageFile(url) {
if (!url) return;
if (typeof window !== "undefined" && window.open) {
window.open(url, "_blank");
return;
}
uni.downloadFile({
url,
});
},
// JSON // JSON
processMessageContent(message) { processMessageContent(message) {
return processChatMessageContent(message); return processChatMessageContent(message);
@ -1120,11 +1389,11 @@ export default {
// //
handleGetConversationDetail() { handleGetConversationDetail() {
if (!this.currentDMid) return; // if (!this.currentDMid) return;
this.$u.api return this.$u.api
.GetConversationDetail({ .GetConversationDetail({
"Item1.Id": this.currentDMid, // "Item1.Id": "08de2e31-4cdf-4b0c-8b83-d3185f604a5a",
PageIndex: this.pageQuery.PageIndex, PageIndex: this.pageQuery.PageIndex,
PageSize: this.pageQuery.PageSize, PageSize: this.pageQuery.PageSize,
}) })
@ -1145,14 +1414,14 @@ export default {
// >1退 // >1退
refreshPageWithFallback() { refreshPageWithFallback() {
if (!this.currentDMid) return; // if (!this.currentDMid) return;
const currentIndex = this.pageQuery.PageIndex || 1; const currentIndex = this.pageQuery.PageIndex || 1;
const pageSize = this.pageQuery.PageSize; const pageSize = this.pageQuery.PageSize;
return this.$u.api return this.$u.api
.GetConversationDetail({ .GetConversationDetail({
"Item1.Id": this.currentDMid, // "Item1.Id": this.currentDMid,
PageIndex: currentIndex, PageIndex: currentIndex,
PageSize: pageSize, PageSize: pageSize,
}) })
@ -1162,7 +1431,7 @@ export default {
const prevIndex = currentIndex - 1; const prevIndex = currentIndex - 1;
return this.$u.api return this.$u.api
.GetConversationDetail({ .GetConversationDetail({
"Item1.Id": this.currentDMid, // "Item1.Id": this.currentDMid,
PageIndex: prevIndex, PageIndex: prevIndex,
PageSize: pageSize, PageSize: pageSize,
}) })
@ -1193,6 +1462,9 @@ export default {
// //
handleStartChat() { handleStartChat() {
this.resetChatState(); this.resetChatState();
//
return this.handleGetConversationDetail();
}, },
// //
@ -1204,7 +1476,7 @@ export default {
onScrollToUpper() { onScrollToUpper() {
console.log("触发上拉刷新"); console.log("触发上拉刷新");
if (!this.currentDMid) return; // if (!this.currentDMid) return;
if (this.isLiveAgentChat) return; if (this.isLiveAgentChat) return;
// //
@ -1219,7 +1491,7 @@ export default {
this.pageQuery.PageIndex++; this.pageQuery.PageIndex++;
this.$u.api this.$u.api
.GetConversationDetail({ .GetConversationDetail({
"Item1.Id": this.currentDMid, // "Item1.Id": this.currentDMid,
PageIndex: this.pageQuery.PageIndex, PageIndex: this.pageQuery.PageIndex,
PageSize: this.pageQuery.PageSize, PageSize: this.pageQuery.PageSize,
}) })
@ -1252,8 +1524,15 @@ export default {
// /&退 // /&退
handleFeedback(message, isHelp) { handleFeedback(message, isHelp) {
this.$u.api.ModifyStatus({ id: message.id, isHelp }).then((res) => { this.$u.api.ModifyStatus({ id: message.id, isHelp }).then((res) => {
if (!res.succeed) return; if (!res.succeed) {
this.$u.toast("操作成功"); uni.showToast({
title: res.error || "操作失败",
icon: "none",
});
return;
}
// this.$u.toast(""); //
// 退 // 退
this.refreshPageWithFallback(); this.refreshPageWithFallback();
}); });
@ -1326,6 +1605,11 @@ export default {
/* Header样式移至HeaderBar组件 */ /* Header样式移至HeaderBar组件 */
.header-icon {
width: 40rpx;
height: 40rpx;
}
.main-content { .main-content {
padding: 30rpx; padding: 30rpx;
padding-top: 60rpx; padding-top: 60rpx;
@ -1464,29 +1748,59 @@ export default {
border-radius: 16rpx; border-radius: 16rpx;
padding: 30rpx; padding: 30rpx;
display: flex; display: flex;
flex-direction: column;
justify-content: space-between; justify-content: space-between;
margin-top: 32rpx; margin-top: 32rpx;
gap: 30rpx; gap: 30rpx;
.feature-item { .feature-item {
height: 150rpx; height: 140rpx;
border-radius: 16rpx; border-radius: 16rpx;
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
padding-left: 30rpx; // padding-left: 30rpx;
gap: 20rpx; gap: 30rpx;
flex: 1; flex: 1;
.feature-icon { .feature-icon {
width: 80rpx; width: 80rpx;
height: 80rpx; height: 80rpx;
margin-top: 16rpx;
} }
.feature-text { .feature-text-wrapper {
font-size: 26rpx; display: flex;
color: #333333; flex-direction: column;
justify-content: space-between;
gap: 12rpx;
flex: 1;
.feature-title {
font-size: 28rpx;
color: #333333;
font-weight: bold;
}
.feature-tip {
font-size: 24rpx;
color: #999;
}
}
.feature-action-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 14rpx 20rpx 14rpx 24rpx;
gap: 10rpx;
// height: 56rpx;
background: linear-gradient(-33deg, #6079ff 0%, #418ded 100%);
box-shadow: 0px 4rpx 16rpx 0rpx rgba(62, 106, 255, 0.6);
border-radius: 28rpx;
text {
font-size: 24rpx;
color: #ffffff;
margin-right: 4rpx;
}
} }
} }
} }
@ -1595,6 +1909,12 @@ export default {
color: #999999; color: #999999;
padding: 20rpx; padding: 20rpx;
} }
.message-tip {
text-align: center;
font-size: 24rpx;
color: #999999;
padding-bottom: 40rpx;
}
.message-left, .message-left,
.message-right { .message-right {
@ -1604,8 +1924,14 @@ export default {
} }
.message-left { .message-left {
justify-content: flex-start; flex-direction: column;
flex-wrap: wrap; /* 允许反馈区换行到消息下方 */
.message-content-container-AI,
.message-content-container-Teacher {
display: flex;
justify-content: flex-start;
flex-wrap: wrap; /* 允许反馈区换行到消息下方 */
}
.ai-avatar { .ai-avatar {
width: 64rpx; width: 64rpx;
@ -1624,6 +1950,12 @@ export default {
font-size: 28rpx; font-size: 28rpx;
line-height: 1.5; line-height: 1.5;
&.message-content-structured {
background-color: transparent;
padding: 0;
border-radius: 0;
}
/* 加载动画样式 */ /* 加载动画样式 */
.loading-dots { .loading-dots {
display: flex; display: flex;
@ -1696,15 +2028,99 @@ export default {
font-size: 24rpx; font-size: 24rpx;
} }
} }
.structured-message {
.structured-item + .structured-item {
margin-top: 16rpx;
}
.structured-item {
max-width: 100%;
}
.structured-item-text {
.structured-text {
display: inline-block;
background-color: #ffffff;
color: #333333;
font-size: 28rpx;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
padding: 20rpx 24rpx;
border-radius: 0 16rpx 16rpx 16rpx;
}
}
.structured-text {
display: inline-block;
color: #333333;
font-size: 28rpx;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
}
.structured-item-image {
.structured-image {
display: block;
max-width: 240rpx;
border-radius: 12rpx;
}
}
.structured-image {
display: block;
max-width: 240rpx;
border-radius: 12rpx;
}
.structured-item-file {
.structured-file {
display: flex;
align-items: center;
width: 500rpx;
max-width: 100%;
padding: 24rpx;
background-color: #ffffff;
border-radius: 16rpx;
box-sizing: border-box;
.structured-file-icon {
width: 48rpx;
height: 62rpx;
flex-shrink: 0;
margin-right: 20rpx;
}
.structured-file-name {
flex: 1;
font-size: 28rpx;
color: #333333;
word-break: break-all;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
overflow: hidden;
}
.structured-file-download {
flex-shrink: 0;
margin-left: 20rpx;
}
}
}
}
} }
.message-content-width { .message-content-width {
width: 70%; max-width: 70%;
} }
// }
/* 回答反馈容器,跟随左侧消息,右下角对齐 */ /* 回答反馈容器,跟随左侧消息,右下角对齐 */
.feedback-container { .feedback-container {
width: 70%; max-width: 70%;
margin-left: 80rpx; /* 头像宽度64rpx + 间距16rpx */ margin-left: 80rpx; /* 头像宽度64rpx + 间距16rpx */
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;

View File

@ -1,4 +1,5 @@
<template> <template>
<!-- 留言板页面 此页面已废弃 -->
<view class="message-board-page"> <view class="message-board-page">
<header-bar title="留言板" @leftClick="handleLeftClick"></header-bar> <header-bar title="留言板" @leftClick="handleLeftClick"></header-bar>
<view class="custom-tabs-box"> <view class="custom-tabs-box">

View File

@ -464,7 +464,7 @@ export default {
// //
const redirect = () => { const redirect = () => {
const url = this.isTeacher const url = this.isTeacher
? "/pages/consultation/index" ? "/pages/transfer/index"
: "/pages/home/index/index"; : "/pages/home/index/index";
uni.reLaunch({ uni.reLaunch({
url: url, url: url,
@ -821,4 +821,3 @@ export default {
} }
</style> </style>

View File

@ -0,0 +1,372 @@
<template>
<view class="change-password-page">
<PageHeader
title="修改密码"
:is-back="true"
:border-bottom="false"
:background="headerBackground"
/>
<view class="content-wrapper">
<view class="form-card single-row-card">
<view class="form-row">
<text class="label">手机号</text>
<input
class="input"
type="number"
v-model="form.phone"
placeholder="请输入手机号"
placeholder-style="color: #c0c0c0;"
/>
</view>
</view>
<view class="form-card single-row-card">
<view class="form-row captcha-row">
<text class="label">图形验证码</text>
<input
class="input"
v-model="form.captcha"
placeholder="请输入验证码"
placeholder-style="color: #c0c0c0;"
/>
<image
class="captcha-image"
:src="captchaUrl"
mode="aspectFill"
@click="refreshCaptcha"
/>
</view>
</view>
<view class="form-card single-row-card">
<view class="form-row">
<text class="label">验证码</text>
<input
class="input"
type="number"
v-model="form.code"
placeholder="请输入验证码"
placeholder-style="color: #c0c0c0;"
/>
<text
class="get-code"
:class="{ disabled: codeText !== '获取验证码' }"
@click="handleGetCode"
>
{{ codeText }}
</text>
</view>
</view>
<view class="form-card">
<view class="form-row">
<text class="label">新密码</text>
<input
class="input"
:password="!showPassword"
v-model="form.pwd"
placeholder="请输入新密码"
placeholder-style="color: #c0c0c0;"
/>
</view>
<view class="form-row">
<text class="label">确认新密码</text>
<input
class="input"
:password="!showPassword"
v-model="form.confirmPwd"
placeholder="请再次输入新密码"
placeholder-style="color: #c0c0c0;"
/>
</view>
</view>
<button class="submit-button" @click="handleSubmit">确定</button>
</view>
<u-toast ref="uToast" />
</view>
</template>
<script>
import PageHeader from "@/components/PageHeader.vue";
import { generateSign } from "@/utils/signUtil.js";
import md5 from "js-md5";
export default {
name: "ChangePassword",
components: {
PageHeader,
},
data() {
return {
headerBackground: {
background: "transparent",
},
form: {
phone: "",
code: "",
captcha: "",
pwd: "",
confirmPwd: "",
},
showPassword: false,
codeText: "获取验证码",
countdown: 60,
timer: null,
captchaId: "",
captchaUrl: "",
};
},
onLoad() {
const userInfo = this.vuex_user || {};
const phone = userInfo.phone || userInfo.Phone || "";
if (phone) {
this.form.phone = phone;
}
this.refreshCaptcha();
},
beforeDestroy() {
if (this.timer) {
clearInterval(this.timer);
}
},
methods: {
refreshCaptcha() {
this.$u.api.GetCaptcha().then((res) => {
this.captchaId = res.captchaId || "";
this.captchaUrl = res.imageBase64
? `data:image/png;base64,${res.imageBase64}`
: "";
});
},
handleGetCode() {
if (this.codeText !== "获取验证码") return;
if (!this.validatePhone()) return;
if (!this.validateCaptcha()) return;
this.requestSmsCode();
},
requestSmsCode() {
const sign = generateSign(this.form.phone);
const params = {
phone: this.form.phone,
sign,
captchaId: this.captchaId,
captchaCode: this.form.captcha,
ip: "",
};
this.$u.api.RequestForgotPasswordSMSCode(params).then((res) => {
if (res && res.succeed) {
this.$refs.uToast.show({
title: "发送成功",
type: "success",
});
this.startCountdown();
} else {
this.$refs.uToast.show({
title: res?.error || "发送失败",
type: "error",
});
this.refreshCaptcha();
}
});
},
startCountdown() {
this.countdown = 60;
this.codeText = `${this.countdown}秒后重试`;
if (this.timer) {
clearInterval(this.timer);
}
this.timer = setInterval(() => {
this.countdown -= 1;
this.codeText = `${this.countdown}秒后重试`;
if (this.countdown <= 0) {
clearInterval(this.timer);
this.codeText = "获取验证码";
}
}, 1000);
},
validatePhone() {
if (!this.form.phone) {
this.$refs.uToast.show({
title: "请输入手机号",
type: "warning",
});
return false;
}
if (!/^1[3-9]\d{9}$/.test(this.form.phone)) {
this.$refs.uToast.show({
title: "手机号格式不正确",
type: "warning",
});
return false;
}
return true;
},
validateCaptcha() {
if (!this.form.captcha) {
this.$refs.uToast.show({
title: "请输入图形验证码",
type: "warning",
});
return false;
}
return true;
},
validateForm() {
if (!this.validatePhone()) return false;
if (!this.validateCaptcha()) return false;
if (!this.form.code) {
this.$refs.uToast.show({
title: "请输入验证码",
type: "warning",
});
return false;
}
if (!this.form.pwd) {
this.$refs.uToast.show({
title: "请输入新密码",
type: "warning",
});
return false;
}
if (!this.form.confirmPwd) {
this.$refs.uToast.show({
title: "请再次输入新密码",
type: "warning",
});
return false;
}
if (this.form.pwd !== this.form.confirmPwd) {
this.$refs.uToast.show({
title: "两次密码输入不一致",
type: "warning",
});
return false;
}
return true;
},
handleSubmit() {
if (!this.validateForm()) return;
const params = {
phone: this.form.phone,
code: this.form.code,
pwd: md5(this.form.pwd),
};
this.$u.api.ForgotPasswordChangePassword(params).then((res) => {
if (res && res.succeed) {
this.$refs.uToast.show({
title: "修改成功",
type: "success",
});
setTimeout(() => {
uni.navigateBack();
}, 800);
} else {
this.$refs.uToast.show({
title: res?.error || "修改失败",
type: "error",
});
}
});
},
},
};
</script>
<style scoped>
.change-password-page {
min-height: 100vh;
background-image: url("@/static/notes/bg.png");
background-position: center top;
background-repeat: no-repeat;
background-size: 100% auto;
display: flex;
flex-direction: column;
}
.content-wrapper {
flex: 1;
padding: 30rpx;
padding-top: 40rpx;
box-sizing: border-box;
}
.form-card {
background-color: #ffffff;
border-radius: 20rpx;
padding: 0 30rpx;
margin-bottom: 24rpx;
}
.form-row {
display: flex;
align-items: center;
padding: 28rpx 0;
/* border-bottom: 1rpx solid #f1f1f1; */
}
.form-row:last-child {
border-bottom: none;
}
.form-row.captcha-row {
align-items: center;
}
.label {
width: 150rpx;
font-weight: 500;
font-size: 27rpx;
color: #333333;
}
.input {
flex: 1;
font-size: 28rpx;
color: #333333;
text-align: right;
}
.get-code {
font-size: 26rpx;
color: #4f6aff;
padding-left: 16rpx;
border-left: 1rpx solid #e5e5e5;
margin-left: 16rpx;
}
.get-code.disabled {
color: #b8b8b8;
}
.captcha-image {
width: 180rpx;
height: 72rpx;
border-radius: 10rpx;
background-color: #f5f5f5;
margin-left: 16rpx;
}
.submit-button {
margin-top: 30rpx;
width: 100%;
height: 88rpx;
line-height: 88rpx;
background-color: #4f6aff;
color: #ffffff;
border-radius: 16rpx;
font-size: 30rpx;
font-weight: 600;
}
.single-row-card .form-row {
border-bottom: none;
}
.captcha-row .input {
text-align: left;
}
</style>

View File

@ -123,9 +123,8 @@ export default {
url: '/pages/my/personalInfo' url: '/pages/my/personalInfo'
}); });
} else if (route === 'change-password') { } else if (route === 'change-password') {
uni.showToast({ uni.navigateTo({
title: '功能开发中', url: '/pages/my/change-password'
icon: 'none'
}); });
} else if (route === 'logout-records') { } else if (route === 'logout-records') {
uni.showModal({ uni.showModal({

View File

@ -10,7 +10,18 @@
<view class="content-wrapper"> <view class="content-wrapper">
<view class="form-container"> <view class="form-container">
<view class="form-card calendar-card"> <view class="form-card calendar-card">
<view class="card-title">{{ monthTitle }}</view> <view class="calendar-header">
<text class="month-action" @click="handlePrevMonth"></text>
<picker
mode="date"
fields="month"
:value="monthValue"
@change="handleMonthPickerChange"
>
<view class="card-title month-title">{{ monthTitle }}</view>
</picker>
<text class="month-action" @click="handleNextMonth"></text>
</view>
<view class="week-row"> <view class="week-row">
<text <text
v-for="(label, index) in weekLabels" v-for="(label, index) in weekLabels"
@ -82,6 +93,9 @@ export default {
monthTitle() { monthTitle() {
return `${this.currentYear}${String(this.currentMonth).padStart(2, "0")}`; return `${this.currentYear}${String(this.currentMonth).padStart(2, "0")}`;
}, },
monthValue() {
return `${this.currentYear}-${String(this.currentMonth).padStart(2, "0")}`;
},
selectedDateTitle() { selectedDateTitle() {
const dayText = String(this.selectedDay).padStart(2, "0"); const dayText = String(this.selectedDay).padStart(2, "0");
return `${this.currentYear}${String(this.currentMonth).padStart(2, "0")}${dayText}`; return `${this.currentYear}${String(this.currentMonth).padStart(2, "0")}${dayText}`;
@ -97,16 +111,21 @@ export default {
}, },
}, },
methods: { methods: {
fetchSchedule() { fetchSchedule(
year = this.currentYear,
month = this.currentMonth,
day = this.selectedDay
) {
const userInfo = this.vuex_user || {}; const userInfo = this.vuex_user || {};
const teacherId = userInfo.id || userInfo.Id; const teacherId = userInfo.id || userInfo.Id;
if (!teacherId) { if (!teacherId) {
this.$u.toast("缺少教师ID"); this.$u.toast("缺少教师ID");
return; return;
} }
const scheduleDate = new Date(year, month - 1, day, 12, 0, 0);
this.$u.api this.$u.api
.getShiftSchedulingData({ .getShiftSchedulingData({
SchedulingTime: new Date().toISOString(), SchedulingTime: scheduleDate.toISOString(),
TeacherManagementId: teacherId, TeacherManagementId: teacherId,
}) })
.then((res) => { .then((res) => {
@ -177,6 +196,40 @@ export default {
this.scheduleList this.scheduleList
); );
}, },
handlePrevMonth() {
this.changeMonth(-1);
},
handleNextMonth() {
this.changeMonth(1);
},
handleMonthPickerChange(event) {
const value = event?.detail?.value || "";
const [yearText, monthText] = value.split("-");
const year = Number(yearText);
const month = Number(monthText);
if (!year || !month) return;
this.updateMonth(year, month);
},
changeMonth(offset) {
const date = new Date(this.currentYear, this.currentMonth - 1 + offset, 1);
this.updateMonth(date.getFullYear(), date.getMonth() + 1);
},
updateMonth(year, month) {
this.currentYear = year;
this.currentMonth = month;
const today = new Date();
if (today.getFullYear() === year && today.getMonth() + 1 === month) {
this.selectedDay = today.getDate();
} else {
this.selectedDay = 1;
}
this.calendarDays = this.buildCalendarDays(
this.currentYear,
this.currentMonth,
this.scheduleList
);
this.fetchSchedule(year, month, this.selectedDay);
},
}, },
}; };
</script> </script>
@ -213,11 +266,33 @@ export default {
margin-bottom: 20rpx; margin-bottom: 20rpx;
} }
.calendar-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20rpx;
}
.card-title { .card-title {
font-size: 30rpx; font-size: 30rpx;
font-weight: 600; font-weight: 600;
color: #333333; color: #333333;
margin-bottom: 20rpx; }
.month-title {
padding: 6rpx 18rpx;
border-radius: 999rpx;
background-color: #f5f6ff;
text-align: center;
min-width: 200rpx;
}
.month-action {
font-size: 40rpx;
color: #4f6aff;
width: 40rpx;
text-align: center;
line-height: 40rpx;
} }
.week-row { .week-row {

View File

@ -250,7 +250,7 @@ export default {
// //
testLocalServer() { testLocalServer() {
this.wsUrl = 'ws://localhost:8082/ws/chat?token=test-token' this.wsUrl = 'ws://localhost:8073/ws/chat?token=test-token'
this.messageToSend = JSON.stringify({ this.messageToSend = JSON.stringify({
type: 'message', type: 'message',
fromUserId: 'test_user', fromUserId: 'test_user',

View File

@ -231,7 +231,7 @@ export default {
this.isLoading = true; this.isLoading = true;
try { try {
const res = await this.$u.api.GetDialogueListApi({ const res = await this.$u.api.GetDialogueListApi({
"Item1.OnlineConsultationType": 2, "Item1.OnlineConsultationType": 1,
}); });
const list = (res && res.data && res.data.item1) || []; const list = (res && res.data && res.data.item1) || [];
this.chatList = this.normalizeDialogueList(list); this.chatList = this.normalizeDialogueList(list);

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 872 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -149,6 +149,14 @@ const store = new Vuex.Store({
}); });
} }
}, },
// 更新当前聊天用户的部分属性
update_MsgUser(state, payload) {
if (state.vuex_msgUser) {
state.vuex_msgUser = { ...state.vuex_msgUser, ...payload };
} else {
state.vuex_msgUser = payload;
}
},
// 现在没有id先覆盖整个list // 现在没有id先覆盖整个list
push_MsgList(state, list) { push_MsgList(state, list) {
state.vuex_msgList = list || []; state.vuex_msgList = list || [];
@ -173,7 +181,7 @@ const store = new Vuex.Store({
// 注现在的消息没有id无法去重暂时用push_MsgList // 注现在的消息没有id无法去重暂时用push_MsgList
if (!msg || !msg.id) return; if (!msg || !msg.id) return;
const exists = (state.vuex_msgList || []).some( const exists = (state.vuex_msgList || []).some(
(item) => item && item.id === msg.id (item) => item && item.id === msg.id,
); );
if (!exists) { if (!exists) {
state.vuex_msgList.push(msg); state.vuex_msgList.push(msg);
@ -198,15 +206,14 @@ const store = new Vuex.Store({
if (!msgId) return; if (!msgId) return;
const exists = (state.vuex_aiMessageGroups || []).some( const exists = (state.vuex_aiMessageGroups || []).some(
(item) => item && item.id === msgId (item) => item && item.id === msgId,
); );
if (exists) return; if (exists) return;
const userId = const userId =
(state.vuex_user && (state.vuex_user.Id || state.vuex_user.id)) || ""; (state.vuex_user && (state.vuex_user.Id || state.vuex_user.id)) || "";
const senderId = msg.senderId || msg.SenderId || ""; const senderId = msg.senderId || msg.SenderId || "";
const isSelf = const isSelf = userId && senderId && String(userId) === String(senderId);
userId && senderId && String(userId) === String(senderId);
state.vuex_aiMessageGroups.push({ state.vuex_aiMessageGroups.push({
id: msgId, id: msgId,
@ -216,6 +223,7 @@ const store = new Vuex.Store({
isRead: true, isRead: true,
interactMode: isSelf ? 0 : 8, interactMode: isSelf ? 0 : 8,
messageType: msg.messageType || msg.MessageType || 0, messageType: msg.messageType || msg.MessageType || 0,
sendUserType: msg.sendUserType,
timeLabel: 0, timeLabel: 0,
displayTime: "", displayTime: "",
}); });
@ -243,7 +251,7 @@ const store = new Vuex.Store({
push_AiMsg(state, msg) { push_AiMsg(state, msg) {
if (!msg || !msg.id) return; if (!msg || !msg.id) return;
const exists = (state.vuex_aiMessageGroups || []).some( const exists = (state.vuex_aiMessageGroups || []).some(
(item) => item && item.id === msg.id (item) => item && item.id === msg.id,
); );
if (!exists) { if (!exists) {
state.vuex_aiMessageGroups.push(msg); state.vuex_aiMessageGroups.push(msg);
@ -266,7 +274,7 @@ const store = new Vuex.Store({
// WebSocket 实时消息:更新会话列表的未读数、文案与时间 // WebSocket 实时消息:更新会话列表的未读数、文案与时间
apply_RealtimeMessageToList( apply_RealtimeMessageToList(
state, state,
{ dialogueId, message, sendDate, senderId, receiverId } { dialogueId, message, sendDate, senderId, receiverId },
) { ) {
if (!dialogueId) return; if (!dialogueId) return;
const activeId = const activeId =
@ -286,10 +294,7 @@ const store = new Vuex.Store({
found = true; found = true;
const currentUnread = const currentUnread =
Number( Number(
item?.unReadCount ?? item?.unReadCount ?? item?.unreadCount ?? item?.unread ?? 0,
item?.unreadCount ??
item?.unread ??
0
) || 0; ) || 0;
const nextUnread = isActive ? 0 : currentUnread + 1; const nextUnread = isActive ? 0 : currentUnread + 1;
return { return {
@ -386,7 +391,7 @@ const store = new Vuex.Store({
// avatar: item.avatar, // avatar: item.avatar,
unReadCount, unReadCount,
}; };
} },
); );
commit("set_UserMsgList", list); commit("set_UserMsgList", list);
return list; return list;
@ -403,7 +408,7 @@ const store = new Vuex.Store({
// 获取聊天记录(私聊)——仅在进入聊天页时加载一次 // 获取聊天记录(私聊)——仅在进入聊天页时加载一次
async fetchChatRecord( async fetchChatRecord(
{ commit }, { commit },
{ dialogueManagementId, PageIndex = 1, PageSize = 20 } { dialogueManagementId, PageIndex = 1, PageSize = 20 },
) { ) {
return Vue.prototype.$u.api return Vue.prototype.$u.api
.GetChatHistoryDataApi({ .GetChatHistoryDataApi({
@ -412,11 +417,13 @@ const store = new Vuex.Store({
PageSize, PageSize,
}) })
.then((res) => { .then((res) => {
const list = const list = (
(res && res.data && Array.isArray(res.data.item1) res && res.data && Array.isArray(res.data.item1)
? res.data.item1 ? res.data.item1
: [] : []
).slice().reverse(); )
.slice()
.reverse();
commit("push_MsgList", list); commit("push_MsgList", list);
return list; return list;
}); });
@ -425,7 +432,7 @@ const store = new Vuex.Store({
// 获取下一页历史消息(滚动到顶部触发) // 获取下一页历史消息(滚动到顶部触发)
async fetchChatRecordNextPage( async fetchChatRecordNextPage(
{ commit }, { commit },
{ dialogueManagementId, PageIndex = 1, PageSize = 20 } { dialogueManagementId, PageIndex = 1, PageSize = 20 },
) { ) {
return Vue.prototype.$u.api return Vue.prototype.$u.api
.GetChatHistoryDataApi({ .GetChatHistoryDataApi({
@ -434,11 +441,13 @@ const store = new Vuex.Store({
PageSize, PageSize,
}) })
.then((res) => { .then((res) => {
const list = const list = (
(res && res.data && Array.isArray(res.data.item1) res && res.data && Array.isArray(res.data.item1)
? res.data.item1 ? res.data.item1
: [] : []
).slice().reverse(); )
.slice()
.reverse();
if (!list.length) return []; if (!list.length) return [];
commit("prepend_MsgList", list); commit("prepend_MsgList", list);
return list; return list;
@ -462,7 +471,7 @@ const store = new Vuex.Store({
// 点击聊天记录,切换到该会话 // 点击聊天记录,切换到该会话
selectTeacherChatItem( selectTeacherChatItem(
{ commit, dispatch }, { commit, dispatch },
{ id, receiverId, navigate = true } = {} { id, receiverId, navigate = true } = {},
) { ) {
if (!id || !receiverId) return; if (!id || !receiverId) return;
// 清空消息列表,避免旧消息干扰 // 清空消息列表,避免旧消息干扰
@ -472,7 +481,7 @@ const store = new Vuex.Store({
.GetReceiverUserInfoApi({ Id: receiverId }) .GetReceiverUserInfoApi({ Id: receiverId })
.then((res) => { .then((res) => {
if (res.succeed && res.data) { if (res.succeed && res.data) {
commit("set_MsgUser", { ...res.data, dialogueManagementId: id }); commit("update_MsgUser", { ...res.data, dialogueManagementId: id });
if (navigate) { if (navigate) {
uni.navigateTo({ uni.navigateTo({
url: `/pages/chat/index`, url: `/pages/chat/index`,

View File

@ -164,6 +164,7 @@ export function processChatMessageContent(message) {
export function sortChatMessages(list = []) { export function sortChatMessages(list = []) {
const processedList = (list || []).map((item) => ({ const processedList = (list || []).map((item) => ({
...item, ...item,
rawMessage: item && item.message,
message: processChatMessageContent(item && item.message), message: processChatMessageContent(item && item.message),
})); }));