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

355 lines
10 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.
<!-- =============================================================================
// 企微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>