2e526cd056
员工列表点「好友数」原先跳全局 /customers?owner_account_id=x,布局与全局
列表无异且计数/分页绑全局声明口径(同日 dfa4fdd 修的显示 bug),与云客
「员工名下客户列表」体验割裂。新页 /staff-friends/:ownerId/:tab:
- 微信客户/微信群两 tab(对齐云客截图);标题=员工显示名(入口直带,
直链经 /api/accounts 解析),返回箭头回员工列表
- 搜索(昵称/微信号/备注名/手机号,服务端 q)+ 查询/导出 CSV
- 发送/接收条数=Customer.send_num/receive_num 累计值(CustomerOut 补
两列,同步自云客 friend 记录,与截图同源)
- 勾选批量「导出聊天记录」:新端点 GET /api/export/chat-html-zip,复用
chat-html 渲染器每会话一个 HTML 打 zip(文件名=备注>昵称>friend_id,
非法字符清洗;单会话 5000 行截断告警;一次上限 50 会话防误选打爆)
- 行内:查看聊天记录→staff-chat;导出HTML聊天记录(时间范围弹窗)
- WA 行保持原入口(该页暂仅微信)
后端 605 测试绿(zip 新 4 例);本地浏览器端到端验证:过滤 9 条/勾选
激活/群 tab/返回箭头;zip 产物 application/zip 双会话双气泡。
Co-Authored-By: Claude Code <noreply@anthropic.com>
166 lines
5.0 KiB
TypeScript
166 lines
5.0 KiB
TypeScript
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
|
|
import { isLoggedIn } from '@/api/client'
|
|
|
|
// `/` redirects to `/customers`. Chat timeline params are path segments so the
|
|
// URL is shareable + survives refresh:
|
|
// /chat/:channel/:ownerId/:talkerId? (1:1 DM; talkerId optional → account-level)
|
|
// /group/:channel/:ownerId/:roomId? (group chat — same component, room_id)
|
|
const routes: RouteRecordRaw[] = [
|
|
{
|
|
path: '/',
|
|
redirect: { name: 'customers' },
|
|
},
|
|
{
|
|
path: '/customers',
|
|
name: 'customers',
|
|
component: () => import('@/views/CustomerList.vue'),
|
|
meta: { title: '客户列表' },
|
|
},
|
|
{
|
|
path: '/staff',
|
|
name: 'staff',
|
|
component: () => import('@/views/StaffList.vue'),
|
|
meta: { title: '员工列表' },
|
|
},
|
|
{
|
|
// Standalone 员工详情 page mirroring 云客's
|
|
// /cms/customer/userDetailNew?userId=... — replaces the all-fields drawer
|
|
// degradation for the 员工姓名 click. Row-click in StaffList still opens
|
|
// the drawer for a quick preview.
|
|
path: '/staff-detail/:userId',
|
|
name: 'staff-detail',
|
|
component: () => import('@/views/StaffDetail.vue'),
|
|
props: true,
|
|
meta: { title: '员工详情' },
|
|
},
|
|
{
|
|
path: '/accounts',
|
|
redirect: { name: 'staff' },
|
|
},
|
|
{
|
|
path: '/groups',
|
|
name: 'groups',
|
|
component: () => import('@/views/GroupList.vue'),
|
|
meta: { title: '群列表' },
|
|
},
|
|
{
|
|
path: '/calls',
|
|
name: 'calls',
|
|
component: () => import('@/views/CallList.vue'),
|
|
meta: { title: '通话记录' },
|
|
},
|
|
{
|
|
path: '/sms',
|
|
name: 'sms',
|
|
component: () => import('@/views/SmsList.vue'),
|
|
meta: { title: '短信记录' },
|
|
},
|
|
{
|
|
path: '/sync',
|
|
name: 'sync',
|
|
component: () => import('@/views/SyncStatus.vue'),
|
|
meta: { title: '同步状态' },
|
|
},
|
|
{
|
|
path: '/search',
|
|
name: 'search',
|
|
component: () => import('@/views/SearchView.vue'),
|
|
meta: { title: '搜索' },
|
|
},
|
|
{
|
|
// 数据分析板块(spec 2026-09-04):四页
|
|
path: '/analytics/dashboard',
|
|
name: 'analytics-dashboard',
|
|
component: () => import('@/views/analytics/DashboardView.vue'),
|
|
meta: { title: '数据看板' },
|
|
},
|
|
{
|
|
path: '/analytics/churn',
|
|
name: 'analytics-churn',
|
|
component: () => import('@/views/analytics/ChurnView.vue'),
|
|
meta: { title: '流失预警' },
|
|
},
|
|
{
|
|
path: '/analytics/unreplied',
|
|
name: 'analytics-unreplied',
|
|
component: () => import('@/views/analytics/UnrepliedView.vue'),
|
|
meta: { title: '该回未回' },
|
|
},
|
|
{
|
|
path: '/analytics/sensitive',
|
|
name: 'analytics-sensitive',
|
|
component: () => import('@/views/analytics/SensitiveView.vue'),
|
|
meta: { title: '敏感词' },
|
|
},
|
|
{
|
|
path: '/chat/:channel/:ownerId/:talkerId?',
|
|
name: 'chat',
|
|
component: () => import('@/views/ChatTimeline.vue'),
|
|
props: true,
|
|
meta: { title: '聊天记录' },
|
|
},
|
|
{
|
|
// Group chat timeline — same component, but with a `roomId` prop so it
|
|
// issues a room_id query instead of a talker_id query.
|
|
path: '/group/:channel/:ownerId/:roomId?',
|
|
name: 'group-chat',
|
|
component: () => import('@/views/ChatTimeline.vue'),
|
|
props: true,
|
|
meta: { title: '群聊记录' },
|
|
},
|
|
{
|
|
// 员工聊天窗口(对齐云客 wechat-window 五视图): tab=single|group|moments|contacts|stats
|
|
// tab=group 时 talkerId 语义为 roomId; ?msgid= 定位消息(红包转账明细跳转)
|
|
path: '/staff-chat/:ownerId/:tab(single|group|moments|contacts|stats)?/:talkerId?',
|
|
name: 'staff-chat',
|
|
component: () => import('@/views/StaffChat.vue'),
|
|
props: true,
|
|
meta: { title: '员工聊天窗口' },
|
|
},
|
|
{
|
|
// 员工客户列表(2026-09-08 对齐云客员工详情):员工列表「好友数/微信群数」
|
|
// 账号维度下钻,与全局 /customers 分离; ?name= 直带员工显示名(直链自动解析)
|
|
path: '/staff-friends/:ownerId/:tab(customers|groups)?',
|
|
name: 'staff-friends',
|
|
component: () => import('@/views/StaffFriends.vue'),
|
|
props: true,
|
|
meta: { title: '员工客户列表' },
|
|
},
|
|
{
|
|
// 登录页(public:未登录可进;守卫据此放行)
|
|
path: '/login',
|
|
name: 'login',
|
|
component: () => import('@/views/LoginView.vue'),
|
|
meta: { title: '登录', public: true },
|
|
},
|
|
{
|
|
// Anything else → back to the list.
|
|
path: '/:pathMatch(.*)*',
|
|
redirect: { name: 'customers' },
|
|
},
|
|
]
|
|
|
|
export const router = createRouter({
|
|
history: createWebHistory(),
|
|
routes,
|
|
scrollBehavior() {
|
|
return { top: 0 }
|
|
},
|
|
})
|
|
|
|
// 登录守卫:非 public 页面未登录一律去 /login,登录后回跳。判定用
|
|
// isLoggedIn(key 存在)而非 token 非空 — 后端鉴权关闭时 token 是空串,
|
|
// 但用户确实登录过,不能把人锁在登录页。
|
|
router.beforeEach((to) => {
|
|
if (to.meta.public) return true
|
|
if (!isLoggedIn()) {
|
|
return { name: 'login', query: { redirect: to.fullPath } }
|
|
}
|
|
return true
|
|
})
|
|
|
|
router.afterEach((to) => {
|
|
const title = (to.meta.title as string | undefined) ?? ''
|
|
document.title = title ? `${title} · 云客数据镜像` : '云客数据镜像'
|
|
})
|