feat(staffchat): 页面骨架+左侧资料卡/5导航/折叠(三栏布局对齐云客)

- StaffChat.vue: 路由驱动 tab(single/group/moments/contacts/stats)+折叠态,
  三栏 flex(左栏240/中列280/右列flex:1,高calc(100vh-120px));资料卡走
  getAccounts(channel=wechat) 按 account_id 匹配,失败退 ownerId 兜底
- StaffChatSide.vue: 员工资料卡(头像/昵称/真名·电话/微信号)+竖排5导航+
  底部收起;墨玉主题沿用 ChatSidebar 手法(点阵底/玉绿渐变分隔线/药丸按钮)
- 5 占位组件(SessionPane/ChatMain/MomentsPane/ContactsPane/StatsPane),
  defineProps/defineEmits 签名按 Task 10-14 锁定
- relativeChatTime 签名可空友好(ms?: number|null,null→''),spec+1例

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
浅念
2026-08-25 17:03:19 +08:00
parent f2bcac0a4d
commit eb0388c8b0
9 changed files with 344 additions and 10 deletions
@@ -0,0 +1,13 @@
<!-- 占位组件: Task 11 实现聊天窗口(消息流+定位 msgid),签名锁定如下。 -->
<script setup lang="ts">
defineProps<{
ownerId: string
/** single=单聊(talkerId=客户 wxid), group=群聊(talkerId=room_id) */
kind: 'single' | 'group'
talkerId: string
}>()
</script>
<template>
<div class="yk-empty">聊天窗口 — Task 11 实现</div>
</template>
@@ -0,0 +1,8 @@
<!-- 占位组件: Task 13 实现通讯录视图,签名锁定如下。 -->
<script setup lang="ts">
defineProps<{ ownerId: string }>()
</script>
<template>
<div class="yk-empty">通讯录 — Task 13 实现</div>
</template>
@@ -0,0 +1,8 @@
<!-- 占位组件: Task 12 实现朋友圈视图,签名锁定如下。 -->
<script setup lang="ts">
defineProps<{ ownerId: string }>()
</script>
<template>
<div class="yk-empty">朋友圈 — Task 12 实现</div>
</template>
@@ -0,0 +1,18 @@
<!-- 占位组件: Task 10 实现会话列表(好友/群,含搜索+今日条数),签名锁定如下。 -->
<script setup lang="ts">
import type { Channel, ChatSession } from '@/api/types'
defineProps<{
channel: Channel
ownerId: string
/** single=好友会话, group=群会话 */
kind: 'single' | 'group'
/** 当前选中会话 id(talker_id 或 room_id) */
activeId: string | null
}>()
defineEmits<{ select: [session: ChatSession] }>()
</script>
<template>
<div class="yk-empty">会话列表 — Task 10 实现</div>
</template>
@@ -0,0 +1,149 @@
<script setup lang="ts">
import type { Account } from '@/api/types'
const props = defineProps<{ profile: Account | null; ownerIdFallback: string; tab: string }>()
const emit = defineEmits<{ select: [tab: string]; collapse: [] }>()
const NAVS = [
{ key: 'single', label: '单聊' }, { key: 'group', label: '群聊' }, { key: 'moments', label: '朋友圈' },
{ key: 'contacts', label: '通讯录' }, { key: 'stats', label: '统计分析' },
] as const
</script>
<template>
<div class="sc-side">
<!-- 员工资料卡: 头像+昵称 / 真名·电话 / 微信号 (对齐云客资料卡四要素) -->
<div class="sc-side__card">
<el-avatar :size="44" :src="props.profile?.avatar ?? undefined">
{{ (props.profile?.nickname || props.ownerIdFallback).slice(0, 1) }}
</el-avatar>
<div class="sc-side__who">
<div class="sc-side__nick">{{ props.profile?.nickname || props.ownerIdFallback }}</div>
<div v-if="props.profile?.user_name || props.profile?.phone" class="sc-side__meta">
{{ props.profile?.user_name }} {{ props.profile?.phone }}
</div>
<div class="sc-side__meta">{{ props.profile?.account_id ?? props.ownerIdFallback }}</div>
</div>
</div>
<nav class="sc-side__nav">
<button
v-for="n in NAVS"
:key="n.key"
class="sc-side__navbtn"
:class="{ 'is-active': props.tab === n.key }"
@click="emit('select', n.key)"
>{{ n.label }}</button>
</nav>
<button class="sc-side__collapse" @click="emit('collapse')">收起侧边栏</button>
</div>
</template>
<style scoped>
/* ══ 员工聊天窗口左栏(墨玉主题,手法沿用 ChatSidebar) ══
点阵底 + 玉绿渐变分隔线 + 药丸按钮;card 顶 / nav 中 / collapse 贴底。 */
.sc-side {
display: flex;
flex-direction: column;
width: 240px;
flex: none;
position: relative;
background:
radial-gradient(circle at 1px 1px, rgba(23,29,43,.03) 1px, transparent 0) 0 0/18px 18px,
linear-gradient(180deg, #fafbfd 0%, #f6f8fa 100%);
}
.sc-side::after {
content: '';
position: absolute;
top: 0; right: 0; bottom: 0;
width: 1px;
background: linear-gradient(180deg, transparent 0%, rgba(13,159,110,.12) 20%, rgba(23,29,43,.06) 100%);
pointer-events: none;
}
/* ── 员工资料卡(毛玻璃名片) ── */
.sc-side__card {
display: flex;
align-items: center;
gap: 12px;
margin: 12px;
padding: 12px;
background: rgba(255,255,255,.85);
backdrop-filter: blur(8px);
border: 1px solid rgba(23,29,43,.05);
border-radius: 12px;
box-shadow: 0 1px 3px rgba(23,29,43,.05);
}
.sc-side__card .el-avatar {
flex: none;
border-radius: 12px;
border: 1px solid rgba(23,29,43,.05);
box-shadow: 0 1px 3px rgba(23,29,43,.06);
font-weight: 600;
}
.sc-side__who { min-width: 0; }
.sc-side__nick {
font-size: 15px;
font-weight: 600;
color: #2b3240;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sc-side__meta {
font-size: 11.5px;
color: #8a94a4;
margin-top: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* ── 竖排导航(玉绿茶丸) ── */
.sc-side__nav {
flex: 1;
display: flex;
flex-direction: column;
gap: 6px;
padding: 12px 10px;
overflow-y: auto;
}
.sc-side__navbtn {
padding: 10px 16px;
font-size: 13.5px;
text-align: left;
color: #5a6472;
background: rgba(255,255,255,.7);
border: 1px solid rgba(23,29,43,.06);
border-radius: 999px;
cursor: pointer;
transition: all 0.2s cubic-bezier(.22,.8,.36,1);
}
.sc-side__navbtn:hover:not(.is-active) {
color: #2b3240;
background: #fff;
box-shadow: 0 1px 4px rgba(23,29,43,.06);
}
.sc-side__navbtn.is-active {
color: var(--el-color-primary);
background: linear-gradient(135deg, #fff 0%, #e8f7ef 100%);
border-color: rgba(13,159,110,.25);
font-weight: 600;
box-shadow: 0 2px 8px -2px rgba(13,159,110,.25);
}
/* ── 底部收起 ── */
.sc-side__collapse {
margin: 10px 12px 12px;
padding: 8px;
font-size: 12px;
color: #8a94a4;
background: rgba(23,29,43,.04);
border: none;
border-radius: 999px;
cursor: pointer;
transition: all 0.2s ease;
}
.sc-side__collapse:hover {
color: #5a6472;
background: rgba(23,29,43,.07);
}
</style>
@@ -0,0 +1,8 @@
<!-- 占位组件: Task 14 实现统计分析视图,签名锁定如下。 -->
<script setup lang="ts">
defineProps<{ ownerId: string }>()
</script>
<template>
<div class="yk-empty">统计分析 — Task 14 实现</div>
</template>