111
This commit is contained in:
1 parent
2191e6e4cf
commit
107946c964
13 files changed
+1315
-690
No files matched your search
+21
-20
@@ -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",
|
||||
|
||||
Generated
+390
-448
File diff suppressed because it is too large.
Load diff
+2
-2
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
@@ -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();
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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 '近期';
|
||||
};
|
||||
Reference in new issue
Block a user