Files
wecom_it_smart_desk/frontend-h5/src/components/chat/ContactCard.vue
T

355 lines
10 KiB
Vue
Raw Normal View History

<!-- =============================================================================
// 企微IT智能服务台 — H5员工名片卡片组件
// =============================================================================
// 说明:当员工提出非IT业务问题时,AI 自动推送此名片卡片。
// 员工点击「联系TA」按钮可跳转到与联系人的企微单聊。
//
// 布局参考原型:docs/04-原型设计/prototypes-原型图/employee-contact-card-demo.html
// - 头像(44px) / 姓名+性别 / 部门·岗位 / 负责业务(绿色高亮#07C160)
// - 分机号 / 服务区域 / 「员工名片」标识 / 「联系TA」按钮
//
// 交互:
// - 点击「联系TA」→ wx.invoke('openEnterpriseChat', {userids: contact.wecom_userid})
// - userid 为空或 SDK 未就绪 → Toast 提示
// ============================================================================= -->
<template>
<div class="contact-card">
<!-- 卡片头部头像 + 姓名 + 部门岗位 + 名片标识 -->
<div class="contact-card__header">
<!-- 头像有URL用图片无URL用姓名首字渐变色块 -->
<div class="contact-card__avatar" :class="avatarGradient">
<img v-if="contact.avatar_url" :src="contact.avatar_url" alt="" class="contact-card__avatar-img" />
<span v-else>{{ avatarText }}</span>
</div>
<!-- 姓名 + 部门岗位 -->
<div class="contact-card__name-row">
<div class="contact-card__name">
{{ contact.name }}
<span v-if="contact.gender" class="contact-card__gender" :class="genderClass">{{ genderIcon }}</span>
</div>
<div class="contact-card__dept">{{ contact.department }} · {{ contact.position }}</div>
</div>
<!-- 名片标识kfid 服务模式显示服务窗口 -->
<span class="contact-card__badge">{{ contact.service_url ? '服务窗口' : '员工名片' }}</span>
</div>
<!-- 卡片信息区负责业务/服务描述(绿色高亮) / 分机号 / 服务区域 -->
<div class="contact-card__body">
<div class="contact-card__info-item">
<span class="contact-card__info-label">{{ contact.service_url ? '服务内容' : '负责业务' }}</span>
<span class="contact-card__info-value contact-card__info-value--highlight">
{{ contact.responsibility }}
</span>
</div>
<div v-if="contact.extension" class="contact-card__info-item">
<span class="contact-card__info-label">分机号</span>
<span class="contact-card__info-value">{{ contact.extension }}</span>
</div>
<div v-if="contact.service_area" class="contact-card__info-item">
<span class="contact-card__info-label">{{ serviceAreaLabel }}</span>
<span class="contact-card__info-value">{{ contact.service_area }}</span>
</div>
</div>
<!-- 卡片底部联系TA按钮 -->
<div class="contact-card__footer">
<button class="contact-card__action-btn" @click="handleContact">
<span class="contact-card__action-icon">💬</span>
<span>{{ contact.service_url ? '立即咨询' : '联系TA' }}</span>
</button>
</div>
</div>
</template>
<script setup lang="ts">
/**
* ContactCard 员工名片卡片组件
*
* 接收联系人信息,渲染名片卡片。
* 点击「联系TA」调用企微 JS-SDK 的 openEnterpriseChat 打开单聊。
*/
import { computed } from 'vue'
import { showToast } from 'vant'
/** 联系人信息接口(与后端 extra_data.contact 结构一致) */
export interface ContactInfo {
id: number
name: string
gender: string
department: string
position: string
responsibility: string
extension: string
service_area: string
wecom_userid: string
avatar_url: string
business_category: string
/** v4.0 kfid 服务模式:企微客服窗口链接(存在时点击打开链接而非 openEnterpriseChat */
service_url?: string
}
const props = defineProps<{
/** 联系人信息 */
contact: ContactInfo
}>()
// ============================================================================
// 计算属性
// ============================================================================
/** 头像文字(姓名首字) */
const avatarText = computed(() => {
return props.contact.name ? props.contact.name.charAt(0) : '?'
})
/** 头像渐变色块样式类(根据姓名首字 hash 分配不同渐变) */
const avatarGradient = computed(() => {
const gradients = [
'gradient-1', 'gradient-2', 'gradient-3', 'gradient-4', 'gradient-5',
]
if (!props.contact.name) return gradients[0]
const charCode = props.contact.name.charCodeAt(0)
return gradients[charCode % gradients.length]
})
/** 性别图标 */
const genderIcon = computed(() => {
return props.contact.gender === 'female' ? '♀' : '♂'
})
/** 性别样式类 */
const genderClass = computed(() => {
return props.contact.gender === 'female' ? 'gender-female' : 'gender-male'
})
/** 服务区域标签文案(物业类用「办公地点」,其他用「服务区域」) */
const serviceAreaLabel = computed(() => {
return props.contact.business_category === '行政-物业' ? '办公地点' : '服务区域'
})
// ============================================================================
// 交互方法
// ============================================================================
/**
* 点击「联系TA」按钮,调用企微 JS-SDK 打开单聊。
*
* 容错处理:
* - wecom_userid 为空 → Toast 提示
* - wx SDK 未就绪 → Toast 提示
* - openEnterpriseChat 调用失败 → Toast 提示
*/
function handleContact(): void {
// v4.0 kfid 服务模式:有 service_url 时直接打开企微客服会话链接
if (props.contact.service_url) {
// 企微 H5 环境:window.open 打开 kfid 链接会唤起客服会话窗口
window.open(props.contact.service_url, '_blank')
return
}
const userid = props.contact.wecom_userid
// 1. 检查 userid 是否有效
if (!userid) {
showToast('暂时无法发起聊天,请联系管理员')
return
}
// 2. 检查企微 JS-SDK 是否可用
const wx = (window as any).wx
if (!wx || !wx.invoke) {
showToast('暂时无法发起聊天,请联系管理员')
return
}
// 3. 调用 openEnterpriseChat 打开与联系人的企微单聊
try {
wx.invoke(
'openEnterpriseChat',
{
userids: userid,
chatTitle: props.contact.name,
},
(res: any) => {
if (res && res.err_msg && res.err_msg !== 'openEnterpriseChat:ok') {
console.error('[ContactCard] openEnterpriseChat 失败:', res.err_msg)
showToast('暂时无法发起聊天,请联系管理员')
}
}
)
} catch (err) {
console.error('[ContactCard] openEnterpriseChat 调用异常:', err)
showToast('暂时无法发起聊天,请联系管理员')
}
}
</script>
<style scoped>
/* ============================================================================
// 名片卡片容器
// ============================================================================ */
.contact-card {
width: 260px;
background: #fff;
border-radius: 8px;
overflow: hidden;
border-top-left-radius: 2px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
}
/* ============================================================================
// 卡片头部
// ============================================================================ */
.contact-card__header {
padding: 14px 16px 10px;
display: flex;
align-items: center;
gap: 10px;
border-bottom: 1px solid #f0f0f0;
}
/* 头像 */
.contact-card__avatar {
width: 44px;
height: 44px;
border-radius: 6px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
color: #fff;
font-weight: 600;
overflow: hidden;
}
.contact-card__avatar-img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 6px;
}
/* 头像渐变色块(无头像时用姓名首字) */
.gradient-1 { background: linear-gradient(135deg, #667eea, #764ba2); }
.gradient-2 { background: linear-gradient(135deg, #f093fb, #f5576c); }
.gradient-3 { background: linear-gradient(135deg, #4facfe, #00f2fe); }
.gradient-4 { background: linear-gradient(135deg, #43e97b, #38f9d7); }
.gradient-5 { background: linear-gradient(135deg, #fa709a, #fee140); }
/* 姓名行 */
.contact-card__name-row {
flex: 1;
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.contact-card__name {
font-size: 16px;
font-weight: 600;
color: #1a1a1a;
display: flex;
align-items: center;
gap: 6px;
}
.contact-card__gender {
font-size: 12px;
}
.gender-male { color: #4facfe; }
.gender-female { color: #f5576c; }
.contact-card__dept {
font-size: 13px;
color: #888;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 名片标识 */
.contact-card__badge {
display: inline-block;
font-size: 11px;
padding: 1px 6px;
border-radius: 3px;
background: #f0f0f0;
color: #888;
flex-shrink: 0;
margin-left: auto;
}
/* ============================================================================
// 卡片信息区
// ============================================================================ */
.contact-card__body {
padding: 10px 16px;
}
.contact-card__info-item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
font-size: 13px;
color: #555;
}
.contact-card__info-label {
color: #aaa;
width: 56px;
flex-shrink: 0;
}
.contact-card__info-value {
color: #333;
word-break: break-all;
}
/* 负责业务:绿色高亮 #07C160 */
.contact-card__info-value--highlight {
color: #07C160;
font-weight: 500;
}
/* ============================================================================
// 卡片底部按钮
// ============================================================================ */
.contact-card__footer {
padding: 0 16px 12px;
}
.contact-card__action-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
width: 100%;
height: 36px;
background: #07C160;
color: #fff;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: opacity 0.2s;
padding: 0;
}
.contact-card__action-btn:active {
opacity: 0.85;
}
.contact-card__action-icon {
font-size: 16px;
}
</style>