This commit is contained in:
dzc committed 2026-07-31 20:40:19 +08:00
1 parent 2191e6e4cf
commit 107946c964
13 files changed
+1315 -690

No files matched your search

+21 -20
View File
@@ -35,22 +35,22 @@
"type-check": "vue-tsc --noEmit"
},
"dependencies": {
"@dcloudio/uni-app": "3.0.0-5000520260324001",
"@dcloudio/uni-app-harmony": "3.0.0-5000520260324001",
"@dcloudio/uni-app-plus": "3.0.0-5000520260324001",
"@dcloudio/uni-components": "3.0.0-5000520260324001",
"@dcloudio/uni-h5": "3.0.0-5000520260324001",
"@dcloudio/uni-mp-alipay": "3.0.0-5000520260324001",
"@dcloudio/uni-mp-baidu": "3.0.0-5000520260324001",
"@dcloudio/uni-mp-harmony": "3.0.0-5000520260324001",
"@dcloudio/uni-mp-jd": "3.0.0-5000520260324001",
"@dcloudio/uni-mp-kuaishou": "3.0.0-5000520260324001",
"@dcloudio/uni-mp-lark": "3.0.0-5000520260324001",
"@dcloudio/uni-mp-qq": "3.0.0-5000520260324001",
"@dcloudio/uni-mp-toutiao": "3.0.0-5000520260324001",
"@dcloudio/uni-mp-weixin": "3.0.0-5000520260324001",
"@dcloudio/uni-mp-xhs": "3.0.0-5000520260324001",
"@dcloudio/uni-quickapp-webview": "3.0.0-5000520260324001",
"@dcloudio/uni-app": "3.0.0-5010520260709002",
"@dcloudio/uni-app-harmony": "3.0.0-5010520260709002",
"@dcloudio/uni-app-plus": "3.0.0-5010520260709002",
"@dcloudio/uni-components": "3.0.0-5010520260709002",
"@dcloudio/uni-h5": "3.0.0-5010520260709002",
"@dcloudio/uni-mp-alipay": "3.0.0-5010520260709002",
"@dcloudio/uni-mp-baidu": "3.0.0-5010520260709002",
"@dcloudio/uni-mp-harmony": "3.0.0-5010520260709002",
"@dcloudio/uni-mp-jd": "3.0.0-5010520260709002",
"@dcloudio/uni-mp-kuaishou": "3.0.0-5010520260709002",
"@dcloudio/uni-mp-lark": "3.0.0-5010520260709002",
"@dcloudio/uni-mp-qq": "3.0.0-5010520260709002",
"@dcloudio/uni-mp-toutiao": "3.0.0-5010520260709002",
"@dcloudio/uni-mp-weixin": "3.0.0-5010520260709002",
"@dcloudio/uni-mp-xhs": "3.0.0-5010520260709002",
"@dcloudio/uni-quickapp-webview": "3.0.0-5010520260709002",
"@dcloudio/uni-ui": "^1.5.11",
"@vant/area-data": "^2.1.0",
"dayjs": "^1.11.7",
@@ -68,16 +68,17 @@
},
"devDependencies": {
"@dcloudio/types": "^3.4.8",
"@dcloudio/uni-automator": "3.0.0-5000520260324001",
"@dcloudio/uni-cli-shared": "3.0.0-5000520260324001",
"@dcloudio/uni-stacktracey": "3.0.0-5000520260324001",
"@dcloudio/vite-plugin-uni": "3.0.0-5000520260324001",
"@dcloudio/uni-automator": "3.0.0-5010520260709002",
"@dcloudio/uni-cli-shared": "3.0.0-5010520260709002",
"@dcloudio/uni-stacktracey": "3.0.0-5010520260709002",
"@dcloudio/vite-plugin-uni": "3.0.0-5010520260709002",
"@types/node": "^24.9.1",
"@uni-helper/uni-app-types": "1.0.0-alpha.6",
"@uni-helper/uni-ui-types": "1.0.0-alpha.6",
"@vue/runtime-core": "^3.4.21",
"@vue/tsconfig": "^0.1.3",
"prettier": "^2.8.7",
"rollup": "4.14.3",
"sass": "1.78.0",
"typescript": "^4.9.4",
"unocss": "^0.58.5",
+390 -448
View File
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -42,7 +42,7 @@ export default {
unreadCount() {
let count = 0;
this.chats?.forEach((chat) => {
if (!chat.isDnd && !chat.delete && chat.targetId != -200) {
if (!chat.isDnd && !chat.delete && !chat.hidden && chat.targetId != -200) {
count += chat.unreadCount;
}
});
@@ -60,7 +60,7 @@ export default {
let count = this.unreadCount
if (count <= 0) {
this.chatStore?.chats?.forEach((chat) => {
if (!chat.isDnd && !chat.delete && chat.targetId != -200) {
if (!chat.isDnd && !chat.delete && !chat.hidden && chat.targetId != -200) {
count += chat.unreadCount;
}
});
+25 -1
View File
@@ -36,10 +36,16 @@ export default defineStore('chatStore', {
openChat(chatInfo) {
let chats = this.curChats;
let chat = null;
let restored = false;
for (let idx in chats) {
if (chats[idx].type == chatInfo.type &&
chats[idx].targetId === chatInfo.targetId) {
chat = chats[idx];
if (chat.hidden) {
chat.hidden = false;
chat.stored = false;
restored = true;
}
// 放置头部
this.moveTop(idx)
break;
@@ -61,10 +67,13 @@ export default defineStore('chatStore', {
messages: [],
atMe: false,
atAll: false,
hidden: false,
stored: false
};
chats.unshift(chat);
this.saveToStorage();
} else if (restored) {
this.saveToStorage();
}
},
activeChat(idx) {
@@ -116,6 +125,19 @@ export default defineStore('chatStore', {
chat.messages = [];
this.saveToStorage(true);
},
hideChat(chatInfo) {
let chats = this.curChats;
let chat = typeof chatInfo === 'object' && chatInfo !== null
? this.findChat(chatInfo)
: chats[chatInfo];
if (!chat) return;
chat.hidden = true;
chat.unreadCount = 0;
chat.atMe = false;
chat.atAll = false;
chat.stored = false;
this.saveToStorage();
},
removeChat(idx) {
let chats = this.curChats;
chats[idx].delete = true;
@@ -157,6 +179,9 @@ export default defineStore('chatStore', {
insertMessage(msgInfo, chatInfo) {
// 获取对方id或群id
let type = chatInfo.type;
let chat = this.findChat(chatInfo);
if (!chat) return;
chat.hidden = false;
// 记录消息的最大id
if (msgInfo.id && type == "PRIVATE" && msgInfo.id > this.privateMsgMaxId) {
this.privateMsgMaxId = msgInfo.id;
@@ -165,7 +190,6 @@ export default defineStore('chatStore', {
this.groupMsgMaxId = msgInfo.id;
}
// 如果是已存在消息,则覆盖旧的消息数据
let chat = this.findChat(chatInfo);
let message = this.findMessage(chat, msgInfo);
if (message) {
console.log("message:", message)
@@ -0,0 +1,279 @@
<template>
<view class="swipe-action">
<view class="swipe-action__actions" :style="actionStyle">
<button
class="swipe-action__delete"
hover-class="swipe-action__delete--pressed"
aria-label="删除聊天"
@tap.stop="handleAction"
>
<uni-icons type="trash" color="#ffffff" size="20" />
</button>
</view>
<view
class="swipe-action__content"
:class="{ 'swipe-action__content--dragging': dragging }"
:style="contentStyle"
@tap.capture="handleContentTap"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
@touchcancel="handleTouchCancel"
@mousedown="handleMouseDown"
>
<slot />
</view>
</view>
</template>
<script>
export default {
name: 'SwipeAction',
props: {
open: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
actionWidth: {
type: Number,
default: 96,
},
},
emits: ['action', 'open', 'close', 'swipe-start'],
data() {
return {
offset: 0,
startX: 0,
startY: 0,
startOffset: 0,
gestureDirection: '',
dragging: false,
mouseTracking: false,
suppressTap: false,
suppressTapTimer: null,
};
},
computed: {
actionWidthPx() {
return typeof uni.upx2px === 'function' ? uni.upx2px(this.actionWidth) : this.actionWidth / 2;
},
actionStyle() {
return {
width: `${this.actionWidth}rpx`,
};
},
contentStyle() {
return {
transform: `translate3d(${this.offset}px, 0, 0)`,
};
},
},
watch: {
open: {
immediate: true,
handler(value) {
if (!this.dragging) {
this.offset = value ? -this.actionWidthPx : 0;
}
},
},
},
beforeUnmount() {
this.clearSuppressTapTimer();
this.removeMouseListeners();
},
methods: {
beginGesture(x, y) {
if (this.disabled) return;
this.startX = x;
this.startY = y;
this.startOffset = this.offset;
this.gestureDirection = '';
this.dragging = false;
},
moveGesture(x, y, event) {
if (this.disabled) return;
const deltaX = x - this.startX;
const deltaY = y - this.startY;
if (!this.gestureDirection) {
if (Math.max(Math.abs(deltaX), Math.abs(deltaY)) < 8) return;
this.gestureDirection = Math.abs(deltaX) > Math.abs(deltaY) ? 'horizontal' : 'vertical';
if (this.gestureDirection === 'horizontal') {
if (this.startOffset === 0 && deltaX > 0) {
this.gestureDirection = 'ignored';
return;
}
this.dragging = true;
this.$emit('swipe-start');
}
}
if (this.gestureDirection !== 'horizontal') return;
event?.preventDefault?.();
event?.stopPropagation?.();
this.offset = Math.max(-this.actionWidthPx, Math.min(0, this.startOffset + deltaX));
},
endGesture() {
if (this.gestureDirection !== 'horizontal') {
this.dragging = false;
return;
}
const shouldOpen = this.startOffset === 0
? this.offset <= -this.actionWidthPx * 0.35
: this.offset < -this.actionWidthPx * 0.65;
this.markTapAsSuppressed();
this.settle(shouldOpen);
this.gestureDirection = '';
this.dragging = false;
},
settle(shouldOpen) {
this.offset = shouldOpen ? -this.actionWidthPx : 0;
this.$emit(shouldOpen ? 'open' : 'close');
},
markTapAsSuppressed() {
this.clearSuppressTapTimer();
this.suppressTap = true;
this.suppressTapTimer = setTimeout(() => {
this.suppressTap = false;
this.suppressTapTimer = null;
}, 120);
},
clearSuppressTapTimer() {
if (this.suppressTapTimer) {
clearTimeout(this.suppressTapTimer);
this.suppressTapTimer = null;
}
},
handleContentTap(event) {
if (!this.open && !this.suppressTap) return;
event?.preventDefault?.();
event?.stopPropagation?.();
if (this.suppressTap) return;
this.settle(false);
},
handleAction() {
if (this.disabled) return;
this.settle(false);
this.$emit('action');
},
handleTouchStart(event) {
const touch = event.touches?.[0];
if (touch) this.beginGesture(touch.clientX, touch.clientY);
},
handleTouchMove(event) {
const touch = event.touches?.[0];
if (touch) this.moveGesture(touch.clientX, touch.clientY, event);
},
handleTouchEnd() {
this.endGesture();
},
handleTouchCancel() {
this.dragging = false;
this.gestureDirection = '';
this.offset = this.open ? -this.actionWidthPx : 0;
},
handleMouseDown(event) {
// #ifdef H5
if (event.button !== 0 || this.disabled) return;
this.mouseTracking = true;
this.beginGesture(event.clientX, event.clientY);
document.addEventListener('mousemove', this.handleMouseMove);
document.addEventListener('mouseup', this.handleMouseUp);
// #endif
},
handleMouseMove(event) {
// #ifdef H5
if (this.mouseTracking) this.moveGesture(event.clientX, event.clientY, event);
// #endif
},
handleMouseUp() {
// #ifdef H5
if (this.mouseTracking) this.endGesture();
this.mouseTracking = false;
this.removeMouseListeners();
// #endif
},
removeMouseListeners() {
// #ifdef H5
document.removeEventListener('mousemove', this.handleMouseMove);
document.removeEventListener('mouseup', this.handleMouseUp);
// #endif
},
},
};
</script>
<style scoped lang="scss">
.swipe-action {
position: relative;
width: 100%;
overflow: hidden;
background-color: $im-bg;
}
.swipe-action__actions {
position: absolute;
top: 0;
right: 0;
bottom: 0;
display: flex;
overflow: hidden;
border-radius: 0;
}
.swipe-action__delete {
display: flex;
flex: 1;
align-items: center;
justify-content: center;
min-width: 88rpx;
min-height: 88rpx;
margin: 0;
padding: 0;
color: #ffffff;
font-size: $im-font-size-smaller;
line-height: 1;
background-color: $im-color-danger;
border: 0;
border-radius: 0 !important;
box-shadow: none;
&::after {
border: 0;
border-radius: 0;
}
}
.swipe-action__delete--pressed {
background-color: darken($im-color-danger, 8%);
}
.swipe-action__content {
position: relative;
z-index: 1;
width: 100%;
overflow: hidden;
background-color: $im-bg;
touch-action: pan-y;
transition: transform 220ms cubic-bezier(0.22, 1, 0.36, 1);
will-change: transform;
user-select: none;
}
.swipe-action__content--dragging {
transition: none;
}
@media (prefers-reduced-motion: reduce) {
.swipe-action__content {
transition-duration: 0ms;
}
}
</style>
+2 -2
View File
@@ -2,8 +2,8 @@
"name" : "荷塘月色",
"appid" : "__UNI__0A84F60",
"description" : "聊天交友app",
"versionName" : "1.0.6",
"versionCode" : 106,
"versionName" : "1.1.2",
"versionCode" : 112,
"transformPx" : false,
"app-plus" : {
"usingComponents" : true,
+84 -125
View File
@@ -1,41 +1,26 @@
<template>
<page-meta :page-style="$statusBarPageStyle" />
<wd-config-provider theme="dark" :theme-vars="{ darkBackground2: '#1b1b1b' }">
<wd-navbar
title="修改资料"
fixed
safeAreaInsetTop
style="z-index: 999999 !important"
:bordered="false"
>
<wd-navbar title="修改资料" safeAreaInsetTop :bordered="false">
<template #left>
<uni-icons type="back" size="30" color="#D29E52" @click="back"></uni-icons>
</template>
<template #right>
<navigator hover-class="navigator-hover">
<view
class="bg-color-theme px-3 py-1 rounded-full c-black h-1/2 line-height-normal"
@click="saveUserInfo"
>
<view class="bg-color-theme px-3 py-1 rounded-full c-black h-1/2 line-height-normal" @click="saveUserInfo">
保存
</view>
</navigator>
</template>
</wd-navbar>
<scroll-view scroll-y>
<view class="flex-row-center mt-300rpx mb-200rpx">
<s-uploader
v-model:url="userinfo.avatar"
fileMediatype="image"
limit="1"
mode="grid"
:imageStyles="{
width: '268rpx',
height: '268rpx',
border: { width: '6rpx', color: '#D29E52', radius: '50rpx' },
}"
/>
<scroll-view style="height: calc(100dvh - 44px - var(--status-bar-height, 0px));" scroll-y>
<view class="flex-row-center mt-100rpx mb-50rpx">
<s-uploader v-model:url="userinfo.avatar" fileMediatype="image" limit="1" mode="grid" :imageStyles="{
width: '268rpx',
height: '268rpx',
border: { width: '6rpx', color: '#D29E52', radius: '50rpx' },
}" />
</view>
<template v-if="false">
@@ -62,39 +47,13 @@
<input type="text" v-model="userinfo.nickname" :maxlength="12" />
</template>
</wd-cell>
<wd-datetime-picker
v-model="userinfo.birthday"
label="生日"
title="生日"
type="date"
align-right
:default-value="Date.now()"
:min-date="new Date(1980, 1, 1).getTime()"
:max-date="Date.now()"
/>
<wd-picker
v-model="userinfo.height"
:columns="columnsHeight"
label="身高"
title="身高"
align-right
:default-value="180"
/>
<wd-picker
v-model="userinfo.weight"
:columns="columnsWeight"
label="体重"
title="体重"
align-right
:default-value="60"
/>
<wd-picker
v-model="userinfo.occ"
:columns="columnsOcc"
label="从事职业"
title="从事职业"
align-right
/>
<wd-datetime-picker v-model="userinfo.birthday" label="生日" title="生日" type="date" align-right
:default-value="Date.now()" :min-date="new Date(1980, 1, 1).getTime()" :max-date="Date.now()" />
<wd-picker v-model="userinfo.height" :columns="columnsHeight" label="身高" title="身高" align-right
:default-value="180" />
<wd-picker v-model="userinfo.weight" :columns="columnsWeight" label="体重" title="体重" align-right
:default-value="60" />
<wd-picker v-model="userinfo.occ" :columns="columnsOcc" label="从事职业" title="从事职业" align-right />
</wd-cell-group>
<view class="ml-5 my-3">个人简介</view>
@@ -116,81 +75,81 @@
</template>
<script lang="ts">
// 身高选择器选项
const columnsHeight = Array.from({ length: 100 }, (_, i) => ({
label: i + 100 + 'cm',
value: i + 100,
})).reverse();
// 体重选择器选项
const columnsWeight = Array.from({ length: 80 }, (_, i) => ({
label: i + 20 + 'kg',
value: i + 20,
}));
// 职业选择器选项
const columnsOcc = [
{ text: '互联网', value: '互联网' },
{ text: '金融', value: '金融' },
{ text: '娱乐', value: '娱乐' },
{ text: 'CEO', value: 'CEO' },
{ text: '高管', value: '高管' },
{ text: '投资人', value: '投资人' },
{ text: '学生', value: '学生' },
{ text: '其他', value: '其他' },
{ text: '老师', value: '老师' },
{ text: '白领', value: '白领' },
{ text: '模特', value: '模特' },
{ text: '主播', value: '主播' },
{ text: '幼师', value: '幼师' },
{ text: 'coser', value: 'coser' },
{ text: '空乘', value: '空乘' },
{ text: '公关', value: '公关' },
{ text: '护士', value: '护士' },
];
// 身高选择器选项
const columnsHeight = Array.from({ length: 100 }, (_, i) => ({
label: i + 100 + 'cm',
value: i + 100,
})).reverse();
// 体重选择器选项
const columnsWeight = Array.from({ length: 80 }, (_, i) => ({
label: i + 20 + 'kg',
value: i + 20,
}));
// 职业选择器选项
const columnsOcc = [
{ text: '互联网', value: '互联网' },
{ text: '金融', value: '金融' },
{ text: '娱乐', value: '娱乐' },
{ text: 'CEO', value: 'CEO' },
{ text: '高管', value: '高管' },
{ text: '投资人', value: '投资人' },
{ text: '学生', value: '学生' },
{ text: '其他', value: '其他' },
{ text: '老师', value: '老师' },
{ text: '白领', value: '白领' },
{ text: '模特', value: '模特' },
{ text: '主播', value: '主播' },
{ text: '幼师', value: '幼师' },
{ text: 'coser', value: 'coser' },
{ text: '空乘', value: '空乘' },
{ text: '公关', value: '公关' },
{ text: '护士', value: '护士' },
];
</script>
<script setup lang="ts">
import { onLoad } from '@dcloudio/uni-app';
import { reactive } from 'vue';
// @ts-ignore
import UserApi from '@/sheep/api/member/user';
// @ts-ignore
import sheep from '@/sheep';
import SUploader from '@/sheep/components/s-uploader/s-uploader.vue';
import { onLoad } from '@dcloudio/uni-app';
import { reactive } from 'vue';
// @ts-ignore
import UserApi from '@/sheep/api/member/user';
// @ts-ignore
import sheep from '@/sheep';
import SUploader from '@/sheep/components/s-uploader/s-uploader.vue';
onLoad(async () => {
uni.showLoading({ title: '加载中...' });
// 获取用户信息
const { data } = await UserApi.getUserInfo().finally(() => uni.hideLoading());
Object.assign(userinfo, data, {
// photo: data.photo?.map((item: { address: string }) => item.address),
});
onLoad(async () => {
uni.showLoading({ title: '加载中...' });
// 获取用户信息
const { data } = await UserApi.getUserInfo().finally(() => uni.hideLoading());
Object.assign(userinfo, data, {
// photo: data.photo?.map((item: { address: string }) => item.address),
});
});
// 用户信息
const userinfo = reactive<any>({
avatar: '',
nickname: '',
birthday: '',
height: '',
weight: '',
occ: '',
description: '',
photo: [],
});
// 用户信息
const userinfo = reactive<any>({
avatar: '',
nickname: '',
birthday: '',
height: '',
weight: '',
occ: '',
description: '',
photo: [],
});
// 保存用户信息
const saveUserInfo = async () => {
const params = {
...userinfo,
// photo: userinfo.photo?.map((url: string) => ({ resourceType: 'IMAGE', address: url })) || [],
};
await UserApi.updateUser(params);
sheep.$store('user').getInfo();
back();
// 保存用户信息
const saveUserInfo = async () => {
const params = {
...userinfo,
// photo: userinfo.photo?.map((url: string) => ({ resourceType: 'IMAGE', address: url })) || [],
};
await UserApi.updateUser(params);
sheep.$store('user').getInfo();
back();
};
// 返回
function back() {
uni.navigateBack();
}
// 返回
function back() {
uni.navigateBack();
}
</script>
+59 -58
View File
@@ -33,44 +33,46 @@
<view class="chat-tip" v-else-if="visibleChats.length == 0">
<wd-status-tip image="message" tip="点击私聊,然后开始精彩的对话吧" />
</view>
<scroll-view class="scroll-bar" v-else scroll-with-animation="true" scroll-y="true">
<view v-for="(chat, index) in visibleChats" :key="index">
<!-- <long-press-menu v-if="isShowChat(chat)" :items="menu.items" @select="onSelectMenu($event, index)"> -->
<chat-item v-if="chat.targetId != -200" :user-id="chat.targetId" :chat="chat" :index="index"
:active="menu.chatIdx == index"></chat-item>
<!-- </long-press-menu> -->
</view>
<scroll-view
class="scroll-bar"
v-else
scroll-with-animation="true"
scroll-y="true"
@scroll="closeSwipeAction"
>
<swipe-action
v-for="chat in visibleChats"
:key="getChatKey(chat)"
:open="openedChatKey === getChatKey(chat)"
@swipe-start="onSwipeStart(chat)"
@open="onSwipeOpen(chat)"
@close="onSwipeClose(chat)"
@action="onDeleteChat(chat)"
>
<chat-item
:user-id="chat.targetId"
:chat="chat"
:index="getChatIndex(chat)"
/>
</swipe-action>
</scroll-view>
</wd-config-provider>
</view>
</template>
<script>
import SwipeAction from '@/components/swipe-action/swipe-action.vue';
export default {
components: {
SwipeAction,
},
data() {
return {
pageReady: false,
showSearch: false,
searchText: '',
menu: {
show: false,
style: '',
chatIdx: -1,
isTouchMove: false,
items: [
{
key: 'DELETE',
name: '删除聊天',
icon: 'trash',
color: '#e64e4e',
},
{
key: 'TOP',
name: '置顶聊天',
icon: 'arrow-up',
},
],
},
openedChatKey: '',
};
},
methods: {
@@ -107,42 +109,35 @@ export default {
this.chatStore.updateChatFromFriend(friend);
});
},
onSelectMenu(item, chatIdx) {
switch (item.key) {
case 'DELETE':
uni.showModal({
title: '删除聊天',
content: '确定要删除此聊天吗?',
confirmText: '删除',
confirmColor: '#e64e4e',
success: (res) => {
if (res.confirm) {
this.removeChat(chatIdx);
}
},
});
break;
case 'TOP':
this.moveToTop(chatIdx);
break;
default:
break;
getChatKey(chat) {
return `${chat.type}-${chat.targetId}`;
},
getChatIndex(chat) {
return this.chatStore?.curChats?.indexOf(chat) ?? -1;
},
onSwipeStart(chat) {
const chatKey = this.getChatKey(chat);
if (this.openedChatKey && this.openedChatKey !== chatKey) {
this.openedChatKey = '';
}
this.menu.show = false;
},
removeChat(chatIdx) {
this.chatStore?.removeChat?.(chatIdx);
onSwipeOpen(chat) {
this.openedChatKey = this.getChatKey(chat);
},
moveToTop(chatIdx) {
this.chatStore?.moveTop?.(chatIdx);
},
isShowChat(chat) {
if (chat.delete) {
return false;
onSwipeClose(chat) {
if (this.openedChatKey === this.getChatKey(chat)) {
this.openedChatKey = '';
}
return !this.searchText || chat.showName.includes(this.searchText);
},
closeSwipeAction() {
this.openedChatKey = '';
},
onDeleteChat(chat) {
this.openedChatKey = '';
this.chatStore?.hideChat?.(chat);
},
onSearch() {
this.closeSwipeAction();
this.showSearch = !this.showSearch;
this.searchText = '';
},
@@ -173,7 +168,7 @@ export default {
unreadCount() {
let count = 0;
this.chatList.forEach((chat) => {
if (!chat.isDnd && !chat.delete && chat.targetId != -200) {
if (!chat.isDnd && !chat.delete && !chat.hidden && chat.targetId != -200) {
count += chat.unreadCount;
}
});
@@ -187,7 +182,12 @@ export default {
},
showChats() {
return this.chatList.filter(
(chat) => !chat.delete && chat.showName && chat.showName.includes(this.searchText),
(chat) =>
!chat.delete &&
!chat.hidden &&
chat.targetId != -200 &&
chat.showName &&
chat.showName.includes(this.searchText),
);
},
},
@@ -198,6 +198,7 @@ export default {
},
async onShow() {
this.pageReady = false;
this.openedChatKey = '';
try {
await this.ensurePageDataReady();
this.refreshUnreadBadge();
+1 -1
View File
@@ -23,7 +23,7 @@
<image class="size-24rpx ml-2" :src="userGenderImageSrc" mode="scaleToFill" />
</view>
<view class="mb-2 text-xs text-#ffffff80 flex items-center">
<text>ID: {{ userInfo.id }}</text>
<text>ID: {{ userInfo.userCode }}</text>
<view v-if="userInfo.memberVip?.vip"
class="text-20rpx ml-2 flex-row-center bg-color-theme-dark px-1 rounded-1">
<uni-icons type="vip-filled" size="14" />
+2 -2
View File
@@ -1,5 +1,5 @@
<template>
<page-meta :page-style="$statusBarPageStyle" />
<page-meta :page-style="$statusBarPageStyle" />
<wd-config-provider theme="dark">
<wd-navbar title="我的" safeAreaInsetTop :bordered="false" />
<scroll-view class="h-[calc(100dvh-144rpx)]" scroll-y>
@@ -10,7 +10,7 @@
<view v-if="isLogin" class="flex-1 ml-20rpx">
<view class="mb-1 text-base">{{ userInfo.nickname }}</view>
<view class="mb-2 text-xs text-#ffffff80 flex items-center">
<text>ID: {{ userInfo.id }}</text>
<text>ID: {{ userInfo.userCode }}</text>
<image class="size-24rpx ml-4" :src="userGenderImageSrc" mode="scaleToFill" />
</view>
</view>
+8 -10
View File
@@ -23,11 +23,13 @@
}}
</text>
</view>
<view v-for="(item, index) in data.memberTagList" :key="index">
<view v-if="tagVisibleTypeList.includes(item.tagBizType)"
class="mt-2 inline-flex px-1 py-6rpx text-18rpx c-black rounded-4rpx bg-color-theme-dark mr-2">
<wd-icon name="discount-filled" size="10px" color="#000" />
<text>{{ item.tagTitle }}</text>
<view class="flex-row flex-wrap">
<view v-for="(item, index) in data.memberTagList" :key="index" class="inline-flex">
<view v-if="tagVisibleTypeList.includes(item.tagBizType)"
class="mt-2 px-1 py-6rpx text-18rpx c-black rounded-4rpx bg-color-theme-dark mr-2">
<wd-icon name="discount-filled" size="10px" color="#000" />
<text>{{ item.tagTitle }}</text>
</view>
</view>
</view>
<view class="mt-2 flex flex-wrap gap-3">
@@ -75,11 +77,7 @@ const userInfo = computed(() => sheep.$store('user').userInfo);
// 只有男性用户才显示VIP标签
const tagVisibleTypeList = computed(() => {
const list = ['1'];
const sex = userInfo.value?.sex ?? null;
if (sex == 1) {
list.push('4');
}
const list = ['1', '4'];
return list;
});
+438 -17
View File
@@ -4,6 +4,432 @@
* @param {Function} successCallback 保存成功回调
* @param {Function} errorCallback 保存失败回调
*/
// #ifdef H5
const getImageExtension = (filePath: string) => {
try {
const pathname = new URL(filePath, window.location.href).pathname;
return pathname.match(/\.(?:jpe?g|png|gif|webp|bmp|heic)$/i)?.[0] || '.jpg';
} catch (error) {
return '.jpg';
}
};
const getImageFileName = (filePath: string) => {
try {
const pathname = new URL(filePath, window.location.href).pathname;
const encodedFileName = pathname.split('/').pop();
if (encodedFileName) {
return decodeURIComponent(encodedFileName);
}
} catch (error) {
// 使用下方的默认文件名。
}
return `poster-${Date.now()}${getImageExtension(filePath)}`;
};
const isIOS = () => {
const userAgent = navigator.userAgent;
return (
/iPad|iPhone|iPod/i.test(userAgent) ||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)
);
};
const isEmbeddedWebView = () => {
const userAgent = navigator.userAgent;
const isAndroidWebView =
/;\s*wv\)/i.test(userAgent) || /Version\/\d+(?:\.\d+)*\s+Chrome\//i.test(userAgent);
const isIOSWebView = isIOS() && !/Safari\//i.test(userAgent);
return isAndroidWebView || isIOSWebView || /Html5Plus/i.test(userAgent);
};
const loadShareableImageFile = async (filePath: string) => {
if (typeof File !== 'function') {
return null;
}
const response = await fetch(filePath);
if (!response.ok) {
throw new Error(`图片加载失败: ${response.status}`);
}
const blob = await response.blob();
return new File([blob], getImageFileName(filePath), {
type: blob.type || 'image/jpeg',
});
};
const IMAGE_PREVIEW_ID = 'friendship-image-save-preview';
const IMAGE_PREVIEW_HISTORY_KEY = '__friendshipImageSavePreview';
const openManualImagePreview = (filePath: string) => {
if (document.getElementById(IMAGE_PREVIEW_ID)) {
return;
}
const previewId = `${Date.now()}`;
const previousBodyOverflow = document.body.style.overflow;
const preview = document.createElement('div');
preview.id = IMAGE_PREVIEW_ID;
preview.setAttribute('role', 'dialog');
preview.setAttribute('aria-modal', 'true');
preview.setAttribute('aria-label', '海报预览');
Object.assign(preview.style, {
position: 'fixed',
inset: '0',
zIndex: '2147483647',
display: 'flex',
flexDirection: 'column',
boxSizing: 'border-box',
background: '#111111',
});
const header = document.createElement('div');
Object.assign(header.style, {
position: 'relative',
flex: '0 0 auto',
height: 'calc(56px + env(safe-area-inset-top, 0px))',
paddingTop: 'env(safe-area-inset-top, 0px)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxSizing: 'border-box',
color: '#ffffff',
background: '#111111',
});
const title = document.createElement('span');
title.textContent = '长按图片保存';
Object.assign(title.style, {
fontSize: '17px',
fontWeight: '600',
lineHeight: '24px',
});
const closeButton = document.createElement('button');
closeButton.type = 'button';
closeButton.textContent = '\u00d7';
closeButton.setAttribute('aria-label', '关闭预览');
closeButton.title = '关闭预览';
Object.assign(closeButton.style, {
position: 'absolute',
right: '8px',
bottom: '4px',
width: '48px',
height: '48px',
padding: '0',
border: '0',
color: '#ffffff',
background: 'transparent',
fontSize: '36px',
fontWeight: '300',
lineHeight: '48px',
textAlign: 'center',
cursor: 'pointer',
});
const imageContainer = document.createElement('div');
Object.assign(imageContainer.style, {
flex: '1 1 auto',
minHeight: '0',
padding: '12px 16px calc(20px + env(safe-area-inset-bottom, 0px))',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxSizing: 'border-box',
overflow: 'hidden',
});
const image = document.createElement('img');
image.src = filePath;
image.alt = '邀请海报';
Object.assign(image.style, {
display: 'block',
maxWidth: '100%',
maxHeight: '100%',
width: 'auto',
height: 'auto',
objectFit: 'contain',
});
image.style.setProperty('user-select', 'auto', 'important');
image.style.setProperty('-webkit-user-select', 'auto', 'important');
image.style.setProperty('-webkit-touch-callout', 'default', 'important');
const footer = document.createElement('div');
Object.assign(footer.style, {
flex: '0 0 auto',
padding: '10px 20px calc(16px + env(safe-area-inset-bottom, 0px))',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
boxSizing: 'border-box',
background: '#111111',
});
const saveHint = document.createElement('span');
saveHint.textContent = '长按图片保存,或点击下方按钮';
Object.assign(saveHint.style, {
color: '#cccccc',
fontSize: '14px',
lineHeight: '20px',
textAlign: 'center',
});
const saveButton = document.createElement('button');
saveButton.type = 'button';
saveButton.textContent = '保存海报';
saveButton.disabled = true;
Object.assign(saveButton.style, {
width: 'min(320px, 100%)',
height: '48px',
marginTop: '10px',
padding: '0 20px',
border: '0',
borderRadius: '8px',
color: '#111111',
background: '#d29e52',
fontSize: '16px',
fontWeight: '600',
lineHeight: '48px',
textAlign: 'center',
opacity: '0.5',
});
header.append(closeButton, title);
imageContainer.appendChild(image);
footer.append(saveHint, saveButton);
preview.append(header, imageContainer, footer);
document.body.appendChild(preview);
document.body.style.overflow = 'hidden';
let pushedPreviewState = false;
let closed = false;
let shareableFile: File | null = null;
let sharing = false;
let touchStartedAt = 0;
const setSaveButtonEnabled = (enabled: boolean) => {
saveButton.disabled = !enabled;
saveButton.style.opacity = enabled ? '1' : '0.5';
saveButton.style.cursor = enabled ? 'pointer' : 'default';
};
const canShareFile = (file: File) => {
if (typeof navigator.share !== 'function') {
return false;
}
try {
return typeof navigator.canShare !== 'function' || navigator.canShare({ files: [file] });
} catch (error) {
return false;
}
};
const openSystemSave = async () => {
if (!shareableFile || !canShareFile(shareableFile) || sharing) {
return;
}
sharing = true;
setSaveButtonEnabled(false);
try {
await navigator.share({
files: [shareableFile],
title: '邀请海报',
});
if (!closed) {
saveHint.textContent = '操作已完成';
}
} catch (error: any) {
if (!closed && error?.name !== 'AbortError') {
saveHint.textContent = '无法调用系统保存,请尝试长按图片';
}
} finally {
sharing = false;
if (!closed) {
setSaveButtonEnabled(Boolean(shareableFile && canShareFile(shareableFile)));
}
}
};
const cleanup = () => {
if (closed) {
return;
}
closed = true;
window.removeEventListener('popstate', handlePopState);
document.removeEventListener('keydown', handleKeyDown);
preview.remove();
document.body.style.overflow = previousBodyOverflow;
};
const closePreview = () => {
const shouldRestoreHistory =
pushedPreviewState && window.history.state?.[IMAGE_PREVIEW_HISTORY_KEY] === previewId;
cleanup();
if (shouldRestoreHistory) {
window.history.back();
}
};
function handlePopState() {
cleanup();
}
function handleKeyDown(event: KeyboardEvent) {
if (event.key === 'Escape') {
closePreview();
}
}
closeButton.addEventListener('click', closePreview);
saveButton.addEventListener('click', openSystemSave);
image.addEventListener(
'touchstart',
() => {
touchStartedAt = Date.now();
},
{ passive: true },
);
image.addEventListener(
'touchend',
() => {
if (Date.now() - touchStartedAt >= 600) {
void openSystemSave();
}
touchStartedAt = 0;
},
{ passive: true },
);
image.addEventListener(
'touchcancel',
() => {
touchStartedAt = 0;
},
{ passive: true },
);
window.addEventListener('popstate', handlePopState);
document.addEventListener('keydown', handleKeyDown);
void loadShareableImageFile(filePath)
.then((file) => {
if (closed) {
return;
}
shareableFile = file;
if (file && canShareFile(file)) {
setSaveButtonEnabled(true);
return;
}
saveHint.textContent = '长按无响应时,请截屏保存';
saveButton.style.display = 'none';
})
.catch(() => {
if (!closed) {
saveHint.textContent = '长按无响应时,请截屏保存';
saveButton.style.display = 'none';
}
});
try {
window.history.pushState(
{
...(window.history.state || {}),
[IMAGE_PREVIEW_HISTORY_KEY]: previewId,
},
'',
);
pushedPreviewState = true;
} catch (error) {
// 无法写入历史记录时仍可使用关闭按钮退出预览。
}
};
const removeHtml5PlusTempFile = (plusObject: any, filePath: string) => {
try {
plusObject.io?.resolveLocalFileSystemURL(
filePath,
(entry: any) => {
try {
entry.remove?.(
() => {},
() => {},
);
} catch (error) {
// 临时文件清理失败不应改变保存结果。
}
},
() => {},
);
} catch (error) {
// 临时文件清理失败不应改变保存结果。
}
};
const saveImageWithHtml5Plus = (plusObject: any, filePath: string) => {
return new Promise<void>((resolve, reject) => {
const extension = getImageExtension(filePath);
const tempFilePath = `_doc/poster-${Date.now()}${extension}`;
try {
const downloadTask = plusObject.downloader.createDownload(
filePath,
{ filename: tempFilePath },
(download: any, status: number) => {
const downloadedFilePath = download?.filename;
if (status < 200 || status >= 300 || !downloadedFilePath) {
if (downloadedFilePath) {
removeHtml5PlusTempFile(plusObject, downloadedFilePath);
}
reject(new Error(`图片下载失败: ${status || '未知状态'}`));
return;
}
try {
plusObject.gallery.save(
downloadedFilePath,
() => {
removeHtml5PlusTempFile(plusObject, downloadedFilePath);
resolve();
},
(error: any) => {
removeHtml5PlusTempFile(plusObject, downloadedFilePath);
reject(error);
},
);
} catch (error) {
removeHtml5PlusTempFile(plusObject, downloadedFilePath);
reject(error);
}
},
);
downloadTask.start();
} catch (error) {
reject(error);
}
});
};
const downloadImageForBrowser = async (filePath: string) => {
const response = await fetch(filePath);
if (!response.ok) {
throw new Error(`图片下载失败: ${response.status}`);
}
const blob = await response.blob();
const fileName = getImageFileName(filePath);
const objectUrl = URL.createObjectURL(blob);
try {
const link = document.createElement('a');
link.href = objectUrl;
link.download = fileName;
link.style.display = 'none';
document.body.appendChild(link);
link.click();
link.remove();
} finally {
window.setTimeout(() => URL.revokeObjectURL(objectUrl), 1000);
}
};
// #endif
export const saveImageToPhotosAlbum = (
filePath: string,
successCallback?: (res: any) => void,
@@ -17,28 +443,23 @@ export const saveImageToPhotosAlbum = (
// #ifdef H5
const saveImageForH5 = async () => {
let objectUrl = '';
try {
const response = await fetch(filePath);
if (!response.ok) {
throw new Error(`图片下载失败: ${response.status}`);
}
const plusObject = (globalThis as any).plus;
const canSaveToSystemAlbum =
typeof plusObject?.downloader?.createDownload === 'function' &&
typeof plusObject?.gallery?.save === 'function';
const blob = await response.blob();
objectUrl = URL.createObjectURL(blob);
const link = document.createElement('a');
const fileName = filePath.split('/').pop()?.split('?')[0] || `image-${Date.now()}.jpg`;
link.href = objectUrl;
link.download = decodeURIComponent(fileName);
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
if (canSaveToSystemAlbum) {
await saveImageWithHtml5Plus(plusObject, filePath);
} else if (isIOS() || isEmbeddedWebView()) {
openManualImagePreview(filePath);
return;
} else {
await downloadImageForBrowser(filePath);
}
successCallback?.({ errMsg: 'saveImageToPhotosAlbum:ok' });
} catch (error) {
errorCallback?.(error);
} finally {
if (objectUrl) setTimeout(() => URL.revokeObjectURL(objectUrl), 1000);
}
};
+4 -4
View File
@@ -167,7 +167,7 @@ export const getGender = (genderType) => {
/**
* 将时间转换为相对当前时间的文字描述
* @param {dayjs.ConfigType} date 需要转换的时间
* @returns {string} 几分钟前、几小时前、几天前或近期
* @returns {string} 在线、几分钟前、几小时前、几天前或近期
*/
export const formatRelativeTime = (date) => {
if (!date) return '';
@@ -175,12 +175,12 @@ export const formatRelativeTime = (date) => {
if (!targetTime.isValid()) return '';
const now = dayjs();
const minutes = now.diff(targetTime, 'minute');
// 未来时间和不足5分钟都按近期处理。
if (minutes < 5) return '近期';
// 未来时间和不足 15 分钟都按在线处理。
if (minutes < 15) return '在线';
if (minutes < 60) return `${minutes}分钟前`;
const hours = now.diff(targetTime, 'hour');
if (hours < 24) return `${hours}小时前`;
const days = now.diff(targetTime, 'day');
if (days < 7) return `${days}天前`;
if (days < 3) return `${days}天前`;
return '近期';
};