初始化项目

This commit is contained in:
zhihao
2025-12-11 15:37:54 +08:00
parent f9988ae675
commit 5d85ddfa83
28 changed files with 2102 additions and 9 deletions

View File

@@ -0,0 +1 @@
{"containers":[],"config":{}}

14
CommunicationRecords/.gitignore vendored Normal file
View 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/

View File

@@ -15,7 +15,11 @@ App({
},
globalData: {
userInfo: null,
baseUrl: 'https://你的域名' // 后端 API 根地址
baseUrl: 'https://你的域名', // 后端 API 根地址
userKey:"",
avatarUrl:"",
weChatName:"",
}
})

View File

@@ -1,4 +1,4 @@
/**app.wxss**/
.container {
height: 100%;
display: flex;

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 490 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 457 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 498 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 540 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 402 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 497 B

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
{
"usingComponents": {}
}

View 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>

View 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; /* 可选,圆角 */
}

View 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
}
]

View File

@@ -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 =>{
if (res.data.success && res.data.data.userName && res.data.data.weChatName && res.data.data.phoneNumber) {
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});
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: `登录失败: 用户未注册。请先填写完整信息再进行登录`,

View File

@@ -112,7 +112,7 @@ page {
.tip {
margin-top: 8rpx;
font-size: 24rpx;
font-size: 28rpx;
color: #888;
}
/* wxss */

View 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() // 开始串行
})
}

View 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];

View 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 链式调用,确保用户依次处理授权请求。

View 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]** 消息撤回功能。