Files
oa_app/pages/tabs/contacts.vue
T
2026-09-23 17:38:24 +08:00

158 lines
6.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="page">
<fy-header title="通讯录">
<template #right>
<text class="ico" @click="goGroup">建群</text>
</template>
</fy-header>
<view class="search">
<input v-model="q" placeholder="搜索同事 / 部门 / 手机号" />
</view>
<view class="org" @click="goGroups">
<view class="obox">👥</view>
<view class="ometa">
<text class="on">群聊</text>
<text class="os">{{ groupCount }} 个群</text>
</view>
<text class="arr"></text>
</view>
<view class="org" @click="goSearch">
<view class="obox">🏛</view>
<view class="ometa">
<text class="on">全员架构</text>
<text class="os"> {{ deptCount }} 个部门 · {{ peopleCount }} 位在职成员</text>
</view>
<text class="arr"></text>
</view>
<view v-for="g in groups" :key="g.name" class="block">
<view class="dept" @click="toggle(g.name)">
<text>{{ g.name }}</text>
<text class="cnt">({{ g.people.length }})</text>
</view>
<view v-if="open[g.name] !== false" class="card">
<view v-for="p in g.people" :key="p.id" class="item" @click="openPerson(p)">
<view class="av-wrap">
<image v-if="p.avatar" class="av" :src="p.avatar" mode="aspectFill" />
<view v-else class="av txt">{{ p.name.slice(0, 1) }}</view>
<view v-if="p.online" class="dot" />
</view>
<view class="meta">
<text class="name">{{ p.name }}</text>
<text class="sub">{{ p.online ? '在线' : '离线' }} · {{ p.title }}</text>
</view>
<text class="chip">{{ p.title }}</text>
<text class="mini" @click.stop="chat(p)">💬</text>
<text class="mini" @click.stop="call(p)"></text>
</view>
</view>
</view>
<view v-if="!groups.length" class="empty">没有匹配的同事</view>
<text class="foot">仅显示当前所属企业成员 · 数据实时同步</text>
</view>
</template>
<script>
import FyHeader from '../../components/fy-header.vue'
import { store } from '../../utils/store.js'
import { toast } from '../../utils/format.js'
export default {
components: { FyHeader },
data() {
return { q: '', open: {}, off: null }
},
computed: {
peopleCount() {
return Object.keys(store.people).length
},
deptCount() {
return new Set(Object.values(store.people).map((p) => p.department || '未分组')).size
},
groupCount() {
return store.conversations.filter((c) => c.type === 'group').length
},
groups() {
const q = this.q.trim()
const list = Object.values(store.people).filter((p) => {
if (store.me && p.id === store.me.id) return false
if (!q) return true
return (p.name + p.department + p.phone + p.title).includes(q)
})
const map = {}
list.forEach((p) => {
const d = p.department || '未分组'
if (!map[d]) map[d] = []
map[d].push(p)
})
return Object.keys(map).sort().map((name) => ({ name, people: map[name] }))
}
},
onShow() {
if (store.phase !== 'app') uni.reLaunch({ url: '/pages/login/login' })
if (this.off) this.off()
store.refreshPeoplePhotos()
this.off = store.on(() => this.$forceUpdate())
},
onHide() { if (this.off) this.off() },
methods: {
toggle(name) {
this.open[name] = this.open[name] === false
},
openPerson(p) {
uni.navigateTo({ url: '/pages/contact/detail?id=' + encodeURIComponent(p.id) })
},
chat(p) {
const conv = store.openDirect(p)
uni.navigateTo({ url: '/pages/chat/chat?sid=' + encodeURIComponent(conv.id) })
},
call(p) {
if (!p.phone) return toast('该同事未登记手机号')
uni.makePhoneCall({ phoneNumber: String(p.phone) })
},
goSearch() { uni.navigateTo({ url: '/pages/search/search' }) },
goGroup() { uni.navigateTo({ url: '/pages/chat/create' }) },
goGroups() {
const groups = store.conversations.filter((c) => c.type === 'group')
if (!groups.length) {
uni.navigateTo({ url: '/pages/chat/create' })
return
}
uni.showActionSheet({
itemList: groups.slice(0, 6).map((c) => c.name || '群聊'),
success: (res) => {
const c = groups[res.tapIndex]
if (c) uni.navigateTo({ url: '/pages/chat/chat?sid=' + encodeURIComponent(c.id) })
}
})
}
}
}
</script>
<style scoped>
.search { padding: 16rpx 24rpx; }
.search input { height: 80rpx; background: #fff; border-radius: 20rpx; padding: 0 28rpx; font-size: 28rpx; border: 1rpx solid rgba(17,24,39,0.06); }
.org { margin: 0 24rpx 16rpx; background: #fff; border-radius: 16rpx; padding: 20rpx; display: flex; align-items: center; }
.obox { width: 64rpx; height: 64rpx; border-radius: 12rpx; background: #E8ECF2; display: flex; align-items: center; justify-content: center; }
.ometa { flex: 1; margin-left: 16rpx; }
.on { display: block; font-size: 28rpx; font-weight: 700; }
.os { font-size: 22rpx; color: #6B7280; }
.arr { color: #D1D5DB; }
.block { margin: 8rpx 24rpx 24rpx; }
.dept { display: flex; align-items: center; padding: 16rpx 12rpx; font-size: 26rpx; font-weight: 600; background: #F3F6FB; border-radius: 12rpx 12rpx 0 0; }
.cnt { color: #6B7280; font-weight: 400; font-size: 22rpx; margin-left: 8rpx; }
.item { display: flex; align-items: center; padding: 20rpx 20rpx; }
.av-wrap { position: relative; width: 72rpx; height: 72rpx; }
.av { width: 72rpx; height: 72rpx; border-radius: 50%; }
.av.txt { background: #E8ECF2; color: #4B5563; display: flex; align-items: center; justify-content: center; font-weight: 600; }
.dot { position: absolute; right: 0; bottom: 0; width: 16rpx; height: 16rpx; border-radius: 50%; background: #006c49; border: 2rpx solid #fff; }
.meta { flex: 1; margin-left: 16rpx; min-width: 0; }
.name { display: block; font-size: 28rpx; font-weight: 600; }
.sub { font-size: 22rpx; color: #6B7280; }
.chip { font-size: 20rpx; color: #1677FF; background: #E8F1FF; padding: 4rpx 12rpx; border-radius: 999rpx; margin-right: 8rpx; }
.mini { width: 56rpx; text-align: center; font-size: 28rpx; color: #1677FF; }
.empty { text-align: center; color: #6B7280; padding: 80rpx; }
.foot { display: block; text-align: center; color: #6B7280; font-size: 22rpx; padding: 20rpx 0 40rpx; }
.ico { padding: 8rpx 16rpx; color: #1677FF; font-size: 26rpx; }
</style>