初始化项目
1
CommunicationRecords/.cloudbase/container/debug.json
Normal file
@@ -0,0 +1 @@
|
||||
{"containers":[],"config":{}}
|
||||
14
CommunicationRecords/.gitignore
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
# Windows
|
||||
[Dd]esktop.ini
|
||||
Thumbs.db
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
|
||||
# Node.js
|
||||
node_modules/
|
||||
@@ -15,7 +15,11 @@ App({
|
||||
},
|
||||
globalData: {
|
||||
userInfo: null,
|
||||
baseUrl: 'https://你的域名' // 后端 API 根地址
|
||||
baseUrl: 'https://你的域名', // 后端 API 根地址
|
||||
userKey:"",
|
||||
avatarUrl:"",
|
||||
weChatName:"",
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**app.wxss**/
|
||||
|
||||
.container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
|
||||
BIN
CommunicationRecords/assets/default.png
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
CommunicationRecords/images/Blvlogo.png
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
CommunicationRecords/images/Icon_阅读模式_7.487_7.504.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
CommunicationRecords/images/Keyboard.png
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
CommunicationRecords/images/Voice.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
CommunicationRecords/images/lock.png
Normal file
|
After Width: | Height: | Size: 490 B |
BIN
CommunicationRecords/images/lock2.png
Normal file
|
After Width: | Height: | Size: 457 B |
BIN
CommunicationRecords/images/lock_3.png
Normal file
|
After Width: | Height: | Size: 498 B |
BIN
CommunicationRecords/images/lock_r.png
Normal file
|
After Width: | Height: | Size: 540 B |
BIN
CommunicationRecords/images/logo.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
CommunicationRecords/images/touxiang.png
Normal file
|
After Width: | Height: | Size: 402 B |
BIN
CommunicationRecords/images/unlock_b.png
Normal file
|
After Width: | Height: | Size: 504 B |
BIN
CommunicationRecords/images/unlock_blue.png
Normal file
|
After Width: | Height: | Size: 497 B |
1126
CommunicationRecords/pages/chat/chat.js
Normal file
3
CommunicationRecords/pages/chat/chat.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"usingComponents": {}
|
||||
}
|
||||
167
CommunicationRecords/pages/chat/chat.wxml
Normal file
@@ -0,0 +1,167 @@
|
||||
<!--顶部栏-->
|
||||
<view class="top-bar " >
|
||||
<view class="user-info " style="flex: 4; display: flex;justify-content: flex-start;">
|
||||
|
||||
<view class="avatar-box " style="margin-left:10rpx;">
|
||||
<button open-type="chooseAvatar" bindchooseavatar="onAvatar" style="margin: 0; padding: 0;height: 32px;width:90%; ">
|
||||
<image src="{{avatarUrl}}" style="height: 30px;width: 30px;" />
|
||||
</button>
|
||||
|
||||
</view>
|
||||
<text style="color: dimgrey; margin-left:5rpx;">{{weChatName}}</text>
|
||||
</view>
|
||||
<view style="flex: 4; display: flex;justify-content: flex-end;">
|
||||
|
||||
<scroll-view scroll-x class=" nav text-center">
|
||||
<view class="cu-item {{TabCur==1?'text-blue curline':''}}" bindtap="tabSelect" data-id="{{1}}">
|
||||
全部信息
|
||||
</view >
|
||||
<view class="cu-item {{TabCur==2?'text-blue curline':''}}" bindtap="tabSelect" data-id="{{2}}">
|
||||
公开信息
|
||||
</view >
|
||||
<view class="cu-item {{TabCur==3?'text-blue curline':''}}" bindtap="tabSelect" data-id="{{3}}">
|
||||
个人信息
|
||||
</view >
|
||||
</scroll-view>
|
||||
|
||||
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
<!--消息区-->
|
||||
<scroll-view class="msg-area"
|
||||
id="myScroll"
|
||||
scroll-y
|
||||
refresher-enabled
|
||||
refresher-threshold="80"
|
||||
refresher-default-style="none"
|
||||
refresher-triggered="{{triggered}}"
|
||||
bindrefresherpulling="onPulling"
|
||||
bindrefresherrefresh="onRefresh"
|
||||
bindrefresherrestore="onRestore"
|
||||
scroll-into-view="{{toView}}" style="height: {{scrollHeight}}px;">
|
||||
<view class="refresh" style="height: 70rpx; width: 100%;text-align: center;" wx:if="{{pulling}}">
|
||||
<!-- <image class="refresh-icon" src="/assets/refresh.png" animation="{{rotateAni}}" /> -->
|
||||
<text>{{ pullText }}</text>
|
||||
</view>
|
||||
<block wx:for="{{msgList}}" wx:key="id">
|
||||
|
||||
<!-- 日期分隔条 -->
|
||||
<!-- 修改消息展示,添加状态图标 -->
|
||||
<view class="msg-item {{item.isSelf === 1 ? 'self' : ''}} {{item.isSelf === 2 ? 'self1' : ''}}" id="msg-{{index}}">
|
||||
<view class="bubble" id="{{item.id}}" bindlongpress="handleLongPressmsg">
|
||||
<view style=" text-align:{{item.isSelf === 1 ? 'right' :item.isSelf === 2 ? 'right' : 'left'}} ;">
|
||||
<image wx:if="{{item.lock=='Unlock'}}" src="/images/unlock_blue.png" style=" width:16rpx;height:16rpx;margin-left: 10rpx;" ></image>
|
||||
<image wx:else src="/images/lock_r.png" style=" width:16rpx;height:16rpx;margin-left: 10rpx;" ></image>
|
||||
|
||||
<image wx:if="{{item.sendMethod=='voice'}}" src="/images/Voice.png" style=" width:25rpx;height:25rpx;margin-left: 10rpx;" ></image>
|
||||
<image wx:else src="/images/Keyboard.png" style=" width:25rpx;height:25rpx;margin-left: 10rpx;" ></image>
|
||||
|
||||
<text class="time">({{!item.voicetime? 0:item.voicetime}} s)</text>
|
||||
<text class="time">{{item.time}}</text>
|
||||
</view>
|
||||
|
||||
<text id="{{item.id}}" bindlongpress="handleLongPressmsg">{{item.content}}</text>
|
||||
<text wx:if="{{item.status === 'sending'}}" class="msg-status">发送中...</text>
|
||||
<text wx:elif="{{item.status === 'failed'}}" class="msg-status error" bindtap="retryMessage" data-id="{{item.id}}">发送失败,点击重发</text>
|
||||
|
||||
</view>
|
||||
|
||||
|
||||
</view>
|
||||
</block>
|
||||
</scroll-view>
|
||||
|
||||
|
||||
|
||||
<!-- 实时语音转文字显示区域 -->
|
||||
<view class="recording-toast" wx:if="{{(recording || recording1) }}">
|
||||
<view class="recording-text">
|
||||
<text wx:if="{{resultText}}">{{resultText}}</text>
|
||||
<text wx:else style="color: beige;">正在转换中...</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!--底部输入栏-->
|
||||
<view class="input-bar" id="BottomFrame">
|
||||
<!-- 语音/键盘切换 -->
|
||||
<button class="switch-btn" style="width: 65rpx; height: 80%;display: flex; flex-direction:column; justify-content: center; " bindtap="switchInputMode">
|
||||
<!-- <text style="line-height: 100%; " wx:if="{{!voiceMode}}"> 🎤 </text>
|
||||
<text style="top: 25%;" wx:else>⌨</text> -->
|
||||
<image wx:if="{{!voiceMode}}" src="/images/Keyboard.png" style=" width: 60rpx;height:60rpx;" ></image>
|
||||
<image wx:else src="/images/Voice.png" style=" width: 60rpx;height:60rpx;" ></image>
|
||||
|
||||
</button>
|
||||
|
||||
<!-- 文字模式 -->
|
||||
<view wx:if="{{!voiceMode}}" class="text-row">
|
||||
<view class="input-box" style="border: 1rpx solid #ddd;">
|
||||
|
||||
<textarea class="textarea" maxlength="-1" confirm-type="search" bindinput="qonInput" value="{{qinputTxt}}" placeholder="输入消息" />
|
||||
</view>
|
||||
|
||||
<view style="width: 20%;">
|
||||
<button class="send-btn" style="width:100%;background-color:#ADD8E6;color: black;" bindtap="gsendText">
|
||||
<text>个人发送</text>
|
||||
</button>
|
||||
<button class="send-btn" style="width: 100%; background-color: #95ec69;color: black;" bindtap="qsendText">
|
||||
<text>公开发送</text>
|
||||
</button>
|
||||
|
||||
</view>
|
||||
|
||||
|
||||
</view>
|
||||
|
||||
<!-- 语音模式 -->
|
||||
<view wx:else class="voice-row">
|
||||
<view class="voice-btn-group" style="height: 100%; " >
|
||||
<button
|
||||
class="voice-record-btn {{recording1?'recording':''}} {{cancelSend1?'cancel':''}}"
|
||||
catchtouchstart="handleTouchStart1"
|
||||
catchtouchmove="handleTouchMove1"
|
||||
catchtouchend="handleTouchEnd1"
|
||||
catchtouchcancel="handleTouchCancel1"
|
||||
style="width: 45%;height: 100%;display: flex; flex-direction:column; justify-content: center;background-color:#ADD8E6;color: black;"
|
||||
>
|
||||
<text style="font-size: 15px;font-weight: bold;"> {{recording1?(cancelSend1?'松开手指,取消发送':'松开 结束'):'按住说话\n个人消息'}}</text>
|
||||
</button>
|
||||
<button
|
||||
class="voice-record-btn {{recording?'recording':''}} {{cancelSend?'cancel':''}}"
|
||||
catchtouchstart="handleTouchStart"
|
||||
catchtouchmove="handleTouchMove"
|
||||
catchtouchend="handleTouchEnd"
|
||||
catchtouchcancel="handleTouchCancel"
|
||||
style="width: 45%;height: 100%;display: flex; flex-direction:column; justify-content: center;background-color: #95ec69;color: black; "
|
||||
>
|
||||
<text style="font-size: 15px;font-weight: bold;"> {{recording?(cancelSend?'松开手指,取消发送':'松开 结束'):'按住说话\n公开消息'}}</text>
|
||||
</button>
|
||||
|
||||
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="cu-modal {{modalName=='DialogModal1'?'show':''}}" >
|
||||
<view class="cu-dialog" style="top: {{isFocused ? '2vh':'30vh'}};">
|
||||
<view class="cu-bar bg-white justify-end">
|
||||
<view class="content">修改信息</view>
|
||||
|
||||
</view>
|
||||
<!-- <input class="solids cu-btn1" focus="true" bindinput="inputSearchForHotels" confirm-type="search" style="width: 100%;"/> -->
|
||||
<textarea class="Btextarea" maxlength="-1" confirm-type="search" auto-height bindinput="inputSearchForHotels" style="width: 100%;" bindfocus="onTextareaFocus"
|
||||
bindblur="onTextareaBlur" value="{{inputValue}}" />
|
||||
|
||||
|
||||
<view class="cu-bar bg-white justify-end">
|
||||
<view class="action">
|
||||
<button id="Withdrawal" class="cu-btn line-green text-red" bindtap="hideModal">撤回</button>
|
||||
<button id="no" class="cu-btn line-green text-green" bindtap="hideModal">取消</button>
|
||||
|
||||
<button id="ok" class="cu-btn bg-green margin-left" bindtap="hideModal">确定</button>
|
||||
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
408
CommunicationRecords/pages/chat/chat.wxss
Normal file
@@ -0,0 +1,408 @@
|
||||
page{background:#ededed;}
|
||||
|
||||
.top-bar{
|
||||
height:88rpx;
|
||||
line-height:88rpx;
|
||||
text-align:center;
|
||||
color :#000;
|
||||
background:#f0f5f5;
|
||||
font-size:34rpx;
|
||||
display: flex;
|
||||
}
|
||||
.msg-area{
|
||||
position:fixed;
|
||||
top:88rpx;
|
||||
bottom:20rpx;
|
||||
left:0;
|
||||
right:0;
|
||||
padding:20rpx;
|
||||
box-sizing:border-box;
|
||||
}
|
||||
.date-bar{
|
||||
text-align:center;
|
||||
font-size:24rpx;
|
||||
color:#888;
|
||||
background:#e5e5e5;
|
||||
line-height:44rpx;
|
||||
border-radius:22rpx;
|
||||
margin:20rpx auto;
|
||||
padding:0 20rpx;
|
||||
display:inline-block;
|
||||
}
|
||||
.msg-item{
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
margin-bottom:20rpx;
|
||||
}
|
||||
.msg-item.self{
|
||||
align-items:flex-end;
|
||||
}
|
||||
.msg-item.self1{
|
||||
align-items:flex-end;
|
||||
}
|
||||
.bubble{
|
||||
max-width:70%;
|
||||
padding: 0rpx 20rpx 12rpx 20rpx;
|
||||
border-radius:12rpx;
|
||||
background:#fff;
|
||||
font-size:32rpx;
|
||||
|
||||
}
|
||||
.msg-item.self .bubble{
|
||||
background:#95ec69;
|
||||
}
|
||||
.msg-item.self1 .bubble{
|
||||
background:#ADD8E6;
|
||||
}
|
||||
.time{
|
||||
font-size:24rpx;
|
||||
color:#888;
|
||||
margin-top:8rpx;
|
||||
margin-left:10rpx;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 底部整体 */
|
||||
.input-bar{
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 180rpx;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20rpx;
|
||||
border-top: 1rpx solid #e0e0e0;
|
||||
}
|
||||
|
||||
/* 语音/键盘切换 */
|
||||
.switch-btn{
|
||||
width: 36rpx; /* 微信同宽 */
|
||||
height: 76rpx;
|
||||
line-height: 76rpx;
|
||||
background: #f5f5f5;
|
||||
border-radius: 12rpx;
|
||||
margin-right: 16rpx;
|
||||
padding: 0;
|
||||
font-size: 40rpx;
|
||||
flex-shrink: 0; /* 防止被挤压 */
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 文字模式容器 */
|
||||
.text-row{
|
||||
flex: 9;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 输入框:占满剩余空间 */
|
||||
.input-box{
|
||||
flex: 4;
|
||||
height: 150rpx;
|
||||
background: #f5f5f5;
|
||||
border-radius: 18rpx;
|
||||
padding: 6 6rpx;
|
||||
margin-right: 16rpx;
|
||||
margin: 8rpx;
|
||||
|
||||
}
|
||||
|
||||
/* 发送按钮:固定 76 rpx */
|
||||
.send-btn{
|
||||
width: 36rpx;
|
||||
height: 70rpx;
|
||||
line-height: 76rpx;
|
||||
padding: 0;
|
||||
background: #07c160;
|
||||
color: #fff;
|
||||
font-size: 26rpx;
|
||||
border-radius: 12rpx;
|
||||
flex-shrink: 0;
|
||||
flex: 1;
|
||||
margin: 8rpx;
|
||||
}
|
||||
|
||||
/* 语音模式大按钮 */
|
||||
|
||||
.voice-row{
|
||||
flex: 9;
|
||||
height: 80%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.voice-record-btn{
|
||||
font-size: 20rpx;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
/* flex: 1; */
|
||||
/*
|
||||
height: 76rpx;
|
||||
line-height: 76rpx;
|
||||
border-radius: 12rpx;
|
||||
background: #f5f5f5;
|
||||
|
||||
padding: 0 12rpx;
|
||||
white-space: pre-line;
|
||||
transition: background .15s; */
|
||||
}
|
||||
.voice-record-btn.recording{
|
||||
background: #FF3B30;
|
||||
color: #fff;
|
||||
}
|
||||
.voice-record-btn.cancel{
|
||||
background: #FF3B30;
|
||||
color: #fff;
|
||||
}
|
||||
.voice-btn-group{
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
gap: 1rpx; /* 两按钮间距 10 rpx */
|
||||
}
|
||||
|
||||
.nav {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.nav .cu-item {
|
||||
height: 90%;
|
||||
display: inline-block;
|
||||
line-height: 90rpx;
|
||||
margin: 0 10rpx;
|
||||
padding: 0 5rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
.text-blue {
|
||||
color: #007aff;
|
||||
}
|
||||
.curline {
|
||||
border-bottom: 10rpx solid #007aff; /* 蓝色,粗细 5rpx */
|
||||
}
|
||||
|
||||
/* 实时语音转文字显示区域 */
|
||||
.recording-toast {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
/* height: 50rpx; */
|
||||
width: 100vw;
|
||||
transform: translate(-30%, -30%);
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
padding: 30rpx;
|
||||
border-radius: 16rpx;
|
||||
z-index: 1000;
|
||||
/* max-width: 80%;
|
||||
min-width: 300rpx; */
|
||||
max-height: 50vh;
|
||||
min-height: 50rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
/* 添加消息状态样式 */
|
||||
.msg-status {
|
||||
font-size: 24rpx;
|
||||
margin-left: 10rpx;
|
||||
}
|
||||
.msg-status.error {
|
||||
color: #ff3b30;
|
||||
}
|
||||
.recording-text {
|
||||
color: #fff;
|
||||
font-size: 34rpx;
|
||||
text-align: center;
|
||||
line-height: 1.4;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
}
|
||||
/* 实时语音转文字显示区域 */
|
||||
.recording-toast {
|
||||
position: fixed;
|
||||
top: 40%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
padding: 30rpx;
|
||||
border-radius: 16rpx;
|
||||
z-index: 1000;
|
||||
max-width: 80%;
|
||||
min-width: 300rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.recording-text {
|
||||
color: #fff;
|
||||
font-size: 34rpx;
|
||||
text-align: center;
|
||||
line-height: 1.4;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
}
|
||||
.cu-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1110;
|
||||
opacity: 0;
|
||||
outline: 0;
|
||||
text-align: center;
|
||||
-ms-transform: scale(1);
|
||||
transform: scale(1);
|
||||
backface-visibility: hidden;
|
||||
perspective: 2000rpx;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
transition: all 0.3s ease-in-out 0s;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.cu-modal::before {
|
||||
content: "\200B";
|
||||
display: inline-block;
|
||||
/* height: 100%; */
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.cu-modal.show {
|
||||
opacity: 1;
|
||||
transition-duration: 0.3s;
|
||||
-ms-transform: scale(1);
|
||||
transform: scale(1);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.cu-dialog {
|
||||
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: 680rpx;
|
||||
max-width: 100%;
|
||||
background-color: #f8f8f8;
|
||||
border-radius: 10rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cu-modal.bottom-modal::before {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.cu-modal.bottom-modal .cu-dialog {
|
||||
width: 100%;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.cu-modal.bottom-modal {
|
||||
margin-bottom: -1000rpx;
|
||||
}
|
||||
|
||||
.cu-modal.bottom-modal.show {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.bg-white {
|
||||
background-color: #ffffff;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.justify-end {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.cu-bar .action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.cu-bar .content {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
width: calc(100% - 340rpx);
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
margin: auto;
|
||||
height: 60rpx;
|
||||
font-size: 32rpx;
|
||||
line-height: 60rpx;
|
||||
cursor: none;
|
||||
pointer-events: none;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
.cu-bar {
|
||||
display: flex;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
min-height: 100rpx;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.cu-bar .action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
max-width: 100%;
|
||||
}
|
||||
.text-red,
|
||||
.line-red,
|
||||
.lines-red {
|
||||
color: #ff3b30;
|
||||
}
|
||||
.cu-bar .action>text[class*="cuIcon-"],
|
||||
.cu-bar .action>view[class*="cuIcon-"] {
|
||||
font-size: 36rpx;
|
||||
}
|
||||
|
||||
[class*="cuIcon-"] {
|
||||
font-family: "cuIcon";
|
||||
font-size: inherit;
|
||||
font-style: normal;
|
||||
}
|
||||
.cuIcon-close:before {
|
||||
content: "\e646";
|
||||
}
|
||||
.textarea {
|
||||
width: 100%;
|
||||
height: 150rpx;
|
||||
padding: 10rpx;
|
||||
box-sizing: border-box; /* 确保内边距不影响总宽度 */
|
||||
word-wrap: break-word; /* 确保长内容也能换行 */
|
||||
/*border: 1rpx solid #ddd; 可选,添加边框 */
|
||||
border-radius: 8rpx; /* 可选,圆角 */
|
||||
}
|
||||
.Btextarea {
|
||||
width: 100%;
|
||||
height: 150rpx;
|
||||
padding: 10rpx;
|
||||
box-sizing: border-box; /* 确保内边距不影响总宽度 */
|
||||
word-wrap: break-word; /* 确保长内容也能换行 */
|
||||
border: 1rpx solid #ddd; /*可选,添加边框 */
|
||||
border-radius: 8rpx; /* 可选,圆角 */
|
||||
}
|
||||
30
CommunicationRecords/pages/chat/mock.js
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* 本地历史消息 Mock
|
||||
* 字段与真实接口保持一致,方便后续直接替换
|
||||
*/
|
||||
module.exports = [
|
||||
{
|
||||
id: 1,
|
||||
content: '你好,这是本地历史消息!',
|
||||
date: '2025-10-31',
|
||||
time: '2025-10-31 10:30',
|
||||
isSelf: 2,
|
||||
showDate: true
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
content: '收到,准备替换为接口数据~',
|
||||
date: '2025-10-31',
|
||||
time: '2025-10-31 10:32',
|
||||
isSelf: 1,
|
||||
showDate: false
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
content: '跨天消息示例',
|
||||
date: '2025-11-01',
|
||||
time: '2025-11-01 09:00',
|
||||
isSelf: 2,
|
||||
showDate: true
|
||||
}
|
||||
]
|
||||
@@ -1,6 +1,6 @@
|
||||
import { authorizeBatch } from '../../utils/authorize'
|
||||
const util = require('../../utils/util.js')
|
||||
|
||||
const app = getApp()
|
||||
Page({
|
||||
data: {
|
||||
isAgree: false,
|
||||
@@ -8,7 +8,6 @@ Page({
|
||||
showAuthModal: false,
|
||||
userInfo: null,
|
||||
needReg: false,
|
||||
openid: '',
|
||||
avatarUrl: '/images/Blvlogo.png',
|
||||
nickName: '',
|
||||
form: {
|
||||
@@ -17,6 +16,14 @@ Page({
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
|
||||
const isAgree = wx.getStorageSync('isAgree');
|
||||
|
||||
if (typeof isAgree !== 'undefined'){
|
||||
this.setData({ isAgree:isAgree });
|
||||
}
|
||||
|
||||
|
||||
this.handleLogin()
|
||||
},
|
||||
onAvatar(e) {
|
||||
@@ -61,13 +68,18 @@ Page({
|
||||
UserKey: openid,
|
||||
WeChatName:nickName,
|
||||
PhoneNumber:form.phone,
|
||||
AvatarUrl:avatarUrl
|
||||
AvatarUrl:""
|
||||
},
|
||||
|
||||
success: res =>{
|
||||
|
||||
this.uploadAvatarToServer(avatarUrl,res.data.data.userKey)
|
||||
if (res.data.success && res.data.data.userName && res.data.data.weChatName && res.data.data.phoneNumber) {
|
||||
wx.setStorageSync('openid', res.data.data.userKey);
|
||||
this.setData({openid:res.data.data.userKey});
|
||||
app.globalData.userKey=res.data.data.userKey
|
||||
app.globalData.weChatName=res.data.data.weChatName
|
||||
app.globalData.avatarUrl=this.data.avatarUrl
|
||||
wx.navigateTo({url: '/pages/chat/chat'});
|
||||
}
|
||||
else{
|
||||
@@ -99,6 +111,8 @@ Page({
|
||||
|
||||
console.log(e)
|
||||
this.setData({ isAgree: e.detail.value.length > 0 });
|
||||
wx.setStorageSync('isAgree', this.data.isAgree);
|
||||
|
||||
},
|
||||
|
||||
openContract() {
|
||||
@@ -134,6 +148,7 @@ Page({
|
||||
//////////debugger
|
||||
// 保存到全局
|
||||
getApp().globalData.userInfo = userInfo;
|
||||
|
||||
await this.onGetAuth()
|
||||
// 继续登录流程
|
||||
await this.completeLogin();
|
||||
@@ -162,7 +177,40 @@ async onGetAuth() {
|
||||
})
|
||||
}
|
||||
},
|
||||
uploadAvatarToServer(tempFilePath,userKey) {
|
||||
// 显示加载提示,提升用户体验
|
||||
wx.showLoading({ title: '上传中...' });
|
||||
|
||||
wx.uploadFile({
|
||||
url: 'https://wx-xcx-check.blv-oa.com:4433/api/Check/UploadFile', // 你的服务器上传接口
|
||||
filePath: tempFilePath,
|
||||
name: 'file', // 与后端约定的文件参数名
|
||||
formData: {
|
||||
'rootPathType': 'Avatar', // 指定保存到 wwwroot/Avatar 目录
|
||||
'userKey': userKey // 指定要更新头像的用户
|
||||
},
|
||||
success: (res) => {
|
||||
wx.hideLoading();
|
||||
const data = JSON.parse(res.data); // 注意: uploadFile返回的data是字符串,需解析
|
||||
if (data.success) {
|
||||
// 上传成功,拿到服务器返回的永久链接
|
||||
const permanentUrl = data.url;
|
||||
this.setData({
|
||||
avatarUrl: permanentUrl // 更新为永久链接
|
||||
});
|
||||
// 接下来可以将 permanentUrl 保存到本地缓存或发送给后端更新用户信息
|
||||
wx.showToast({ title: '上传成功' });
|
||||
} else {
|
||||
wx.showToast({ title: '上传失败: ' + data.message, icon: 'none' });
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
wx.hideLoading();
|
||||
console.error('上传接口调用失败', err);
|
||||
wx.showToast({ title: '网络错误', icon: 'none' });
|
||||
}
|
||||
});
|
||||
},
|
||||
// 完整的登录流程
|
||||
async completeLogin() {
|
||||
|
||||
@@ -190,14 +238,20 @@ async completeLogin() {
|
||||
success: res =>{
|
||||
wx.hideLoading()
|
||||
if (res.data.success){
|
||||
|
||||
this.setData({openid: res.data.data.userKey});
|
||||
console.log(this.data.openid)
|
||||
wx.setStorageSync('openid', res.data.data.userKey);
|
||||
app.globalData.userKey=res.data.data.userKey
|
||||
app.globalData.weChatName=res.data.data.weChatName
|
||||
app.globalData.avatarUrl=res.data.data.avatarUrl
|
||||
|
||||
}
|
||||
|
||||
if (res.data.success && res.data.data.userName && res.data.data.weChatName && res.data.data.phoneNumber) {
|
||||
wx.navigateTo({url: '/pages/chat/chat'});
|
||||
}
|
||||
|
||||
else{
|
||||
wx.showToast({
|
||||
title: `登录失败: 用户未注册。请先填写完整信息再进行登录`,
|
||||
|
||||
@@ -112,7 +112,7 @@ page {
|
||||
|
||||
.tip {
|
||||
margin-top: 8rpx;
|
||||
font-size: 24rpx;
|
||||
font-size: 28rpx;
|
||||
color: #888;
|
||||
}
|
||||
/* wxss */
|
||||
|
||||
52
CommunicationRecords/utils/authorize.js
Normal file
@@ -0,0 +1,52 @@
|
||||
// utils/authorize.js
|
||||
const SCOPE_MAP = { // 中文提示,可扩展
|
||||
'scope.record':'录音',
|
||||
'scope.userLocation': '位置信息'
|
||||
}
|
||||
|
||||
/**
|
||||
* 一次性申请多个权限
|
||||
* @param {Array<string>} scopes 例:['scope.camera','scope.record']
|
||||
* @returns {Promise<Object>} { ok: boolean, granted: Array, denied: Array }
|
||||
*/
|
||||
export function authorizeBatch(scopes) {
|
||||
return new Promise((resolve) => {
|
||||
const granted = [], denied = []
|
||||
let idx = 0
|
||||
//debugger
|
||||
// 单步授权
|
||||
function next() {
|
||||
if (idx >= scopes.length) { // 队列走完
|
||||
resolve({ ok: denied.length === 0, granted, denied })
|
||||
return
|
||||
}
|
||||
const cur = scopes[idx++]
|
||||
wx.getSetting({
|
||||
success: ({ authSetting }) => {
|
||||
if (authSetting[cur]) { // 已授权
|
||||
granted.push(cur)
|
||||
return next()
|
||||
}
|
||||
// 未授权 → 发起授权
|
||||
wx.authorize({
|
||||
scope: cur,
|
||||
success() {
|
||||
granted.push(cur)
|
||||
next()
|
||||
},
|
||||
fail() { // 用户拒绝
|
||||
denied.push(cur)
|
||||
next()
|
||||
}
|
||||
})
|
||||
},
|
||||
fail: () => { // 异常也算拒绝
|
||||
denied.push(cur)
|
||||
next()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
next() // 开始串行
|
||||
})
|
||||
}
|
||||
24
CommunicationRecords/utils/config.js
Normal file
@@ -0,0 +1,24 @@
|
||||
// utils/config.js
|
||||
const getEnv = () => {
|
||||
const accountInfo = wx.getAccountInfoSync().miniProgram;
|
||||
return 'release';//accountInfo.envVersion || 'release';
|
||||
};
|
||||
|
||||
const env = getEnv();
|
||||
|
||||
const config = {
|
||||
develop: {
|
||||
baseUrl: 'https://wx-xcx-check-dev.blv-oa.com:4433',
|
||||
timeout: 10000
|
||||
},
|
||||
trial: {
|
||||
baseUrl: 'https://wx-xcx-check-trial.blv-oa.com:4433',
|
||||
timeout: 10000
|
||||
},
|
||||
release: {
|
||||
baseUrl: 'https://wx-xcx-check.blv-oa.com:4433',
|
||||
timeout: 10000
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = config[env];
|
||||
134
CommunicationRecords/项目文档.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# 项目文档
|
||||
|
||||
## 1. 技术框架 (Technical Framework)
|
||||
|
||||
本项目基于 **微信小程序 (WeChat Mini Program)** 平台开发。
|
||||
|
||||
* **核心框架**: 微信小程序原生框架 (Native)
|
||||
* **组件框架**: `glass-easel` (在 `app.json` 中配置)
|
||||
* **样式版本**: v2
|
||||
* **插件**: `WechatSI` (微信同声传译插件,版本 0.3.6) - 用于语音识别。
|
||||
* **基础库版本**: 3.11.0
|
||||
|
||||
## 2. 环境 (Environment)
|
||||
|
||||
* **开发工具**: 微信开发者工具 (WeChat DevTools)
|
||||
* **运行环境**: 微信客户端 (iOS/Android/PC)
|
||||
* **API 服务器**: `https://wx-xcx-check.blv-oa.com:4433`
|
||||
* **权限要求**:
|
||||
* `scope.userLocation`: 用于获取用户地理位置 (打卡/显示附近门店)。
|
||||
* `scope.record`: 用于语音输入功能。
|
||||
* `scope.speechRecognition`: 用于语音识别。
|
||||
|
||||
## 3. 项目结构 (Project Structure)
|
||||
|
||||
```text
|
||||
e:\Sync\WxCheck\CommunicationRecords
|
||||
├── app.js # 小程序逻辑
|
||||
├── app.json # 小程序公共配置 (页面路由、窗口设置、插件声明)
|
||||
├── app.wxss # 小程序公共样式
|
||||
├── project.config.json # 项目配置文件
|
||||
├── sitemap.json # 站点地图配置
|
||||
├── assets/ # 静态资源目录
|
||||
├── images/ # 图片资源目录
|
||||
├── pages/ # 页面目录
|
||||
│ ├── chat/ # 聊天/主功能页面
|
||||
│ │ ├── chat.js # 页面逻辑 (核心业务)
|
||||
│ │ ├── chat.json # 页面配置
|
||||
│ │ ├── chat.wxml # 页面结构
|
||||
│ │ ├── chat.wxss # 页面样式
|
||||
│ │ └── mock.js # 本地模拟数据
|
||||
│ └── logs/ # 日志页面 (标准模板)
|
||||
└── utils/ # 工具类目录
|
||||
├── authorize.js # 权限申请工具
|
||||
└── util.js # 通用工具函数 (时间格式化等)
|
||||
```
|
||||
|
||||
## 4. 功能 (Features)
|
||||
|
||||
本项目主要功能为 **"宝来威智能AI"** 对话记录系统,包含以下核心功能:
|
||||
|
||||
1. **智能对话/记录**:
|
||||
* 支持 **文本输入**。
|
||||
* 支持 **语音输入** (按住说话),利用 `WechatSI` 插件实时转换为文本。
|
||||
2. **消息类型区分**:
|
||||
* 支持发送不同类型的消息 (代码中体现为 `qsendText` 和 `gsendText`,对应 `MessageType` 1 和 2)。
|
||||
* **Tab 切换**: 可在界面上筛选显示 "全部"、"私有" (Type 1) 或 "全局" (Type 2) 消息。
|
||||
3. **历史记录**:
|
||||
* 从服务器分页加载历史对话记录。
|
||||
* **下拉刷新**: 下拉页面加载更早的历史消息。
|
||||
4. **地理位置**:
|
||||
* 发送消息时自动获取当前地理位置 (GCJ02坐标) 并上传。
|
||||
5. **本地缓存**:
|
||||
* 使用 `wx.getStorageSync('openid')` 获取用户标识。
|
||||
|
||||
## 5. 用途 (Usage/Purpose)
|
||||
|
||||
该项目用于 **宝来威 (BLV)** 内部或客户的沟通记录与检查 (WxCheck)。用户可以通过文字或语音的方式记录信息,系统会自动识别语音内容并上传至服务器,同时记录发送时的地理位置。主要用于工作流中的信息采集、汇报或智能助手交互。
|
||||
|
||||
## 6. 数据结构 (Data Structure)
|
||||
|
||||
### 6.1 前端消息对象 (Message Object)
|
||||
在 `chat.js` 的 `msgList` 中使用的数据格式:
|
||||
|
||||
```javascript
|
||||
{
|
||||
id: Number | String, // 消息ID (本地生成或服务器返回)
|
||||
content: String, // 消息内容
|
||||
date: String, // 日期字符串 (YYYY-MM-DD)
|
||||
time: String, // 完整时间字符串 (YYYY-MM-DD HH:mm)
|
||||
isSelf: Number, // 消息类型标识 (1: 私有/用户, 2: 全局/AI)
|
||||
showDate: Boolean // 是否显示日期分割线
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 API 接口数据
|
||||
|
||||
**发送消息 (AddConversation)**
|
||||
* **URL**: `/api/Check/AddConversation`
|
||||
* **Method**: POST
|
||||
* **Request**:
|
||||
```javascript
|
||||
{
|
||||
UserKey: String, // 用户 OpenID
|
||||
ConversationContent: String,// 内容
|
||||
SendMethod: String, // 发送方式 ('text' 或 'voice')
|
||||
UserLocation: String, // 坐标 'latitude,longitude'
|
||||
MessageType: Number // 消息类型 (1 或 2)
|
||||
}
|
||||
```
|
||||
|
||||
**获取历史记录 (GetConversationsByPage)**
|
||||
* **URL**: `/api/Check/GetConversationsByPage`
|
||||
* **Method**: POST
|
||||
* **Request**:
|
||||
```javascript
|
||||
{
|
||||
UserKey: String,
|
||||
Page: Number,
|
||||
PageSize: Number,
|
||||
MessageType: Number // 0 代表全部
|
||||
}
|
||||
```
|
||||
|
||||
## 7. 前端结构和操作流程 (Frontend Structure & Flow)
|
||||
|
||||
### 7.1 核心页面: `pages/chat/chat`
|
||||
|
||||
* **初始化 (`onLoad`)**:
|
||||
1. 调用 `loadHistory()` -> `GetConversations()` 从服务器获取第一页历史数据。
|
||||
2. 调用 `initRecordManager()` 初始化语音识别插件,绑定 `onStart`, `onRecognize`, `onStop`, `onError` 事件。
|
||||
3. 调用 `GetmyScrollhight()` 计算滚动区域高度,适配不同屏幕。
|
||||
|
||||
* **发送消息流程**:
|
||||
* **文本**: 用户输入 -> 点击发送 -> `qsendText`/`gsendText` -> `pushMsg` (渲染到列表) -> `upload` (调用 `wx.getLocation` 获取位置 -> 调用 `post` 上传服务器)。
|
||||
* **语音**: 用户按住按钮 (`handleTouchStart`) -> `manager.start` 开始录音 -> 用户松开 (`handleTouchEnd`) -> `manager.stop` 停止录音 -> `onStop` 回调获取识别文本 -> `pushMsg` & `upload`。
|
||||
|
||||
* **交互细节**:
|
||||
* **上滑取消**: 在录音过程中,如果手指上滑超过一定距离 (`handleTouchMove`),则触发 "取消发送" 逻辑。
|
||||
* **Tab 切换**: `tabSelect` 函数根据 `isSelf` 字段过滤 `OriginalmsgList` 中的数据,更新 `msgList` 用于渲染。
|
||||
* **下拉加载**: 使用 `scroll-view` 的 `refresher-enabled` 特性,触发 `onRefresh` -> `loadData` 加载下一页数据并拼接到列表头部。
|
||||
|
||||
### 7.2 工具类
|
||||
|
||||
* **`utils/authorize.js`**: 封装了 `authorizeBatch` 函数,用于批量申请小程序权限 (如录音、位置),采用串行 Promise 链式调用,确保用户依次处理授权请求。
|
||||
76
CommunicationRecords/项目评审会议记录.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# 项目评审会议记录 (Project Review Meeting Minutes)
|
||||
|
||||
**日期**: 2025年12月1日
|
||||
**参与人**: 产品经理 (PM), 后端开发 (Backend), 测试工程师 (QA)
|
||||
**主题**: 现有功能评审与迭代建议
|
||||
|
||||
---
|
||||
|
||||
## 1. 产品经理 (PM) 评审
|
||||
|
||||
**观点**: 核心功能闭环已完成,但用户体验 (UX) 细节有待打磨。
|
||||
|
||||
* **UI/交互体验**:
|
||||
* **语音交互**: 目前“上滑取消”的阈值是固定的 (`diff > 100`),用户没有视觉反馈。建议增加一个“松开手指取消发送”的视觉提示区域(类似微信的红色区域),让用户明确知道现在的状态是“录音中”还是“即将取消”。
|
||||
* **消息类型**: 界面上有 `qsendText` (Type 1) 和 `gsendText` (Type 2) 两种发送逻辑,对应“私有”和“全局”。这对用户来说可能比较晦涩。建议在 UI 上明确区分,例如使用不同的按钮颜色或图标,或者通过 Tab 切换当前发送模式,而不是并列两个按钮(如果目前是并列的话)。
|
||||
* **加载状态**: 下拉刷新时的 `pullText` 变化(“下拉刷新” -> “释放立即刷新”)是好的,但建议增加更平滑的动画过渡。
|
||||
|
||||
* **业务逻辑**:
|
||||
* **位置信息**: 每次发送消息都强制获取位置 (`wx.getLocation`)。如果用户在室内 GPS 信号弱,会导致发送延迟或失败吗?
|
||||
* *建议*: 位置获取改为异步或非阻塞,或者允许用户在设置中关闭“发送位置”功能,除非这是核心考勤业务必须的。
|
||||
|
||||
---
|
||||
|
||||
## 2. 后端开发 (Backend) 评审
|
||||
|
||||
**观点**: 代码结构尚可,但配置管理和数据一致性存在风险。
|
||||
|
||||
* **配置管理**:
|
||||
* **硬编码 URL**: API 地址 `https://wx-xcx-check.blv-oa.com:4433` 直接写死在 `chat.js` 中。
|
||||
* *风险*: 切换测试/生产环境非常麻烦。
|
||||
* *建议*: 提取到单独的 `config.js` 文件中,根据 `wx.getAccountInfoSync().miniProgram.envVersion` 自动切换环境。
|
||||
|
||||
* **数据一致性与分页**:
|
||||
* **分页逻辑脆弱**: 前端计算页码的逻辑是 `Math.floor(oldli.length / 20) + 1`。
|
||||
* *风险*: 如果在用户浏览期间,有新消息产生(或者删除了消息),基于数量的分页会导致数据重复或遗漏。
|
||||
* *建议*: 建议改为基于 `last_id` (游标分页) 或时间戳分页,这样更稳定。
|
||||
* **时间格式化**: 前端自己处理 `formatTime` 并替换 `T` 为空格。建议后端直接返回标准格式,或者前端使用 `dayjs` 等库统一处理,避免手动字符串操作带来的潜在 Bug。
|
||||
|
||||
* **网络层**:
|
||||
* **乐观更新风险**: `qsendText` 中先 `pushMsg` (渲染) 再 `upload` (上传)。
|
||||
* *风险*: 如果 `upload` 失败(断网、服务器错误),用户界面上显示消息已发送,但实际上服务器没收到。
|
||||
* *建议*: 消息对象应增加 `status` 字段 (`sending`, `success`, `failed`)。发送失败应显示红色感叹号,并支持点击重发。
|
||||
|
||||
---
|
||||
|
||||
## 3. 测试工程师 (QA) 评审
|
||||
|
||||
**观点**: 异常处理覆盖不足,存在边界情况 Bug。
|
||||
|
||||
* **语音功能边界测试**:
|
||||
* **中断测试**: 在录音过程中,如果接到来电、闹钟响铃或切出小程序,`manager.onStop` 是否会正常触发?目前 `onUnload` 做了停止,但 `onHide` (切后台) 似乎没有处理,可能导致后台继续录音或状态卡死。
|
||||
* **短语音**: 代码中 `cost < 350` 毫秒会被丢弃。这个阈值是否合理?用户快速点击可能会被误判为录音失败。建议给用户一个 "说话时间太短" 的 Toast 提示(目前代码里有 `onError` 处理 `-30003`,但手动停止的逻辑里直接 return 了,没有提示)。
|
||||
|
||||
* **权限流程**:
|
||||
* **拒绝后重试**: `authorize.js` 封装得不错。但如果用户在系统层面(手机设置)关闭了微信的麦克风权限,`wx.authorize` 会直接失败。需要确保 `fail` 回调里的引导文案足够清晰。
|
||||
|
||||
* **兼容性**:
|
||||
* **屏幕适配**: `GetmyScrollhight` 使用了 `boundingClientRect`。在不同机型(特别是带刘海屏/灵动岛的 iPhone)上,底部安全区 (`safe-area-inset-bottom`) 是否处理得当?建议检查 CSS 中是否有 `padding-bottom: env(safe-area-inset-bottom)`。
|
||||
|
||||
---
|
||||
|
||||
## 4. 迭代计划 (Action Items)
|
||||
|
||||
### 优先级 P0 (必须修复)
|
||||
1. **[Dev]** 提取 API URL 到配置文件。
|
||||
2. **[Dev]** 修复“乐观更新”问题:添加消息发送状态(发送中/失败),并在上传失败时给予用户反馈。
|
||||
3. **[Dev]** 处理 `onHide` 生命周期,确保切后台时停止录音。
|
||||
|
||||
### 优先级 P1 (建议优化)
|
||||
1. **[UX]** 优化语音录制的 UI 反馈(上滑取消的视觉提示)。
|
||||
2. **[Dev]** 优化分页逻辑,由后端支持或前端改为更稳健的游标方式。
|
||||
3. **[QA]** 进行弱网环境下的发送测试,验证位置获取超时对发送的影响。
|
||||
|
||||
### 优先级 P2 (未来规划)
|
||||
1. **[Feature]** 增加语音转文字后的“编辑”功能(目前是识别完直接发送,用户无法修改错别字)。
|
||||
2. **[Feature]** 消息撤回功能。
|
||||