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
+131 -9
View File
@@ -1,14 +1,136 @@
<!-- 占位组件: Task 8 只加路由, 五视图实现在后续任务替换本文件。 -->
<!-- 员工聊天窗口骨架(对齐云客 wechat-window): 左栏资料卡+导航 / 中列会话 / 右列内容。
各 pane 由 Task 10-14 逐个替换占位实现。 -->
<script setup lang="ts">
// Route props (/staff-chat/:ownerId/:tab?/:talkerId?) — see router/index.ts.
// tab=group 时 talkerId 语义为 roomId; ?msgid= 定位消息(红包转账明细跳转)。
const props = defineProps<{
ownerId: string
tab?: string
talkerId?: string
}>()
import { computed, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { getAccounts } from '@/api'
import type { Account } from '@/api/types'
import StaffChatSide from '@/components/staffchat/StaffChatSide.vue'
import SessionPane from '@/components/staffchat/SessionPane.vue'
import ChatMain from '@/components/staffchat/ChatMain.vue'
import MomentsPane from '@/components/staffchat/MomentsPane.vue'
import ContactsPane from '@/components/staffchat/ContactsPane.vue'
import StatsPane from '@/components/staffchat/StatsPane.vue'
const props = defineProps<{ ownerId: string; tab?: string; talkerId?: string }>()
const route = useRoute()
const router = useRouter()
const VALID = ['single', 'group', 'moments', 'contacts', 'stats']
// 注: 路由 regex 已过滤非法 tab(BOGUS 会进 talkerId 位而非 tab 位,3 段非法 URL 被 catch-all 弹走)
// 这里 computed 兜底 undefined/'' → single
const tab = computed(() => (props.tab && VALID.includes(props.tab) ? props.tab : 'single'))
const profile = ref<Account | null>(null)
const collapsed = ref(false)
async function loadProfile() {
try {
const res = await getAccounts({ channel: 'wechat', page: 1, page_size: 100 })
profile.value = res.items.find(a => a.account_id === props.ownerId) ?? null
} catch {
// 后端未起/请求失败: 资料卡退化为 ownerId 兜底显示
profile.value = null
}
}
watch(() => props.ownerId, loadProfile, { immediate: true })
function onSelectTab(t: string) {
router.push({ name: 'staff-chat', params: { ownerId: props.ownerId, tab: t } })
}
function onSelectSession(s: { type: 'friend' | 'group'; talker_id?: string | null; room_id?: string | null }) {
router.push({
name: 'staff-chat',
params: {
ownerId: props.ownerId,
tab: s.type === 'group' ? 'group' : 'single',
talkerId: s.talker_id ?? s.room_id ?? '',
},
query: route.query.msgid ? { msgid: String(route.query.msgid) } : {},
})
}
const chatMode = computed(() => tab.value === 'single' || tab.value === 'group')
const paneKind = computed<'single' | 'group'>(() => (tab.value === 'group' ? 'group' : 'single'))
</script>
<template>
<div data-staff-chat :data-owner-id="props.ownerId" />
<div class="sc-root">
<button v-if="collapsed" class="sc-root__expand" title="展开侧边栏" @click="collapsed = false">
展开侧边栏
</button>
<StaffChatSide
v-else
:profile="profile"
:owner-id-fallback="props.ownerId"
:tab="tab"
@select="onSelectTab"
@collapse="collapsed = true"
/>
<template v-if="chatMode">
<SessionPane
class="sc-root__sessions"
channel="wechat"
:owner-id="props.ownerId"
:kind="paneKind"
:active-id="props.talkerId || null"
@select="onSelectSession"
/>
<ChatMain
class="sc-root__chat"
:owner-id="props.ownerId"
:kind="paneKind"
:talker-id="props.talkerId ?? ''"
/>
</template>
<MomentsPane v-else-if="tab === 'moments'" class="sc-root__pane" :owner-id="props.ownerId" />
<ContactsPane v-else-if="tab === 'contacts'" class="sc-root__pane" :owner-id="props.ownerId" />
<StatsPane v-else-if="tab === 'stats'" class="sc-root__pane" :owner-id="props.ownerId" />
</div>
</template>
<style scoped>
/* 三栏 flex: 左栏 240px(StaffChatSide 自带) / 中列 280px / 右列 flex:1。
高度预留 app header,对齐 ChatTimeline 的 calc(100vh - 120px)。 */
.sc-root {
display: flex;
gap: 12px;
height: calc(100vh - 120px);
min-height: 400px;
}
/* 折叠态竖条展开按钮 */
.sc-root__expand {
flex: none;
width: 28px;
border: 1px solid rgba(23,29,43,.06);
border-radius: 8px;
background: rgba(255,255,255,.85);
backdrop-filter: blur(8px);
color: #5a6472;
font-size: 12px;
letter-spacing: 0.2em;
writing-mode: vertical-lr;
cursor: pointer;
transition: all 0.2s ease;
}
.sc-root__expand:hover {
color: var(--el-color-primary);
border-color: rgba(13,159,110,.25);
box-shadow: 0 2px 8px -2px rgba(13,159,110,.25);
}
/* 中列(会话列表)与右列(内容) — pane 组件自带内部样式,这里只定占位 */
.sc-root__sessions {
width: 280px;
flex: none;
min-width: 0;
}
.sc-root__chat {
flex: 1;
min-width: 0;
}
.sc-root__pane {
flex: 1;
min-width: 0;
overflow-y: auto;
}
</style>