Files
oa_app/pages/tabs/messages.vue
T
2026-09-23 18:54:02 +08:00

258 lines
8.4 KiB
Vue

<template>
<view class="page msg edge">
<fy-header title="消息" :extra="unreadLabel">
<template #right>
<view class="plus" @click.stop="menu = !menu">
<view class="plus-h" />
<view class="plus-v" />
</view>
</template>
</fy-header>
<view v-if="menu" class="mask" @click="menu = false" />
<view v-if="menu" class="drop" :style="{ top: dropTop + 'px' }">
<view class="caret" />
<view class="drop-item" @click="newGroup">
<wd-icon name="usergroup" size="32rpx" color="#374151" />
<text>发起群聊</text>
</view>
<view class="drop-item" @click="addMate">
<wd-icon name="user-add" size="32rpx" color="#374151" />
<text>添加同事</text>
</view>
</view>
<fy-scroll class="edge-scroll list" dock @refresh="reload">
<view class="finder" @click="goSearch">
<wd-icon name="search" size="28rpx" color="#9CA3AF" />
<text class="finder-txt">搜索</text>
</view>
<view v-if="!rows.length" class="empty">暂无会话</view>
<view
v-for="c in rows"
:key="c.id"
class="item"
:class="{ pin: c.pinned }"
@click="open(c)"
@longpress="act(c)"
>
<view class="av-wrap">
<image v-if="c.avatar && c.type !== 'files'" class="av" :src="c.avatar" mode="aspectFill" />
<view v-else-if="c.type === 'files'" class="av"><fy-file-mark /></view>
<view v-else class="av txt" :class="avClass(c)">{{ avText(c) }}</view>
<text v-if="c.unread && !c.muted" class="badge abs">{{ c.unread > 99 ? '99+' : c.unread }}</text>
<text v-else-if="c.unread && c.muted" class="dotu" />
</view>
<view class="meta">
<view class="top">
<text class="name">{{ c.name }}</text>
<text class="time">{{ c.lastTime }}</text>
</view>
<view class="bot">
<text class="prev">{{ c.muted && c.unread ? '[' + c.unread + '条]' : '' }}{{ c.lastMessage || ' ' }}</text>
<text v-if="c.muted" class="mute">🔕</text>
</view>
</view>
</view>
</fy-scroll>
</view>
</template>
<script>
import FyHeader from '../../components/fy-header.vue'
import FyScroll from '../../components/fy-scroll.vue'
import FyFileMark from '../../components/fy-file-mark.vue'
import { store } from '../../utils/store.js'
export default {
components: { FyHeader, FyFileMark, FyScroll },
data() {
return { rows: [], unread: 0, off: null, menu: false, dropTop: 64 }
},
computed: {
unreadLabel() {
if (!this.unread) return ''
return '(' + (this.unread > 99 ? '99+' : this.unread) + ')'
}
},
created() {
let status = 20
try { status = uni.getSystemInfoSync().statusBarHeight || 20 } catch (_) {}
this.dropTop = status + uni.upx2px(88) + 6
},
onShow() {
if (this.off) this.off()
this.guard()
store.refreshPeoplePhotos()
this.pull()
this.off = store.on(() => this.pull())
},
onHide() {
this.menu = false
if (this.off) this.off()
this.off = null
},
methods: {
guard() {
if (store.phase === 'login') uni.reLaunch({ url: '/pages/login/login' })
if (store.phase === 'forcePassword') uni.redirectTo({ url: '/pages/password/password' })
},
pull() {
this.rows = store.conversations.slice()
this.unread = store.unreadTotal()
},
reload() {
store.refreshPeoplePhotos().finally(() => this.pull())
},
avText(c) {
if (c.type === 'work') return '工'
if (c.type === 'group') return '群'
return (c.name || '?').slice(0, 1)
},
avClass(c) {
if (c.type === 'work') return 'work'
return 'plain'
},
open(c) {
if (c.type === 'work') {
uni.navigateTo({ url: '/pages/chat/work?sid=' + encodeURIComponent(c.id) })
return
}
uni.navigateTo({ url: '/pages/chat/chat?sid=' + encodeURIComponent(c.id) })
},
act(c) {
const items = [
c.pinned ? '取消置顶' : '置顶聊天',
c.unread ? '标为已读' : '标为未读',
c.muted ? '关闭免打扰' : '消息免打扰',
'删除该聊天'
]
uni.showActionSheet({
itemList: items,
success: (res) => {
if (res.tapIndex === 0) store.setSession(c, { pinned: !c.pinned })
else if (res.tapIndex === 1) {
if (c.unread) {
c.unread = 0
store.emit()
} else store.markUnread(c)
} else if (res.tapIndex === 2) store.setSession(c, { muted: !c.muted })
else if (res.tapIndex === 3) {
uni.showModal({
title: '删除聊天',
content: '仅从消息列表移除,聊天记录仍保留在服务器。',
success: (r) => { if (r.confirm) store.hideConv(c) }
})
}
}
})
},
newGroup() {
this.menu = false
uni.navigateTo({ url: '/pages/chat/create' })
},
addMate() {
this.menu = false
uni.navigateTo({ url: '/pages/search/search' })
},
goSearch() {
uni.navigateTo({
url: '/pages/search/search',
animationType: 'slide-in-right',
animationDuration: 280
})
}
}
}
</script>
<style scoped>
.msg { display: flex; flex-direction: column; height: 100%; background: #F5F7FA; overflow: hidden; }
.finder {
margin: 8rpx 24rpx 12rpx;
height: 56rpx;
border-radius: 12rpx;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
color: #9CA3AF;
}
.finder-txt { margin-left: 8rpx; font-size: 26rpx; line-height: 1; }
.list { background: #F5F7FA; }
.ico.sm { font-size: 34rpx; color: #1677FF; }
.item { display: flex; align-items: center; padding: 22rpx 28rpx; background: #fff; }
.item.pin { background: #F7F8FA; }
.av-wrap { position: relative; width: 92rpx; height: 92rpx; flex-shrink: 0; }
.av { width: 92rpx; height: 92rpx; border-radius: 20rpx; overflow: hidden; }
.av.txt { display: flex; align-items: center; justify-content: center; font-size: 32rpx; font-weight: 600; }
.av.work { background: #1677FF; color: #fff; }
.av.file { background: #F3F6FB; color: #4B5563; }
.av.plain { background: #E8F1FF; color: #1677FF; }
.abs { position: absolute; top: -8rpx; right: -8rpx; }
.dotu { position: absolute; top: -4rpx; right: -4rpx; width: 16rpx; height: 16rpx; border-radius: 50%; background: #DC2626; }
.meta { flex: 1; margin-left: 20rpx; min-width: 0; padding: 8rpx 0 18rpx; border-bottom: 1rpx solid #F0F2F5; }
.top { display: flex; justify-content: space-between; align-items: center; }
.name { font-size: 30rpx; font-weight: 600; max-width: 420rpx; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.time { font-size: 22rpx; color: #9CA3AF; }
.bot { display: flex; align-items: center; margin-top: 8rpx; }
.prev { flex: 1; font-size: 24rpx; color: #6B7280; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mute { font-size: 22rpx; margin-left: 8rpx; color: #9CA3AF; }
.empty { padding: 160rpx 40rpx; text-align: center; color: #6B7280; font-size: 28rpx; background: transparent; }
.ico { font-size: 36rpx; color: #1677FF; padding: 8rpx 12rpx; }
.plus {
width: 56rpx;
height: 56rpx;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.plus-h, .plus-v {
position: absolute;
background: #111827;
border-radius: 2rpx;
transition: transform 180ms ease;
}
.plus-h { width: 28rpx; height: 3rpx; }
.plus-v { width: 3rpx; height: 28rpx; }
.mask { position: fixed; left: 0; right: 0; top: 0; bottom: 0; z-index: 30; background: rgba(17, 24, 39, 0.04); }
.drop {
position: fixed;
right: 16rpx;
z-index: 31;
width: auto;
padding: 8rpx;
background: #fff;
border-radius: 20rpx;
box-shadow: 0 12rpx 40rpx rgba(17, 24, 39, 0.1);
transform-origin: calc(100% - 28rpx) 0;
animation: drop-in 180ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.caret {
position: absolute;
top: -10rpx;
right: 28rpx;
width: 20rpx;
height: 20rpx;
background: #fff;
transform: rotate(45deg);
border-radius: 4rpx;
box-shadow: -2rpx -2rpx 6rpx rgba(17, 24, 39, 0.04);
}
.drop-item {
height: 80rpx;
padding: 0 20rpx 0 16rpx;
display: flex;
align-items: center;
gap: 12rpx;
font-size: 28rpx;
color: #111827;
border-radius: 14rpx;
white-space: nowrap;
}
.drop-item + .drop-item { margin-top: 4rpx; }
@keyframes drop-in {
from { opacity: 0; transform: translateY(-8rpx) scale(0.96); }
to { opacity: 1; transform: none; }
}
</style>