feat(staffchat): 会话中列(双模式搜索+已删除好友tab+置顶区+相对时间,样式沿用墨玉主题)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -62,7 +62,7 @@ watch(() => [props.ownerId, props.channel] as const, load, { immediate: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="yk-chat-side">
|
||||
<div class="yk-chat-side yk-panel-bg yk-panel-sep">
|
||||
<div class="yk-chat-side__top">
|
||||
<div class="yk-chat-side__tabs">
|
||||
<button
|
||||
@@ -109,18 +109,7 @@ watch(() => [props.ownerId, props.channel] as const, load, { immediate: true })
|
||||
height: 100%;
|
||||
width: 280px;
|
||||
border-right: none;
|
||||
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%);
|
||||
position: relative;
|
||||
}
|
||||
.yk-chat-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;
|
||||
/* 点阵底+右缘玉绿线: 全局 yk-panel-bg / yk-panel-sep(style.css) */
|
||||
}
|
||||
|
||||
/* ── 顶部区(tab + 搜索) ── */
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
<!-- 占位组件: Task 10 实现会话列表(好友/群,含搜索+今日条数),签名锁定如下。 -->
|
||||
<!-- 员工聊天窗口中列会话(对齐云客 wechat-window): 好友/群|聊天记录双模式 +
|
||||
聊天好友|已删除好友 tab + 置顶区(折叠条) + 今日N红badge + 相对时间。
|
||||
根元素必须保持单 div——父组件 StaffChat 靠 attrs fallthrough 挂 sc-root__sessions 列宽。 -->
|
||||
<script setup lang="ts">
|
||||
import type { Channel, ChatSession } from '@/api/types'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { ArrowDown } from '@element-plus/icons-vue'
|
||||
import { getMessages, getSessions } from '@/api'
|
||||
import type { ChatSession, Channel } from '@/api/types'
|
||||
import { relativeChatTime } from '@/utils/format'
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
channel: Channel
|
||||
ownerId: string
|
||||
/** single=好友会话, group=群会话 */
|
||||
@@ -10,9 +16,393 @@ defineProps<{
|
||||
/** 当前选中会话 id(talker_id 或 room_id) */
|
||||
activeId: string | null
|
||||
}>()
|
||||
defineEmits<{ select: [session: ChatSession] }>()
|
||||
const emit = defineEmits<{ select: [session: ChatSession] }>()
|
||||
|
||||
/** 顶部下拉双模式: list=好友/群(会话列表), search=聊天记录(全文搜索聚合)。 */
|
||||
const mode = ref<'list' | 'search'>('list')
|
||||
/** 单聊专属 tab: 聊天好友(customer.delete_flag=0) | 已删除好友(=1)。群聊无此行。 */
|
||||
const friendTab = ref<'live' | 'deleted'>('live')
|
||||
const keyword = ref('')
|
||||
const sessions = ref<ChatSession[]>([])
|
||||
const loading = ref(false)
|
||||
const errorMsg = ref<string | null>(null)
|
||||
const pinnedOpen = ref(true)
|
||||
/** search 模式聚合结果(q 命中的消息按会话去重,取首条作为该会话快照)。 */
|
||||
const searchResults = ref<ChatSession[]>([])
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
errorMsg.value = null
|
||||
searchResults.value = []
|
||||
try {
|
||||
const res = await getSessions({
|
||||
owner_account_id: props.ownerId,
|
||||
channel: props.channel,
|
||||
...(props.kind === 'single' ? { is_del: friendTab.value === 'deleted' ? 1 : 0 } : {}),
|
||||
})
|
||||
sessions.value = res.sessions
|
||||
} catch (e) {
|
||||
errorMsg.value = (e as Error).message
|
||||
sessions.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
watch(() => [props.ownerId, props.kind, friendTab.value] as const, load, { immediate: true })
|
||||
|
||||
/** 切模式清关键词与旧结果,避免 list 过滤词被误当搜索词。 */
|
||||
watch(mode, () => {
|
||||
keyword.value = ''
|
||||
searchResults.value = []
|
||||
})
|
||||
/** search 模式清空关键词 → 回到未搜索态(不残留上次聚合结果)。 */
|
||||
watch(keyword, (k) => {
|
||||
if (mode.value === 'search' && !k.trim()) searchResults.value = []
|
||||
})
|
||||
|
||||
/** 置顶区: 仅 单聊+未删好友+list 模式(后端 pinned 目前只有 filehelper)。 */
|
||||
const pinned = computed(() =>
|
||||
mode.value === 'list' && props.kind === 'single' && friendTab.value === 'live'
|
||||
? sessions.value.filter(s => s.pinned)
|
||||
: [],
|
||||
)
|
||||
|
||||
/** 主列表: list=会话(昵称/微信号前端过滤,置顶剔除), search=聚合结果(不再过滤关键词)。 */
|
||||
const visible = computed(() => {
|
||||
let list = mode.value === 'list' ? sessions.value : searchResults.value
|
||||
list = list.filter(s => (props.kind === 'group') === (s.type === 'group'))
|
||||
if (mode.value === 'list' && props.kind === 'single' && friendTab.value === 'live') {
|
||||
list = list.filter(s => !s.pinned)
|
||||
}
|
||||
const k = keyword.value.trim()
|
||||
if (k && mode.value === 'list') {
|
||||
list = list.filter(s =>
|
||||
(s.name ?? '').includes(k) || (s.talker_id ?? '').includes(k) || (s.room_id ?? '').includes(k),
|
||||
)
|
||||
}
|
||||
return list
|
||||
})
|
||||
|
||||
/** 空态文案: search 未搜=引导语, 搜了无果=无匹配; list=暂无会话。 */
|
||||
const emptyText = computed(() => {
|
||||
if (mode.value === 'search') {
|
||||
return keyword.value.trim() ? '无匹配聊天记录' : '输入关键词后回车搜索'
|
||||
}
|
||||
return '暂无会话'
|
||||
})
|
||||
|
||||
/** 会话显示名:WA 号码去 @s.whatsapp.net 后缀;空→'--'。 */
|
||||
function displayName(s: ChatSession): string {
|
||||
const n = s.name ?? s.talker_id ?? s.room_id ?? ''
|
||||
return (n.includes('@s.whatsapp.net') ? n.replace('@s.whatsapp.net', '') : n) || '--'
|
||||
}
|
||||
|
||||
/** 会话去重/active 键: 好友=talker_id, 群=room_id(与后端 SessionOut 对齐)。 */
|
||||
function sessionKey(s: ChatSession): string {
|
||||
return s.talker_id ?? s.room_id ?? ''
|
||||
}
|
||||
|
||||
/** search 模式回车: q 全量搜消息(后端 page_size 上限 100),按会话聚合去重。 */
|
||||
async function runSearch() {
|
||||
const k = keyword.value.trim()
|
||||
if (mode.value !== 'search' || !k) {
|
||||
searchResults.value = []
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
errorMsg.value = null
|
||||
try {
|
||||
const res = await getMessages({
|
||||
channel: props.channel,
|
||||
owner_account_id: props.ownerId,
|
||||
q: k,
|
||||
page: 1,
|
||||
page_size: 100,
|
||||
})
|
||||
// 已加载的会话列表提供昵称/头像;未命中(search 只回消息)再以 sender_name/键兜底
|
||||
const known = new Map(sessions.value.map(s => [sessionKey(s), s]))
|
||||
const seen = new Map<string, ChatSession>()
|
||||
for (const m of res.items) {
|
||||
const key = m.room_id ?? m.talker_id ?? ''
|
||||
if (!key || seen.has(key)) continue
|
||||
const hit = known.get(key)
|
||||
seen.set(key, {
|
||||
// 群消息的 talker_id 是群内发言人,不能当群会话键——群只落 room_id
|
||||
type: m.room_id ? 'group' : 'friend',
|
||||
talker_id: m.room_id ? undefined : m.talker_id ?? undefined,
|
||||
room_id: m.room_id ?? undefined,
|
||||
name: hit?.name ?? m.sender_name ?? key,
|
||||
avatar: hit?.avatar ?? undefined,
|
||||
today_count: 0,
|
||||
last_chat_time: m.timestamp ?? null,
|
||||
})
|
||||
}
|
||||
searchResults.value = [...seen.values()].sort(
|
||||
(a, b) => (b.last_chat_time ?? 0) - (a.last_chat_time ?? 0),
|
||||
)
|
||||
} catch (e) {
|
||||
errorMsg.value = (e as Error).message
|
||||
searchResults.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="yk-empty">会话列表 — Task 10 实现</div>
|
||||
<div class="sc-sessions yk-panel-bg yk-panel-sep">
|
||||
<!-- ── 顶部: 模式下拉 + 搜索框 (+ 单聊的好友/已删 tab) ── -->
|
||||
<div class="sc-sessions__top">
|
||||
<div class="sc-sessions__row">
|
||||
<el-select v-model="mode" size="small" class="sc-sessions__mode">
|
||||
<el-option label="好友/群" value="list" />
|
||||
<el-option label="聊天记录" value="search" />
|
||||
</el-select>
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
size="small"
|
||||
clearable
|
||||
class="sc-sessions__search"
|
||||
:placeholder="mode === 'search' ? '请输入关键词搜索' : kind === 'group' ? '群名称,群ID' : '客户昵称,微信号'"
|
||||
@keyup.enter="mode === 'search' && runSearch()"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="kind === 'single'" class="sc-sessions__tabs">
|
||||
<button
|
||||
class="sc-sessions__tab"
|
||||
:class="{ 'is-active': friendTab === 'live' }"
|
||||
@click="friendTab = 'live'"
|
||||
>聊天好友</button>
|
||||
<button
|
||||
class="sc-sessions__tab"
|
||||
:class="{ 'is-active': friendTab === 'deleted' }"
|
||||
@click="friendTab = 'deleted'"
|
||||
>已删除好友</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-alert v-if="errorMsg" :title="errorMsg" type="error" :closable="false" />
|
||||
|
||||
<div v-loading="loading" class="sc-sessions__list">
|
||||
<!-- ── 置顶区(折叠条 + 置顶会话卡) ── -->
|
||||
<div v-if="pinned.length" class="sc-sessions__pinned">
|
||||
<button class="sc-sessions__fold" @click="pinnedOpen = !pinnedOpen">
|
||||
<el-icon class="sc-sessions__fold-icon" :class="{ 'is-open': pinnedOpen }">
|
||||
<ArrowDown />
|
||||
</el-icon>
|
||||
折叠置顶聊天
|
||||
</button>
|
||||
<template v-if="pinnedOpen">
|
||||
<div
|
||||
v-for="s in pinned"
|
||||
:key="'pinned-' + sessionKey(s)"
|
||||
class="sc-sessions__item"
|
||||
:class="{ 'is-active': props.activeId === sessionKey(s) }"
|
||||
@click="emit('select', s)"
|
||||
>
|
||||
<el-avatar :size="36" :src="s.avatar ?? undefined">{{ displayName(s).slice(0, 1) }}</el-avatar>
|
||||
<div class="sc-sessions__meta">
|
||||
<div class="sc-sessions__name">
|
||||
<span>{{ displayName(s) }}</span>
|
||||
<span v-if="s.today_count > 0" class="sc-sessions__today">今日{{ s.today_count }}</span>
|
||||
</div>
|
||||
<div class="sc-sessions__time">{{ relativeChatTime(s.last_chat_time) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- ── 主列表 ── -->
|
||||
<div v-if="!loading && visible.length === 0" class="yk-empty">{{ emptyText }}</div>
|
||||
<div
|
||||
v-for="s in visible"
|
||||
:key="sessionKey(s)"
|
||||
class="sc-sessions__item"
|
||||
:class="{ 'is-active': props.activeId === sessionKey(s) }"
|
||||
@click="emit('select', s)"
|
||||
>
|
||||
<el-avatar :size="36" :src="s.avatar ?? undefined">{{ displayName(s).slice(0, 1) }}</el-avatar>
|
||||
<div class="sc-sessions__meta">
|
||||
<div class="sc-sessions__name">
|
||||
<span>{{ displayName(s) }}</span>
|
||||
<span v-if="s.today_count > 0" class="sc-sessions__today">今日{{ s.today_count }}</span>
|
||||
</div>
|
||||
<div class="sc-sessions__time">{{ relativeChatTime(s.last_chat_time) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* ══ 员工聊天中列(墨玉主题,手法沿用 ChatSidebar) ══
|
||||
点阵底/右缘玉绿线走全局 yk-panel-bg/yk-panel-sep(style.css);这里只写布局。 */
|
||||
.sc-sessions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* ── 顶部区 ── */
|
||||
.sc-sessions__top {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 12px 12px 10px;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
backdrop-filter: blur(8px);
|
||||
border-bottom: 1px solid rgba(23, 29, 43, 0.05);
|
||||
}
|
||||
.sc-sessions__row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.sc-sessions__mode {
|
||||
flex: none;
|
||||
width: 104px;
|
||||
}
|
||||
.sc-sessions__search {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* 聊天好友 | 已删除好友 药丸 tab(单聊专属) */
|
||||
.sc-sessions__tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
background: rgba(23, 29, 43, 0.04);
|
||||
border-radius: 10px;
|
||||
padding: 3px;
|
||||
}
|
||||
.sc-sessions__tab {
|
||||
flex: 1;
|
||||
padding: 6px 10px;
|
||||
font-size: 12.5px;
|
||||
color: #5a6472;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
line-height: 1.3;
|
||||
transition: all 0.2s cubic-bezier(0.22, 0.8, 0.36, 1);
|
||||
}
|
||||
.sc-sessions__tab:hover:not(.is-active) {
|
||||
color: #2b3240;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
.sc-sessions__tab.is-active {
|
||||
color: var(--el-color-primary);
|
||||
background: #fff;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 1px 3px rgba(23, 29, 43, 0.08), 0 2px 8px -2px rgba(13, 159, 110, 0.15);
|
||||
}
|
||||
|
||||
/* ── 列表 ── */
|
||||
.sc-sessions__list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
/* 置顶折叠条 */
|
||||
.sc-sessions__fold {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
padding: 6px 8px;
|
||||
margin-bottom: 2px;
|
||||
font-size: 11.5px;
|
||||
color: #8a94a4;
|
||||
background: rgba(23, 29, 43, 0.035);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.sc-sessions__fold:hover {
|
||||
color: #5a6472;
|
||||
background: rgba(23, 29, 43, 0.06);
|
||||
}
|
||||
.sc-sessions__fold-icon {
|
||||
font-size: 12px;
|
||||
transition: transform 0.25s cubic-bezier(0.22, 0.8, 0.36, 1);
|
||||
}
|
||||
/* 展开时箭头朝下(未转),折叠时转 180° 朝上 */
|
||||
.sc-sessions__fold-icon.is-open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.sc-sessions__pinned {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
/* 会话卡(置顶/主列表共用) */
|
||||
.sc-sessions__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 9px 10px;
|
||||
cursor: pointer;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 2px;
|
||||
border: 1px solid transparent;
|
||||
transition: background 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
.sc-sessions__item:hover {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
box-shadow: 0 1px 4px rgba(23, 29, 43, 0.04);
|
||||
}
|
||||
.sc-sessions__item.is-active {
|
||||
background: #fff;
|
||||
border-color: rgba(13, 159, 110, 0.15);
|
||||
box-shadow: 0 2px 8px -2px rgba(13, 159, 110, 0.15);
|
||||
}
|
||||
.sc-sessions__item.is-active .sc-sessions__name > span:first-child {
|
||||
color: var(--el-color-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sc-sessions__item .el-avatar {
|
||||
flex: none;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(23, 29, 43, 0.05);
|
||||
box-shadow: 0 1px 3px rgba(23, 29, 43, 0.06);
|
||||
}
|
||||
|
||||
.sc-sessions__meta {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
.sc-sessions__name {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
font-size: 13.5px;
|
||||
color: #2b3240;
|
||||
}
|
||||
.sc-sessions__name > span:first-child {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.sc-sessions__today {
|
||||
font-size: 10.5px;
|
||||
color: #fff;
|
||||
background: #f56c6c;
|
||||
padding: 0 5px;
|
||||
border-radius: 8px;
|
||||
flex-shrink: 0;
|
||||
line-height: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.sc-sessions__time {
|
||||
font-size: 11px;
|
||||
color: #8a94a4;
|
||||
margin-top: 3px;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
<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: [] }>()
|
||||
<!-- TabKey 从 NAVS 派生(单独 script 块——<script setup> 不允许 export);StaffChat.vue 复用做路由 tab 收窄。 -->
|
||||
<script lang="ts">
|
||||
const NAVS = [
|
||||
{ key: 'single', label: '单聊' }, { key: 'group', label: '群聊' }, { key: 'moments', label: '朋友圈' },
|
||||
{ key: 'contacts', label: '通讯录' }, { key: 'stats', label: '统计分析' },
|
||||
] as const
|
||||
|
||||
/** 员工聊天窗口导航键(与路由 :tab 段同名)。 */
|
||||
export type TabKey = (typeof NAVS)[number]['key']
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Account } from '@/api/types'
|
||||
|
||||
const props = defineProps<{ profile: Account | null; ownerIdFallback: string; tab: TabKey }>()
|
||||
const emit = defineEmits<{ select: [tab: TabKey]; collapse: [] }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="sc-side">
|
||||
<div class="sc-side yk-panel-bg yk-panel-sep">
|
||||
<!-- 员工资料卡: 头像+昵称 / 真名·电话 / 微信号 (对齐云客资料卡四要素) -->
|
||||
<div class="sc-side__card">
|
||||
<el-avatar :size="44" :src="props.profile?.avatar ?? undefined">
|
||||
@@ -30,6 +37,7 @@ const NAVS = [
|
||||
:key="n.key"
|
||||
class="sc-side__navbtn"
|
||||
:class="{ 'is-active': props.tab === n.key }"
|
||||
:aria-current="props.tab === n.key ? 'page' : undefined"
|
||||
@click="emit('select', n.key)"
|
||||
>{{ n.label }}</button>
|
||||
</nav>
|
||||
@@ -45,18 +53,7 @@ const NAVS = [
|
||||
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;
|
||||
/* 点阵底+右缘玉绿线: 全局 yk-panel-bg / yk-panel-sep(style.css) */
|
||||
}
|
||||
|
||||
/* ── 员工资料卡(毛玻璃名片) ── */
|
||||
|
||||
@@ -479,6 +479,29 @@ h2 {
|
||||
box-shadow: inset 3px 0 0 var(--el-color-primary);
|
||||
}
|
||||
|
||||
/* ══ 墨玉面板皮肤 utility(2026-08-25) ══
|
||||
ChatSidebar / StaffChatSide / SessionPane 三处共用的点阵纸面底 + 右缘玉绿
|
||||
渐变分隔线(原为三份 scoped 复制,提取到全局统一维护)。
|
||||
用法: 根元素挂 `yk-panel-bg yk-panel-sep`,scoped 内只写布局。 */
|
||||
.yk-panel-bg {
|
||||
background:
|
||||
radial-gradient(circle at 1px 1px, rgba(23, 29, 43, 0.03) 1px, transparent 0) 0 0 / 18px 18px,
|
||||
linear-gradient(180deg, #fafbfd 0%, #f6f8fa 100%);
|
||||
}
|
||||
.yk-panel-sep {
|
||||
position: relative;
|
||||
}
|
||||
.yk-panel-sep::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
background: linear-gradient(180deg, transparent 0%, rgba(13, 159, 110, 0.12) 20%, rgba(23, 29, 43, 0.06) 100%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ═══════════════════ 详情抽屉美化层(2026-08-22) ═══════════════════
|
||||
覆盖所有页面的全字段抽屉(客户/员工/群/通话/短信/搜索/聊天/员工详情)
|
||||
—— 卡片式信息档案风格,玉绿主色系呼应全站主题。
|
||||
|
||||
@@ -5,7 +5,7 @@ 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 StaffChatSide, { type TabKey } 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'
|
||||
@@ -15,10 +15,14 @@ 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']
|
||||
// TabKey 从 StaffChatSide 导出(单一事实源);VALID 仅做运行时兜底
|
||||
const VALID: readonly TabKey[] = ['single', 'group', 'moments', 'contacts', 'stats']
|
||||
function isTabKey(v: string | undefined): v is TabKey {
|
||||
return !!v && (VALID as readonly string[]).includes(v)
|
||||
}
|
||||
// 注: 路由 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 tab = computed<TabKey>(() => (isTabKey(props.tab) ? props.tab : 'single'))
|
||||
|
||||
const profile = ref<Account | null>(null)
|
||||
const collapsed = ref(false)
|
||||
@@ -33,7 +37,7 @@ async function loadProfile() {
|
||||
}
|
||||
watch(() => props.ownerId, loadProfile, { immediate: true })
|
||||
|
||||
function onSelectTab(t: string) {
|
||||
function onSelectTab(t: TabKey) {
|
||||
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 }) {
|
||||
|
||||
Reference in New Issue
Block a user