Files
yunzerwebsiteallinone/uniapp/pages/oa/contact/index.vue
T

364 lines
7.6 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">
<view class="toolbar">
<u-search
v-model="keyword"
placeholder="搜索姓名、手机号、邮箱"
:show-action="false"
shape="round"
bg-color="#f1f5f9"
color="#0f172a"
@search="onSearch"
@clear="clearSearch"
/>
<u-tabs
:list="TYPE_TABS"
:current="typeIndex"
:scrollable="false"
line-color="#2563eb"
:line-width="40"
:line-height="6"
:active-style="{ color: '#2563eb', fontWeight: '600' }"
:inactive-style="{ color: '#475569' }"
:item-style="{ height: '84rpx' }"
@change="onTabChange"
/>
<u-dropdown active-color="#2563eb" :height="84">
<u-dropdown-item
v-model="selectedOrgId"
title="部门"
:options="deptOptions"
height="520rpx"
@change="onDeptChange"
/>
</u-dropdown>
</view>
<scroll-view class="list-scroll" scroll-y :show-scrollbar="false" @scrolltolower="loadMore">
<view v-if="loading" class="state-box">
<text class="state-text">加载中...</text>
</view>
<u-empty
v-else-if="list.length === 0"
mode="list"
:icon-size="80"
:text="keyword ? '未找到相关联系人' : '暂无联系人'"
/>
<block v-else>
<view class="contact-list">
<view v-for="item in list" :key="item.id" class="contact-card" @tap="openDetail(item.id)">
<u-avatar
:src="item.avatar"
:text="firstChar(item.contact_name)"
:size="48"
bg-color="#2563eb"
font-size="18"
/>
<view class="contact-body">
<view class="contact-head">
<text class="contact-name">{{ item.contact_name }}</text>
<text class="type-tag" :class="item.contact_type === 1 ? 'inner' : 'outer'">
{{ getContactTypeLabel(item.contact_type) }}
</text>
</view>
<text class="contact-sub">{{ subLine(item) }}</text>
<text v-if="primaryPhone(item)" class="contact-phone">{{ primaryPhone(item) }}</text>
</view>
<view v-if="primaryPhone(item)" class="call-btn" @tap.stop="call(item)">
<u-icon name="phone-fill" color="#ffffff" size="16" />
</view>
</view>
</view>
<view v-if="hasMore" class="more-tip" @tap="loadMore">
<text>{{ loadingMore ? '加载中...' : '加载更多' }}</text>
</view>
<view v-else class="more-tip end">
<text>已加载全部 {{ total }} 位联系人</text>
</view>
</block>
<view class="bottom-space" />
</scroll-view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import {
fetchContacts,
fetchContactOrgTree,
getContactTypeLabel,
primaryPhone
} from '@/api/contact.js'
import { isLoggedIn } from '@/utils/auth.js'
/** 顶部大类 tab:全部 / 内部 / 外部,索引与 contact_type 对应(0 表示不筛选) */
const TYPE_TABS = [{ name: '全部' }, { name: '内部' }, { name: '外部' }]
const TYPE_VALUES = [0, 1, 2]
const keyword = ref('')
const orgs = ref([])
const selectedOrgId = ref(0)
const typeIndex = ref(0)
const contactType = ref(0)
const list = ref([])
const total = ref(0)
const page = ref(1)
const pageSize = 20
const loading = ref(false)
const loadingMore = ref(false)
const hasMore = ref(false)
onMounted(() => {
if (!isLoggedIn()) {
uni.reLaunch({ url: '/pages/login/login' })
return
}
loadOrgTree()
loadContacts(true)
})
/** 部门下拉选项:全部 + 有内部联系人的部门(带人数) */
const deptOptions = computed(() => {
const options = [{ label: '全部部门', value: 0 }]
orgs.value.forEach((org) => {
const count = org.contact_count ? `(${org.contact_count})` : ''
options.push({ label: `${org.org_name}${count}`, value: org.id })
})
return options
})
/** 组织树拍平为部门列表,只保留有内部联系人的部门 */
function flattenOrgs(nodes, out) {
;(nodes || []).forEach((n) => {
if (Number(n.contact_count) > 0) {
out.push({ id: n.id, org_name: n.org_name, contact_count: n.contact_count })
}
if (n.children && n.children.length) {
flattenOrgs(n.children, out)
}
})
return out
}
async function loadOrgTree() {
try {
const tree = await fetchContactOrgTree()
orgs.value = flattenOrgs(Array.isArray(tree) ? tree : [], [])
} catch (e) {
orgs.value = []
}
}
async function loadContacts(reset) {
if (reset) {
page.value = 1
loading.value = true
} else {
loadingMore.value = true
}
try {
const res = await fetchContacts({
keyword: keyword.value,
orgId: selectedOrgId.value,
contactType: contactType.value,
page: page.value,
pageSize
})
list.value = reset ? res.list : list.value.concat(res.list)
total.value = res.total
hasMore.value = list.value.length < res.total
} catch (e) {
if (reset) list.value = []
} finally {
loading.value = false
loadingMore.value = false
}
}
function loadMore() {
if (!hasMore.value || loading.value || loadingMore.value) return
page.value += 1
loadContacts(false)
}
function onSearch() {
loadContacts(true)
}
function clearSearch() {
keyword.value = ''
loadContacts(true)
}
function onTabChange(item, index) {
typeIndex.value = index
contactType.value = TYPE_VALUES[index] || 0
loadContacts(true)
}
function onDeptChange() {
loadContacts(true)
}
function subLine(item) {
const parts = []
if (item.position_title) parts.push(item.position_title)
const dept = item.org_name || item.dept_name
if (dept) parts.push(dept)
return parts.length ? parts.join(' · ') : '暂无职位信息'
}
function firstChar(name) {
return String(name || '?').charAt(0)
}
function call(item) {
const phoneNumber = primaryPhone(item)
if (!phoneNumber) return
uni.makePhoneCall({ phoneNumber })
}
function openDetail(id) {
uni.navigateTo({ url: `/pages/oa/contact/detail?id=${id}` })
}
</script>
<style lang="scss" scoped>
@import '@/src/styles/page-common.scss';
.page {
min-height: 100vh;
background: $color-bg-page;
display: flex;
flex-direction: column;
}
.toolbar {
position: relative;
z-index: 10;
flex-shrink: 0;
padding: 20rpx $page-padding-x 0;
background: $color-card;
box-shadow: $shadow-header;
}
.list-scroll {
flex: 1;
height: 0;
}
.contact-list {
padding: 24rpx $page-padding-x 0;
}
.contact-card {
@include card;
display: flex;
align-items: center;
gap: 22rpx;
padding: 24rpx;
margin-bottom: 18rpx;
&:active {
background: $color-bg-page;
}
}
.contact-body {
flex: 1;
min-width: 0;
}
.contact-head {
display: flex;
align-items: center;
gap: 12rpx;
}
.contact-name {
max-width: 60%;
font-size: 31rpx;
font-weight: 600;
color: $color-text;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.type-tag {
flex-shrink: 0;
padding: 2rpx 12rpx;
font-size: 20rpx;
border-radius: $radius-sm;
color: $color-text-muted;
background: $color-bg-page;
&.inner {
color: $color-primary;
background: $color-primary-bg;
}
}
.contact-sub {
display: block;
margin-top: 8rpx;
font-size: 25rpx;
color: $color-text-secondary;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.contact-phone {
display: block;
margin-top: 6rpx;
font-size: 24rpx;
color: $color-text-muted;
}
.call-btn {
flex-shrink: 0;
width: 72rpx;
height: 72rpx;
border-radius: 50%;
background: $color-primary;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 6rpx 16rpx rgba(37, 99, 235, 0.28);
&:active {
opacity: 0.85;
}
}
.more-tip {
padding: 12rpx 0 4rpx;
text-align: center;
font-size: 24rpx;
color: $color-primary;
&.end {
color: $color-text-muted;
}
}
.state-box {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 160rpx 48rpx;
gap: 20rpx;
}
.state-text {
font-size: 26rpx;
color: $color-text-muted;
text-align: center;
}
.bottom-space {
height: 40rpx;
}
</style>