聊天问题修复优化

This commit is contained in:
2025-12-05 19:19:54 +08:00
parent b534344b25
commit 28041ddb09
6 changed files with 1322 additions and 382 deletions

View File

@@ -40,12 +40,14 @@
> >
<view class="user u-flex u-row-right"> <view class="user u-flex u-row-right">
<view class="u-p-r-18"> <view class="u-p-r-18">
<view class="color-000 u-line-1 text-right">{{ item.nick_name }}</view> <view class="color-000 u-line-1 text-right">{{
item.nick_name
}}</view>
<view <view
class="u-m-t-14 msg u-p-l-30" class="u-m-t-14 msg u-p-l-30"
:class="['type' + item.msg_type]" :class="['type' + item.msg_type]"
> >
<chatItem :item="item"></chatItem> <chatItem :item="item" @previewImage="previewImage"></chatItem>
</view> </view>
</view> </view>
<up-avatar <up-avatar
@@ -60,21 +62,26 @@
<template v-else> <template v-else>
<view class="shop u-flex"> <view class="shop u-flex">
<up-avatar <up-avatar
:src="shopInfo.logo" :src="item.avatar"
size="122rpx" size="122rpx"
shape="square" shape="square"
bg-color="#fff" bg-color="#fff"
></up-avatar> ></up-avatar>
<view class="u-p-l-18"> <view class="u-p-l-18">
<view class="u-flex"> <view class="u-flex">
<template v-if="item.is_shop == 1">
<view class="tag">商家</view> <view class="tag">商家</view>
<view class="color-000">{{ shopInfo.shopName }}</view> <view class="color-000">{{ item.nick_name }}</view>
</template>
<template v-else>
<view class="color-000">{{ item.nick }}</view>
</template>
</view> </view>
<view <view
class="u-m-t-14 msg" class="u-m-t-14 msg"
:class="['type' + item.msg_type]" :class="['type' + item.msg_type]"
> >
<chatItem :item="item" @getCoupon="getCoupon"></chatItem> <chatItem :item="item" @getCoupon="getCoupon" @previewImage="previewImage"></chatItem>
</view> </view>
</view> </view>
</view> </view>
@@ -92,7 +99,11 @@
<!-- <view :style="bottomSlotHeight"></view> --> <!-- <view :style="bottomSlotHeight"></view> -->
<view class="bottom" :class="[showMoreBtn ? '' : 'safe-bottom']"> <view class="bottom" :class="[showMoreBtn ? '' : 'safe-bottom']">
<view class="u-flex" style="padding: 14rpx 28rpx" v-if="groupInfo&&!groupInfo.is_mute"> <view
class="u-flex"
style="padding: 14rpx 28rpx"
v-if="groupInfo && !groupInfo.is_mute"
>
<!-- <input <!-- <input
type="text" type="text"
class="u-flex-1 u-m-r-52 iput" class="u-flex-1 u-m-r-52 iput"
@@ -112,12 +123,14 @@
<up-icon <up-icon
name="plus-circle" name="plus-circle"
color="#666" color="#666"
size="40rpx" size="60rpx"
@click="showMoreBtnToggle" @click="showMoreBtnToggle"
v-else v-else
></up-icon> ></up-icon>
</view> </view>
<view class="color-666 u-font-28 text-center u-m-t-28" v-else>商家已禁言</view> <view class="color-666 u-font-28 text-center u-m-t-28" v-else
>商家已禁言</view
>
<view class="more-btn" v-if="showMoreBtn"> <view class="more-btn" v-if="showMoreBtn">
<view <view
v-for="(item, index) in moreBtns" v-for="(item, index) in moreBtns"
@@ -144,7 +157,7 @@
<view class="header-wrap"> <view class="header-wrap">
<text class="t">退出群{{ "{" + groupInfo.name + "}" }}</text> <text class="t">退出群{{ "{" + groupInfo.name + "}" }}</text>
<view class="close" @click="popupShow = false"> <view class="close" @click="popupShow = false">
<u-icon name="close" size="16" color="#666"></u-icon> <u-icon name="close" size="20" color="#666"></u-icon>
</view> </view>
</view> </view>
<view class="u-p-40 text-center u-font-28 color-333 border-bottom" <view class="u-p-40 text-center u-font-28 color-333 border-bottom"
@@ -168,10 +181,16 @@
</view> </view>
</view> </view>
</u-popup> </u-popup>
<!-- 预览图 -->
<xbSwiperPreview :visable="showPrveImg" :imgs="prveImgsList" @update:visable="showPrveImg = $event"></xbSwiperPreview>
</template> </template>
<script setup> <script setup>
import { import {
onReady, onReady,
onShow,
onReachBottom, onReachBottom,
onLoad, onLoad,
onPageScroll, onPageScroll,
@@ -183,12 +202,18 @@ import {
nextTick, nextTick,
reactive, reactive,
watch, watch,
onUnmounted,
computed, computed,
} from "vue"; } from "vue";
import { useChatStore } from "@/stores/chat"; import { useChatStore } from "@/stores/chat";
import * as chatApi from "@/http/php/chat"; import * as chatApi from "@/http/php/chat";
import { uploadFile } from "@/common/api/upload.js"; import { uploadFile } from "@/common/api/upload.js";
import * as javaChatApi from "@/common/api/market/chat"; import * as javaChatApi from "@/common/api/market/chat";
import xbSwiperPreview from '@/components/xb-swiper-preview/index.vue';
const showPrveImg = ref(false);
const prveImgsList = ref([]);
function exitGroup() { function exitGroup() {
chatApi chatApi
@@ -214,7 +239,8 @@ function getCoupon(item) {
javaChatApi javaChatApi
.couponGrant({ .couponGrant({
id: item.coupon.activity_id, id: item.coupon.activity_id,
shopUserId: shopInfo.id, // shopUserId: shopUserInfo.id,
shopId: groupInfo.value.shop_id,
userId: uni.cache.get("userInfo").id, userId: uni.cache.get("userInfo").id,
}) })
.then((res) => { .then((res) => {
@@ -224,13 +250,12 @@ function getCoupon(item) {
icon: "none", icon: "none",
duration: 2000, duration: 2000,
}); });
refresh()
} }
}); });
} }
function refresh() { function refresh() {
query.page=1; query.page = 1;
getMsgList(); getMsgList();
} }
const go = { const go = {
@@ -282,12 +307,7 @@ const modalData = reactive({
}); });
const chatStore = useChatStore(); const chatStore = useChatStore();
chatStore.onReceiveMsg = (msg) => {
nextTick(() => {
scrollView.intoView = "msg-0";
});
};
chatStore.connectSocket();
const msg = ref(""); const msg = ref("");
const shopInfo = uni.cache.get("shopInfo"); const shopInfo = uni.cache.get("shopInfo");
@@ -323,19 +343,115 @@ function moreBtnsClick(item, index) {
sendVideo(); sendVideo();
} }
} }
function videoErrorCallback(e) { // 通用相机权限校验函数参考openLocationAuth逻辑
console.error("视频播放失败", e); async function checkCameraAuth() {
} return new Promise((resolve, reject) => {
function sendImg() { try {
uni.chooseImage({ // 1. 检查当前相机权限状态
count: 3, //默认9 uni.getSetting({
sizeType: ["original", "compressed"], //可以指定是原图还是压缩图,默认二者都有 success: (settingRes) => {
sourceType: ["album", "camera "], const authSetting = settingRes.authSetting || {};
success: async function (res) {
uni.showLoading({ if (authSetting["scope.camera"]) {
title: "发送中", // 2. 已授权:直接返回成功
resolve(true);
} else if (authSetting["scope.camera"] === undefined) {
// 3. 未请求过授权:发起授权请求
uni.authorize({
scope: "scope.camera",
success: () => {
// 授权成功
resolve(true);
},
fail: (authErr) => {
// 授权失败(用户拒绝)
console.error("相机授权失败:", authErr);
reject(false);
}
}); });
console.log(res); } else {
// 4. 已拒绝授权:提示用户去设置页开启
uni.showModal({
title: "开启相机权限",
content: "请允许“零点八零”使用您的相机,方便您拍摄图片/视频",
confirmText: "去设置",
cancelText: "取消",
success: (modalRes) => {
if (modalRes.confirm) {
// 跳转微信小程序授权设置页
uni.openSetting({
success: (openRes) => {
if (openRes.authSetting["scope.camera"]) {
// 用户在设置页开启授权
resolve(true);
} else {
// 用户未开启授权
reject(false);
}
},
fail: (openErr) => {
console.error("打开设置页失败:", openErr);
reject(false);
}
});
} else {
// 用户取消设置
reject(false);
}
},
fail: (modalErr) => {
console.error("弹窗失败:", modalErr);
reject(false);
}
});
}
},
fail: (settingErr) => {
console.error("获取权限设置失败:", settingErr);
reject(false);
}
});
} catch (err) {
console.error("相机权限校验异常:", err);
reject(false);
}
});
}
// 图片选择与发送(优化后)
async function sendImg() {
try {
// 1. 调用相机权限校验
const isAuth = await checkCameraAuth();
if (!isAuth) {
// 权限校验失败,终止流程
return;
}
// 2. 权限校验通过调用图片选择API
const res = await new Promise((resolve, reject) => {
uni.chooseImage({
count: 3,
sizeType: ["original", "compressed"],
sourceType: ["album", "camera"],
success: resolve,
fail: (err) => {
// 捕获选择失败(如用户取消选择)
if (err.errMsg && err.errMsg.includes("cancel")) {
// 用户主动取消,静默处理
reject(new Error("user cancel"));
} else {
// 其他错误(如系统异常)
reject(err);
}
}
});
});
uni.showLoading({ title: "发送中" });
console.log("选择图片成功", res);
// 3. 批量上传图片
for (let i = 0; i < res.tempFiles.length; i++) { for (let i = 0; i < res.tempFiles.length; i++) {
const fileRes = await uploadFile(res.tempFiles[i]); const fileRes = await uploadFile(res.tempFiles[i]);
if (fileRes) { if (fileRes) {
@@ -343,25 +459,58 @@ function sendImg() {
image_url: fileRes, image_url: fileRes,
msg_type: 2, msg_type: 2,
}); });
} else {
} }
} }
}, } catch (err) {
console.error("图片选择/发送失败", err);
// 仅处理非用户取消的错误
if (err.message !== "user cancel") {
uni.showToast({
title: "图片发送失败",
icon: "none",
duration: 2000
}); });
}
} finally {
uni.hideLoading();
}
} }
function sendVideo() { // 视频选择与发送(优化后)
async function sendVideo() {
try {
// 1. 调用相机权限校验(与图片共用)
const isAuth = await checkCameraAuth();
if (!isAuth) {
// 权限校验失败,终止流程
return;
}
// 2. 权限校验通过调用视频选择API
const res = await new Promise((resolve, reject) => {
uni.chooseVideo({ uni.chooseVideo({
count: 1, //默认9 count: 1,
sizeType: ["original", "compressed"], //可以指定是原图还是压缩图,默认二者都有 sizeType: ["original", "compressed"],
sourceType: ["album", "camera "], sourceType: ["album", "camera"],
success: async function (res) { success: resolve,
uni.showLoading({ fail: (err) => {
title: "发送中", // 捕获选择失败(如用户取消选择)
if (err.errMsg && err.errMsg.includes("cancel")) {
// 用户主动取消,静默处理
reject(new Error("user cancel"));
} else {
// 其他错误(如系统异常)
reject(err);
}
}
}); });
console.log(res); });
uni.showLoading({ title: "发送中" });
console.log("选择视频成功", res);
// 3. 上传视频
const fileRes = await uploadFile({ path: res.tempFilePath }); const fileRes = await uploadFile({ path: res.tempFilePath });
uni.hideLoading();
if (fileRes) { if (fileRes) {
sendMsg({ sendMsg({
image_url: fileRes, image_url: fileRes,
@@ -369,21 +518,47 @@ function sendVideo() {
}); });
} else { } else {
uni.showToast({ uni.showToast({
title: "发送失败", title: "视频发送失败",
icon: "none", icon: "none",
duration: 2000
}); });
} }
}, } catch (err) {
console.error("视频选择/发送失败", err);
// 仅处理非用户取消的错误
if (err.message !== "user cancel") {
uni.showToast({
title: "视频发送失败",
icon: "none",
duration: 2000
}); });
}
} finally {
uni.hideLoading();
}
} }
const groupInfo = ref({}); const groupInfo = ref({});
const handleReceiveMsg = (msg) => {
nextTick(() => {
scrollView.intoView = "msg-0";
});
};
// 注册消息回调
chatStore.registerReceiveMsgCallback(handleReceiveMsg);
async function init() { async function init() {
const res = await chatApi.groupInfo({ const res = await chatApi.groupInfo({
group_id: options.group_id, group_id: options.group_id,
}); });
console.log(res); console.log(res);
groupInfo.value = res || {}; groupInfo.value = res || {};
if (res) {
chatStore.shop_id = groupInfo.value.shop_id;
chatStore.connectSocket();
chatStore.init();
}
getMsgList(); getMsgList();
} }
const query = reactive({ const query = reactive({
@@ -395,7 +570,6 @@ async function getMsgList() {
if (isEnd.value) { if (isEnd.value) {
scrollView.refresherTriggered = false; scrollView.refresherTriggered = false;
isLoading.value = false; isLoading.value = false;
uni.showToast({ title: "没有更多了", icon: "none" });
return; return;
} }
@@ -443,8 +617,9 @@ onLoad((opt) => {
Object.assign(options, opt); Object.assign(options, opt);
init(); init();
chatApi.messageMarkReadAll({ chatApi.messageMarkReadAll({
session_ids: options.session_id, session_ids: options.group_id,
}); });
chatStore.group_id = options.group_id;
chatApi.groupMembers({ group_id: options.group_id }).then((res) => { chatApi.groupMembers({ group_id: options.group_id }).then((res) => {
console.log(res); console.log(res);
membersRes.value = res; membersRes.value = res;
@@ -474,7 +649,7 @@ function sendMsg(msg) {
content: msg.value, content: msg.value,
image_url: "", image_url: "",
order_id: "", order_id: "",
session_id: "", session_id: groupInfo.value.session_id || options.session_id,
...msg, ...msg,
}); });
} }
@@ -519,9 +694,9 @@ function confirmCoupon() {
} }
function previewImage(url) { function previewImage(url) {
uni.previewImage({
urls: [url], prveImgsList.value = [url];
}); showPrveImg.value = true;
} }
//是否到底了 //是否到底了
@@ -556,6 +731,18 @@ const pageHeight = computed(() => {
} }
return ""; return "";
}); });
onShow(() => {
// 页面显示时,检查连接状态
if (!chatStore.isConnect || !chatStore.socketTask) {
console.log("聊天页显示检查Socket连接");
chatStore.forceReconnect();
}
});
onUnmounted(() => {
// 组件卸载时,移除消息回调
chatStore.removeReceiveMsgCallback(handleReceiveMsg);
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.top { .top {

View File

@@ -13,11 +13,10 @@
:src="item.image_url" :src="item.image_url"
class="img" class="img"
mode="widthFix" mode="widthFix"
@click="previewVideo(item.video_url)"
></video> ></video>
<view class="" v-if="item.msg_type == 4"> <view class="" v-if="item.msg_type == 4">
<view>{{ item.coupon.title }}</view> <view>{{ item.coupon.title }}</view>
<view class="u-m-t-16 bg-f7 coupon u-flex" style="min-width: 500rpx;"> <view class="u-m-t-16 bg-f7 coupon u-flex u-col-stretch" style="min-width: 500rpx;">
<template v-if="item.coupon.type == 1"> <template v-if="item.coupon.type == 1">
<view class="left"> <view class="left">
<view class="price"> <view class="price">
@@ -77,11 +76,9 @@
</view> </view>
</template> </template>
<view class="right u-p-l-28"> <view class="right u-p-l-28 u-flex u-col-center">
<view class="u-font-32">{{ item.coupon.couponName }}</view> <view class="u-font-32">{{ item.coupon.couponName }}</view>
<view class="u-font-24 color-999 u-m-t-8"
>有效期{{ returnTime(item.coupon) }}</view
>
</view> </view>
</view> </view>
<view class="u-flex u-row-center u-m-t-32"> <view class="u-flex u-row-center u-m-t-32">
@@ -99,14 +96,12 @@ const props = defineProps({
}, },
}); });
const emits = defineEmits(["getCoupon"]); const emits = defineEmits(["getCoupon",'previewImage']);
function getCoupon() { function getCoupon() {
emits("getCoupon", props.item); emits("getCoupon", props.item);
} }
function previewImage(url) { function previewImage(url) {
uni.previewImage({ emits('previewImage',url)
urls: [url],
});
} }
function returnTime(coupon){ function returnTime(coupon){
let startTime = coupon.useStartTime; let startTime = coupon.useStartTime;
@@ -149,4 +144,7 @@ function returnTime(coupon){
color: #999; color: #999;
} }
} }
.u-col-stretch{
align-items: stretch;
}
</style> </style>

View File

@@ -2,7 +2,7 @@
<view class="min-page bg-f7 color-333 u-font-28"> <view class="min-page bg-f7 color-333 u-font-28">
<up-sticky> <up-sticky>
<view class="top u-flex u-row-between u-col-center"> <view class="top u-flex u-row-between u-col-center">
<view style="width: 420rpx;"> <view style="width: 420rpx">
<up-search <up-search
v-model="query.key" v-model="query.key"
placeholder="搜索群名称" placeholder="搜索群名称"
@@ -19,12 +19,11 @@
</up-sticky> </up-sticky>
<view class="list"> <view class="list">
<up-swipe-action> <up-swipe-action>
<template v-for="(item, index) in list" :key="item.id">
<up-swipe-action-item <up-swipe-action-item
:options="options1" :options="options1"
v-for="(item, index) in list"
v-model:show="item.showOptions" v-model:show="item.showOptions"
@click="optionsClick($event, item, index)" @click="optionsClick($event, item, index)"
:key="item.id"
> >
<view class="item u-flex" @click="toDetail(item)"> <view class="item u-flex" @click="toDetail(item)">
<view class="u-flex avatar"> <view class="u-flex avatar">
@@ -41,14 +40,16 @@
<view class="u-flex-1 u-flex u-row-between u-p-l-14"> <view class="u-flex-1 u-flex u-row-between u-p-l-14">
<view style="max-width: 364rpx"> <view style="max-width: 364rpx">
<view class="color-000 u-line-1">{{ item.name }}</view> <view class="color-000 u-line-1">{{ item.name }}</view>
<view class="u-m-t-28 u-line-1 u-font-24 color-999" <view class="u-m-t-28 u-line-1 u-font-24 color-999">{{
>{{ item.msg }}</view item.msg
> }}</view>
</view> </view>
<view class="color-333 u-font-24">{{ item.send_time }}</view> <view class="color-333 u-font-24">{{ item.send_time }}</view>
</view> </view>
</view> </view>
</up-swipe-action-item> </up-swipe-action-item>
<view style="height: 16rpx" class="bg-f7"></view>
</template>
</up-swipe-action> </up-swipe-action>
</view> </view>
</view> </view>
@@ -56,8 +57,64 @@
<script setup> <script setup>
import * as chatApi from "@/http/php/chat"; import * as chatApi from "@/http/php/chat";
import { ref, reactive } from "vue"; import { ref, reactive, onMounted, onUnmounted } from "vue";
import { onShow } from "@dcloudio/uni-app"; import { onShow } from "@dcloudio/uni-app";
import { useChatStore } from "@/stores/chat";
import dayjs from "dayjs";
const chatStore = useChatStore();
const originalOnReceiveMsg = chatStore.onReceiveMsg;
// 定义消息回调函数
const handleReceiveMsg = (msg) => {
// 先执行原始回调(如果有)
if (typeof originalOnReceiveMsg === "function") {
originalOnReceiveMsg(msg);
}
if (msg.operate_type == "receive_msg" && msg.chat_type == 2) {
const index = allList.value.findIndex((v) => v.group_id == msg.group_id);
if (index != -1) {
allList.value[index].unread_count += 1;
allList.value[index].msg = returnMsg(msg);
allList.value[index].send_time = msg.send_time;
allList.value[index].send_time_origin = msg.send_time_origin;
allList.value = listSort(allList.value);
}
const index1 = list.value.findIndex((v) => v.group_id == msg.group_id);
if (index1 != -1) {
list.value[index1].unread_count += 1;
list.value[index1].msg = returnMsg(msg);
list.value[index1].send_time = msg.send_time;
list.value[index1].send_time_origin = msg.send_time_origin;
list.value = listSort(list.value);
}
}
};
onMounted(() => {
chatStore.registerReceiveMsgCallback(handleReceiveMsg);
});
onUnmounted(() => {
// 移除消息回调,避免内存泄漏
chatStore.removeReceiveMsgCallback(handleReceiveMsg);
});
function returnMsg(msg) {
if (msg.msg_type == 1) {
return msg.nick + "" + msg.content;
}
if (msg.msg_type == 2) {
return msg.nick + "" + "[图片]";
}
if (msg.msg_type == 5) {
return msg.nick + "" + "[视频]";
}
if (msg.msg_type == 4) {
return msg.nick + "" + "[优惠券]";
}
}
chatStore.connectSocket();
// 使用 reactive 创建响应式对象 // 使用 reactive 创建响应式对象
const options1 = reactive([ const options1 = reactive([
@@ -95,7 +152,7 @@ function optionsClick(e, item, index) {
} }
const list = ref([]); const list = ref([]);
let allList = []; let allList = ref([]);
const query = reactive({ const query = reactive({
key: "", key: "",
@@ -116,7 +173,7 @@ function throttle(fn, delay) {
//使用节流函数 //使用节流函数
const throttleSearch = throttle(search, 500); const throttleSearch = throttle(search, 500);
function search() { function search() {
list.value = allList const arr = allList.value
.filter((v) => v.name.includes(query.key.trim())) .filter((v) => v.name.includes(query.key.trim()))
.map((v) => { .map((v) => {
return { return {
@@ -124,10 +181,20 @@ function search() {
showOptions: false, showOptions: false,
}; };
}); });
list.value = listSort(arr);
} }
function listSort(arr) {
return arr.sort((a, b) => {
return dayjs(b.send_time_origin).unix() - dayjs(a.send_time_origin).unix();
});
}
async function getList() { async function getList() {
const res = await chatApi.messageSessionList({}); const res = await chatApi.messageSessionList({});
allList = (res.list || []).filter((v) => !v.is_del); const arr = (res.list || []).filter((v) => !v.is_del);
allList.value = listSort(arr);
search(); search();
} }
@@ -138,7 +205,7 @@ async function clearAllmsg() {
success: async (res) => { success: async (res) => {
if (res.confirm) { if (res.confirm) {
const res = await chatApi.messageMarkReadAll({ const res = await chatApi.messageMarkReadAll({
session_ids: list.value.map((v) => v.session_id).join(","), session_ids: list.value.map((v) => v.group_id).join(","),
}); });
if (res) { if (res) {
uni.showToast({ uni.showToast({
@@ -173,6 +240,15 @@ function toDetail(item) {
const messageUnreadCount = ref(0); const messageUnreadCount = ref(0);
onShow(() => { onShow(() => {
getList(); getList();
// 检查连接状态,确保连接活跃
if (!chatStore.isConnect || !chatStore.socketTask) {
console.log("列表页显示检查Socket连接");
chatStore.forceReconnect();
} else {
chatStore.shop_id = "";
chatStore.init();
}
// 获取未读消息总数 // 获取未读消息总数
chatApi.messageUnreadCount({}).then((res) => { chatApi.messageUnreadCount({}).then((res) => {
console.log(res); console.log(res);
@@ -192,13 +268,11 @@ onShow(() => {
} }
.list { .list {
padding: 36rpx 28rpx; padding: 36rpx 28rpx;
.item { .item {
padding: 32rpx 28rpx; padding: 32rpx 28rpx;
background-color: #fff; background-color: #fff;
border-radius: 16rpx; border-radius: 16rpx;
display: flex;
flex-direction: column;
gap: 16rpx;
.avatar { .avatar {
position: relative; position: relative;
.bandage { .bandage {
@@ -220,4 +294,10 @@ onShow(() => {
} }
} }
} }
:deep(.u-swipe-action-item__content) {
background-color: transparent;
}
:deep(.u-swipe-action-item) {
border-radius: 16rpx;
}
</style> </style>

View File

@@ -223,7 +223,7 @@ async function getCouponList() {
userId: uni.cache.get('userInfo').id, userId: uni.cache.get('userInfo').id,
name: querForm.value.searchValue, name: querForm.value.searchValue,
status: statusList.value[querForm.value.statusActiveIndex].value, status: statusList.value[querForm.value.statusActiveIndex].value,
shopId: querForm.value.shopId ? querForm.value.shopId : uni.cache.get('shopId'), shopId: querForm.value.shopId ? querForm.value.shopId :'',
page: list.page, page: list.page,
size: list.size size: list.size
}); });

View File

@@ -1,7 +1,4 @@
import { import { defineStore } from "pinia";
defineStore
} from "pinia";
// import * as shopApi from "@/http/api/shop.js";
// #ifdef H5 // #ifdef H5
const socketUrl = "http://192.168.1.42:2348"; const socketUrl = "http://192.168.1.42:2348";
@@ -19,9 +16,41 @@ export const useChatStore = defineStore("chat", {
socketTask: null, socketTask: null,
onReceiveMsg: () => {}, onReceiveMsg: () => {},
chatList: [], chatList: [],
shop_id: "",
group_id:'',
// ========== Socket 保活与重连状态 ==========
isManualClose: false, // 是否主动关闭true主动关闭不重连false意外断开自动重连
heartbeatTimer: null, // 心跳定时器
reconnectTimer: null, // 重连定时器
connectionMonitorTimer: null, // 连接状态监控定时器
reconnectCount: 0, // 当前重连次数
maxReconnectCount: 8, // 最大重连次数
reconnectDelay: 1000, // 初始重连延迟ms
maxReconnectDelay: 8000, // 最大重连延迟ms
heartbeatInterval: 15000, // 心跳间隔15s
// ========== 应用与网络状态监听 ==========
isAppActive: true, // 应用是否在前台
lastHeartbeatTime: 0, // 上次心跳时间戳
_listenersInitialized: false, // 状态监听器是否已初始化
// ========== 消息去重机制 ==========
recentMessages: new Map(), // 最近处理的消息映射表key=消息特征哈希value=处理时间戳
deduplicationWindow: 5000, // 去重时间窗口5秒内相同消息只处理一次
maxRecentMessages: 100, // 最近消息缓存最大数量
// ========== 连接状态锁 ==========
isConnecting: false, // 是否正在建立连接(防止重复创建连接)
lastConnectTime: 0, // 上次连接时间(防止短时间内频繁连接)
// ========== 消息回调队列 ==========
receiveMsgCallbacks: [], // 消息接收回调队列,支持多页面同时监听
}; };
}, },
actions: { actions: {
// ========== 初始化 ==========
init() { init() {
if (!this.isConnect) { if (!this.isConnect) {
return uni.showToast({ return uni.showToast({
@@ -29,98 +58,554 @@ export const useChatStore = defineStore("chat", {
icon: "none", icon: "none",
}); });
} }
this.sendMessage({ this.sendMessage(
{
type: "OnbocChat", type: "OnbocChat",
operate_type: "init", operate_type: "init",
shop_id: uni.cache.get('shopInfo').id || '', shop_id: this.shop_id,
token: uni.cache.get('token'), token: uni.cache.get("token"),
}, },
false false
); );
}, },
// ========== 发送消息(核心优化) ==========
sendMessage(msg, isAutoAppend = true) { sendMessage(msg, isAutoAppend = true) {
if (!this.isConnect) { // 1. 主动关闭状态:提示用户
if (this.isManualClose) {
return uni.showToast({ return uni.showToast({
title: "请先连接socket", title: "Socket已主动关闭请重新连接",
icon: "none", icon: "none",
}); });
} }
console.log(this.socketTask);
const message = isAutoAppend ? { // 2. 连接未建立或 SocketTask 已失效:立即重连并延迟发送
if (!this.isConnect || !this.socketTask) {
console.warn("Socket连接未建立尝试重连...");
// 立即重连
this.forceReconnect();
// 延迟发送,确保重连成功
setTimeout(() => {
this.sendMessage(msg, isAutoAppend);
}, 1500); // 延长延迟时间,确保重连完成
return;
}
// 3. 正常发送消息
console.log("发送的消息", msg);
const message = isAutoAppend
? {
type: "OnbocChat", type: "OnbocChat",
operate_type: "sendMsg", operate_type: "sendMsg",
shop_id: uni.cache.get('shopInfo').id || '', shop_id: this.shop_id,
token: uni.cache.get('token'), token:
uni.getStorageSync("iToken")?.tokenValue ||
uni.cache.get("token") ||
"",
...msg, ...msg,
} : }
msg; : msg;
try {
this.socketTask.send({ this.socketTask.send({
data: JSON.stringify(message), data: JSON.stringify(message),
success: (res) => { success: (res) => {
console.log("发送成功", message); console.log("发送成功", res);
}, },
fail: (error) => { fail: (error) => {
console.log("发送失败", error); console.error("发送失败,触发重连", error);
// 发送失败,立即重连
this.forceReconnect();
// 重连后重试发送
setTimeout(() => {
this.sendMessage(msg, isAutoAppend);
}, 1500);
},
});
} catch (error) {
console.error("发送消息异常,触发重连", error);
// 捕获异常,立即重连
this.forceReconnect();
// 重连后重试发送
setTimeout(() => {
this.sendMessage(msg, isAutoAppend);
}, 1500);
}
},
// ========== 发送心跳包 ==========
sendHeartbeat() {
if (!this.isConnect || this.isManualClose || !this.socketTask) return;
const now = Date.now();
this.lastHeartbeatTime = now; // 记录心跳时间
this.socketTask.send({
data: JSON.stringify({
type: "ping_interval",
}),
fail: (error) => {
console.log("心跳发送失败,触发重连", error);
this.forceReconnect();
}, },
}); });
}, },
// ========== 启动心跳定时器 ==========
startHeartbeat() {
// 清除旧定时器,避免重复
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
}
// 立即发送一次心跳,确保连接活跃
this.sendHeartbeat();
// 每15s发送一次心跳
this.heartbeatTimer = setInterval(() => {
this.sendHeartbeat();
}, this.heartbeatInterval);
},
// ========== 停止心跳定时器 ==========
stopHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
},
// ========== 启动连接状态监控 ==========
startConnectionMonitor() {
// 清除旧定时器
if (this.connectionMonitorTimer) {
clearInterval(this.connectionMonitorTimer);
}
// 每30秒检查一次连接状态
this.connectionMonitorTimer = setInterval(() => {
// 如果应该连接但实际未连接,触发重连
if (!this.isManualClose && this.isConnect && !this.socketTask) {
console.warn("连接监控:发现连接状态异常,强制重置");
this.forceReconnect();
}
}, 30000);
},
// ========== 停止连接状态监控 ==========
stopConnectionMonitor() {
if (this.connectionMonitorTimer) {
clearInterval(this.connectionMonitorTimer);
this.connectionMonitorTimer = null;
}
},
// ========== 智能重连逻辑 ==========
reconnect() {
// 1. 主动关闭或已连接:不重连
if (this.isManualClose || this.isConnect) return;
// 2. 正在连接中:不重连
if (this.isConnecting) {
console.log("正在建立连接中,跳过重连");
return;
}
// 3. 短时间内已尝试连接:不重连(防止频繁连接)
const now = Date.now();
if (now - this.lastConnectTime < 1000) {
console.log("短时间内已尝试连接,跳过重连");
return;
}
// 4. 超过最大重连次数:停止重连
if (this.reconnectCount >= this.maxReconnectCount) {
console.log("已达最大重连次数,停止重连");
return;
}
// 5. 清除旧重连定时器
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
}
// 6. 计算重连延迟(退避算法)
const baseDelay = this.isAppActive ? 500 : this.reconnectDelay;
const delay = Math.min(
baseDelay * Math.pow(2, this.reconnectCount),
this.maxReconnectDelay
);
console.log(`${this.reconnectCount + 1}次重连,延迟${delay}ms`);
// 7. 延迟执行重连
this.reconnectTimer = setTimeout(() => {
this.connectSocket();
this.reconnectCount++;
}, delay);
},
// ========== 强制重连方法 ==========
forceReconnect() {
// 主动关闭状态下不重连
if (this.isManualClose) return;
console.log("执行强制重连");
// 1. 清理现有连接资源,强制重置所有状态
if (this.socketTask) {
try {
this.socketTask.close({
code: 1000, // 正常关闭
reason: "force reconnect",
});
} catch (error) {
console.error("关闭Socket失败", error);
} finally {
// 强制重置所有状态
this.socketTask = null;
this.isConnect = false;
this.isConnecting = false;
}
}
// 2. 重置重连状态
this.reconnectCount = 0;
this.stopHeartbeat();
// 3. 立即重新连接
setTimeout(() => {
this.connectSocket();
}, 100);
},
// ========== 内部关闭Socket方法不设置手动关闭标记 ==========
closeSocketInternal() {
// 1. 停止所有定时器
this.stopHeartbeat();
this.stopConnectionMonitor();
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
// 2. 关闭Socket连接Uniapp会自动清理监听器
if (this.socketTask) {
try {
this.socketTask.close({
code: 1000, // 正常关闭
reason: "internal close",
});
} catch (error) {
console.error("关闭Socket失败", error);
} finally {
// 3. 重置状态
this.socketTask = null;
this.isConnect = false;
this.isConnecting = false;
}
}
},
// ========== 初始化状态监听器 ==========
initStateListeners() {
// 避免重复初始化
if (this._listenersInitialized) return;
console.log("初始化状态监听器");
// 监听应用前后台切换
uni.onAppShow(() => {
console.log("应用切前台检查Socket连接");
this.isAppActive = true;
// 应用切前台,强制重连
this.forceReconnect();
});
uni.onAppHide(() => {
console.log("应用切后台");
this.isAppActive = false;
});
// 监听网络状态变化
uni.onNetworkStatusChange((res) => {
console.log("网络状态变化", res);
if (res.isConnected) {
// 网络恢复,自动重连
console.log("网络恢复,触发重连");
this.forceReconnect();
}
});
this._listenersInitialized = true;
},
// ========== 连接Socket ==========
connectSocket() { connectSocket() {
// 1. 严格检查:避免重复连接
if (this.isConnect || this.isConnecting || this.isManualClose) {
console.warn("跳过连接:当前状态不允许建立新连接", {
isConnect: this.isConnect,
isConnecting: this.isConnecting,
isManualClose: this.isManualClose,
});
// 关键:如果 socketTask 已失效但 isConnect 为 true强制重置状态
if (this.isConnect && !this.socketTask) {
console.warn("Socket连接状态异常强制重置");
this.isConnect = false;
this.forceReconnect();
}
return;
}
// 2. 设置连接中状态
this.isConnecting = true;
this.lastConnectTime = Date.now();
// 3. 重置主动关闭标记
this.isManualClose = false;
console.log("开始创建新的Socket连接");
// 4. 创建Socket连接
this.socketTask = uni.connectSocket({ this.socketTask = uni.connectSocket({
url: socketUrl, url: this.socketUrl,
success: (res) => {}, success: (res) => {
console.log("Socket连接请求发送成功");
},
fail: (res) => { fail: (res) => {
console.log(res); console.log("Socket连接请求失败", res);
// 请求失败,重置状态
this.isConnecting = false;
// 触发重连
this.reconnect();
}, },
}); });
// 5. 连接成功回调
this.socketTask.onOpen((res) => { this.socketTask.onOpen((res) => {
console.log("Socket连接成功");
// 重置连接状态
this.isConnect = true; this.isConnect = true;
this.init(); this.isConnecting = false;
this.reconnectCount = 0; // 重置重连次数
this.init(); // 初始化聊天
this.startHeartbeat(); // 启动心跳
this.startConnectionMonitor(); // 启动连接状态监控
// 初始化状态监听器
this.initStateListeners();
}); });
// 6. 接收消息回调 - 支持多回调
this.socketTask.onMessage((res) => { this.socketTask.onMessage((res) => {
try {
const data = JSON.parse(res.data); const data = JSON.parse(res.data);
console.log("收到服务器消息", data); console.log("收到服务器消息", data);
if (data.msg) {
uni.showToast({
title: data.msg,
icon: "none",
});
}
if (data && data.operate_type == "sendMsg") { // 处理发送消息(服务器转发的自己发送的消息)
this.chatList.unshift(data.data); if (data?.operate_type === "sendMsg") {
this.onReceiveMsg(data.data); // 添加方向标识send
console.log(this.chatList); const sendMsg = {
}
if (data && data.operate_type == "receive_msg") {
const msg={
...data.data, ...data.data,
coupon:data.data.coupon?JSON.parse(data.data.coupon):{}, direction: "send",
operate_type:"receive_msg", };
this.processMessage(sendMsg);
} }
this.chatList.unshift(msg);
this.onReceiveMsg(msg); // 处理接收消息(他人发送的消息)
console.log(this.chatList); if (data?.operate_type === "receive_msg") {
// 添加方向标识receive
const receiveMsg = {
...data.data,
coupon: data.data.coupon ? JSON.parse(data.data.coupon) : {},
operate_type: "receive_msg",
direction: "receive",
};
this.processMessage(receiveMsg);
}
// 处理心跳响应
if (data?.operate_type === "heartbeat_ack") {
console.log("心跳响应正常");
}
} catch (error) {
console.error("消息解析失败", error);
} }
}); });
// 7. 连接错误回调
this.socketTask.onError((res) => { this.socketTask.onError((res) => {
console.log("Socket连接错误", res);
// 重置连接状态
this.isConnect = false; this.isConnect = false;
console.log("连接错误", res); this.isConnecting = false;
// 错误触发重连
this.forceReconnect();
}); });
// 8. 连接关闭回调
this.socketTask.onClose(() => { this.socketTask.onClose(() => {
console.log("Socket连接已关闭");
// 重置连接状态
this.isConnect = false; this.isConnect = false;
console.log("连接已关闭"); this.isConnecting = false;
this.connectSocket(); this.stopHeartbeat(); // 停止心跳
this.stopConnectionMonitor(); // 停止连接监控
// 只有非主动关闭时才重连
if (!this.isManualClose) {
console.log("意外断开,触发重连");
this.reconnect();
} else {
console.log("主动关闭,不重连");
}
}); });
}, },
// ========== 消息处理与去重 ==========
processMessage(msg) {
// 1. 生成消息特征哈希
const msgHash = this.generateMessageHash(msg);
const now = Date.now();
// 2. 清理过期消息
this.cleanupExpiredMessages(now);
// 3. 去重逻辑:只对接收的消息应用严格去重
if (msg.direction === "receive") {
// 接收消息5秒内相同特征的消息只处理一次
const lastProcessTime = this.recentMessages.get(msgHash);
if (
lastProcessTime &&
now - lastProcessTime < this.deduplicationWindow
) {
console.log("重复接收消息,已忽略(特征哈希:", msgHash, "");
return; // 重复消息,直接返回
}
// 记录接收消息的处理时间
this.recentMessages.set(msgHash, now);
} else {
// 发送消息:直接通过,不做严格去重
console.log("发送消息,直接处理(特征哈希:", msgHash, "");
}
// 4. 限制最近消息数量,避免内存泄漏
if (this.recentMessages.size > this.maxRecentMessages) {
// 移除最早的消息
const firstKey = this.recentMessages.keys().next().value;
this.recentMessages.delete(firstKey);
}
// 5. 正常处理消息
if (this.group_id == msg.to_id) {
this.chatList.unshift(msg);
}
// 6. 触发所有消息回调(支持多页面同时监听)
this.triggerReceiveMsgCallbacks(msg);
},
// ========== 触发消息回调 ==========
triggerReceiveMsgCallbacks(msg) {
// 调用旧的单个回调(兼容原有代码)
if (typeof this.onReceiveMsg === "function") {
this.onReceiveMsg(msg);
}
// 调用所有注册的回调
this.receiveMsgCallbacks.forEach((callback) => {
if (typeof callback === "function") {
callback(msg);
}
});
},
// ========== 注册消息回调 ==========
registerReceiveMsgCallback(callback) {
if (
typeof callback === "function" &&
!this.receiveMsgCallbacks.includes(callback)
) {
this.receiveMsgCallbacks.push(callback);
}
},
// ========== 移除消息回调 ==========
removeReceiveMsgCallback(callback) {
const index = this.receiveMsgCallbacks.indexOf(callback);
if (index > -1) {
this.receiveMsgCallbacks.splice(index, 1);
}
},
// ========== 生成消息特征哈希 ==========
generateMessageHash(msg) {
// 基于消息的核心字段生成哈希
const {
content, // 消息内容
msg_type, // 消息类型
timestamp, // 时间戳
operate_type, // 操作类型
from_user_id, // 发送者ID
to_user_id, // 接收者ID
image_url, // 图片URL
coupon, // 优惠券信息
direction, // 消息方向
} = msg;
// 组合核心字段,生成唯一字符串
// 发送消息添加随机数,确保每次发送的消息哈希唯一
const msgFeatures = JSON.stringify({
content: content || "",
msg_type: msg_type || "",
operate_type: operate_type || "",
from_user_id: from_user_id || "",
to_user_id: to_user_id || "",
image_url: image_url || "",
coupon_id: coupon?.id || "",
direction: direction || "",
// 发送消息添加随机数,确保每次发送的消息哈希唯一
random:
direction === "send" ? Math.random().toString(36).substr(2, 9) : "",
});
// 使用简单的哈希算法生成32位哈希值
return this.simpleHash(msgFeatures);
},
// ========== 简单哈希算法 ==========
simpleHash(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash; // 转换为32位整数
}
return Math.abs(hash).toString(16); // 转换为16进制字符串
},
// ========== 清理过期消息 ==========
cleanupExpiredMessages(currentTime) {
for (const [hash, time] of this.recentMessages.entries()) {
if (currentTime - time > this.deduplicationWindow) {
this.recentMessages.delete(hash);
}
}
},
// ========== 主动关闭Socket ==========
closeSocket() { closeSocket() {
this.socketTask.close(); // 1. 设置主动关闭标记
this.isConnect = false; this.isManualClose = true;
// 2. 关闭Socket连接内部方法会处理状态重置
this.closeSocketInternal();
console.log("Socket已主动关闭");
}, },
}, },
unistorage: false, // 开启后对 state 的数据读写都将持久化 unistorage: false, // 开启后对 state 的数据读写都将持久化
}); });

View File

@@ -24,10 +24,10 @@ export function desensitizePhone(phone) {
*/ */
export function validateName(name) { export function validateName(name) {
// 1. 空值校验 // 1. 空值校验
if (!name || name.trim() === '') { if (!name || name.trim() === "") {
return { return {
valid: false, valid: false,
msg: '姓名不能为空' msg: "姓名不能为空",
}; };
} }
const pureName = name.trim(); const pureName = name.trim();
@@ -36,7 +36,7 @@ export function validateName(name) {
if (pureName.length < 2 || pureName.length > 6) { if (pureName.length < 2 || pureName.length > 6) {
return { return {
valid: false, valid: false,
msg: '姓名长度应为2-6位' msg: "姓名长度应为2-6位",
}; };
} }
@@ -46,7 +46,7 @@ export function validateName(name) {
if (!nameReg.test(pureName)) { if (!nameReg.test(pureName)) {
return { return {
valid: false, valid: false,
msg: '姓名仅支持中文和少数民族中间点(·),且不能包含数字、字母或特殊符号' msg: "姓名仅支持中文和少数民族中间点(·),且不能包含数字、字母或特殊符号",
}; };
} }
@@ -54,14 +54,14 @@ export function validateName(name) {
if (/[\u00b7]{2,}/.test(pureName)) { if (/[\u00b7]{2,}/.test(pureName)) {
return { return {
valid: false, valid: false,
msg: '姓名中的中间点(·)不能连续' msg: "姓名中的中间点(·)不能连续",
}; };
} }
// 校验通过 // 校验通过
return { return {
valid: true, valid: true,
msg: '姓名格式合法' msg: "姓名格式合法",
}; };
} }
@@ -73,22 +73,24 @@ export function validateName(name) {
*/ */
export function validateIdCard(idCard) { export function validateIdCard(idCard) {
// 1. 空值校验 // 1. 空值校验
if (!idCard || idCard.trim() === '') { if (!idCard || idCard.trim() === "") {
return { return {
valid: false, valid: false,
msg: '身份证号码不能为空' msg: "身份证号码不能为空",
}; };
} }
const pureIdCard = idCard.trim().toUpperCase(); // 统一转为大写处理X const pureIdCard = idCard.trim().toUpperCase(); // 统一转为大写处理X
// 2. 格式校验18位或15位 // 2. 格式校验18位或15位
const id18Reg = /^[1-9]\d{5}(19|20)\d{2}((0[1-9])|(1[0-2]))((0[1-9])|([12]\d)|(3[01]))\d{3}([0-9]|X)$/; const id18Reg =
const id15Reg = /^[1-9]\d{5}\d{2}((0[1-9])|(1[0-2]))((0[1-9])|([12]\d)|(3[01]))\d{3}$/; /^[1-9]\d{5}(19|20)\d{2}((0[1-9])|(1[0-2]))((0[1-9])|([12]\d)|(3[01]))\d{3}([0-9]|X)$/;
const id15Reg =
/^[1-9]\d{5}\d{2}((0[1-9])|(1[0-2]))((0[1-9])|([12]\d)|(3[01]))\d{3}$/;
if (!id18Reg.test(pureIdCard) && !id15Reg.test(pureIdCard)) { if (!id18Reg.test(pureIdCard) && !id15Reg.test(pureIdCard)) {
return { return {
valid: false, valid: false,
msg: '身份证号码格式错误需18位最后一位可含X或15位纯数字' msg: "身份证号码格式错误需18位最后一位可含X或15位纯数字",
}; };
} }
@@ -97,7 +99,12 @@ export function validateIdCard(idCard) {
if (pureIdCard.length === 18) { if (pureIdCard.length === 18) {
// 18位第7-14位为出生日期YYYYMMDD // 18位第7-14位为出生日期YYYYMMDD
birthDateStr = pureIdCard.slice(6, 14); birthDateStr = pureIdCard.slice(6, 14);
birthDate = new Date(`${birthDateStr.slice(0,4)}-${birthDateStr.slice(4,6)}-${birthDateStr.slice(6,8)}`); birthDate = new Date(
`${birthDateStr.slice(0, 4)}-${birthDateStr.slice(
4,
6
)}-${birthDateStr.slice(6, 8)}`
);
} else { } else {
// 15位第7-12位为出生日期YYMMDD补全为YYYYMMDD19xx或20xx默认19xx // 15位第7-12位为出生日期YYMMDD补全为YYYYMMDD19xx或20xx默认19xx
const year = `19${pureIdCard.slice(6, 8)}`; const year = `19${pureIdCard.slice(6, 8)}`;
@@ -111,12 +118,13 @@ export function validateIdCard(idCard) {
if ( if (
isNaN(birthDate.getTime()) || isNaN(birthDate.getTime()) ||
birthDateStr.slice(0, 4) !== birthDate.getFullYear().toString() || birthDateStr.slice(0, 4) !== birthDate.getFullYear().toString() ||
birthDateStr.slice(4, 6) !== (birthDate.getMonth() + 1).toString().padStart(2, '0') || birthDateStr.slice(4, 6) !==
birthDateStr.slice(6, 8) !== birthDate.getDate().toString().padStart(2, '0') (birthDate.getMonth() + 1).toString().padStart(2, "0") ||
birthDateStr.slice(6, 8) !== birthDate.getDate().toString().padStart(2, "0")
) { ) {
return { return {
valid: false, valid: false,
msg: '身份证中的出生日期无效' msg: "身份证中的出生日期无效",
}; };
} }
@@ -125,7 +133,19 @@ export function validateIdCard(idCard) {
// 加权因子 // 加权因子
const weightFactors = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]; const weightFactors = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
// 校验码对应值0-10 → 10对应X // 校验码对应值0-10 → 10对应X
const checkCodeMap = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2']; const checkCodeMap = [
"1",
"0",
"X",
"9",
"8",
"7",
"6",
"5",
"4",
"3",
"2",
];
// 计算前17位与加权因子的乘积和 // 计算前17位与加权因子的乘积和
let sum = 0; let sum = 0;
for (let i = 0; i < 17; i++) { for (let i = 0; i < 17; i++) {
@@ -137,29 +157,31 @@ export function validateIdCard(idCard) {
if (pureIdCard[17] !== expectedCheckCode) { if (pureIdCard[17] !== expectedCheckCode) {
return { return {
valid: false, valid: false,
msg: '身份证校验码错误,可能是无效身份证' msg: "身份证校验码错误,可能是无效身份证",
}; };
} }
} }
// 5. 可选提取性别18位第17位15位第15位奇数=男,偶数=女) // 5. 可选提取性别18位第17位15位第15位奇数=男,偶数=女)
let gender = ''; let gender = "";
if (pureIdCard.length === 18) { if (pureIdCard.length === 18) {
const genderCode = parseInt(pureIdCard[16]); const genderCode = parseInt(pureIdCard[16]);
gender = genderCode % 2 === 1 ? '男' : '女'; gender = genderCode % 2 === 1 ? "男" : "女";
} else { } else {
const genderCode = parseInt(pureIdCard[14]); const genderCode = parseInt(pureIdCard[14]);
gender = genderCode % 2 === 1 ? '男' : '女'; gender = genderCode % 2 === 1 ? "男" : "女";
} }
// 校验通过,返回额外信息(出生日期、性别) // 校验通过,返回额外信息(出生日期、性别)
return { return {
valid: true, valid: true,
msg: '身份证号码合法', msg: "身份证号码合法",
info: { info: {
birthDate: `${birthDate.getFullYear()}-${(birthDate.getMonth() + 1).toString().padStart(2, '0')}-${birthDate.getDate().toString().padStart(2, '0')}`, birthDate: `${birthDate.getFullYear()}-${(birthDate.getMonth() + 1)
gender: gender .toString()
} .padStart(2, "0")}-${birthDate.getDate().toString().padStart(2, "0")}`,
gender: gender,
},
}; };
} }
@@ -201,9 +223,177 @@ export function filterNumberInput(value, isIntegerOnly = false) {
} }
// 处理前导零 // 处理前导零
if (filtered.startsWith("0") && filtered.length > 1 && !filtered.startsWith("0.")) { if (
filtered.startsWith("0") &&
filtered.length > 1 &&
!filtered.startsWith("0.")
) {
filtered = filtered.replace(/^0+(\d)/, "$1"); filtered = filtered.replace(/^0+(\d)/, "$1");
} }
return filtered; return filtered;
} }
/**
* uni-app 跨平台权限判断通用方法
* @param {String} permissionType - 权限类型(通用名称,如 'camera'、'location' 等)
* @returns {Promise<{granted: Boolean, status: String, platform: String}>} - 权限状态结果
* - granted: 是否已授权Boolean
* - status: 详细状态('granted'|'denied'|'undetermined'|'limited'
* - platform: 当前平台
*/
export function checkPermission(permissionType) {
return new Promise((resolve, reject) => {
// 获取当前平台信息
const systemInfo = uni.getSystemInfoSync();
const platform = systemInfo.platform; // 'android'|'ios'|'devtools'(小程序模拟器)
const env = process.env.NODE_ENV; // 环境变量(用于区分小程序/H5/App
// 权限类型映射表:将通用权限名称映射到各平台具体权限名
const permissionMap = {
// 通用权限名称: { 平台: 平台权限名 }
camera: {
weixin: "scope.camera", // 微信小程序
alipay: "scope.camera", // 支付宝小程序
android: "android.permission.CAMERA", // App-Android
ios: "camera", // App-iOS简化处理实际需调用原生 API
h5: "camera", // H5 浏览器权限
},
location: {
weixin: "scope.userLocation",
alipay: "scope.userLocation",
android: "android.permission.ACCESS_FINE_LOCATION",
ios: "location",
h5: "geolocation",
},
microphone: {
weixin: "scope.record",
alipay: "scope.record",
android: "android.permission.RECORD_AUDIO",
ios: "microphone",
h5: "microphone",
},
album: {
weixin: "scope.writePhotosAlbum",
alipay: "scope.album",
android: "android.permission.WRITE_EXTERNAL_STORAGE",
ios: "photoLibrary",
h5: "clipboard-write", // H5 相册权限支持有限
},
// 可根据需求扩展更多权限类型
};
// 获取当前平台对应的权限名
const getPlatformPermission = () => {
if (typeof wx !== "undefined" && wx.getSetting)
return permissionMap[permissionType]?.weixin; // 微信小程序
if (typeof my !== "undefined" && my.getSetting)
return permissionMap[permissionType]?.alipay; // 支付宝小程序
if (platform === "android") return permissionMap[permissionType]?.android; // App-Android
if (platform === "ios") return permissionMap[permissionType]?.ios; // App-iOS
return permissionMap[permissionType]?.h5; // H5
};
const platformPermission = getPlatformPermission();
if (!platformPermission) {
return reject(new Error(`不支持的权限类型: ${permissionType}`));
}
// ----------------- 分平台处理 -----------------
// 1. 微信/支付宝小程序
if (typeof wx !== "undefined" && wx.getSetting) {
uni.getSetting({
success: (res) => {
const authSetting = res.authSetting || {};
const isGranted = authSetting[platformPermission] === true;
const status = isGranted
? "granted"
: authSetting[platformPermission] === false
? "denied"
: "undetermined";
resolve({ granted: isGranted, status, platform: "weixin" });
},
fail: (err) => reject(err),
});
}
// 2. App-Android 平台
else if (platform === "android" && typeof plus !== "undefined") {
try {
const Context = plus.android.importClass("android.content.Context");
const Activity = plus.android.runtimeMainActivity();
const PackageManager = plus.android.importClass(
"android.content.pm.PackageManager"
);
// 检查权限状态
const granted =
Activity.checkSelfPermission(platformPermission) ===
PackageManager.PERMISSION_GRANTED;
resolve({
granted,
status: granted ? "granted" : "denied",
platform: "android",
});
} catch (err) {
reject(err);
}
}
// 3. App-iOS 平台
else if (platform === "ios" && typeof plus !== "undefined") {
try {
// iOS 权限需通过原生 API 检查(以相机为例,其他权限类似)
const result = { granted: false, status: "denied", platform: "ios" };
if (permissionType === "camera") {
// 相机权限检查iOS 需调用 AVCaptureDevice
const AVCaptureDevice = plus.ios.importClass("AVCaptureDevice");
const authStatus =
AVCaptureDevice.authorizationStatusForMediaType("vide");
// AVCaptureDeviceAuthorizationStatus 枚举值:
// 0: notDetermined未请求, 1: restricted受限制, 2: denied拒绝, 3: authorized授权
result.granted = authStatus === 3;
result.status =
authStatus === 3
? "granted"
: authStatus === 0
? "undetermined"
: "denied";
}
// 可扩展其他 iOS 权限(如定位、麦克风等)
else if (permissionType === "location") {
const CLLocationManager = plus.ios.importClass("CLLocationManager");
const authStatus = CLLocationManager.authorizationStatus();
// CLAuthorizationStatus 枚举值:
// 0: notDetermined, 1: restricted, 2: denied, 3: authorizedAlways, 4: authorizedWhenInUse
result.granted = authStatus === 3 || authStatus === 4;
result.status = result.granted
? "granted"
: authStatus === 0
? "undetermined"
: "denied";
}
resolve(result);
} catch (err) {
reject(err);
}
}
// 4. H5 平台(浏览器权限)
else if (typeof navigator !== "undefined" && navigator.permissions) {
navigator.permissions
.query({ name: platformPermission })
.then((permissionStatus) => {
resolve({
granted: permissionStatus.state === "granted",
status: permissionStatus.state, // 'granted'|'denied'|'prompt'
platform: "h5",
});
})
.catch((err) => reject(err));
}
// 5. 其他平台(如百度小程序、字节跳动小程序等,可扩展)
else {
reject(new Error(`当前平台不支持权限判断: ${platform}`));
}
});
}