Compare commits
11 Commits
main-1.0终版
...
main
| Author | SHA1 | Date |
|---|---|---|
|
|
8e9c57a6f9 | |
|
|
3c4aaa0f77 | |
|
|
8f8ace89e1 | |
|
|
11c286c717 | |
|
|
227b836d93 | |
|
|
86b3a51206 | |
|
|
1536637159 | |
|
|
ac49885fad | |
|
|
f738c1db94 | |
|
|
e8009a845c | |
|
|
52a1f58f78 |
2
App.vue
2
App.vue
|
|
@ -109,7 +109,7 @@ export default {
|
|||
window.location.protocol.indexOf("https") === 0 ? "wss" : "ws";
|
||||
const 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 连接
|
||||
initWebSocket() {
|
||||
|
|
|
|||
|
|
@ -41,11 +41,11 @@ const install = (Vue, vm) => {
|
|||
let updateTeacherInfo = (params = {}) =>
|
||||
vm.$u.post("api/BasicDataMaintenance/UpdateTeacher", params);
|
||||
|
||||
let getData = (params = {}) =>
|
||||
vm.$u.get("api/BasicDataMaintenance/GetData", params);
|
||||
// 获取排班数据
|
||||
let getShiftSchedulingData = (params = {}) =>
|
||||
vm.$u.get("api/BasicDataMaintenance/GetShiftSchedulingData", params);
|
||||
let getData = (params = {}) =>
|
||||
vm.$u.get("api/BasicDataMaintenance/GetData", params);
|
||||
// 获取排班数据
|
||||
let getShiftSchedulingData = (params = {}) =>
|
||||
vm.$u.get("api/BasicDataMaintenance/GetShiftSchedulingData", params);
|
||||
|
||||
// 登录
|
||||
let LoginApp = (params = {}) => vm.$u.post("api/Token/LoginApp", params);
|
||||
|
|
@ -205,9 +205,15 @@ const install = (Vue, vm) => {
|
|||
// 密码登录-教师
|
||||
let TeacherLogin = (params = {}) =>
|
||||
vm.$u.post("api/Login/LoginManagementEnd", params);
|
||||
// 验证码登录-教师
|
||||
let TeacherLoginByCode = (params = {}) =>
|
||||
vm.$u.post("api/Login/PhoneLoginManagementEnd", params);
|
||||
// 验证码登录-教师
|
||||
let TeacherLoginByCode = (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,14 +257,17 @@ const install = (Vue, vm) => {
|
|||
// 转人工服务
|
||||
let TransferToALiveAgentApi = (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 = {
|
||||
UploadSingleImage,
|
||||
getTeacherInfo,
|
||||
getData,
|
||||
getShiftSchedulingData,
|
||||
updateTeacherInfo,
|
||||
vm.$u.api = {
|
||||
UploadSingleImage,
|
||||
getTeacherInfo,
|
||||
getData,
|
||||
getShiftSchedulingData,
|
||||
updateTeacherInfo,
|
||||
LoginApp,
|
||||
RegisterUser,
|
||||
saveUserInfo,
|
||||
|
|
@ -307,9 +316,11 @@ const install = (Vue, vm) => {
|
|||
GetHotQuestionsFromId,
|
||||
DeleteDialogueManagement,
|
||||
GetTeacherVerifyCode,
|
||||
TeacherLogin,
|
||||
TeacherLoginByCode,
|
||||
GetUserApi,
|
||||
TeacherLogin,
|
||||
TeacherLoginByCode,
|
||||
RequestForgotPasswordSMSCode,
|
||||
ForgotPasswordChangePassword,
|
||||
GetUserApi,
|
||||
UpdateUserApi,
|
||||
GetTeacherListApi,
|
||||
GetDialogueListApi,
|
||||
|
|
@ -322,6 +333,7 @@ const install = (Vue, vm) => {
|
|||
ReadMessageApi,
|
||||
DeleteDialogueApi,
|
||||
TransferToALiveAgentApi,
|
||||
EndLiveAgentApi,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ const install = (Vue, vm) => {
|
|||
Vue.prototype.$u.http.setConfig({
|
||||
// baseUrl: 'https://xy.apps.service.zheke.com',
|
||||
// imgUrl: 'https://xy.apps.service.zheke.com/',
|
||||
baseUrl: "http://120.55.234.65:8082",
|
||||
imgUrl: "http://120.55.234.65:8082/",
|
||||
baseUrl: "http://120.55.234.65:8073",
|
||||
imgUrl: "http://120.55.234.65:8073/",
|
||||
// imgUrl:'http://115.238.47.235:8987/',
|
||||
// baseUrl: 'http://115.238.47.235:8993',
|
||||
// 如果将此值设置为true,拦截回调中将会返回服务端返回的所有数据response,而不是response.data
|
||||
|
|
|
|||
|
|
@ -10,11 +10,31 @@
|
|||
>
|
||||
<view class="phone-popup">
|
||||
<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">
|
||||
<u-button class="cancel-button" type="default" @click="closePopup"
|
||||
>取消</u-button
|
||||
>
|
||||
<view class="cancel-btn" @click="closePopup">取消</view>
|
||||
</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
|
|
@ -48,6 +68,11 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
makeCall(phoneNumber) {
|
||||
uni.makePhoneCall({
|
||||
phoneNumber,
|
||||
});
|
||||
},
|
||||
closePopup() {
|
||||
this.showPopup = false;
|
||||
this.$emit("update:show", false);
|
||||
|
|
@ -66,7 +91,6 @@ export default {
|
|||
background-size: 630rpx 100rpx;
|
||||
background-position: -20rpx 0;
|
||||
|
||||
|
||||
.phone-title {
|
||||
text-align: center;
|
||||
font-family: DouyinSans;
|
||||
|
|
@ -76,19 +100,72 @@ export default {
|
|||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.phone-content {
|
||||
margin-bottom: 120rpx;
|
||||
text-align: center;
|
||||
font-family: PingFang SC;
|
||||
font-size: 32rpx;
|
||||
color: #333333;
|
||||
.phone-card-list {
|
||||
padding: 0 48rpx;
|
||||
margin-bottom: 60rpx;
|
||||
|
||||
.phone-card {
|
||||
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 {
|
||||
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;
|
||||
font-size: 32rpx;
|
||||
color: #333333;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,15 +4,15 @@
|
|||
*/
|
||||
|
||||
export const TAB_BAR_CONFIG = [
|
||||
{
|
||||
text: "在线咨询",
|
||||
icon: "/static/tabbar/tabbar-icon1.png",
|
||||
activeIcon: "/static/tabbar/tabbar-icon1-active.png",
|
||||
pagePath: "/pages/consultation/index",
|
||||
// 可选配置
|
||||
badge: "", // 角标文字
|
||||
dot: false, // 是否显示小红点
|
||||
},
|
||||
// {
|
||||
// text: "在线咨询",
|
||||
// icon: "/static/tabbar/tabbar-icon1.png",
|
||||
// activeIcon: "/static/tabbar/tabbar-icon1-active.png",
|
||||
// pagePath: "/pages/consultation/index",
|
||||
// // 可选配置
|
||||
// badge: "", // 角标文字
|
||||
// dot: false, // 是否显示小红点
|
||||
// },
|
||||
{
|
||||
text: "人工转接",
|
||||
icon: "/static/tabbar/tabbar-icon4.png",
|
||||
|
|
|
|||
|
|
@ -9,10 +9,10 @@ const getWebSocketUrl = () => {
|
|||
// #ifdef H5
|
||||
// H5 开发环境
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
return 'ws://localhost:8082/ws/chat'
|
||||
return 'ws://localhost:8073/ws/chat'
|
||||
}
|
||||
// H5 生产环境
|
||||
return 'wss://120.55.234.65:8082/ws/chat'
|
||||
return 'wss://120.55.234.65:8073/ws/chat'
|
||||
// #endif
|
||||
}
|
||||
|
||||
|
|
|
|||
56
pages.json
56
pages.json
|
|
@ -43,13 +43,13 @@
|
|||
}
|
||||
},
|
||||
|
||||
{
|
||||
"path": "pages/my/personalInfo",
|
||||
"style": {
|
||||
"navigationBarTitleText": "个人信息",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/my/personalInfo",
|
||||
"style": {
|
||||
"navigationBarTitleText": "个人信息",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/my/schedule",
|
||||
"style": {
|
||||
|
|
@ -58,10 +58,17 @@
|
|||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/home/admissions/index",
|
||||
"path": "pages/my/change-password",
|
||||
"style": {
|
||||
"navigationBarTitleText": "在线咨询",
|
||||
"enablePullDownRefresh": false,
|
||||
"navigationBarTitleText": "修改密码",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/home/admissions/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "在线咨询",
|
||||
"enablePullDownRefresh": false,
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
|
|
@ -73,14 +80,14 @@
|
|||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/consultation/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "在线咨询",
|
||||
"enablePullDownRefresh": false,
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
// {
|
||||
// "path": "pages/consultation/index",
|
||||
// "style": {
|
||||
// "navigationBarTitleText": "在线咨询",
|
||||
// "enablePullDownRefresh": false,
|
||||
// "navigationStyle": "custom"
|
||||
// }
|
||||
// },
|
||||
{
|
||||
"path": "pages/transfer/index",
|
||||
"style": {
|
||||
|
|
@ -134,12 +141,13 @@
|
|||
"borderStyle": "black",
|
||||
"backgroundColor": "#ffffff",
|
||||
"list": [
|
||||
{
|
||||
"pagePath": "pages/consultation/index",
|
||||
"iconPath": "static/tabbar/icon_home.png",
|
||||
"selectedIconPath": "static/tabbar/icon_home_active.png",
|
||||
"text": "在线咨询"
|
||||
},
|
||||
// 暂时不用在线咨询页面,保留配置便于恢复
|
||||
// {
|
||||
// "pagePath": "pages/consultation/index",
|
||||
// "iconPath": "static/tabbar/icon_home.png",
|
||||
// "selectedIconPath": "static/tabbar/icon_home_active.png",
|
||||
// "text": "在线咨询"
|
||||
// },
|
||||
{
|
||||
"pagePath": "pages/transfer/index",
|
||||
"iconPath": "static/tabbar/icon_message.png",
|
||||
|
|
|
|||
|
|
@ -2,9 +2,26 @@
|
|||
<view class="home-container">
|
||||
<header-bar
|
||||
title="源小新"
|
||||
:showLeftIcon="false"
|
||||
leftIcon="list"
|
||||
@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">
|
||||
|
|
@ -59,10 +76,16 @@
|
|||
:style="{
|
||||
background: item.background,
|
||||
}"
|
||||
@click="handleFeatureClick(item)"
|
||||
>
|
||||
<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>
|
||||
|
|
@ -225,25 +248,69 @@
|
|||
"
|
||||
:id="'msg-' + message.id"
|
||||
>
|
||||
<image
|
||||
class="ai-avatar"
|
||||
src="/static/common/images/avatar_ai.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<view
|
||||
class="message-content"
|
||||
:class="{
|
||||
'message-content-width': !message.isLoading,
|
||||
}"
|
||||
>
|
||||
<!-- 加载动画 -->
|
||||
<view v-if="message.isLoading" class="loading-dots">
|
||||
<view class="dot"></view>
|
||||
<view class="dot"></view>
|
||||
<view class="dot"></view>
|
||||
<view class="message-content-container-AI">
|
||||
<image
|
||||
class="ai-avatar"
|
||||
src="/static/common/images/avatar_ai.png"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<view
|
||||
class="message-content"
|
||||
: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>
|
||||
<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)"
|
||||
>
|
||||
<u-icon
|
||||
name="download"
|
||||
size="28"
|
||||
color="#4370fe"
|
||||
></u-icon>
|
||||
<text class="structured-file-name">{{
|
||||
block.name
|
||||
}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<markdown-viewer v-else :content="message.message" />
|
||||
</view>
|
||||
<!-- 正常消息内容 -->
|
||||
<markdown-viewer v-else :content="message.message" />
|
||||
</view>
|
||||
|
||||
<!-- 回答反馈:点赞/点踩 -->
|
||||
|
|
@ -287,23 +354,68 @@
|
|||
v-else-if="message.interactMode === 8"
|
||||
:id="'msg-' + message.id"
|
||||
>
|
||||
<image
|
||||
class="ai-avatar"
|
||||
:src="receiverHeadSculptureUrl"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<view
|
||||
class="message-content"
|
||||
:class="{
|
||||
'message-content-width': !message.isLoading,
|
||||
}"
|
||||
>
|
||||
<view v-if="message.isLoading" class="loading-dots">
|
||||
<view class="dot"></view>
|
||||
<view class="dot"></view>
|
||||
<view class="dot"></view>
|
||||
<view class="message-content-container-Teacher">
|
||||
<image
|
||||
class="ai-avatar"
|
||||
:src="receiverHeadSculptureUrl"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
<view
|
||||
class="message-content"
|
||||
: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>
|
||||
<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)"
|
||||
>
|
||||
<u-icon
|
||||
name="download"
|
||||
size="28"
|
||||
color="#4370fe"
|
||||
></u-icon>
|
||||
<text class="structured-file-name">{{
|
||||
block.name
|
||||
}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<markdown-viewer v-else :content="message.message" />
|
||||
</view>
|
||||
<markdown-viewer v-else :content="message.message" />
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
|
@ -316,7 +428,7 @@
|
|||
<view class="floating-tabs">
|
||||
<view
|
||||
class="tab-item"
|
||||
v-for="(tab, index) in floatingTabs"
|
||||
v-for="(tab, index) in displayFloatingTabs"
|
||||
:key="index"
|
||||
@click="handleFeatureClick({ title: tab.title, path: tab.path })"
|
||||
>
|
||||
|
|
@ -422,16 +534,17 @@ export default {
|
|||
"我什么时候能推知道自己是否被录取?",
|
||||
],
|
||||
features: [
|
||||
{
|
||||
title: "在线咨询",
|
||||
icon: "/static/common/images/icon_admissions.png",
|
||||
path: "/pages/home/admissions/index",
|
||||
background: "linear-gradient(0deg, #F4FBFE 0%, #F4FBFE 100%)",
|
||||
},
|
||||
// {
|
||||
// title: "在线咨询",
|
||||
// icon: "/static/common/images/icon_admissions.png",
|
||||
// path: "/pages/home/admissions/index",
|
||||
// background: "linear-gradient(0deg, #F4FBFE 0%, #F4FBFE 100%)",
|
||||
// },
|
||||
{
|
||||
title: "电话咨询",
|
||||
icon: "/static/common/images/icon_phone.png",
|
||||
background: "linear-gradient(0deg, #F4FBF9 0%, #F4FBF9 100%)",
|
||||
tip: "欢迎致电本校招生咨询热线",
|
||||
icon: "/static/common/images/icon-phone1.png",
|
||||
background: "#fff",
|
||||
},
|
||||
],
|
||||
floatingTabs: [
|
||||
|
|
@ -439,11 +552,11 @@ export default {
|
|||
title: "首页",
|
||||
icon: "/static/common/images/icon_home.png",
|
||||
},
|
||||
{
|
||||
title: "招生在线",
|
||||
icon: "/static/common/images/icon_admissions2.png",
|
||||
path: "/pages/home/admissions/index",
|
||||
},
|
||||
// {
|
||||
// title: "招生在线",
|
||||
// icon: "/static/common/images/icon_admissions2.png",
|
||||
// path: "/pages/home/admissions/index",
|
||||
// },
|
||||
{
|
||||
title: "转人工",
|
||||
icon: "/static/common/images/icon_conversation.png",
|
||||
|
|
@ -516,6 +629,10 @@ export default {
|
|||
},
|
||||
displayFeatures() {
|
||||
const list = Array.isArray(this.features) ? this.features : [];
|
||||
return list;
|
||||
},
|
||||
displayFloatingTabs() {
|
||||
const list = Array.isArray(this.floatingTabs) ? this.floatingTabs : [];
|
||||
return list.map((item) => {
|
||||
if (item && item.title === "转人工") {
|
||||
return {
|
||||
|
|
@ -576,6 +693,18 @@ export default {
|
|||
this.handlePopupShow();
|
||||
},
|
||||
|
||||
// 处理设置点击
|
||||
handleSettingClick() {
|
||||
uni.navigateTo({
|
||||
url: "/pages/home/userSetting/index",
|
||||
});
|
||||
},
|
||||
|
||||
// 处理记录点击
|
||||
// handleRecordClick() {
|
||||
// console.log("点击了记录");
|
||||
// },
|
||||
|
||||
// 重置对话状态
|
||||
resetChatState({
|
||||
conversationId = "",
|
||||
|
|
@ -658,38 +787,46 @@ export default {
|
|||
extraOffset: 200,
|
||||
});
|
||||
},
|
||||
|
||||
// 处理功能点击
|
||||
handleFeatureClick(item) {
|
||||
if (item.title === "首页") {
|
||||
this.resetChatState({ isChat: false });
|
||||
return;
|
||||
}
|
||||
if (item.title === "转人工") {
|
||||
this.handleTransferEntryClick();
|
||||
return;
|
||||
}
|
||||
if (item.title === "结束会话") {
|
||||
this.$store.commit("set_IsTransferChat", false);
|
||||
return;
|
||||
}
|
||||
const actions = {
|
||||
首页: () => this.resetChatState({ isChat: false }),
|
||||
转人工: () => this.handleTransferEntryClick(),
|
||||
结束会话: () => this.handleEndConversation(),
|
||||
电话咨询: () => {
|
||||
this.advicePhoneShow = true;
|
||||
},
|
||||
};
|
||||
|
||||
if (item.title === "电话咨询") {
|
||||
this.advicePhoneShow = true;
|
||||
return;
|
||||
const action = actions[item.title];
|
||||
if (action) {
|
||||
action();
|
||||
} else if (item.path) {
|
||||
uni.navigateTo({
|
||||
url: item.path,
|
||||
});
|
||||
return;
|
||||
uni.navigateTo({ url: item.path });
|
||||
} else {
|
||||
this.$refs.uToast.show({
|
||||
title: "暂未开放",
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
return;
|
||||
this.$refs.uToast.show({ title: "暂未开放", type: "warning" });
|
||||
}
|
||||
},
|
||||
|
||||
// 独立出结束会话逻辑
|
||||
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) {
|
||||
return {
|
||||
id: Math.random().toString(36).substring(2, 15),
|
||||
|
|
@ -704,6 +841,7 @@ export default {
|
|||
};
|
||||
},
|
||||
|
||||
// 创建转人工卡片消息
|
||||
createTransferCardMessage() {
|
||||
return {
|
||||
id: "transfer_" + Math.random().toString(36).substring(2, 15),
|
||||
|
|
@ -722,6 +860,7 @@ export default {
|
|||
};
|
||||
},
|
||||
|
||||
// 处理转人工点击
|
||||
handleTransferEntryClick() {
|
||||
if (!this.isChat) {
|
||||
this.resetChatState();
|
||||
|
|
@ -807,22 +946,14 @@ export default {
|
|||
return;
|
||||
}
|
||||
|
||||
// dialogueManagementId 结束转人工用
|
||||
const dialogueManagementId =
|
||||
this.currentDMid ||
|
||||
(res &&
|
||||
res.data &&
|
||||
(res.data.dialogueManagementId ||
|
||||
res.data.DialogueManagementId)) ||
|
||||
"";
|
||||
const receiverId =
|
||||
(res &&
|
||||
res.data &&
|
||||
(res.data.receiverId ||
|
||||
res.data.ReceiverId ||
|
||||
res.data.liveAgentId ||
|
||||
res.data.agentId ||
|
||||
res.data.userId)) ||
|
||||
"";
|
||||
(res && res.data && res.data.dialogueManagementId) || "";
|
||||
// receiverId 接线老师
|
||||
const receiverId = (res && res.data && res.data.receiverId) || "";
|
||||
|
||||
console.log(dialogueManagementId, "dialogueManagementId");
|
||||
console.log(receiverId, "receiverId");
|
||||
|
||||
if (!this.currentDMid && dialogueManagementId) {
|
||||
this.currentDMid = dialogueManagementId;
|
||||
|
|
@ -835,12 +966,17 @@ export default {
|
|||
|
||||
if (dialogueManagementId && receiverId) {
|
||||
this.$store.commit("set_IsTransferChat", true);
|
||||
this.$store.commit("update_MsgUser", {
|
||||
dialogueManagementId,
|
||||
receiverId,
|
||||
});
|
||||
this.$store.dispatch("selectTeacherChatItem", {
|
||||
id: dialogueManagementId,
|
||||
receiverId,
|
||||
navigate: false,
|
||||
});
|
||||
}
|
||||
console.log(this.vuex_msgUser, "vuex_msgUser");
|
||||
})
|
||||
.catch(() => {
|
||||
updateTransferCard({
|
||||
|
|
@ -965,10 +1101,11 @@ export default {
|
|||
},
|
||||
|
||||
// 点击热门问题
|
||||
handleQAClick(item) {
|
||||
async handleQAClick(item) {
|
||||
// 如果不在对话模式,切换到对话模式并初始化
|
||||
if (!this.isChat) {
|
||||
this.resetChatState();
|
||||
// this.resetChatState();
|
||||
await this.handleStartChat(); // 切换到对话模式并初始化,加载历史数据
|
||||
}
|
||||
|
||||
const sendMessage = item.content;
|
||||
|
|
@ -1035,12 +1172,20 @@ export default {
|
|||
(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回复消息对象
|
||||
const aiMessage = {
|
||||
id:
|
||||
data.conversationId ||
|
||||
Math.random().toString(36).substring(2, 15),
|
||||
message: data.detailedExplanation,
|
||||
this.currentDMid || Math.random().toString(36).substring(2, 15),
|
||||
message: message,
|
||||
sendDate: "",
|
||||
isSend: true,
|
||||
isRead: false,
|
||||
|
|
@ -1108,6 +1253,107 @@ export default {
|
|||
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格式则解析并格式化
|
||||
processMessageContent(message) {
|
||||
return processChatMessageContent(message);
|
||||
|
|
@ -1120,11 +1366,11 @@ export default {
|
|||
|
||||
// 刷新当前对话的消息详情
|
||||
handleGetConversationDetail() {
|
||||
if (!this.currentDMid) return;
|
||||
// if (!this.currentDMid) return;
|
||||
|
||||
this.$u.api
|
||||
return this.$u.api
|
||||
.GetConversationDetail({
|
||||
"Item1.Id": this.currentDMid,
|
||||
// "Item1.Id": "08de2e31-4cdf-4b0c-8b83-d3185f604a5a",
|
||||
PageIndex: this.pageQuery.PageIndex,
|
||||
PageSize: this.pageQuery.PageSize,
|
||||
})
|
||||
|
|
@ -1145,14 +1391,14 @@ export default {
|
|||
|
||||
// 刷新当前页数据;若当前页为空且页码>1,则自动回退上一页
|
||||
refreshPageWithFallback() {
|
||||
if (!this.currentDMid) return;
|
||||
// if (!this.currentDMid) return;
|
||||
|
||||
const currentIndex = this.pageQuery.PageIndex || 1;
|
||||
const pageSize = this.pageQuery.PageSize;
|
||||
|
||||
return this.$u.api
|
||||
.GetConversationDetail({
|
||||
"Item1.Id": this.currentDMid,
|
||||
// "Item1.Id": this.currentDMid,
|
||||
PageIndex: currentIndex,
|
||||
PageSize: pageSize,
|
||||
})
|
||||
|
|
@ -1162,7 +1408,7 @@ export default {
|
|||
const prevIndex = currentIndex - 1;
|
||||
return this.$u.api
|
||||
.GetConversationDetail({
|
||||
"Item1.Id": this.currentDMid,
|
||||
// "Item1.Id": this.currentDMid,
|
||||
PageIndex: prevIndex,
|
||||
PageSize: pageSize,
|
||||
})
|
||||
|
|
@ -1193,6 +1439,9 @@ export default {
|
|||
// 开始新对话
|
||||
handleStartChat() {
|
||||
this.resetChatState();
|
||||
|
||||
// 获取历史对话
|
||||
return this.handleGetConversationDetail();
|
||||
},
|
||||
|
||||
// 滚动到底部事件处理
|
||||
|
|
@ -1219,7 +1468,7 @@ export default {
|
|||
this.pageQuery.PageIndex++;
|
||||
this.$u.api
|
||||
.GetConversationDetail({
|
||||
"Item1.Id": this.currentDMid,
|
||||
// "Item1.Id": this.currentDMid,
|
||||
PageIndex: this.pageQuery.PageIndex,
|
||||
PageSize: this.pageQuery.PageSize,
|
||||
})
|
||||
|
|
@ -1252,7 +1501,14 @@ export default {
|
|||
// 回答反馈:点赞/点踩(统一调用刷新&上一页回退逻辑)
|
||||
handleFeedback(message, isHelp) {
|
||||
this.$u.api.ModifyStatus({ id: message.id, isHelp }).then((res) => {
|
||||
if (!res.succeed) return;
|
||||
if (!res.succeed) {
|
||||
uni.showToast({
|
||||
title: res.error || "操作失败",
|
||||
icon: "none",
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
this.$u.toast("操作成功");
|
||||
// 刷新当前页;若空则自动回退上一页并刷新
|
||||
this.refreshPageWithFallback();
|
||||
|
|
@ -1326,6 +1582,11 @@ export default {
|
|||
|
||||
/* Header样式移至HeaderBar组件 */
|
||||
|
||||
.header-icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: 30rpx;
|
||||
padding-top: 60rpx;
|
||||
|
|
@ -1464,29 +1725,59 @@ export default {
|
|||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
margin-top: 32rpx;
|
||||
gap: 30rpx;
|
||||
|
||||
.feature-item {
|
||||
height: 150rpx;
|
||||
height: 140rpx;
|
||||
border-radius: 16rpx;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
padding-left: 30rpx;
|
||||
gap: 20rpx;
|
||||
// padding-left: 30rpx;
|
||||
gap: 30rpx;
|
||||
flex: 1;
|
||||
|
||||
.feature-icon {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.feature-text {
|
||||
font-size: 26rpx;
|
||||
color: #333333;
|
||||
.feature-text-wrapper {
|
||||
display: flex;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1604,8 +1895,14 @@ export default {
|
|||
}
|
||||
|
||||
.message-left {
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap; /* 允许反馈区换行到消息下方 */
|
||||
flex-direction: column;
|
||||
|
||||
.message-content-container-AI,
|
||||
.message-content-container-Teacher {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap; /* 允许反馈区换行到消息下方 */
|
||||
}
|
||||
|
||||
.ai-avatar {
|
||||
width: 64rpx;
|
||||
|
|
@ -1624,6 +1921,12 @@ export default {
|
|||
font-size: 28rpx;
|
||||
line-height: 1.5;
|
||||
|
||||
&.message-content-structured {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/* 加载动画样式 */
|
||||
.loading-dots {
|
||||
display: flex;
|
||||
|
|
@ -1696,15 +1999,88 @@ export default {
|
|||
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: inline-flex;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
padding: 16rpx 20rpx;
|
||||
background-color: #ffffff;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.structured-file {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
padding: 8rpx 0;
|
||||
}
|
||||
|
||||
.structured-file-name {
|
||||
margin-left: 8rpx;
|
||||
font-size: 26rpx;
|
||||
color: #4370fe;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message-content-width {
|
||||
width: 70%;
|
||||
max-width: 70%;
|
||||
}
|
||||
// }
|
||||
|
||||
/* 回答反馈容器,跟随左侧消息,右下角对齐 */
|
||||
.feedback-container {
|
||||
width: 70%;
|
||||
max-width: 70%;
|
||||
margin-left: 80rpx; /* 头像宽度64rpx + 间距16rpx */
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<template>
|
||||
<!-- 留言板页面 —— 此页面已废弃 -->
|
||||
<view class="message-board-page">
|
||||
<header-bar title="留言板" @leftClick="handleLeftClick"></header-bar>
|
||||
<view class="custom-tabs-box">
|
||||
|
|
|
|||
|
|
@ -464,7 +464,7 @@ export default {
|
|||
// 获取用户信息后再跳转,确保数据加载完成
|
||||
const redirect = () => {
|
||||
const url = this.isTeacher
|
||||
? "/pages/consultation/index"
|
||||
? "/pages/transfer/index"
|
||||
: "/pages/home/index/index";
|
||||
uni.reLaunch({
|
||||
url: url,
|
||||
|
|
@ -821,4 +821,3 @@ export default {
|
|||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -123,9 +123,8 @@ export default {
|
|||
url: '/pages/my/personalInfo'
|
||||
});
|
||||
} else if (route === 'change-password') {
|
||||
uni.showToast({
|
||||
title: '功能开发中',
|
||||
icon: 'none'
|
||||
uni.navigateTo({
|
||||
url: '/pages/my/change-password'
|
||||
});
|
||||
} else if (route === 'logout-records') {
|
||||
uni.showModal({
|
||||
|
|
|
|||
|
|
@ -10,7 +10,18 @@
|
|||
<view class="content-wrapper">
|
||||
<view class="form-container">
|
||||
<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">
|
||||
<text
|
||||
v-for="(label, index) in weekLabels"
|
||||
|
|
@ -82,6 +93,9 @@ export default {
|
|||
monthTitle() {
|
||||
return `${this.currentYear}年${String(this.currentMonth).padStart(2, "0")}月`;
|
||||
},
|
||||
monthValue() {
|
||||
return `${this.currentYear}-${String(this.currentMonth).padStart(2, "0")}`;
|
||||
},
|
||||
selectedDateTitle() {
|
||||
const dayText = String(this.selectedDay).padStart(2, "0");
|
||||
return `${this.currentYear}年${String(this.currentMonth).padStart(2, "0")}月${dayText}日`;
|
||||
|
|
@ -97,16 +111,21 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
fetchSchedule() {
|
||||
fetchSchedule(
|
||||
year = this.currentYear,
|
||||
month = this.currentMonth,
|
||||
day = this.selectedDay
|
||||
) {
|
||||
const userInfo = this.vuex_user || {};
|
||||
const teacherId = userInfo.id || userInfo.Id;
|
||||
if (!teacherId) {
|
||||
this.$u.toast("缺少教师ID");
|
||||
return;
|
||||
}
|
||||
const scheduleDate = new Date(year, month - 1, day, 12, 0, 0);
|
||||
this.$u.api
|
||||
.getShiftSchedulingData({
|
||||
SchedulingTime: new Date().toISOString(),
|
||||
SchedulingTime: scheduleDate.toISOString(),
|
||||
TeacherManagementId: teacherId,
|
||||
})
|
||||
.then((res) => {
|
||||
|
|
@ -177,6 +196,40 @@ export default {
|
|||
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>
|
||||
|
|
@ -213,11 +266,33 @@ export default {
|
|||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.calendar-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -250,7 +250,7 @@ export default {
|
|||
|
||||
// 本地服务器测试
|
||||
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({
|
||||
type: 'message',
|
||||
fromUserId: 'test_user',
|
||||
|
|
|
|||
|
|
@ -231,7 +231,7 @@ export default {
|
|||
this.isLoading = true;
|
||||
try {
|
||||
const res = await this.$u.api.GetDialogueListApi({
|
||||
"Item1.OnlineConsultationType": 2,
|
||||
"Item1.OnlineConsultationType": 1,
|
||||
});
|
||||
const list = (res && res.data && res.data.item1) || [];
|
||||
this.chatList = this.normalizeDialogueList(list);
|
||||
|
|
|
|||
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 |
|
|
@ -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
|
||||
push_MsgList(state, list) {
|
||||
state.vuex_msgList = list || [];
|
||||
|
|
@ -173,7 +181,7 @@ const store = new Vuex.Store({
|
|||
// 注:现在的消息没有id,无法去重,暂时用push_MsgList
|
||||
if (!msg || !msg.id) return;
|
||||
const exists = (state.vuex_msgList || []).some(
|
||||
(item) => item && item.id === msg.id
|
||||
(item) => item && item.id === msg.id,
|
||||
);
|
||||
if (!exists) {
|
||||
state.vuex_msgList.push(msg);
|
||||
|
|
@ -198,15 +206,14 @@ const store = new Vuex.Store({
|
|||
if (!msgId) return;
|
||||
|
||||
const exists = (state.vuex_aiMessageGroups || []).some(
|
||||
(item) => item && item.id === msgId
|
||||
(item) => item && item.id === msgId,
|
||||
);
|
||||
if (exists) return;
|
||||
|
||||
const userId =
|
||||
(state.vuex_user && (state.vuex_user.Id || state.vuex_user.id)) || "";
|
||||
const senderId = msg.senderId || msg.SenderId || "";
|
||||
const isSelf =
|
||||
userId && senderId && String(userId) === String(senderId);
|
||||
const isSelf = userId && senderId && String(userId) === String(senderId);
|
||||
|
||||
state.vuex_aiMessageGroups.push({
|
||||
id: msgId,
|
||||
|
|
@ -243,7 +250,7 @@ const store = new Vuex.Store({
|
|||
push_AiMsg(state, msg) {
|
||||
if (!msg || !msg.id) return;
|
||||
const exists = (state.vuex_aiMessageGroups || []).some(
|
||||
(item) => item && item.id === msg.id
|
||||
(item) => item && item.id === msg.id,
|
||||
);
|
||||
if (!exists) {
|
||||
state.vuex_aiMessageGroups.push(msg);
|
||||
|
|
@ -266,7 +273,7 @@ const store = new Vuex.Store({
|
|||
// WebSocket 实时消息:更新会话列表的未读数、文案与时间
|
||||
apply_RealtimeMessageToList(
|
||||
state,
|
||||
{ dialogueId, message, sendDate, senderId, receiverId }
|
||||
{ dialogueId, message, sendDate, senderId, receiverId },
|
||||
) {
|
||||
if (!dialogueId) return;
|
||||
const activeId =
|
||||
|
|
@ -286,10 +293,7 @@ const store = new Vuex.Store({
|
|||
found = true;
|
||||
const currentUnread =
|
||||
Number(
|
||||
item?.unReadCount ??
|
||||
item?.unreadCount ??
|
||||
item?.unread ??
|
||||
0
|
||||
item?.unReadCount ?? item?.unreadCount ?? item?.unread ?? 0,
|
||||
) || 0;
|
||||
const nextUnread = isActive ? 0 : currentUnread + 1;
|
||||
return {
|
||||
|
|
@ -386,7 +390,7 @@ const store = new Vuex.Store({
|
|||
// avatar: item.avatar,
|
||||
unReadCount,
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
commit("set_UserMsgList", list);
|
||||
return list;
|
||||
|
|
@ -403,7 +407,7 @@ const store = new Vuex.Store({
|
|||
// 获取聊天记录(私聊)——仅在进入聊天页时加载一次
|
||||
async fetchChatRecord(
|
||||
{ commit },
|
||||
{ dialogueManagementId, PageIndex = 1, PageSize = 20 }
|
||||
{ dialogueManagementId, PageIndex = 1, PageSize = 20 },
|
||||
) {
|
||||
return Vue.prototype.$u.api
|
||||
.GetChatHistoryDataApi({
|
||||
|
|
@ -412,11 +416,13 @@ const store = new Vuex.Store({
|
|||
PageSize,
|
||||
})
|
||||
.then((res) => {
|
||||
const list =
|
||||
(res && res.data && Array.isArray(res.data.item1)
|
||||
const list = (
|
||||
res && res.data && Array.isArray(res.data.item1)
|
||||
? res.data.item1
|
||||
: []
|
||||
).slice().reverse();
|
||||
)
|
||||
.slice()
|
||||
.reverse();
|
||||
commit("push_MsgList", list);
|
||||
return list;
|
||||
});
|
||||
|
|
@ -425,7 +431,7 @@ const store = new Vuex.Store({
|
|||
// 获取下一页历史消息(滚动到顶部触发)
|
||||
async fetchChatRecordNextPage(
|
||||
{ commit },
|
||||
{ dialogueManagementId, PageIndex = 1, PageSize = 20 }
|
||||
{ dialogueManagementId, PageIndex = 1, PageSize = 20 },
|
||||
) {
|
||||
return Vue.prototype.$u.api
|
||||
.GetChatHistoryDataApi({
|
||||
|
|
@ -434,11 +440,13 @@ const store = new Vuex.Store({
|
|||
PageSize,
|
||||
})
|
||||
.then((res) => {
|
||||
const list =
|
||||
(res && res.data && Array.isArray(res.data.item1)
|
||||
const list = (
|
||||
res && res.data && Array.isArray(res.data.item1)
|
||||
? res.data.item1
|
||||
: []
|
||||
).slice().reverse();
|
||||
)
|
||||
.slice()
|
||||
.reverse();
|
||||
if (!list.length) return [];
|
||||
commit("prepend_MsgList", list);
|
||||
return list;
|
||||
|
|
@ -462,7 +470,7 @@ const store = new Vuex.Store({
|
|||
// 点击聊天记录,切换到该会话
|
||||
selectTeacherChatItem(
|
||||
{ commit, dispatch },
|
||||
{ id, receiverId, navigate = true } = {}
|
||||
{ id, receiverId, navigate = true } = {},
|
||||
) {
|
||||
if (!id || !receiverId) return;
|
||||
// 清空消息列表,避免旧消息干扰
|
||||
|
|
@ -472,7 +480,7 @@ const store = new Vuex.Store({
|
|||
.GetReceiverUserInfoApi({ Id: receiverId })
|
||||
.then((res) => {
|
||||
if (res.succeed && res.data) {
|
||||
commit("set_MsgUser", { ...res.data, dialogueManagementId: id });
|
||||
commit("update_MsgUser", { ...res.data, dialogueManagementId: id });
|
||||
if (navigate) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/chat/index`,
|
||||
|
|
|
|||
|
|
@ -164,6 +164,7 @@ export function processChatMessageContent(message) {
|
|||
export function sortChatMessages(list = []) {
|
||||
const processedList = (list || []).map((item) => ({
|
||||
...item,
|
||||
rawMessage: item && item.message,
|
||||
message: processChatMessageContent(item && item.message),
|
||||
}));
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue