@quitting/ui 1.2.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"im-ui.es.js","names":[],"sources":["../src/mount.ts","../src/client-access.ts","../src/components/ImToast.vue","../src/components/ImToast.vue","../src/toast.ts","../src/error-text.ts","../src/directives/draggable.ts","../src/components/Icon.vue","../src/components/Icon.vue","../src/components/CallWindow.vue","../src/components/CallWindow.vue","../src/sounds/ring-caller.mp3","../src/sounds/ring-callee.mp3","../src/components/CallDialog.vue","../src/components/CallDialog.vue","../src/components/meeting/icons.ts","../src/components/meeting/MeetingHeader.vue","../src/components/meeting/MeetingHeader.vue","../src/components/meeting/MainVideoTile.vue","../src/components/meeting/MainVideoTile.vue","../src/components/meeting/SmallVideoTile.vue","../src/components/meeting/SmallVideoTile.vue","../src/components/meeting/VideoListPanel.vue","../src/components/meeting/VideoListPanel.vue","../src/components/meeting/MemberItem.vue","../src/components/meeting/MemberItem.vue","../src/components/meeting/MemberListPanel.vue","../src/components/meeting/MemberListPanel.vue","../src/components/meeting/SidebarTabs.vue","../src/components/meeting/SidebarTabs.vue","../src/components/meeting/MeetingControlBar.vue","../src/components/meeting/MeetingControlBar.vue","../src/components/ConfirmDialog.vue","../src/components/ConfirmDialog.vue","../src/components/MeetingEndedToast.vue","../src/components/MeetingEndedToast.vue","../src/components/UserPickerList.vue","../src/components/UserPickerList.vue","../src/components/UserPickerDialog.vue","../src/components/UserPickerDialog.vue","../src/components/MeetWindow.vue","../src/components/MeetWindow.vue","../src/components/MeetingInvite.vue","../src/components/MeetingInvite.vue","../src/components/MeetingCreateDialog.vue","../src/components/MeetingCreateDialog.vue","../src/components/ChatPanel.vue","../src/components/ChatPanel.vue","../src/global-methods.ts","../src/components/CallOutgoing.vue","../src/components/CallOutgoing.vue","../src/components/IncomingRing.vue","../src/components/IncomingRing.vue","../src/index.ts"],"sourcesContent":["import { createApp, h, type App, type Component } from 'vue'\n\n// 动态 mount 工具:把弹窗组件挂到独立 DOM(不依赖业务方在 template 里放组件)\n// global-methods 的自动弹窗、MeetWindow 内置邀请弹窗共用\n\nexport interface MountedRef {\n subApp: App\n div: HTMLDivElement\n}\n\n// 主 app 实例:setupGlobalMethods 时注册,动态子应用继承其全局配置(组件/插件)\n// 未注册(业务方直接在 template 用组件、未装插件)时子应用不继承,组件自包含不受影响\nlet rootApp: App | null = null\n\nexport function setRootApp(app: App | null) {\n rootApp = app\n}\n\n// props 里的 onXxx(onConfirm/onAccepted 等)透传为组件事件监听;\n// onClose 由本函数接管负责清理,调用方可额外传 onClose 做状态回置\nexport function mountComponent(\n component: Component,\n props: Record<string, unknown>,\n mountPoint?: HTMLElement,\n): MountedRef {\n const { onClose: onCloseProp, ...rest } = props\n const onClose = onCloseProp as (() => void) | undefined\n const div = document.createElement('div')\n div.classList.add('im-ui-root')\n // 挂进 pointer-events:none 的容器(如邀请弹窗堆叠层)时子树保持可交互;\n // 挂 body 时 auto 即默认值,无影响\n div.style.pointerEvents = 'auto'\n ;(mountPoint || document.body).appendChild(div)\n const subApp = createApp({\n render: () =>\n h(component, {\n ...rest,\n onClose: () => {\n onClose?.()\n subApp.unmount()\n div.remove()\n },\n }),\n })\n if (rootApp) {\n // 继承主 app 的全局组件/插件\n subApp.config = rootApp.config\n }\n subApp.mount(div)\n return { subApp, div }\n}\n\nexport function unmountRef(ref: MountedRef | null) {\n if (!ref) return\n ref.subApp.unmount()\n ref.div.remove()\n}\n","import type { ImClient } from '@quitting/core'\n\n// window.__imClient 的类型化访问。\n// setupGlobalMethods 注入实例;UI 组件内部 getClient() 从这里读取。\n// (历史组件各自用 (window as any).__imClient,新代码统一走本模块)\ndeclare global {\n interface Window {\n __imClient?: ImClient\n }\n}\n\nexport function getImClient(): ImClient | undefined {\n // eslint-disable-next-line eslint/no-underscore-dangle -- 全局键名沿用历史约定(组件 getClient 依赖)\n return window.__imClient\n}\n\nexport function setImClient(c: ImClient | undefined) {\n // eslint-disable-next-line eslint/no-underscore-dangle -- 同上\n window.__imClient = c\n}\n","<script setup lang=\"ts\">\n// 全局 toast 宿主:渲染 toast.ts 维护的消息列表(fixed 顶部居中,多条向下堆叠)\n// 样式变量 --im-toast-* 可在 :root/祖先节点覆盖(无需 :deep() 穿透 scoped)\nexport interface ToastItem {\n id: number\n message: string\n type: 'info' | 'success' | 'error'\n}\n\ndefineProps<{ items: ToastItem[] }>()\n</script>\n\n<template>\n <div class=\"im-toast-host\">\n <div v-for=\"t in items\" :key=\"t.id\" :class=\"['im-toast', `im-toast-${t.type}`]\" role=\"status\">\n {{ t.message }}\n </div>\n </div>\n</template>\n\n<style scoped>\n.im-toast-host {\n /* 宿主仅定位,颜色/尺寸全部走变量;默认值业务方可整体覆盖 */\n --im-toast-bg: #323232;\n --im-toast-bg-success: #2e7d32;\n --im-toast-bg-error: #d32f2f;\n --im-toast-color: #fff;\n --im-toast-radius: 6px;\n --im-toast-font-size: 14px;\n --im-toast-z: 100000;\n\n position: fixed;\n top: 24px;\n left: 50%;\n transform: translateX(-50%);\n z-index: var(--im-toast-z);\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 8px;\n pointer-events: none;\n}\n.im-toast {\n max-width: 70vw;\n padding: 9px 18px;\n border-radius: var(--im-toast-radius);\n background: var(--im-toast-bg);\n color: var(--im-toast-color);\n font-size: var(--im-toast-font-size);\n line-height: 1.5;\n word-break: break-all;\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.18);\n animation: im-toast-in 0.2s ease-out;\n}\n.im-toast-success {\n background: var(--im-toast-bg-success);\n}\n.im-toast-error {\n background: var(--im-toast-bg-error);\n}\n@keyframes im-toast-in {\n from {\n opacity: 0;\n transform: translateY(-8px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n</style>\n","<script setup lang=\"ts\">\n// 全局 toast 宿主:渲染 toast.ts 维护的消息列表(fixed 顶部居中,多条向下堆叠)\n// 样式变量 --im-toast-* 可在 :root/祖先节点覆盖(无需 :deep() 穿透 scoped)\nexport interface ToastItem {\n id: number\n message: string\n type: 'info' | 'success' | 'error'\n}\n\ndefineProps<{ items: ToastItem[] }>()\n</script>\n\n<template>\n <div class=\"im-toast-host\">\n <div v-for=\"t in items\" :key=\"t.id\" :class=\"['im-toast', `im-toast-${t.type}`]\" role=\"status\">\n {{ t.message }}\n </div>\n </div>\n</template>\n\n<style scoped>\n.im-toast-host {\n /* 宿主仅定位,颜色/尺寸全部走变量;默认值业务方可整体覆盖 */\n --im-toast-bg: #323232;\n --im-toast-bg-success: #2e7d32;\n --im-toast-bg-error: #d32f2f;\n --im-toast-color: #fff;\n --im-toast-radius: 6px;\n --im-toast-font-size: 14px;\n --im-toast-z: 100000;\n\n position: fixed;\n top: 24px;\n left: 50%;\n transform: translateX(-50%);\n z-index: var(--im-toast-z);\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 8px;\n pointer-events: none;\n}\n.im-toast {\n max-width: 70vw;\n padding: 9px 18px;\n border-radius: var(--im-toast-radius);\n background: var(--im-toast-bg);\n color: var(--im-toast-color);\n font-size: var(--im-toast-font-size);\n line-height: 1.5;\n word-break: break-all;\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.18);\n animation: im-toast-in 0.2s ease-out;\n}\n.im-toast-success {\n background: var(--im-toast-bg-success);\n}\n.im-toast-error {\n background: var(--im-toast-bg-error);\n}\n@keyframes im-toast-in {\n from {\n opacity: 0;\n transform: translateY(-8px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n</style>\n","import { reactive } from 'vue'\nimport { createApp, h } from 'vue'\n\nimport ImToast, { type ToastItem } from './components/ImToast.vue'\n\n// 轻量 toast:首次调用时往 body 动态挂一个宿主(.im-toast-root),之后只维护响应式列表\n// 用法:toast.error('对方正忙') / toast.success('已发送') / ImToast.info(...)\n// 颜色/层级定制::root 覆盖 --im-toast-* 变量(见 ImToast.vue)\n\nconst state = reactive<{ items: ToastItem[] }>({ items: [] })\nlet hostMounted = false\nlet seq = 0\n\nfunction ensureHost() {\n if (hostMounted) return\n const div = document.createElement('div')\n div.className = 'im-toast-root'\n document.body.appendChild(div)\n createApp({ render: () => h(ImToast, { items: state.items }) }).mount(div)\n hostMounted = true\n}\n\nfunction show(message: string, type: ToastItem['type'], duration: number) {\n ensureHost()\n const id = ++seq\n state.items.push({ id, message, type })\n setTimeout(() => {\n const idx = state.items.findIndex((t) => t.id === id)\n if (idx >= 0) state.items.splice(idx, 1)\n }, duration)\n}\n\nfunction info(message: string, duration = 3000) {\n show(message, 'info', duration)\n}\nfunction success(message: string, duration = 3000) {\n show(message, 'success', duration)\n}\nfunction error(message: string, duration = 4000) {\n show(message, 'error', duration)\n}\n\n// 业务方入口:ImToast.error(...);与组件名不冲突(导出为值而非组件)\nexport const ImToastApi = { info, success, error }\n","import { ApiError, ApiErrorCode } from '@quitting/core'\n\n// ApiError → 用户可读文案的统一映射(demo 三处红条逻辑收编于此)\n// 业务方不满足默认文案时可自行 catch 后用 toast 显示自定义文案\n\nexport function friendlyApiError(err: unknown, fallback = '请求失败'): string {\n if (err instanceof ApiError) {\n switch (err.code) {\n case ApiErrorCode.Busy:\n return '对方正忙,请稍后再试'\n case ApiErrorCode.Conflict:\n return '操作冲突,请重试'\n case ApiErrorCode.NotFound:\n return '用户不存在'\n case ApiErrorCode.Unauth:\n case ApiErrorCode.SessionKicked:\n return '登录状态已失效,请重新登录'\n case ApiErrorCode.Forbidden:\n return '无权执行该操作'\n default:\n return err.message || fallback\n }\n }\n return '网络异常,请稍后重试'\n}\n","import type { Directive, DirectiveBinding } from 'vue'\n\n// v-draggable:让元素可在视口内拖拽(基于 position: fixed)\n//\n// 用法:\n// <div v-draggable>整块都可拖</div>\n// <div v-draggable=\"'.header'\">只有 .header 子元素是拖拽句柄</div>\n//\n// 与 im-web/src/directives/draggable.ts 的差异:\n// - 基于 viewport(fixed)而非父元素(absolute)—— 通话窗口可挪到屏幕任意位置\n// - 用 addEventListener 而非覆盖 onmousedown,可与其他指令/事件共存\n// - unmounted 时清理 listener,避免泄漏\n// - 支持指定拖拽句柄(header),避免整块内容都触发拖拽(如输入框)\n//\n// 边界约束:元素中心点不超出视口;初始位置由元素 CSS 决定(不强制 left/top)\n\ninterface DragState {\n startX: number\n startY: number\n origLeft: number\n origTop: number\n}\n\nfunction getHandle(el: HTMLElement, binding: DirectiveBinding): HTMLElement {\n const selector = binding.value as string | undefined\n if (!selector) return el\n const handle = el.querySelector<HTMLElement>(selector)\n return handle || el\n}\n\nfunction onStart(e: MouseEvent, el: HTMLElement, state: DragState) {\n // 输入控件不触发拖拽(避免与 textarea/input select 冲突)\n const tag = (e.target as HTMLElement).tagName\n if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return\n\n const rect = el.getBoundingClientRect()\n state.startX = e.clientX\n state.startY = e.clientY\n state.origLeft = rect.left\n state.origTop = rect.top\n // 切到 fixed 并锁定当前坐标,避免后续 CSS 影响跳变\n // 关键:清掉 transform(如居中用的 translate(-50%, -50%)),否则 left/top 会双重偏移\n el.style.position = 'fixed'\n el.style.transform = 'none'\n el.style.left = `${rect.left}px`\n el.style.top = `${rect.top}px`\n el.style.right = 'auto'\n el.style.bottom = 'auto'\n el.style.margin = '0'\n el.classList.add('v-dragging')\n\n const onMove = (ev: MouseEvent) => {\n const dx = ev.clientX - state.startX\n const dy = ev.clientY - state.startY\n const newLeft = state.origLeft + dx\n const newTop = state.origTop + dy\n // 视口边界约束(保证元素至少有 50px 可见)\n const maxX = window.innerWidth - 50\n const maxY = window.innerHeight - 50\n const minX = 50 - el.offsetWidth\n const minY = 0\n el.style.left = `${Math.min(Math.max(newLeft, minX), maxX)}px`\n el.style.top = `${Math.min(Math.max(newTop, minY), maxY)}px`\n }\n const onUp = () => {\n document.removeEventListener('mousemove', onMove)\n document.removeEventListener('mouseup', onUp)\n el.classList.remove('v-dragging')\n }\n document.addEventListener('mousemove', onMove)\n document.addEventListener('mouseup', onUp)\n}\n\nconst draggable: Directive<HTMLElement> = {\n mounted(el, binding) {\n const handle = getHandle(el, binding)\n handle.style.cursor = 'move'\n const state: DragState = { startX: 0, startY: 0, origLeft: 0, origTop: 0 }\n const handler = (e: MouseEvent) => onStart(e, el, state)\n handle.addEventListener('mousedown', handler)\n // 存到 el 上方便 unmounted 清理\n ;(el as any).__dragHandle__ = handle\n ;(el as any).__dragHandler__ = handler\n },\n unmounted(el) {\n const handle = (el as any).__dragHandle__ as HTMLElement | undefined\n const handler = (el as any).__dragHandler__ as ((e: MouseEvent) => void) | undefined\n if (handle && handler) {\n handle.removeEventListener('mousedown', handler)\n }\n },\n}\n\nexport default draggable\n","<script setup lang=\"ts\">\n// 通用图标组件:SVG 路径内联在 ICON_PATHS 常量里,无运行时文件加载\n// 这样通过 npm 安装到其他项目后无需额外配置即可正常显示\n//\n// 用法:\n// <Icon name=\"mic-on\" :size=\"18\" />\n// <Icon name=\"mic-off\" :size=\"18\" color=\"#ef4444\" />\n//\n// 颜色继承 currentColor(父元素的 color CSS 属性),或显式传 color prop\n\n// 所有图标统一 viewBox=\"0 0 1024 1024\",方便对齐\n// fill 默认移除(继承 currentColor),少数图标有特殊 fill 在 path 上保留\nconst ICON_PATHS = {\n // 麦克风开启\n 'mic-on': `<path d=\"M514.986667 112c80.170667 0 145.109333 62.08 145.109333 138.666667v277.248c0 76.586667-64.938667 138.624-145.066667 138.624-80.213333 0-145.152-62.08-145.152-138.624V250.666667c0-76.586667 64.981333-138.666667 145.109334-138.666667z m236.970666 411.221333c0 118.485333-93.013333 215.893333-212.608 228.992v135.637334a24.149333 24.149333 0 0 1-48.341333 0v-135.04c-122.709333-10.24-219.008-109.013333-219.008-229.546667v-96.426667a24.704 24.704 0 0 1 49.408 0v109.184c0 98.56 83.712 169.088 190.592 169.088 106.837333 0 196.309333-92.8 196.309333-169.088V423.936a21.845333 21.845333 0 0 1 43.690667 0v99.285333z\"/>`,\n // 麦克风关闭\n 'mic-off': `<path d=\"M656.384 559.232L369.877333 272.725333v-22.058666c0-76.586667 64.981333-138.666667 145.109334-138.666667 80.170667 0 145.109333 62.08 145.109333 138.666667v277.248c0 10.794667-1.28 21.248-3.712 31.317333z m-84.693333 96.341333a150.528 150.528 0 0 1-56.704 10.965334c-80.128 0-145.066667-62.08-145.066667-138.624v-74.154667l201.770667 201.813333z m153.898666-27.136l-29.610666-29.568a165.973333 165.973333 0 0 0 12.373333-62.848V423.936a21.845333 21.845333 0 0 1 43.648 0v99.285333c0 37.930667-9.557333 73.685333-26.453333 105.216z m-89.173333 91.904a245.290667 245.290667 0 0 1-97.066667 31.872v135.637334a24.149333 24.149333 0 0 1-48.341333 0v-135.04c-122.709333-10.24-219.008-109.013333-219.008-229.546667v-96.426667a24.704 24.704 0 0 1 49.408 0v109.184c0 98.56 86.613333 178.474667 193.450667 178.474667 33.92 0 65.834667-8.064 93.568-22.186667l28.032 28.032zM145.28 183.893333l45.226667-45.226666 678.826666 678.826666-45.226666 45.226667z\"/>`,\n // 摄像头开启\n 'camera-on': `<path d=\"M873.770667 314.922667c19.797333-12.202667 37.632-2.048 37.632 18.304v335.232c0 24.362667-15.872 32.512-37.632 18.261333l-112.938667-67.029333c-19.797333-12.202667-37.632-26.453333-37.632-46.72V424.618667c0-18.261333 17.834667-30.464 37.632-42.666667l112.938667-67.029333zM207.232 288h391.936c43.562667 0 79.232 32 79.232 71.125333v305.749334c0 39.125333-35.669333 71.125333-79.232 71.125333H207.232C163.669333 736 128 704 128 664.874667V359.125333C128 320 163.669333 288 207.232 288z\"/>`,\n // 摄像头关闭\n 'camera-off': `<path d=\"M873.770667 314.922667c19.797333-12.202667 37.632-2.048 37.632 18.304v335.232c0 24.362667-15.872 32.512-37.632 18.261333l-112.938667-67.029333c-19.797333-12.202667-37.632-26.453333-37.632-46.72V424.618667c0-18.261333 17.834667-30.464 37.632-42.666667l112.938667-67.029333zM385.152 288h214.016c43.562667 0 79.232 32 79.232 71.125333v222.122667L385.152 288z m256.042667 437.077333a85.333333 85.333333 0 0 1-42.026667 10.922667H207.232C163.669333 736 128 704 128 664.874667V359.125333c0-38.186667 34.005333-69.632 76.16-71.082666l437.034667 437.034666zM145.28 183.893333l45.226667-45.226666 678.826666 678.826666-45.226666 45.226667z\"/>`,\n // 声音(扬声器开启)\n 'sound-on': `<path d=\"M767.317333 767.018667a33.237333 33.237333 0 0 1-20.565333 6.656 34.56 34.56 0 0 1-25.386667-10.709334 34.986667 34.986667 0 0 1-10.666666-25.557333 35.2 35.2 0 0 1 13.994666-28.501333 5.76 5.76 0 0 0 1.450667-1.109334l0.725333-0.725333c13.738667-9.386667 26.453333-20.48 38.272-33.28 11.733333-12.842667 22.058667-26.794667 30.848-41.813333 8.832-15.061333 15.701333-31.146667 20.608-48.128a190.293333 190.293333 0 0 0 7.338667-52.906667c0-37.034667-9.301333-70.826667-27.946667-101.418667a250.794667 250.794667 0 0 0-69.845333-75.52 34.730667 34.730667 0 0 1-15.445333-29.568 34.986667 34.986667 0 0 1 10.666666-25.557333 34.56 34.56 0 0 1 25.386667-10.709333c8.32 0 15.658667 2.688 22.058667 8.106666 18.133333 12.842667 35.029333 27.776 50.730666 44.8 15.658667 17.066667 29.141333 35.541333 40.448 55.509334 11.264 19.968 20.053333 41.344 26.453334 64 6.4 22.698667 9.557333 46.165333 9.557333 70.314666 0 24.704-3.2 48.384-9.557333 71.082667a285.653333 285.653333 0 0 1-26.837334 64.384 305.450667 305.450667 0 0 1-92.288 100.693333v-0.042666z m-41.173333-332.330667c10.282667 14.336 18.517333 28.757333 24.661333 43.306667 6.101333 14.549333 9.173333 32.213333 9.173334 52.906666 0 19.754667-3.2 37.546667-9.557334 53.333334-6.4 15.786667-14.464 30.08-24.277333 42.88-3.925333 5.461333-8.106667 10.24-12.501333 14.464a23.552 23.552 0 0 1-16.896 6.272 29.866667 29.866667 0 0 1-22.442667-9.6 32.085333 32.085333 0 0 1-9.173333-22.954667 33.706667 33.706667 0 0 1 2.218666-11.861333h-0.768c2.474667-5.418667 5.418667-10.581333 8.832-15.530667 3.413333-4.949333 6.741333-9.984 9.941334-15.146667a84.48 84.48 0 0 0 8.106666-17.792c2.176-6.656 3.285333-14.677333 3.285334-24.064 0-8.874667-1.109333-16.64-3.328-23.296a94.208 94.208 0 0 0-8.106667-18.133333 240.170667 240.170667 0 0 0-9.898667-15.530667 67.072 67.072 0 0 1-8.106666-15.573333 29.994667 29.994667 0 0 1-2.176-11.818667c0-8.874667 3.072-16.512 9.173333-22.954666a29.866667 29.866667 0 0 1 22.442667-9.6c6.826667 0 12.501333 2.090667 16.896 6.272 4.437333 4.181333 8.576 9.002667 12.501333 14.421333z m-237.482667 439.637333a9078.869333 9078.869333 0 0 0-157.354666-139.136 10554.453333 10554.453333 0 0 0-83.797334-70.314666 177.408 177.408 0 0 1-16.213333 0.725333 125.781333 125.781333 0 0 1-13.952 0.768h-12.501333c-13.226667 0-24.021333-4.096-32.341334-12.202667a39.424 39.424 0 0 1-12.501333-29.269333v-186.453333h0.725333l-0.725333-0.768c0-13.824 4.266667-25.173333 12.885333-34.048a42.666667 42.666667 0 0 1 32-13.312h41.856a13012.906667 13012.906667 0 0 1 122.794667-103.637334c15.701333-13.312 31.146667-26.282667 46.336-38.826666 15.189333-12.586667 28.928-24.064 41.173333-34.432 12.245333-10.368 22.570667-18.986667 30.890667-25.898667 8.32-6.912 13.482667-11.093333 15.445333-12.586667 14.677333-10.88 26.581333-15.786667 35.626667-14.805333 9.088 0.981333 16.213333 5.418667 21.333333 13.312s8.704 17.493333 10.666667 28.885333c1.962667 11.349333 2.944 21.674667 2.944 31.061334v599.466666c0 9.386667-1.706667 19.413333-5.12 29.994667-3.456 10.624-8.746667 19.029333-15.829333 25.173333-7.125333 6.186667-16.042667 8.746667-26.88 7.765334-10.752-0.981333-23.253333-8.106667-37.461334-21.461334z\"/>`,\n // 静音(扬声器关闭)\n 'sound-off': `<path d=\"M767.872 670.72c10.624-11.946667 20.010667-24.917333 28.16-38.784 8.789333-15.018667 15.658667-31.061333 20.565333-48.085333a190.293333 190.293333 0 0 0 7.338667-52.906667c0-37.034667-9.301333-70.826667-27.946667-101.418667a250.794667 250.794667 0 0 0-69.845333-75.52 34.730667 34.730667 0 0 1-15.445333-29.568 34.986667 34.986667 0 0 1 10.666666-25.557333 34.56 34.56 0 0 1 25.386667-10.709333c8.32 0 15.658667 2.688 22.058667 8.106666 18.133333 12.842667 35.029333 27.776 50.730666 44.8 15.658667 17.066667 29.141333 35.541333 40.448 55.509334 11.264 19.968 20.053333 41.344 26.453334 64 6.4 22.698667 9.557333 46.165333 9.557333 70.314666 0 24.704-3.2 48.384-9.557333 71.082667a285.653333 285.653333 0 0 1-26.837334 64.384 304.64 304.64 0 0 1-40.533333 55.552l-51.2-51.2zM725.333333 628.224l-46.08-46.08c2.090667-3.072 4.096-6.229333 6.058667-9.386667a84.48 84.48 0 0 0 8.106667-17.792c2.176-6.656 3.285333-14.677333 3.285333-24.064 0-8.874667-1.109333-16.64-3.328-23.296a94.208 94.208 0 0 0-8.106667-18.133333 240.170667 240.170667 0 0 0-9.898666-15.530667 67.072 67.072 0 0 1-8.106667-15.573333 29.994667 29.994667 0 0 1-2.176-11.818667c0-8.874667 3.072-16.512 9.173333-22.954666a29.866667 29.866667 0 0 1 22.442667-9.6c6.826667 0 12.501333 2.090667 16.896 6.272 4.437333 4.181333 8.576 9.002667 12.501333 14.421333 10.282667 14.336 18.517333 28.757333 24.661334 43.306667 6.101333 14.549333 9.173333 32.213333 9.173333 52.906666 0 19.754667-3.2 37.546667-9.557333 53.333334A183.168 183.168 0 0 1 725.333333 628.181333zM278.997333 362.837333l294.954667 294.997334v175.061333c0 9.386667-1.706667 19.370667-5.12 29.952-3.456 10.624-8.746667 19.029333-15.829333 25.173333-7.125333 6.186667-16.042667 8.746667-26.88 7.765334-10.752-0.981333-23.253333-8.106667-37.461334-21.461334a9078.869333 9078.869333 0 0 0-157.354666-139.136 10554.453333 10554.453333 0 0 0-83.797334-70.314666 177.408 177.408 0 0 1-16.213333 0.725333 125.781333 125.781333 0 0 1-13.952 0.768h-12.501333c-13.226667 0-24.021333-4.096-32.341334-12.202667a39.424 39.424 0 0 1-12.501333-29.269333v-186.453333h0.725333l-0.725333-0.768c0-13.824 4.266667-25.173333 12.885333-34.048a42.666667 42.666667 0 0 1 32-13.312h41.856l32.256-27.434667z m98.261334-82.688c13.056-11.050667 25.898667-21.802667 38.613333-32.341333 15.189333-12.544 28.928-24.021333 41.173333-34.389333 12.245333-10.368 22.570667-18.986667 30.890667-25.898667 8.32-6.912 13.482667-11.093333 15.445333-12.586667 14.677333-10.88 26.581333-15.786667 35.626667-14.805333 9.088 0.981333 16.213333 5.418667 21.333333 13.312s8.704 17.493333 10.666667 28.885333c1.962667 11.349333 2.944 21.674667 2.944 31.061334v243.413333l-196.693333-196.693333zM145.28 183.893333l45.226667-45.226666 678.826666 678.826666-45.226666 45.226667z\"/>`,\n // 接听(电话听筒)\n answer: `<path d=\"M864.597333 647.338667l11.52 11.562666c0.341333 0.682667 0.597333 1.408 1.066667 2.005334 11.52 14.762667 14.848 32.213333 10.325333 52.266666-3.285333 14.421333-9.216 28.16-16.597333 41.386667-6.613333 11.861333-13.994667 23.381333-21.034667 34.986667l-9.045333 9.088c-5.845333 3.541333-11.477333 7.594667-17.493333 10.453333-15.018667 7.04-30.421333 8.533333-45.781334 8.490667-20.906667-0.042667-40.96-3.498667-60.970666-7.210667-32.810667-6.101333-64.896-13.952-94.933334-26.88-42.154667-18.133333-79.658667-42.922667-115.2-70.528a987.818667 987.818667 0 0 1-78.08-68.010667c-0.682667-0.682667-1.536-1.152-2.346666-1.706666l-44.373334-44.373334c-0.682667-0.938667-1.322667-1.962667-2.133333-2.816-19.072-22.058667-38.656-43.690667-57.216-66.218666a620.117333 620.117333 0 0 1-60.928-88.746667C236.245333 396.928 216.32 350.421333 212.608 296.362667c-1.450667-20.693333-4.224-41.088-2.389333-62.336 1.237333-14.677333 4.053333-29.184 11.52-43.008 2.048-3.797333 4.522667-7.381333 6.826666-11.050667l9.045334-9.045333c8.32-5.248 16.554667-10.538667 24.917333-15.616 13.909333-8.490667 27.733333-17.109333 42.965333-22.442667 15.957333-5.632 30.634667-4.778667 43.605334 3.2 4.906667 3.029333 9.429333 6.613333 14.122666 9.941333l8.789334 8.789334c0.768 1.066667 1.493333 2.133333 2.304 3.157333 4.266667 5.12 8.746667 10.026667 12.714666 15.36 23.637333 31.616 42.325333 66.56 58.581334 103.082667 8.448 19.029333 6.186667 38.741333-6.826667 58.154666-6.101333 9.045333-13.226667 17.621333-20.736 25.642667-14.506667 15.488-19.84 32.213333-15.616 50.56 1.536 6.570667 3.754667 12.928 6.570667 18.901333 9.258667 19.754667 22.826667 36.266667 37.76 51.626667 27.178667 27.861333 54.869333 55.338667 82.389333 82.901333 15.488 15.488 31.530667 30.378667 50.133333 42.24 11.52 7.381333 23.68 13.525333 37.973334 15.445334 14.08 1.962667 27.562667-1.365333 39.808-11.264 6.442667-5.205333 12.330667-11.136 18.645333-16.512 22.784-19.285333 46.506667-25.642667 71.850667-15.914667 37.546667 14.506667 71.253333 34.645333 102.826666 58.197333 4.778667 3.584 9.472 7.338667 14.208 10.965334z\"/>`,\n // 挂断\n hangup: `<path d=\"M68.820008 642.540165a22.883151 22.883151 0 0 0 13.903686 21.04864c2.800052 1.158641 5.793203 1.737961 8.689804 1.737961l216.37612-0.38621a22.786601 22.786601 0 0 0 22.786601-22.786601l0-87.670471c11.682955-13.807125 25.779752-25.296981 41.517949-33.986785a274.115036 274.115036 0 0 1 140.002394-34.276445l0.482771 0a270.832225 270.832225 0 0 1 139.616184 34.083335c15.641647 8.786365 29.641883 20.276209 41.228289 34.083346l0 87.1877a22.69004 22.69004 0 0 0 22.59349 22.786601l194.072289-0.28966 22.30383 0a22.59349 22.59349 0 0 0 22.690051-22.786601l0.09655-49.531883a22.207281 22.207281 0 0 0-0.67587-5.310432l-0.965541-3.958692a150.140499 150.140499 0 0 0-11.586405-46.828392c-43.352459-100.898276-222.748643-176.982342-417.303691-176.982342l-0.965541 0a868.01487 868.01487 0 0 0-229.024605 32.152275c-76.566836 21.72451-137.105804 52.138824-170.609819 85.64285-17.765829 17.283058-32.152275 37.752367-42.290379 60.442418-8.207045 18.441688-12.551947 38.428237-12.841607 58.607897l-0.09655 47.021491z\"/>`,\n // 举手\n handup: `<path d=\"M166.997333 665.6H150.613333c-0.725333-0.213333-1.450667-0.597333-2.133333-0.682667-18.602667-2.261333-33.28-12.245333-44.288-29.653333a155.818667 155.818667 0 0 1-17.536-41.002667c-3.712-13.056-6.613333-26.410667-9.898667-39.594666v-12.842667c1.621333-6.613333 2.730667-13.482667 4.992-19.754667 5.632-15.573333 15.488-27.52 26.368-38.4 14.805333-14.72 31.402667-26.453333 48.213334-37.973333 27.477333-18.901333 55.765333-36.010667 86.144-48.128 42.624-16.981333 86.613333-25.941333 131.285333-31.573333 34.346667-4.266667 68.778667-6.4 103.296-7.125334 0.981333 0 1.92-0.298667 2.901333-0.469333h62.72c1.152 0.170667 2.346667 0.426667 3.498667 0.512 29.098667 2.090667 58.24 3.541333 87.296 6.314667 35.712 3.413333 70.997333 10.112 105.856 19.712 48.981333 13.44 95.957333 32.256 136.832 67.84 15.616 13.653333 32 26.069333 45.781333 42.368 9.472 11.264 17.749333 23.466667 22.229334 38.570666 1.28 4.138667 2.005333 8.405333 2.986666 12.629334v12.8c-2.176 9.6-4.266667 19.2-6.570666 28.714666-3.84 15.786667-7.552 31.701333-14.506667 46.208-7.338667 15.274667-18.304 25.002667-33.109333 28.586667-5.632 1.28-11.349333 1.962667-17.066667 2.944h-12.373333c-1.28-0.213333-2.56-0.469333-3.84-0.597333-6.656-0.64-13.269333-0.938667-19.882667-1.877334-39.082667-5.632-77.013333-17.152-114.346667-31.488-19.413333-7.466667-31.701333-22.997333-36.266666-45.952a205.098667 205.098667 0 0 1-3.456-32.810666c-0.682667-21.162667-8.746667-36.778667-24.746667-46.762667a94.72 94.72 0 0 0-17.92-8.704c-20.565333-7.424-41.856-9.514667-63.232-9.813333-38.954667-0.469333-77.952-0.341333-116.906667-0.341334-21.888 0-43.776 0.810667-65.365333 5.546667-13.312 2.944-26.24 7.210667-37.717333 15.914667-11.349333 8.618667-18.517333 20.48-20.181334 36.181333-0.853333 8.192-0.853333 16.554667-1.493333 24.789333-2.474667 29.781333-14.805333 51.072-39.594667 62.08-36.736 16.298667-74.88 25.898667-113.834666 31.573334-5.973333 0.853333-11.861333 1.493333-17.792 2.261333z\"/>`,\n // 关闭(叉)\n close: `<path d=\"M649.179 512l212.839-212.84c37.881-37.881 37.881-99.298 0-137.179s-99.298-37.881-137.179 0L512 374.821l-212.839-212.84c-37.881-37.881-99.298-37.881-137.179 0s-37.881 99.298 0 137.179L374.821 512 161.982 724.84c-37.881 37.881-37.881 99.297 0 137.179 18.94 18.94 43.765 28.41 68.589 28.41 24.825 0 49.649-9.47 68.589-28.41L512 649.179l212.839 212.84c18.94 18.94 43.765 28.41 68.589 28.41s49.649-9.47 68.59-28.41c37.881-37.882 37.881-99.298 0-137.179L649.179 512z\"/>`,\n // 用户\n user: `<path d=\"M0 0h1024v1024H0V0z m614.8608 664.439467c-2.973013-30.685867-1.983147-53.4528-1.983147-82.158934 13.88544-7.918933 63.488-55.432533 67.437227-95.0272 10.922667-0.989867 28.757333-11.8784 33.716907-55.432533 2.976427-22.766933-7.932587-36.625067-14.875307-40.584533 17.851733-54.442667 25.78432-224.703147-99.170987-241.53088C587.093333 127.92832 554.366293 116.053333 511.72352 116.053333c-170.571093 2.9696-221.149867 130.6624-183.463253 276.176214-6.94272 3.959467-17.851733 16.827733-14.875307 40.584533 4.959573 43.554133 22.807893 54.442667 33.716907 55.432533 3.966293 40.584533 53.551787 88.098133 67.437226 95.0272 0 28.706133 0.989867 50.4832-1.98656 82.158934C378.835627 756.497067 157.696 708.983467 146.773333 907.946667h733.866667c-5.94944-201.936213-232.05888-151.453013-265.775787-243.510614z\"/>`,\n // 更多(三个点)\n more: `<circle cx=\"180\" cy=\"512\" r=\"90\"/><circle cx=\"512\" cy=\"512\" r=\"90\"/><circle cx=\"844\" cy=\"512\" r=\"90\"/>`,\n // 加号\n plus: `<path d=\"M480 224v256H224a32 32 0 1 0 0 64h256v256a32 32 0 1 0 64 0V544h256a32 32 0 1 0 0-64H544V224a32 32 0 1 0-64 0z\"/>`,\n // 减号\n minus: `<path d=\"M192 480h640a32 32 0 1 1 0 64H192a32 32 0 1 1 0-64z\"/>`,\n} as const\n\nexport type IconName = keyof typeof ICON_PATHS\n\nconst props = withDefaults(\n defineProps<{\n name: IconName\n size?: number | string\n color?: string\n title?: string\n }>(),\n {\n size: 16,\n color: 'currentColor',\n },\n)\n</script>\n\n<template>\n <svg\n :width=\"props.size\"\n :height=\"props.size\"\n viewBox=\"0 0 1024 1024\"\n xmlns=\"http://www.w3.org/2000/svg\"\n :fill=\"props.color\"\n :aria-label=\"props.title\"\n :aria-hidden=\"props.title ? undefined : 'true'\"\n v-html=\"ICON_PATHS[props.name]\"\n />\n</template>\n","<script setup lang=\"ts\">\n// 通用图标组件:SVG 路径内联在 ICON_PATHS 常量里,无运行时文件加载\n// 这样通过 npm 安装到其他项目后无需额外配置即可正常显示\n//\n// 用法:\n// <Icon name=\"mic-on\" :size=\"18\" />\n// <Icon name=\"mic-off\" :size=\"18\" color=\"#ef4444\" />\n//\n// 颜色继承 currentColor(父元素的 color CSS 属性),或显式传 color prop\n\n// 所有图标统一 viewBox=\"0 0 1024 1024\",方便对齐\n// fill 默认移除(继承 currentColor),少数图标有特殊 fill 在 path 上保留\nconst ICON_PATHS = {\n // 麦克风开启\n 'mic-on': `<path d=\"M514.986667 112c80.170667 0 145.109333 62.08 145.109333 138.666667v277.248c0 76.586667-64.938667 138.624-145.066667 138.624-80.213333 0-145.152-62.08-145.152-138.624V250.666667c0-76.586667 64.981333-138.666667 145.109334-138.666667z m236.970666 411.221333c0 118.485333-93.013333 215.893333-212.608 228.992v135.637334a24.149333 24.149333 0 0 1-48.341333 0v-135.04c-122.709333-10.24-219.008-109.013333-219.008-229.546667v-96.426667a24.704 24.704 0 0 1 49.408 0v109.184c0 98.56 83.712 169.088 190.592 169.088 106.837333 0 196.309333-92.8 196.309333-169.088V423.936a21.845333 21.845333 0 0 1 43.690667 0v99.285333z\"/>`,\n // 麦克风关闭\n 'mic-off': `<path d=\"M656.384 559.232L369.877333 272.725333v-22.058666c0-76.586667 64.981333-138.666667 145.109334-138.666667 80.170667 0 145.109333 62.08 145.109333 138.666667v277.248c0 10.794667-1.28 21.248-3.712 31.317333z m-84.693333 96.341333a150.528 150.528 0 0 1-56.704 10.965334c-80.128 0-145.066667-62.08-145.066667-138.624v-74.154667l201.770667 201.813333z m153.898666-27.136l-29.610666-29.568a165.973333 165.973333 0 0 0 12.373333-62.848V423.936a21.845333 21.845333 0 0 1 43.648 0v99.285333c0 37.930667-9.557333 73.685333-26.453333 105.216z m-89.173333 91.904a245.290667 245.290667 0 0 1-97.066667 31.872v135.637334a24.149333 24.149333 0 0 1-48.341333 0v-135.04c-122.709333-10.24-219.008-109.013333-219.008-229.546667v-96.426667a24.704 24.704 0 0 1 49.408 0v109.184c0 98.56 86.613333 178.474667 193.450667 178.474667 33.92 0 65.834667-8.064 93.568-22.186667l28.032 28.032zM145.28 183.893333l45.226667-45.226666 678.826666 678.826666-45.226666 45.226667z\"/>`,\n // 摄像头开启\n 'camera-on': `<path d=\"M873.770667 314.922667c19.797333-12.202667 37.632-2.048 37.632 18.304v335.232c0 24.362667-15.872 32.512-37.632 18.261333l-112.938667-67.029333c-19.797333-12.202667-37.632-26.453333-37.632-46.72V424.618667c0-18.261333 17.834667-30.464 37.632-42.666667l112.938667-67.029333zM207.232 288h391.936c43.562667 0 79.232 32 79.232 71.125333v305.749334c0 39.125333-35.669333 71.125333-79.232 71.125333H207.232C163.669333 736 128 704 128 664.874667V359.125333C128 320 163.669333 288 207.232 288z\"/>`,\n // 摄像头关闭\n 'camera-off': `<path d=\"M873.770667 314.922667c19.797333-12.202667 37.632-2.048 37.632 18.304v335.232c0 24.362667-15.872 32.512-37.632 18.261333l-112.938667-67.029333c-19.797333-12.202667-37.632-26.453333-37.632-46.72V424.618667c0-18.261333 17.834667-30.464 37.632-42.666667l112.938667-67.029333zM385.152 288h214.016c43.562667 0 79.232 32 79.232 71.125333v222.122667L385.152 288z m256.042667 437.077333a85.333333 85.333333 0 0 1-42.026667 10.922667H207.232C163.669333 736 128 704 128 664.874667V359.125333c0-38.186667 34.005333-69.632 76.16-71.082666l437.034667 437.034666zM145.28 183.893333l45.226667-45.226666 678.826666 678.826666-45.226666 45.226667z\"/>`,\n // 声音(扬声器开启)\n 'sound-on': `<path d=\"M767.317333 767.018667a33.237333 33.237333 0 0 1-20.565333 6.656 34.56 34.56 0 0 1-25.386667-10.709334 34.986667 34.986667 0 0 1-10.666666-25.557333 35.2 35.2 0 0 1 13.994666-28.501333 5.76 5.76 0 0 0 1.450667-1.109334l0.725333-0.725333c13.738667-9.386667 26.453333-20.48 38.272-33.28 11.733333-12.842667 22.058667-26.794667 30.848-41.813333 8.832-15.061333 15.701333-31.146667 20.608-48.128a190.293333 190.293333 0 0 0 7.338667-52.906667c0-37.034667-9.301333-70.826667-27.946667-101.418667a250.794667 250.794667 0 0 0-69.845333-75.52 34.730667 34.730667 0 0 1-15.445333-29.568 34.986667 34.986667 0 0 1 10.666666-25.557333 34.56 34.56 0 0 1 25.386667-10.709333c8.32 0 15.658667 2.688 22.058667 8.106666 18.133333 12.842667 35.029333 27.776 50.730666 44.8 15.658667 17.066667 29.141333 35.541333 40.448 55.509334 11.264 19.968 20.053333 41.344 26.453334 64 6.4 22.698667 9.557333 46.165333 9.557333 70.314666 0 24.704-3.2 48.384-9.557333 71.082667a285.653333 285.653333 0 0 1-26.837334 64.384 305.450667 305.450667 0 0 1-92.288 100.693333v-0.042666z m-41.173333-332.330667c10.282667 14.336 18.517333 28.757333 24.661333 43.306667 6.101333 14.549333 9.173333 32.213333 9.173334 52.906666 0 19.754667-3.2 37.546667-9.557334 53.333334-6.4 15.786667-14.464 30.08-24.277333 42.88-3.925333 5.461333-8.106667 10.24-12.501333 14.464a23.552 23.552 0 0 1-16.896 6.272 29.866667 29.866667 0 0 1-22.442667-9.6 32.085333 32.085333 0 0 1-9.173333-22.954667 33.706667 33.706667 0 0 1 2.218666-11.861333h-0.768c2.474667-5.418667 5.418667-10.581333 8.832-15.530667 3.413333-4.949333 6.741333-9.984 9.941334-15.146667a84.48 84.48 0 0 0 8.106666-17.792c2.176-6.656 3.285333-14.677333 3.285334-24.064 0-8.874667-1.109333-16.64-3.328-23.296a94.208 94.208 0 0 0-8.106667-18.133333 240.170667 240.170667 0 0 0-9.898667-15.530667 67.072 67.072 0 0 1-8.106666-15.573333 29.994667 29.994667 0 0 1-2.176-11.818667c0-8.874667 3.072-16.512 9.173333-22.954666a29.866667 29.866667 0 0 1 22.442667-9.6c6.826667 0 12.501333 2.090667 16.896 6.272 4.437333 4.181333 8.576 9.002667 12.501333 14.421333z m-237.482667 439.637333a9078.869333 9078.869333 0 0 0-157.354666-139.136 10554.453333 10554.453333 0 0 0-83.797334-70.314666 177.408 177.408 0 0 1-16.213333 0.725333 125.781333 125.781333 0 0 1-13.952 0.768h-12.501333c-13.226667 0-24.021333-4.096-32.341334-12.202667a39.424 39.424 0 0 1-12.501333-29.269333v-186.453333h0.725333l-0.725333-0.768c0-13.824 4.266667-25.173333 12.885333-34.048a42.666667 42.666667 0 0 1 32-13.312h41.856a13012.906667 13012.906667 0 0 1 122.794667-103.637334c15.701333-13.312 31.146667-26.282667 46.336-38.826666 15.189333-12.586667 28.928-24.064 41.173333-34.432 12.245333-10.368 22.570667-18.986667 30.890667-25.898667 8.32-6.912 13.482667-11.093333 15.445333-12.586667 14.677333-10.88 26.581333-15.786667 35.626667-14.805333 9.088 0.981333 16.213333 5.418667 21.333333 13.312s8.704 17.493333 10.666667 28.885333c1.962667 11.349333 2.944 21.674667 2.944 31.061334v599.466666c0 9.386667-1.706667 19.413333-5.12 29.994667-3.456 10.624-8.746667 19.029333-15.829333 25.173333-7.125333 6.186667-16.042667 8.746667-26.88 7.765334-10.752-0.981333-23.253333-8.106667-37.461334-21.461334z\"/>`,\n // 静音(扬声器关闭)\n 'sound-off': `<path d=\"M767.872 670.72c10.624-11.946667 20.010667-24.917333 28.16-38.784 8.789333-15.018667 15.658667-31.061333 20.565333-48.085333a190.293333 190.293333 0 0 0 7.338667-52.906667c0-37.034667-9.301333-70.826667-27.946667-101.418667a250.794667 250.794667 0 0 0-69.845333-75.52 34.730667 34.730667 0 0 1-15.445333-29.568 34.986667 34.986667 0 0 1 10.666666-25.557333 34.56 34.56 0 0 1 25.386667-10.709333c8.32 0 15.658667 2.688 22.058667 8.106666 18.133333 12.842667 35.029333 27.776 50.730666 44.8 15.658667 17.066667 29.141333 35.541333 40.448 55.509334 11.264 19.968 20.053333 41.344 26.453334 64 6.4 22.698667 9.557333 46.165333 9.557333 70.314666 0 24.704-3.2 48.384-9.557333 71.082667a285.653333 285.653333 0 0 1-26.837334 64.384 304.64 304.64 0 0 1-40.533333 55.552l-51.2-51.2zM725.333333 628.224l-46.08-46.08c2.090667-3.072 4.096-6.229333 6.058667-9.386667a84.48 84.48 0 0 0 8.106667-17.792c2.176-6.656 3.285333-14.677333 3.285333-24.064 0-8.874667-1.109333-16.64-3.328-23.296a94.208 94.208 0 0 0-8.106667-18.133333 240.170667 240.170667 0 0 0-9.898666-15.530667 67.072 67.072 0 0 1-8.106667-15.573333 29.994667 29.994667 0 0 1-2.176-11.818667c0-8.874667 3.072-16.512 9.173333-22.954666a29.866667 29.866667 0 0 1 22.442667-9.6c6.826667 0 12.501333 2.090667 16.896 6.272 4.437333 4.181333 8.576 9.002667 12.501333 14.421333 10.282667 14.336 18.517333 28.757333 24.661334 43.306667 6.101333 14.549333 9.173333 32.213333 9.173333 52.906666 0 19.754667-3.2 37.546667-9.557333 53.333334A183.168 183.168 0 0 1 725.333333 628.181333zM278.997333 362.837333l294.954667 294.997334v175.061333c0 9.386667-1.706667 19.370667-5.12 29.952-3.456 10.624-8.746667 19.029333-15.829333 25.173333-7.125333 6.186667-16.042667 8.746667-26.88 7.765334-10.752-0.981333-23.253333-8.106667-37.461334-21.461334a9078.869333 9078.869333 0 0 0-157.354666-139.136 10554.453333 10554.453333 0 0 0-83.797334-70.314666 177.408 177.408 0 0 1-16.213333 0.725333 125.781333 125.781333 0 0 1-13.952 0.768h-12.501333c-13.226667 0-24.021333-4.096-32.341334-12.202667a39.424 39.424 0 0 1-12.501333-29.269333v-186.453333h0.725333l-0.725333-0.768c0-13.824 4.266667-25.173333 12.885333-34.048a42.666667 42.666667 0 0 1 32-13.312h41.856l32.256-27.434667z m98.261334-82.688c13.056-11.050667 25.898667-21.802667 38.613333-32.341333 15.189333-12.544 28.928-24.021333 41.173333-34.389333 12.245333-10.368 22.570667-18.986667 30.890667-25.898667 8.32-6.912 13.482667-11.093333 15.445333-12.586667 14.677333-10.88 26.581333-15.786667 35.626667-14.805333 9.088 0.981333 16.213333 5.418667 21.333333 13.312s8.704 17.493333 10.666667 28.885333c1.962667 11.349333 2.944 21.674667 2.944 31.061334v243.413333l-196.693333-196.693333zM145.28 183.893333l45.226667-45.226666 678.826666 678.826666-45.226666 45.226667z\"/>`,\n // 接听(电话听筒)\n answer: `<path d=\"M864.597333 647.338667l11.52 11.562666c0.341333 0.682667 0.597333 1.408 1.066667 2.005334 11.52 14.762667 14.848 32.213333 10.325333 52.266666-3.285333 14.421333-9.216 28.16-16.597333 41.386667-6.613333 11.861333-13.994667 23.381333-21.034667 34.986667l-9.045333 9.088c-5.845333 3.541333-11.477333 7.594667-17.493333 10.453333-15.018667 7.04-30.421333 8.533333-45.781334 8.490667-20.906667-0.042667-40.96-3.498667-60.970666-7.210667-32.810667-6.101333-64.896-13.952-94.933334-26.88-42.154667-18.133333-79.658667-42.922667-115.2-70.528a987.818667 987.818667 0 0 1-78.08-68.010667c-0.682667-0.682667-1.536-1.152-2.346666-1.706666l-44.373334-44.373334c-0.682667-0.938667-1.322667-1.962667-2.133333-2.816-19.072-22.058667-38.656-43.690667-57.216-66.218666a620.117333 620.117333 0 0 1-60.928-88.746667C236.245333 396.928 216.32 350.421333 212.608 296.362667c-1.450667-20.693333-4.224-41.088-2.389333-62.336 1.237333-14.677333 4.053333-29.184 11.52-43.008 2.048-3.797333 4.522667-7.381333 6.826666-11.050667l9.045334-9.045333c8.32-5.248 16.554667-10.538667 24.917333-15.616 13.909333-8.490667 27.733333-17.109333 42.965333-22.442667 15.957333-5.632 30.634667-4.778667 43.605334 3.2 4.906667 3.029333 9.429333 6.613333 14.122666 9.941333l8.789334 8.789334c0.768 1.066667 1.493333 2.133333 2.304 3.157333 4.266667 5.12 8.746667 10.026667 12.714666 15.36 23.637333 31.616 42.325333 66.56 58.581334 103.082667 8.448 19.029333 6.186667 38.741333-6.826667 58.154666-6.101333 9.045333-13.226667 17.621333-20.736 25.642667-14.506667 15.488-19.84 32.213333-15.616 50.56 1.536 6.570667 3.754667 12.928 6.570667 18.901333 9.258667 19.754667 22.826667 36.266667 37.76 51.626667 27.178667 27.861333 54.869333 55.338667 82.389333 82.901333 15.488 15.488 31.530667 30.378667 50.133333 42.24 11.52 7.381333 23.68 13.525333 37.973334 15.445334 14.08 1.962667 27.562667-1.365333 39.808-11.264 6.442667-5.205333 12.330667-11.136 18.645333-16.512 22.784-19.285333 46.506667-25.642667 71.850667-15.914667 37.546667 14.506667 71.253333 34.645333 102.826666 58.197333 4.778667 3.584 9.472 7.338667 14.208 10.965334z\"/>`,\n // 挂断\n hangup: `<path d=\"M68.820008 642.540165a22.883151 22.883151 0 0 0 13.903686 21.04864c2.800052 1.158641 5.793203 1.737961 8.689804 1.737961l216.37612-0.38621a22.786601 22.786601 0 0 0 22.786601-22.786601l0-87.670471c11.682955-13.807125 25.779752-25.296981 41.517949-33.986785a274.115036 274.115036 0 0 1 140.002394-34.276445l0.482771 0a270.832225 270.832225 0 0 1 139.616184 34.083335c15.641647 8.786365 29.641883 20.276209 41.228289 34.083346l0 87.1877a22.69004 22.69004 0 0 0 22.59349 22.786601l194.072289-0.28966 22.30383 0a22.59349 22.59349 0 0 0 22.690051-22.786601l0.09655-49.531883a22.207281 22.207281 0 0 0-0.67587-5.310432l-0.965541-3.958692a150.140499 150.140499 0 0 0-11.586405-46.828392c-43.352459-100.898276-222.748643-176.982342-417.303691-176.982342l-0.965541 0a868.01487 868.01487 0 0 0-229.024605 32.152275c-76.566836 21.72451-137.105804 52.138824-170.609819 85.64285-17.765829 17.283058-32.152275 37.752367-42.290379 60.442418-8.207045 18.441688-12.551947 38.428237-12.841607 58.607897l-0.09655 47.021491z\"/>`,\n // 举手\n handup: `<path d=\"M166.997333 665.6H150.613333c-0.725333-0.213333-1.450667-0.597333-2.133333-0.682667-18.602667-2.261333-33.28-12.245333-44.288-29.653333a155.818667 155.818667 0 0 1-17.536-41.002667c-3.712-13.056-6.613333-26.410667-9.898667-39.594666v-12.842667c1.621333-6.613333 2.730667-13.482667 4.992-19.754667 5.632-15.573333 15.488-27.52 26.368-38.4 14.805333-14.72 31.402667-26.453333 48.213334-37.973333 27.477333-18.901333 55.765333-36.010667 86.144-48.128 42.624-16.981333 86.613333-25.941333 131.285333-31.573333 34.346667-4.266667 68.778667-6.4 103.296-7.125334 0.981333 0 1.92-0.298667 2.901333-0.469333h62.72c1.152 0.170667 2.346667 0.426667 3.498667 0.512 29.098667 2.090667 58.24 3.541333 87.296 6.314667 35.712 3.413333 70.997333 10.112 105.856 19.712 48.981333 13.44 95.957333 32.256 136.832 67.84 15.616 13.653333 32 26.069333 45.781333 42.368 9.472 11.264 17.749333 23.466667 22.229334 38.570666 1.28 4.138667 2.005333 8.405333 2.986666 12.629334v12.8c-2.176 9.6-4.266667 19.2-6.570666 28.714666-3.84 15.786667-7.552 31.701333-14.506667 46.208-7.338667 15.274667-18.304 25.002667-33.109333 28.586667-5.632 1.28-11.349333 1.962667-17.066667 2.944h-12.373333c-1.28-0.213333-2.56-0.469333-3.84-0.597333-6.656-0.64-13.269333-0.938667-19.882667-1.877334-39.082667-5.632-77.013333-17.152-114.346667-31.488-19.413333-7.466667-31.701333-22.997333-36.266666-45.952a205.098667 205.098667 0 0 1-3.456-32.810666c-0.682667-21.162667-8.746667-36.778667-24.746667-46.762667a94.72 94.72 0 0 0-17.92-8.704c-20.565333-7.424-41.856-9.514667-63.232-9.813333-38.954667-0.469333-77.952-0.341333-116.906667-0.341334-21.888 0-43.776 0.810667-65.365333 5.546667-13.312 2.944-26.24 7.210667-37.717333 15.914667-11.349333 8.618667-18.517333 20.48-20.181334 36.181333-0.853333 8.192-0.853333 16.554667-1.493333 24.789333-2.474667 29.781333-14.805333 51.072-39.594667 62.08-36.736 16.298667-74.88 25.898667-113.834666 31.573334-5.973333 0.853333-11.861333 1.493333-17.792 2.261333z\"/>`,\n // 关闭(叉)\n close: `<path d=\"M649.179 512l212.839-212.84c37.881-37.881 37.881-99.298 0-137.179s-99.298-37.881-137.179 0L512 374.821l-212.839-212.84c-37.881-37.881-99.298-37.881-137.179 0s-37.881 99.298 0 137.179L374.821 512 161.982 724.84c-37.881 37.881-37.881 99.297 0 137.179 18.94 18.94 43.765 28.41 68.589 28.41 24.825 0 49.649-9.47 68.589-28.41L512 649.179l212.839 212.84c18.94 18.94 43.765 28.41 68.589 28.41s49.649-9.47 68.59-28.41c37.881-37.882 37.881-99.298 0-137.179L649.179 512z\"/>`,\n // 用户\n user: `<path d=\"M0 0h1024v1024H0V0z m614.8608 664.439467c-2.973013-30.685867-1.983147-53.4528-1.983147-82.158934 13.88544-7.918933 63.488-55.432533 67.437227-95.0272 10.922667-0.989867 28.757333-11.8784 33.716907-55.432533 2.976427-22.766933-7.932587-36.625067-14.875307-40.584533 17.851733-54.442667 25.78432-224.703147-99.170987-241.53088C587.093333 127.92832 554.366293 116.053333 511.72352 116.053333c-170.571093 2.9696-221.149867 130.6624-183.463253 276.176214-6.94272 3.959467-17.851733 16.827733-14.875307 40.584533 4.959573 43.554133 22.807893 54.442667 33.716907 55.432533 3.966293 40.584533 53.551787 88.098133 67.437226 95.0272 0 28.706133 0.989867 50.4832-1.98656 82.158934C378.835627 756.497067 157.696 708.983467 146.773333 907.946667h733.866667c-5.94944-201.936213-232.05888-151.453013-265.775787-243.510614z\"/>`,\n // 更多(三个点)\n more: `<circle cx=\"180\" cy=\"512\" r=\"90\"/><circle cx=\"512\" cy=\"512\" r=\"90\"/><circle cx=\"844\" cy=\"512\" r=\"90\"/>`,\n // 加号\n plus: `<path d=\"M480 224v256H224a32 32 0 1 0 0 64h256v256a32 32 0 1 0 64 0V544h256a32 32 0 1 0 0-64H544V224a32 32 0 1 0-64 0z\"/>`,\n // 减号\n minus: `<path d=\"M192 480h640a32 32 0 1 1 0 64H192a32 32 0 1 1 0-64z\"/>`,\n} as const\n\nexport type IconName = keyof typeof ICON_PATHS\n\nconst props = withDefaults(\n defineProps<{\n name: IconName\n size?: number | string\n color?: string\n title?: string\n }>(),\n {\n size: 16,\n color: 'currentColor',\n },\n)\n</script>\n\n<template>\n <svg\n :width=\"props.size\"\n :height=\"props.size\"\n viewBox=\"0 0 1024 1024\"\n xmlns=\"http://www.w3.org/2000/svg\"\n :fill=\"props.color\"\n :aria-label=\"props.title\"\n :aria-hidden=\"props.title ? undefined : 'true'\"\n v-html=\"ICON_PATHS[props.name]\"\n />\n</template>\n","<script setup lang=\"ts\">\nimport { ref, onMounted, onUnmounted, watch } from 'vue'\nimport type { CallType } from '@quitting/contract'\nimport vDraggable from '../directives/draggable'\nimport Icon from './Icon.vue'\n\n// 主叫/被叫统一组件——样式完全一致,行为也一致\n// 大窗默认显示远端(对方),小窗默认显示本地(自己)\n// 点击小窗切换大小窗的视频源(参考 im-web AtVideo 双击切换)\n// 控制栏悬浮于视频底部 30px 处,水平居中 3 个圆形按钮,hover 视频才显示\n\nconst props = defineProps<{\n mode: 'caller' | 'callee'\n calleeUid?: string\n callType: CallType\n onClose?: () => void\n}>()\n\nconst emit = defineEmits<{ (e: 'close'): void }>()\n\nconst status = ref<'connecting' | 'connected' | 'ended'>('connecting')\nconst deviceErrors = ref<string[]>([])\nconst endedReason = ref('')\nconst micEnabled = ref(true)\nconst cameraEnabled = ref(props.callType === 'video')\nconst hasCamera = ref(true)\nconst hasMic = ref(true)\n\n// 大窗 / 小窗 video 元素 ref\nconst mainVideo = ref<HTMLVideoElement | null>(null)\nconst pipVideo = ref<HTMLVideoElement | null>(null)\n// 视频源是否已交换:默认 main=远端、pip=本地;交换后 main=本地、pip=远端\n// 用户点击小窗 toggle\nconst swapped = ref(false)\n\nconst remotePlaying = ref(false)\nconst duration = ref(0)\nlet timer: ReturnType<typeof setInterval> | null = null\n\nfunction getClient() {\n return (window as any).__imClient\n}\n\nasync function syncDeviceCapability() {\n try {\n const devices = await navigator.mediaDevices.enumerateDevices()\n hasCamera.value = devices.some((d) => d.kind === 'videoinput')\n hasMic.value = devices.some((d) => d.kind === 'audioinput')\n if (!hasCamera.value) cameraEnabled.value = false\n if (!hasMic.value) micEnabled.value = false\n } catch (e) {\n console.warn('[im] enumerateDevices 失败,按默认能力继续', e)\n }\n}\n\n// 按 swapped 状态把本地/远端 track attach 到对应 video 元素\n// remoteTrackSubscribed / devices / mount fallback / 交换 都会调此方法\nfunction attachByRole() {\n const session = getClient()?.call?.currentSession\n if (!session) return\n // 先把两个元素上的所有 track detach,避免 LiveKit track 同时挂多元素导致画面重复\n if (mainVideo.value) session.detachAllVideoFromEl(mainVideo.value)\n if (pipVideo.value) session.detachAllVideoFromEl(pipVideo.value)\n if (swapped.value) {\n // 交换后:本地大窗、远端小窗\n if (mainVideo.value && hasCamera.value && props.callType === 'video') {\n session.attachLocalVideo(mainVideo.value)\n }\n if (pipVideo.value) {\n session.attachRemoteVideo(pipVideo.value)\n }\n } else {\n // 默认:远端大窗、本地小窗\n if (mainVideo.value) {\n session.attachRemoteVideo(mainVideo.value)\n }\n if (pipVideo.value && hasCamera.value && props.callType === 'video') {\n session.attachLocalVideo(pipVideo.value)\n }\n }\n}\n\nfunction swapVideo() {\n swapped.value = !swapped.value\n attachByRole()\n}\n\nonMounted(async () => {\n await syncDeviceCapability()\n const client = getClient()\n if (!client) {\n console.error('[im] client 未注入,无法发起通话')\n return\n }\n const session = client.call.currentSession\n if (!session) return\n\n session.onSession('connected', () => {\n status.value = 'connected'\n attachByRole()\n timer = setInterval(() => duration.value++, 1000)\n })\n session.onSession('remoteConnected', () => {\n remotePlaying.value = true\n attachByRole()\n })\n // 远端 track 异步流入触发——participantConnected 时 track 还没到\n session.onSession('remoteTrackSubscribed', (info: { kind: string }) => {\n if (info.kind === 'video') {\n remotePlaying.value = true\n attachByRole()\n }\n })\n session.onSession('remoteDisconnected', () => {\n remotePlaying.value = false\n })\n session.onSession('devices', (info: { mic: boolean; camera: boolean; errors: string[] }) => {\n if (info.errors && info.errors.length > 0) {\n deviceErrors.value = info.errors\n setTimeout(() => (deviceErrors.value = []), 3000)\n }\n // enableDevices 完成后重新 attach 本地 video\n if (info.camera) attachByRole()\n })\n session.onSession('ended', () => {\n status.value = 'ended'\n if (timer) clearInterval(timer)\n setTimeout(() => emit('close'), 1500)\n })\n\n // race 修复:session 可能已 connected(mount 前)\n if (session.isConnected) {\n status.value = 'connected'\n attachByRole()\n timer = setInterval(() => duration.value++, 1000)\n }\n\n // 远端 track 异步流入兜底\n if (props.callType === 'video') {\n let tries = 0\n const remoteRetry = setInterval(() => {\n tries++\n if (tries > 10 || remotePlaying.value) {\n clearInterval(remoteRetry)\n return\n }\n attachByRole()\n }, 300)\n }\n})\n\nonUnmounted(() => {\n if (timer) clearInterval(timer)\n})\n\nwatch(status, (s) => {\n if (s === 'ended') endedReason.value = '通话已结束'\n})\n\nasync function toggleMic() {\n if (!hasMic.value) return\n const session = getClient()?.call?.currentSession\n if (!session) return\n micEnabled.value = !micEnabled.value\n await session.setMicEnabled(micEnabled.value)\n}\n\nasync function toggleCamera() {\n if (!hasCamera.value) return\n const session = getClient()?.call?.currentSession\n if (!session) return\n cameraEnabled.value = !cameraEnabled.value\n await session.setCameraEnabled(cameraEnabled.value)\n // 摄像头开关后必须重新 attach(pub.track 引用变化)\n attachByRole()\n}\n\nasync function hangup() {\n endedReason.value = '已挂断'\n const client = getClient()\n await client?.call?.end('user_left')\n emit('close')\n}\n\nfunction formatDuration(s: number) {\n const m = Math.floor(s / 60)\n .toString()\n .padStart(2, '0')\n const sec = (s % 60).toString().padStart(2, '0')\n return `${m}:${sec}`\n}\n</script>\n\n<template>\n <div v-draggable=\"'.call-status-bar'\" class=\"call-window\">\n <!-- 视频区域(flex 占满,小窗叠加其上) -->\n <div class=\"video-area\">\n <!-- 大窗:默认远端,swapped 后本地 -->\n <video\n ref=\"mainVideo\"\n autoplay\n playsinline\n :class=\"[\n 'video-main',\n { 'network-error': status === 'connected' && !remotePlaying && !swapped },\n ]\"\n ></video>\n <!-- 小窗:默认本地,swapped 后远端。点击交换大小窗 -->\n <video\n v-if=\"callType === 'video'\"\n ref=\"pipVideo\"\n autoplay\n muted\n playsinline\n class=\"video-pip\"\n :title=\"swapped ? '点击切回对方主画面' : '点击切换自己为主画面'\"\n @click=\"swapVideo\"\n ></video>\n <!-- 连接中蒙层 -->\n <div v-if=\"status === 'connecting'\" class=\"connecting-overlay\">连接中...</div>\n <!-- 通话结束蒙层 -->\n <div v-else-if=\"status === 'ended'\" class=\"ended-overlay\">\n {{ endedReason || '通话已结束' }}\n </div>\n <!-- 设备失败提示(3 秒自动隐藏)-->\n <div v-if=\"deviceErrors.length\" class=\"device-warn\">\n {{ deviceErrors.join('; ') }}\n </div>\n <!-- 顶部状态栏(拖拽句柄) -->\n <div class=\"call-status-bar\">\n <span v-if=\"status === 'connecting'\">连接中</span>\n <span v-else-if=\"status === 'connected'\">{{ formatDuration(duration) }}</span>\n <span v-else>{{ endedReason || '通话已结束' }}</span>\n </div>\n </div>\n <!-- 底部控制栏:方形图标按钮,与会议窗口风格一致 -->\n <div class=\"controls-bar\">\n <button v-if=\"hasMic\" class=\"ctrl-btn\" :class=\"{ off: !micEnabled }\" @click=\"toggleMic\">\n <Icon :name=\"micEnabled ? 'mic-on' : 'mic-off'\" :size=\"22\" />\n </button>\n <button\n v-if=\"callType === 'video' && hasCamera\"\n class=\"ctrl-btn\"\n :class=\"{ off: !cameraEnabled }\"\n @click=\"toggleCamera\"\n >\n <Icon :name=\"cameraEnabled ? 'camera-on' : 'camera-off'\" :size=\"22\" />\n </button>\n <button class=\"ctrl-btn hangup\" @click=\"hangup\">\n <Icon name=\"hangup\" :size=\"22\" />\n </button>\n </div>\n </div>\n</template>\n\n<style scoped>\n/* 通话窗口:与会议窗口风格一致——无圆角、flex 布局、深色面板背景 */\n.call-window {\n position: fixed;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n width: min(640px, 90vw);\n aspect-ratio: 16 / 9;\n background: #0f172a;\n border-radius: 0;\n overflow: hidden;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);\n z-index: 9999;\n display: flex;\n flex-direction: column;\n}\n/* 视频区域:flex 占满,相对定位供小窗叠加 */\n.video-area {\n position: relative;\n flex: 1;\n overflow: hidden;\n}\n/* 大窗:铺满视频区域 */\n.video-main {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n object-fit: cover;\n background: #000;\n}\n.video-main.network-error {\n filter: grayscale(1) blur(2px);\n opacity: 0.6;\n}\n/* 小窗:右下角贴边,16:9 比例,无边框无圆角(与会议小视频一致) */\n.video-pip {\n position: absolute;\n right: 0;\n bottom: 0;\n width: 96px;\n aspect-ratio: 16 / 9;\n object-fit: cover;\n background: #000;\n border-radius: 0;\n cursor: pointer;\n z-index: 2;\n}\n.video-pip:hover {\n outline: 1px solid rgba(255, 255, 255, 0.3);\n}\n/* 顶部状态栏(拖拽句柄) */\n.call-status-bar {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n height: 32px;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 13px;\n color: rgba(255, 255, 255, 0.9);\n background: linear-gradient(to bottom, rgba(0, 0, 0, 0.4), transparent);\n cursor: move;\n z-index: 3;\n pointer-events: auto;\n}\n/* 底部控制栏:常驻显示,方形图标按钮(与会议窗口一致) */\n.controls-bar {\n flex: 0 0 auto;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 16px;\n padding: 6px 0 8px;\n background: #0f172a;\n z-index: 4;\n}\n.ctrl-btn {\n width: 32px;\n height: 32px;\n border: none;\n background: transparent;\n color: #fff;\n border-radius: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n opacity: 0.85;\n cursor: pointer;\n transition: opacity 0.15s;\n}\n.ctrl-btn:hover {\n opacity: 1;\n}\n.ctrl-btn:active {\n transform: scale(0.92);\n}\n.ctrl-btn.off {\n color: #ef4444;\n opacity: 1;\n}\n.ctrl-btn.hangup {\n color: #ef4444;\n}\n/* 连接中 / 通话结束蒙层 */\n.connecting-overlay,\n.ended-overlay {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n background: rgba(0, 0, 0, 0.7);\n color: #fff;\n font-size: 15px;\n z-index: 5;\n}\n/* 设备失败提示(顶部居中黄条) */\n.device-warn {\n position: absolute;\n top: 40px;\n left: 50%;\n transform: translateX(-50%);\n background: rgba(212, 160, 23, 0.92);\n color: #fff;\n font-size: 12px;\n padding: 4px 12px;\n border-radius: 4px;\n z-index: 6;\n pointer-events: none;\n max-width: 80%;\n text-align: center;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { ref, onMounted, onUnmounted, watch } from 'vue'\nimport type { CallType } from '@quitting/contract'\nimport vDraggable from '../directives/draggable'\nimport Icon from './Icon.vue'\n\n// 主叫/被叫统一组件——样式完全一致,行为也一致\n// 大窗默认显示远端(对方),小窗默认显示本地(自己)\n// 点击小窗切换大小窗的视频源(参考 im-web AtVideo 双击切换)\n// 控制栏悬浮于视频底部 30px 处,水平居中 3 个圆形按钮,hover 视频才显示\n\nconst props = defineProps<{\n mode: 'caller' | 'callee'\n calleeUid?: string\n callType: CallType\n onClose?: () => void\n}>()\n\nconst emit = defineEmits<{ (e: 'close'): void }>()\n\nconst status = ref<'connecting' | 'connected' | 'ended'>('connecting')\nconst deviceErrors = ref<string[]>([])\nconst endedReason = ref('')\nconst micEnabled = ref(true)\nconst cameraEnabled = ref(props.callType === 'video')\nconst hasCamera = ref(true)\nconst hasMic = ref(true)\n\n// 大窗 / 小窗 video 元素 ref\nconst mainVideo = ref<HTMLVideoElement | null>(null)\nconst pipVideo = ref<HTMLVideoElement | null>(null)\n// 视频源是否已交换:默认 main=远端、pip=本地;交换后 main=本地、pip=远端\n// 用户点击小窗 toggle\nconst swapped = ref(false)\n\nconst remotePlaying = ref(false)\nconst duration = ref(0)\nlet timer: ReturnType<typeof setInterval> | null = null\n\nfunction getClient() {\n return (window as any).__imClient\n}\n\nasync function syncDeviceCapability() {\n try {\n const devices = await navigator.mediaDevices.enumerateDevices()\n hasCamera.value = devices.some((d) => d.kind === 'videoinput')\n hasMic.value = devices.some((d) => d.kind === 'audioinput')\n if (!hasCamera.value) cameraEnabled.value = false\n if (!hasMic.value) micEnabled.value = false\n } catch (e) {\n console.warn('[im] enumerateDevices 失败,按默认能力继续', e)\n }\n}\n\n// 按 swapped 状态把本地/远端 track attach 到对应 video 元素\n// remoteTrackSubscribed / devices / mount fallback / 交换 都会调此方法\nfunction attachByRole() {\n const session = getClient()?.call?.currentSession\n if (!session) return\n // 先把两个元素上的所有 track detach,避免 LiveKit track 同时挂多元素导致画面重复\n if (mainVideo.value) session.detachAllVideoFromEl(mainVideo.value)\n if (pipVideo.value) session.detachAllVideoFromEl(pipVideo.value)\n if (swapped.value) {\n // 交换后:本地大窗、远端小窗\n if (mainVideo.value && hasCamera.value && props.callType === 'video') {\n session.attachLocalVideo(mainVideo.value)\n }\n if (pipVideo.value) {\n session.attachRemoteVideo(pipVideo.value)\n }\n } else {\n // 默认:远端大窗、本地小窗\n if (mainVideo.value) {\n session.attachRemoteVideo(mainVideo.value)\n }\n if (pipVideo.value && hasCamera.value && props.callType === 'video') {\n session.attachLocalVideo(pipVideo.value)\n }\n }\n}\n\nfunction swapVideo() {\n swapped.value = !swapped.value\n attachByRole()\n}\n\nonMounted(async () => {\n await syncDeviceCapability()\n const client = getClient()\n if (!client) {\n console.error('[im] client 未注入,无法发起通话')\n return\n }\n const session = client.call.currentSession\n if (!session) return\n\n session.onSession('connected', () => {\n status.value = 'connected'\n attachByRole()\n timer = setInterval(() => duration.value++, 1000)\n })\n session.onSession('remoteConnected', () => {\n remotePlaying.value = true\n attachByRole()\n })\n // 远端 track 异步流入触发——participantConnected 时 track 还没到\n session.onSession('remoteTrackSubscribed', (info: { kind: string }) => {\n if (info.kind === 'video') {\n remotePlaying.value = true\n attachByRole()\n }\n })\n session.onSession('remoteDisconnected', () => {\n remotePlaying.value = false\n })\n session.onSession('devices', (info: { mic: boolean; camera: boolean; errors: string[] }) => {\n if (info.errors && info.errors.length > 0) {\n deviceErrors.value = info.errors\n setTimeout(() => (deviceErrors.value = []), 3000)\n }\n // enableDevices 完成后重新 attach 本地 video\n if (info.camera) attachByRole()\n })\n session.onSession('ended', () => {\n status.value = 'ended'\n if (timer) clearInterval(timer)\n setTimeout(() => emit('close'), 1500)\n })\n\n // race 修复:session 可能已 connected(mount 前)\n if (session.isConnected) {\n status.value = 'connected'\n attachByRole()\n timer = setInterval(() => duration.value++, 1000)\n }\n\n // 远端 track 异步流入兜底\n if (props.callType === 'video') {\n let tries = 0\n const remoteRetry = setInterval(() => {\n tries++\n if (tries > 10 || remotePlaying.value) {\n clearInterval(remoteRetry)\n return\n }\n attachByRole()\n }, 300)\n }\n})\n\nonUnmounted(() => {\n if (timer) clearInterval(timer)\n})\n\nwatch(status, (s) => {\n if (s === 'ended') endedReason.value = '通话已结束'\n})\n\nasync function toggleMic() {\n if (!hasMic.value) return\n const session = getClient()?.call?.currentSession\n if (!session) return\n micEnabled.value = !micEnabled.value\n await session.setMicEnabled(micEnabled.value)\n}\n\nasync function toggleCamera() {\n if (!hasCamera.value) return\n const session = getClient()?.call?.currentSession\n if (!session) return\n cameraEnabled.value = !cameraEnabled.value\n await session.setCameraEnabled(cameraEnabled.value)\n // 摄像头开关后必须重新 attach(pub.track 引用变化)\n attachByRole()\n}\n\nasync function hangup() {\n endedReason.value = '已挂断'\n const client = getClient()\n await client?.call?.end('user_left')\n emit('close')\n}\n\nfunction formatDuration(s: number) {\n const m = Math.floor(s / 60)\n .toString()\n .padStart(2, '0')\n const sec = (s % 60).toString().padStart(2, '0')\n return `${m}:${sec}`\n}\n</script>\n\n<template>\n <div v-draggable=\"'.call-status-bar'\" class=\"call-window\">\n <!-- 视频区域(flex 占满,小窗叠加其上) -->\n <div class=\"video-area\">\n <!-- 大窗:默认远端,swapped 后本地 -->\n <video\n ref=\"mainVideo\"\n autoplay\n playsinline\n :class=\"[\n 'video-main',\n { 'network-error': status === 'connected' && !remotePlaying && !swapped },\n ]\"\n ></video>\n <!-- 小窗:默认本地,swapped 后远端。点击交换大小窗 -->\n <video\n v-if=\"callType === 'video'\"\n ref=\"pipVideo\"\n autoplay\n muted\n playsinline\n class=\"video-pip\"\n :title=\"swapped ? '点击切回对方主画面' : '点击切换自己为主画面'\"\n @click=\"swapVideo\"\n ></video>\n <!-- 连接中蒙层 -->\n <div v-if=\"status === 'connecting'\" class=\"connecting-overlay\">连接中...</div>\n <!-- 通话结束蒙层 -->\n <div v-else-if=\"status === 'ended'\" class=\"ended-overlay\">\n {{ endedReason || '通话已结束' }}\n </div>\n <!-- 设备失败提示(3 秒自动隐藏)-->\n <div v-if=\"deviceErrors.length\" class=\"device-warn\">\n {{ deviceErrors.join('; ') }}\n </div>\n <!-- 顶部状态栏(拖拽句柄) -->\n <div class=\"call-status-bar\">\n <span v-if=\"status === 'connecting'\">连接中</span>\n <span v-else-if=\"status === 'connected'\">{{ formatDuration(duration) }}</span>\n <span v-else>{{ endedReason || '通话已结束' }}</span>\n </div>\n </div>\n <!-- 底部控制栏:方形图标按钮,与会议窗口风格一致 -->\n <div class=\"controls-bar\">\n <button v-if=\"hasMic\" class=\"ctrl-btn\" :class=\"{ off: !micEnabled }\" @click=\"toggleMic\">\n <Icon :name=\"micEnabled ? 'mic-on' : 'mic-off'\" :size=\"22\" />\n </button>\n <button\n v-if=\"callType === 'video' && hasCamera\"\n class=\"ctrl-btn\"\n :class=\"{ off: !cameraEnabled }\"\n @click=\"toggleCamera\"\n >\n <Icon :name=\"cameraEnabled ? 'camera-on' : 'camera-off'\" :size=\"22\" />\n </button>\n <button class=\"ctrl-btn hangup\" @click=\"hangup\">\n <Icon name=\"hangup\" :size=\"22\" />\n </button>\n </div>\n </div>\n</template>\n\n<style scoped>\n/* 通话窗口:与会议窗口风格一致——无圆角、flex 布局、深色面板背景 */\n.call-window {\n position: fixed;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n width: min(640px, 90vw);\n aspect-ratio: 16 / 9;\n background: #0f172a;\n border-radius: 0;\n overflow: hidden;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);\n z-index: 9999;\n display: flex;\n flex-direction: column;\n}\n/* 视频区域:flex 占满,相对定位供小窗叠加 */\n.video-area {\n position: relative;\n flex: 1;\n overflow: hidden;\n}\n/* 大窗:铺满视频区域 */\n.video-main {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n object-fit: cover;\n background: #000;\n}\n.video-main.network-error {\n filter: grayscale(1) blur(2px);\n opacity: 0.6;\n}\n/* 小窗:右下角贴边,16:9 比例,无边框无圆角(与会议小视频一致) */\n.video-pip {\n position: absolute;\n right: 0;\n bottom: 0;\n width: 96px;\n aspect-ratio: 16 / 9;\n object-fit: cover;\n background: #000;\n border-radius: 0;\n cursor: pointer;\n z-index: 2;\n}\n.video-pip:hover {\n outline: 1px solid rgba(255, 255, 255, 0.3);\n}\n/* 顶部状态栏(拖拽句柄) */\n.call-status-bar {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n height: 32px;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 13px;\n color: rgba(255, 255, 255, 0.9);\n background: linear-gradient(to bottom, rgba(0, 0, 0, 0.4), transparent);\n cursor: move;\n z-index: 3;\n pointer-events: auto;\n}\n/* 底部控制栏:常驻显示,方形图标按钮(与会议窗口一致) */\n.controls-bar {\n flex: 0 0 auto;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 16px;\n padding: 6px 0 8px;\n background: #0f172a;\n z-index: 4;\n}\n.ctrl-btn {\n width: 32px;\n height: 32px;\n border: none;\n background: transparent;\n color: #fff;\n border-radius: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n opacity: 0.85;\n cursor: pointer;\n transition: opacity 0.15s;\n}\n.ctrl-btn:hover {\n opacity: 1;\n}\n.ctrl-btn:active {\n transform: scale(0.92);\n}\n.ctrl-btn.off {\n color: #ef4444;\n opacity: 1;\n}\n.ctrl-btn.hangup {\n color: #ef4444;\n}\n/* 连接中 / 通话结束蒙层 */\n.connecting-overlay,\n.ended-overlay {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n background: rgba(0, 0, 0, 0.7);\n color: #fff;\n font-size: 15px;\n z-index: 5;\n}\n/* 设备失败提示(顶部居中黄条) */\n.device-warn {\n position: absolute;\n top: 40px;\n left: 50%;\n transform: translateX(-50%);\n background: rgba(212, 160, 23, 0.92);\n color: #fff;\n font-size: 12px;\n padding: 4px 12px;\n border-radius: 4px;\n z-index: 6;\n pointer-events: none;\n max-width: 80%;\n text-align: center;\n}\n</style>\n","export default \"data:audio/mpeg;base64,//uQBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASW5mbwAAAA8AAAB0AAC/BAAEBggLDQ8TFhgaHB4jJScpLC4yNDc5Oz1CREZIS01PU1ZYWlxeY2VnaWxucnR3eXt9goSGiIuNj5OWmJqcnqOlp6msrrK0t7m7vcLExsjLzc/T1tja3N7j5efp7O7y9Pf5+/0AAAA8TEFNRTMuMTAwAQAAAAAAAAAAABSAJAL0ZgAAgAAAvwRQljMCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//uQBAAP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAETEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uSBECP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kgRAj/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVdLVEUVJMoAkQIREQYQgmAwGTJkyZMmEECBAgQIECBMmTsLTvwTTvxBAgQL/+5IEQI/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARkyad//xnvf/ZAgQQ/e7IECCER3sgQIECEREE73+z0///+0ECZMmTJk073///+Lvf3u7IECBAgQIIQTTvWiIz97s8mTJkCAAAAAAAAIELsmTJk07//u7PJkyBAghGf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VVVVVVVVVVUD8aij///xVixUUFsWFWf+rrFRbFhcV4t//4szigH41FH///irFiooLYsKs/9XWKi2LC4rxb//xZnFFUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5AEXo/w6QC4mAEQDB0gFxMAIgGAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5IEQI/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uSBECP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kgRAj/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5IEQI/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uSBECP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kgRAj/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5IEQI/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uSBECP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kgRAj/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5IEQI/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uSBECP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kgRAj/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5IEQI/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uSBECP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kgRAj/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5IEQI/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uSBECP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kgRAj/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5IEQI/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uSBECP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kgRAj/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5IEQI/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uSBECP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kgRAj/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5AEQQ/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5IEQI/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uSBECP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kgRAj/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5IEQI/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uSBECP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kgRAj/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5IEQI/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uSBECP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kgRAj/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5IEQI/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uSBECP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kgRAj/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5IEQI/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uSBECP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kgRAj/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5IEQI/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uSBECP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kgRAj/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5IEQI/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uSBECP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kgRAj/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5IEQI/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uSBECP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kARBD/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kgRAj/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5IEQI/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uSBECP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kgRAj/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5IEQI/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uSBECP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kgRAj/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5IEQI/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uSBECP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kgRAj/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5IEQI/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uSBECP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kgRAj/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5IEQI/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uSBECP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kgRAj/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5IEQI/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV\"","export default \"data:audio/mpeg;base64,SUQzBAAAAAAiNlRDT04AAAAHAAADQmx1ZXMAUFJJVgAAIXQAAFhNUAA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/Pgo8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjYtYzE0MyA3OS4xNjEyMTAsIDIwMTcvMDgvMTEtMTA6Mjg6MzYgICAgICAgICI+CiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iCiAgICB4bWxuczp4bXBETT0iaHR0cDovL25zLmFkb2JlLmNvbS94bXAvMS4wL0R5bmFtaWNNZWRpYS8iCiAgICB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIKICAgIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiCiAgICB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIKICAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIKICAgeG1wOkNyZWF0b3JUb29sPSJGaWxlIGNyZWF0ZWQgYnkgR29sZFdhdmUuICBHb2xkV2F2ZSBjb3B5cmlnaHQgKEMpIENocmlzIENyYWlnIgogICB4bXA6TWV0YWRhdGFEYXRlPSIyMDE3LTExLTAxVDE5OjI1OjUyKzA4OjAwIgogICB4bXA6TW9kaWZ5RGF0ZT0iMjAxNy0xMS0wMVQxOToyNTo1MiswODowMCIKICAgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDplZWRmZWNkMi0xNTYzLWJmNDUtOTQxZS03OWJjMzhjZmU0OGEiCiAgIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6ZWVkZmVjZDItMTU2My1iZjQ1LTk0MWUtNzliYzM4Y2ZlNDhhIgogICB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6NmI2MmI5ZGEtOThmNC03NjRkLWE2MGItMmZlOGFkYTZkMjYxIgogICBkYzpmb3JtYXQ9ImF1ZGlvL21wZWciPgogICA8eG1wRE06VHJhY2tzPgogICAgPHJkZjpCYWc+CiAgICAgPHJkZjpsaQogICAgICB4bXBETTp0cmFja05hbWU9IkN1ZVBvaW50IE1hcmtlcnMiCiAgICAgIHhtcERNOnRyYWNrVHlwZT0iQ3VlIgogICAgICB4bXBETTpmcmFtZVJhdGU9ImY4MDAwIi8+CiAgICAgPHJkZjpsaQogICAgICB4bXBETTp0cmFja05hbWU9IkNEIFRyYWNrIE1hcmtlcnMiCiAgICAgIHhtcERNOnRyYWNrVHlwZT0iVHJhY2siCiAgICAgIHhtcERNOmZyYW1lUmF0ZT0iZjgwMDAiLz4KICAgICA8cmRmOmxpCiAgICAgIHhtcERNOnRyYWNrTmFtZT0iU3ViY2xpcCBNYXJrZXJzIgogICAgICB4bXBETTp0cmFja1R5cGU9IkluT3V0IgogICAgICB4bXBETTpmcmFtZVJhdGU9ImY4MDAwIi8+CiAgICA8L3JkZjpCYWc+CiAgIDwveG1wRE06VHJhY2tzPgogICA8eG1wTU06SGlzdG9yeT4KICAgIDxyZGY6U2VxPgogICAgIDxyZGY6bGkKICAgICAgc3RFdnQ6YWN0aW9uPSJzYXZlZCIKICAgICAgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDo2YjYyYjlkYS05OGY0LTc2NGQtYTYwYi0yZmU4YWRhNmQyNjEiCiAgICAgIHN0RXZ0OndoZW49IjIwMTctMTEtMDFUMTk6MjU6NTIrMDg6MDAiCiAgICAgIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIEF1ZGl0aW9uIENDIDIwMTguMCAoV2luZG93cykiCiAgICAgIHN0RXZ0OmNoYW5nZWQ9Ii9tZXRhZGF0YSIvPgogICAgIDxyZGY6bGkKICAgICAgc3RFdnQ6YWN0aW9uPSJzYXZlZCIKICAgICAgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDplZWRmZWNkMi0xNTYzLWJmNDUtOTQxZS03OWJjMzhjZmU0OGEiCiAgICAgIHN0RXZ0OndoZW49IjIwMTctMTEtMDFUMTk6MjU6NTIrMDg6MDAiCiAgICAgIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIEF1ZGl0aW9uIENDIDIwMTguMCAoV2luZG93cykiCiAgICAgIHN0RXZ0OmNoYW5nZWQ9Ii8iLz4KICAgIDwvcmRmOlNlcT4KICAgPC94bXBNTTpIaXN0b3J5PgogICA8eG1wTU06RGVyaXZlZEZyb20KICAgIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6NmI2MmI5ZGEtOThmNC03NjRkLWE2MGItMmZlOGFkYTZkMjYxIgogICAgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo2YjYyYjlkYS05OGY0LTc2NGQtYTYwYi0yZmU4YWRhNmQyNjEiCiAgICBzdFJlZjpvcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6NmI2MmI5ZGEtOThmNC03NjRkLWE2MGItMmZlOGFkYTZkMjYxIi8+CiAgPC9yZGY6RGVzY3JpcHRpb24+CiA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgCjw/eHBhY2tldCBlbmQ9InciPz4AVFNTRQAAABMAAANVUFlVTiBUcmFuc2NvZGluZwAAAAAAAAAAAAAA/+PIwAAAAAAAAAAAAEluZm8AAAAPAAAAKwAAZUAAEREWFhwcIiIiJyctLTMzMzg4Pj5ERERJSU9PVVVVW1tgYGZmZmxscXF3d3d9fYKCiIiIjo6T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AExBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq/+OIxAAAAANIAAAAAExBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq/+OIxAAAAANIAAAAAExBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq/+OIxAAAAANIAAAAAExBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq/+OIxAAAAANIAAAAAExBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq/+OIxAAAAANIAAAAAExBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq/+OIxAAAAANIAAAAAExBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq/+OIxAAAAANIAAAAAExBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq/+OIxAAAAANIAAAAAExBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq/+OIxAAAAANIAAAAAExBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq/+OIxAAAAANIAAAAAExBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq/+OIxAAAAANIAAAAAExBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq/+OIxAAAAANIAAAAAExBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq/+OIxAAAAANIAAAAAExBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq/+OIxAAAAANIAAAAAExBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq/+OIxAAAAANIAAAAAExBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq/+OIxAAAAANIAAAAAExBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq/+OIxAAAAANIAAAAAExBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq/+OIxAAAAANIAAAAAExBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq/+OIxAAAAANIAAAAAExBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq/+OIxAAAAANIAAAAAExBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq/+OIxAAAAANIAAAAAExBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq/+OIxAAAAANIAAAAAExBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\"","<script setup lang=\"ts\">\nimport { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'\nimport type { CallType } from '@quitting/contract'\nimport vDraggable from '../directives/draggable'\nimport ringCallerUrl from '../sounds/ring-caller.mp3'\nimport ringCalleeUrl from '../sounds/ring-callee.mp3'\n\n// 统一通话邀请/等待弹窗\n// mode='outgoing':主叫呼叫等待(响 caller.mp3)\n// - 显示\"呼叫中...\"动画 + 红色\"取消\"按钮\n// - 收到 call:accepted → emit('connected')\n// - 收到 call:rejected/busy/no_answer → emit('close')\n// - 用户点取消 → 调 client.call.end('cancelled', callId) → emit('close')\n// mode='incoming':被叫响铃(响 callee.mp3)\n// - 显示\"邀请你通话\" + 绿色\"接听\" + 红色\"拒接\" 按钮\n// - 用户点接听 → await client.call.accept() → emit('accepted', session) → emit('close')\n// - 用户点拒接 → client.call.end('rejected', callId) → emit('close')\n// - 收到 call:cancelled → emit('close')\n\nconst props = defineProps<{\n mode: 'outgoing' | 'incoming'\n peerUid: string\n peerName?: string\n callType: CallType\n // incoming 必须传 callId(来自 call:invite 事件);outgoing 可省略(取 currentSession.callId)\n callId?: string\n // 主叫超时秒数(默认 30s)\n timeoutSec?: number\n}>()\n\nconst emit = defineEmits<{\n (e: 'close'): void\n // outgoing 收到 call:accepted 时触发——父组件切到 CallWindow\n (e: 'connected'): void\n // incoming 接听成功时触发,携带 session——父组件切到 CallWindow\n (e: 'accepted', session: unknown): void\n}>()\n\ntype DialogStatus = 'ringing' | 'connected' | 'rejected' | 'busy' | 'no_answer' | 'cancelled'\nconst status = ref<DialogStatus>('ringing')\nconst statusText = ref('')\nconst ringAudio = ref<HTMLAudioElement | null>(null)\n\nconst unsubscribers: Array<() => void> = []\nlet timeoutTimer: ReturnType<typeof setTimeout> | null = null\n\nfunction getClient() {\n return (window as any).__imClient\n}\n\nfunction startRing() {\n if (!ringAudio.value) return\n ringAudio.value.currentTime = 0\n ringAudio.value.play().catch(() => {})\n}\n\nfunction stopRing() {\n if (!ringAudio.value) return\n ringAudio.value.pause()\n ringAudio.value.currentTime = 0\n}\n\n// 对端展示名:外部传 peerName 优先;未传时查 SDK 用户目录(client.users),\n// 仍无则回退 peerUid 本身——业务方零配置也能显示姓名\nconst fallbackName = ref('')\nwatch(\n () => props.peerUid,\n async (uid) => {\n fallbackName.value = ''\n if (props.peerName || !uid) return\n const dir = getClient()?.users\n if (!dir?.resolve) return\n const u = await dir.resolve(uid).catch(() => undefined)\n if (u?.name) fallbackName.value = u.name\n },\n { immediate: true },\n)\nconst displayPeerName = computed(() => props.peerName || fallbackName.value || props.peerUid)\nconst avatarText = computed(() => displayPeerName.value[0].toUpperCase())\nconst statusLabel = computed(() => {\n if (status.value === 'ringing') {\n return props.mode === 'outgoing' ? '呼叫中' : '邀请你通话'\n }\n return statusText.value\n})\n\nfunction setStatusAndClose(s: DialogStatus, text: string, delay = 1500) {\n status.value = s\n statusText.value = text\n setTimeout(() => emit('close'), delay)\n}\n\n// outgoing 模式:主叫取消\nasync function cancelOutgoing() {\n stopRing()\n status.value = 'cancelled'\n statusText.value = '已取消'\n const client = getClient()\n const cid = props.callId || client?.call?.currentSession?.callId\n await client?.call?.end('cancelled', cid).catch(() => {})\n setTimeout(() => emit('close'), 800)\n}\n\n// incoming 模式:被叫接听\nasync function acceptIncoming() {\n stopRing()\n const client = getClient()\n if (!client || !props.callId) return\n status.value = 'connected'\n statusText.value = '已接听'\n try {\n const session = await client.call.accept(props.callId, props.callType)\n emit('accepted', session)\n setTimeout(() => emit('close'), 200)\n } catch (err: any) {\n // connect 失败、设备异常等——SDK 已清 currentSession + 调后端兜底\n // UI 显示失败提示后关闭,让用户重试\n console.error('[im] 接听失败', err)\n status.value = 'rejected'\n statusText.value = err?.message || '接听失败'\n setTimeout(() => emit('close'), 1500)\n }\n}\n\n// incoming 模式:被叫拒接\nasync function rejectIncoming() {\n stopRing()\n status.value = 'rejected'\n statusText.value = '已拒接'\n const client = getClient()\n await client?.call?.end('rejected', props.callId).catch(() => {})\n setTimeout(() => emit('close'), 600)\n}\n\nonMounted(() => {\n const client = getClient()\n if (!client) {\n emit('close')\n return\n }\n startRing()\n\n if (props.mode === 'outgoing') {\n // 主叫监听被叫响应\n unsubscribers.push(\n client.on('call:accepted', () => {\n stopRing()\n status.value = 'connected'\n statusText.value = '对方已接听'\n setTimeout(() => emit('connected'), 300)\n }),\n )\n unsubscribers.push(client.on('call:rejected', () => setStatusAndClose('rejected', '对方拒接')))\n unsubscribers.push(client.on('call:busy', () => setStatusAndClose('busy', '对方繁忙')))\n unsubscribers.push(\n client.on('call:no_answer', () => setStatusAndClose('no_answer', '无人接听')),\n )\n // 客户端超时兜底\n const sec = props.timeoutSec ?? 30\n timeoutTimer = setTimeout(() => {\n if (status.value === 'ringing') setStatusAndClose('no_answer', '无人接听')\n }, sec * 1000)\n } else {\n // incoming 监听主叫取消 + 无人接听超时\n // no_answer 必须也监听:后端超时会同时通知主叫和被叫,\n // 只等 cancelled 的话被叫端铃声在无人接听后会永不停止\n unsubscribers.push(\n client.on('call:cancelled', () => {\n stopRing()\n setStatusAndClose('cancelled', '对方已取消', 600)\n }),\n )\n unsubscribers.push(\n client.on('call:no_answer', () => {\n stopRing()\n setStatusAndClose('cancelled', '未接听,已超时', 600)\n }),\n )\n }\n})\n\nonBeforeUnmount(() => {\n stopRing()\n unsubscribers.forEach((fn) => fn())\n if (timeoutTimer) clearTimeout(timeoutTimer)\n})\n</script>\n\n<template>\n <div v-draggable=\"'.call-dialog-status'\" class=\"call-dialog\">\n <audio\n ref=\"ringAudio\"\n :src=\"mode === 'outgoing' ? ringCallerUrl : ringCalleeUrl\"\n loop\n preload=\"auto\"\n />\n <div class=\"call-dialog-header\">\n <span>{{ callType === 'video' ? '视频通话' : '语音通话' }}</span>\n </div>\n <div class=\"call-dialog-body\">\n <div :class=\"['avatar', status === 'ringing' ? 'ringing' : status]\">\n <span>{{ avatarText }}</span>\n </div>\n <div class=\"peer-name\">{{ displayPeerName }}</div>\n <div class=\"status-text\">\n <template v-if=\"status === 'ringing'\">\n {{ statusLabel }}<span class=\"dots\"><i>.</i><i>.</i><i>.</i></span>\n </template>\n <template v-else>\n <span :class=\"status\">{{ statusText }}</span>\n </template>\n </div>\n </div>\n <div class=\"call-dialog-actions\">\n <!-- outgoing 模式:只显示取消 -->\n <template v-if=\"mode === 'outgoing'\">\n <button v-if=\"status === 'ringing'\" class=\"btn cancel\" @click=\"cancelOutgoing\">取消</button>\n </template>\n <!-- incoming 模式:接听 + 拒接 -->\n <template v-else>\n <button v-if=\"status === 'ringing'\" class=\"btn reject\" @click=\"rejectIncoming\">拒接</button>\n <button v-if=\"status === 'ringing'\" class=\"btn accept\" @click=\"acceptIncoming\">接听</button>\n </template>\n </div>\n </div>\n</template>\n\n<style scoped>\n.call-dialog {\n position: fixed;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n width: 320px;\n background: #0f172a;\n border-radius: 0;\n padding: 20px 20px 28px;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);\n color: #fff;\n z-index: 9999;\n text-align: center;\n}\n.call-dialog-header {\n cursor: move;\n font-size: 12px;\n color: #999;\n padding-bottom: 10px;\n border-bottom: 1px solid #333;\n margin-bottom: 16px;\n}\n.avatar {\n width: 80px;\n height: 80px;\n border-radius: 50%;\n background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n color: #fff;\n font-size: 36px;\n font-weight: 600;\n line-height: 80px;\n margin: 0 auto 12px;\n position: relative;\n}\n.avatar.ringing::before {\n content: '';\n position: absolute;\n inset: -8px;\n border: 2px solid #667eea;\n border-radius: 50%;\n animation: pulse 1.5s ease-out infinite;\n}\n.avatar.rejected,\n.avatar.cancelled {\n background: #888;\n}\n.avatar.busy,\n.avatar.no_answer {\n background: #d4a017;\n}\n.avatar.connected {\n background: #52c41a;\n}\n@keyframes pulse {\n 0% {\n transform: scale(1);\n opacity: 0.8;\n }\n 100% {\n transform: scale(1.4);\n opacity: 0;\n }\n}\n.peer-name {\n font-size: 16px;\n font-weight: 600;\n margin-bottom: 6px;\n}\n.status-text {\n font-size: 13px;\n color: #bbb;\n min-height: 20px;\n}\n.status-text .rejected,\n.status-text .cancelled {\n color: #888;\n}\n.status-text .busy,\n.status-text .no_answer {\n color: #d4a017;\n}\n.status-text .connected {\n color: #52c41a;\n}\n.dots i {\n display: inline-block;\n font-style: normal;\n animation: dot-bounce 1.4s infinite;\n}\n.dots i:nth-child(2) {\n animation-delay: 0.2s;\n}\n.dots i:nth-child(3) {\n animation-delay: 0.4s;\n}\n@keyframes dot-bounce {\n 0%,\n 60%,\n 100% {\n opacity: 0.3;\n }\n 30% {\n opacity: 1;\n }\n}\n.call-dialog-actions {\n margin-top: 20px;\n display: flex;\n justify-content: center;\n gap: 20px;\n min-height: 44px;\n}\n.btn {\n padding: 8px 24px;\n border: none;\n border-radius: 0;\n cursor: pointer;\n font-size: 14px;\n font-weight: 500;\n color: #fff;\n transition:\n transform 0.1s,\n opacity 0.1s;\n}\n.btn:active {\n transform: scale(0.96);\n}\n.btn.accept {\n background: #52c41a;\n}\n.btn.reject,\n.btn.cancel {\n background: #ef4444;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'\nimport type { CallType } from '@quitting/contract'\nimport vDraggable from '../directives/draggable'\nimport ringCallerUrl from '../sounds/ring-caller.mp3'\nimport ringCalleeUrl from '../sounds/ring-callee.mp3'\n\n// 统一通话邀请/等待弹窗\n// mode='outgoing':主叫呼叫等待(响 caller.mp3)\n// - 显示\"呼叫中...\"动画 + 红色\"取消\"按钮\n// - 收到 call:accepted → emit('connected')\n// - 收到 call:rejected/busy/no_answer → emit('close')\n// - 用户点取消 → 调 client.call.end('cancelled', callId) → emit('close')\n// mode='incoming':被叫响铃(响 callee.mp3)\n// - 显示\"邀请你通话\" + 绿色\"接听\" + 红色\"拒接\" 按钮\n// - 用户点接听 → await client.call.accept() → emit('accepted', session) → emit('close')\n// - 用户点拒接 → client.call.end('rejected', callId) → emit('close')\n// - 收到 call:cancelled → emit('close')\n\nconst props = defineProps<{\n mode: 'outgoing' | 'incoming'\n peerUid: string\n peerName?: string\n callType: CallType\n // incoming 必须传 callId(来自 call:invite 事件);outgoing 可省略(取 currentSession.callId)\n callId?: string\n // 主叫超时秒数(默认 30s)\n timeoutSec?: number\n}>()\n\nconst emit = defineEmits<{\n (e: 'close'): void\n // outgoing 收到 call:accepted 时触发——父组件切到 CallWindow\n (e: 'connected'): void\n // incoming 接听成功时触发,携带 session——父组件切到 CallWindow\n (e: 'accepted', session: unknown): void\n}>()\n\ntype DialogStatus = 'ringing' | 'connected' | 'rejected' | 'busy' | 'no_answer' | 'cancelled'\nconst status = ref<DialogStatus>('ringing')\nconst statusText = ref('')\nconst ringAudio = ref<HTMLAudioElement | null>(null)\n\nconst unsubscribers: Array<() => void> = []\nlet timeoutTimer: ReturnType<typeof setTimeout> | null = null\n\nfunction getClient() {\n return (window as any).__imClient\n}\n\nfunction startRing() {\n if (!ringAudio.value) return\n ringAudio.value.currentTime = 0\n ringAudio.value.play().catch(() => {})\n}\n\nfunction stopRing() {\n if (!ringAudio.value) return\n ringAudio.value.pause()\n ringAudio.value.currentTime = 0\n}\n\n// 对端展示名:外部传 peerName 优先;未传时查 SDK 用户目录(client.users),\n// 仍无则回退 peerUid 本身——业务方零配置也能显示姓名\nconst fallbackName = ref('')\nwatch(\n () => props.peerUid,\n async (uid) => {\n fallbackName.value = ''\n if (props.peerName || !uid) return\n const dir = getClient()?.users\n if (!dir?.resolve) return\n const u = await dir.resolve(uid).catch(() => undefined)\n if (u?.name) fallbackName.value = u.name\n },\n { immediate: true },\n)\nconst displayPeerName = computed(() => props.peerName || fallbackName.value || props.peerUid)\nconst avatarText = computed(() => displayPeerName.value[0].toUpperCase())\nconst statusLabel = computed(() => {\n if (status.value === 'ringing') {\n return props.mode === 'outgoing' ? '呼叫中' : '邀请你通话'\n }\n return statusText.value\n})\n\nfunction setStatusAndClose(s: DialogStatus, text: string, delay = 1500) {\n status.value = s\n statusText.value = text\n setTimeout(() => emit('close'), delay)\n}\n\n// outgoing 模式:主叫取消\nasync function cancelOutgoing() {\n stopRing()\n status.value = 'cancelled'\n statusText.value = '已取消'\n const client = getClient()\n const cid = props.callId || client?.call?.currentSession?.callId\n await client?.call?.end('cancelled', cid).catch(() => {})\n setTimeout(() => emit('close'), 800)\n}\n\n// incoming 模式:被叫接听\nasync function acceptIncoming() {\n stopRing()\n const client = getClient()\n if (!client || !props.callId) return\n status.value = 'connected'\n statusText.value = '已接听'\n try {\n const session = await client.call.accept(props.callId, props.callType)\n emit('accepted', session)\n setTimeout(() => emit('close'), 200)\n } catch (err: any) {\n // connect 失败、设备异常等——SDK 已清 currentSession + 调后端兜底\n // UI 显示失败提示后关闭,让用户重试\n console.error('[im] 接听失败', err)\n status.value = 'rejected'\n statusText.value = err?.message || '接听失败'\n setTimeout(() => emit('close'), 1500)\n }\n}\n\n// incoming 模式:被叫拒接\nasync function rejectIncoming() {\n stopRing()\n status.value = 'rejected'\n statusText.value = '已拒接'\n const client = getClient()\n await client?.call?.end('rejected', props.callId).catch(() => {})\n setTimeout(() => emit('close'), 600)\n}\n\nonMounted(() => {\n const client = getClient()\n if (!client) {\n emit('close')\n return\n }\n startRing()\n\n if (props.mode === 'outgoing') {\n // 主叫监听被叫响应\n unsubscribers.push(\n client.on('call:accepted', () => {\n stopRing()\n status.value = 'connected'\n statusText.value = '对方已接听'\n setTimeout(() => emit('connected'), 300)\n }),\n )\n unsubscribers.push(client.on('call:rejected', () => setStatusAndClose('rejected', '对方拒接')))\n unsubscribers.push(client.on('call:busy', () => setStatusAndClose('busy', '对方繁忙')))\n unsubscribers.push(\n client.on('call:no_answer', () => setStatusAndClose('no_answer', '无人接听')),\n )\n // 客户端超时兜底\n const sec = props.timeoutSec ?? 30\n timeoutTimer = setTimeout(() => {\n if (status.value === 'ringing') setStatusAndClose('no_answer', '无人接听')\n }, sec * 1000)\n } else {\n // incoming 监听主叫取消 + 无人接听超时\n // no_answer 必须也监听:后端超时会同时通知主叫和被叫,\n // 只等 cancelled 的话被叫端铃声在无人接听后会永不停止\n unsubscribers.push(\n client.on('call:cancelled', () => {\n stopRing()\n setStatusAndClose('cancelled', '对方已取消', 600)\n }),\n )\n unsubscribers.push(\n client.on('call:no_answer', () => {\n stopRing()\n setStatusAndClose('cancelled', '未接听,已超时', 600)\n }),\n )\n }\n})\n\nonBeforeUnmount(() => {\n stopRing()\n unsubscribers.forEach((fn) => fn())\n if (timeoutTimer) clearTimeout(timeoutTimer)\n})\n</script>\n\n<template>\n <div v-draggable=\"'.call-dialog-status'\" class=\"call-dialog\">\n <audio\n ref=\"ringAudio\"\n :src=\"mode === 'outgoing' ? ringCallerUrl : ringCalleeUrl\"\n loop\n preload=\"auto\"\n />\n <div class=\"call-dialog-header\">\n <span>{{ callType === 'video' ? '视频通话' : '语音通话' }}</span>\n </div>\n <div class=\"call-dialog-body\">\n <div :class=\"['avatar', status === 'ringing' ? 'ringing' : status]\">\n <span>{{ avatarText }}</span>\n </div>\n <div class=\"peer-name\">{{ displayPeerName }}</div>\n <div class=\"status-text\">\n <template v-if=\"status === 'ringing'\">\n {{ statusLabel }}<span class=\"dots\"><i>.</i><i>.</i><i>.</i></span>\n </template>\n <template v-else>\n <span :class=\"status\">{{ statusText }}</span>\n </template>\n </div>\n </div>\n <div class=\"call-dialog-actions\">\n <!-- outgoing 模式:只显示取消 -->\n <template v-if=\"mode === 'outgoing'\">\n <button v-if=\"status === 'ringing'\" class=\"btn cancel\" @click=\"cancelOutgoing\">取消</button>\n </template>\n <!-- incoming 模式:接听 + 拒接 -->\n <template v-else>\n <button v-if=\"status === 'ringing'\" class=\"btn reject\" @click=\"rejectIncoming\">拒接</button>\n <button v-if=\"status === 'ringing'\" class=\"btn accept\" @click=\"acceptIncoming\">接听</button>\n </template>\n </div>\n </div>\n</template>\n\n<style scoped>\n.call-dialog {\n position: fixed;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n width: 320px;\n background: #0f172a;\n border-radius: 0;\n padding: 20px 20px 28px;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);\n color: #fff;\n z-index: 9999;\n text-align: center;\n}\n.call-dialog-header {\n cursor: move;\n font-size: 12px;\n color: #999;\n padding-bottom: 10px;\n border-bottom: 1px solid #333;\n margin-bottom: 16px;\n}\n.avatar {\n width: 80px;\n height: 80px;\n border-radius: 50%;\n background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n color: #fff;\n font-size: 36px;\n font-weight: 600;\n line-height: 80px;\n margin: 0 auto 12px;\n position: relative;\n}\n.avatar.ringing::before {\n content: '';\n position: absolute;\n inset: -8px;\n border: 2px solid #667eea;\n border-radius: 50%;\n animation: pulse 1.5s ease-out infinite;\n}\n.avatar.rejected,\n.avatar.cancelled {\n background: #888;\n}\n.avatar.busy,\n.avatar.no_answer {\n background: #d4a017;\n}\n.avatar.connected {\n background: #52c41a;\n}\n@keyframes pulse {\n 0% {\n transform: scale(1);\n opacity: 0.8;\n }\n 100% {\n transform: scale(1.4);\n opacity: 0;\n }\n}\n.peer-name {\n font-size: 16px;\n font-weight: 600;\n margin-bottom: 6px;\n}\n.status-text {\n font-size: 13px;\n color: #bbb;\n min-height: 20px;\n}\n.status-text .rejected,\n.status-text .cancelled {\n color: #888;\n}\n.status-text .busy,\n.status-text .no_answer {\n color: #d4a017;\n}\n.status-text .connected {\n color: #52c41a;\n}\n.dots i {\n display: inline-block;\n font-style: normal;\n animation: dot-bounce 1.4s infinite;\n}\n.dots i:nth-child(2) {\n animation-delay: 0.2s;\n}\n.dots i:nth-child(3) {\n animation-delay: 0.4s;\n}\n@keyframes dot-bounce {\n 0%,\n 60%,\n 100% {\n opacity: 0.3;\n }\n 30% {\n opacity: 1;\n }\n}\n.call-dialog-actions {\n margin-top: 20px;\n display: flex;\n justify-content: center;\n gap: 20px;\n min-height: 44px;\n}\n.btn {\n padding: 8px 24px;\n border: none;\n border-radius: 0;\n cursor: pointer;\n font-size: 14px;\n font-weight: 500;\n color: #fff;\n transition:\n transform 0.1s,\n opacity 0.1s;\n}\n.btn:active {\n transform: scale(0.96);\n}\n.btn.accept {\n background: #52c41a;\n}\n.btn.reject,\n.btn.cancel {\n background: #ef4444;\n}\n</style>\n","// 共享 SVG 渲染函数——所有图标从 design/video-conference-layout.html 原样搬\n// 函数式组件,避免在每个用到的地方重复粘贴 SVG 字符串\n\n/** 麦克风开启:白色描边 */\nexport const MicOnIcon = `<svg class=\"mic-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M12 2a3 3 0 0 1 3 3v6a3 3 0 0 1-6 0V5a3 3 0 0 1 3-3z\"></path>\n <path d=\"M19 10v2a7 7 0 0 1-14 0v-2\"></path>\n <line x1=\"12\" y1=\"19\" x2=\"12\" y2=\"23\"></line>\n <line x1=\"8\" y1=\"23\" x2=\"16\" y2=\"23\"></line>\n</svg>`\n\n/** 麦克风关闭:红色描边 + 斜杠 */\nexport const MicOffIcon = `<svg class=\"mic-icon muted\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <line x1=\"1\" y1=\"1\" x2=\"23\" y2=\"23\"></line>\n <path d=\"M9 9v5a3 3 0 0 0 5.1 2.1\"></path>\n <path d=\"M15 9.34V5a3 3 0 0 0-5.94-1.1\"></path>\n <path d=\"M17 16.95A7 7 0 0 1 5 12v-2\"></path>\n <path d=\"M19 5v4\"></path>\n <path d=\"M12 19v4\"></path>\n <path d=\"M8 23h8\"></path>\n</svg>`\n\n/** 正在发言:声波柱动效(CSS keyframes 控制) */\nexport const SpeakingIndicator = `<span class=\"speaking-indicator\"><span class=\"wave-bar\"></span><span class=\"wave-bar\"></span><span class=\"wave-bar\"></span><span>正在发言</span></span>`\n\n/** 主持人 badge:用 \"👑\" emoji 太重,这里用简洁的\"主\"字徽标 */\nexport const HostBadge = `<span class=\"host-badge\">主持人</span>`\n\n/** 最小化按钮 SVG */\nexport const MinimizeIcon = `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"5\" y1=\"12\" x2=\"19\" y2=\"12\"></line></svg>`\n\n/** 还原(最大化)按钮 SVG:四角向外箭头 */\nexport const MaximizeIcon = `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"15 3 21 3 21 9\"></polyline><polyline points=\"9 21 3 21 3 15\"></polyline><line x1=\"21\" y1=\"3\" x2=\"14\" y2=\"10\"></line><line x1=\"3\" y1=\"21\" x2=\"10\" y2=\"14\"></line></svg>`\n\n/** 关闭按钮 SVG */\nexport const CloseIcon = `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"></line><line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"></line></svg>`\n\n/** 更多操作(⋯)按钮 SVG */\nexport const MoreIcon = `<svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><circle cx=\"5\" cy=\"12\" r=\"2\"></circle><circle cx=\"12\" cy=\"12\" r=\"2\"></circle><circle cx=\"19\" cy=\"12\" r=\"2\"></circle></svg>`\n\n/** 进入全屏宫格按钮 SVG:田字型(四个等大方块) */\nexport const GridEnterIcon = `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"3\" width=\"7.5\" height=\"7.5\"></rect><rect x=\"13.5\" y=\"3\" width=\"7.5\" height=\"7.5\"></rect><rect x=\"3\" y=\"13.5\" width=\"7.5\" height=\"7.5\"></rect><rect x=\"13.5\" y=\"13.5\" width=\"7.5\" height=\"7.5\"></rect></svg>`\n\n/** 退出全屏宫格按钮 SVG:大小正方形交错(大空框 + 右下角小实心块) */\nexport const GridExitIcon = `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"3\" width=\"18\" height=\"18\"></rect><rect x=\"12.5\" y=\"12.5\" width=\"8\" height=\"8\" fill=\"currentColor\" stroke=\"none\"></rect></svg>`\n\n/** 渲染麦克风图标——根据 muted 选 MicOnIcon/MicOffIcon */\nexport function renderMicIcon(muted: boolean): string {\n return muted ? MicOffIcon : MicOnIcon\n}\n","<script setup lang=\"ts\">\nimport { MinimizeIcon, CloseIcon, GridEnterIcon, GridExitIcon } from './icons'\n\ndefineProps<{\n title: string\n duration?: string\n // 全屏宫格模式:图标变为「退出宫格」,且隐藏最小化按钮(与全屏互斥)\n gridMode?: boolean\n}>()\n\nconst emit = defineEmits<{\n (e: 'close'): void\n (e: 'minimize'): void\n (e: 'toggleGrid'): void\n}>()\n</script>\n\n<template>\n <header class=\"meeting-header\">\n <div class=\"title-area\">\n <h1 class=\"title\">{{ title }}</h1>\n <span v-if=\"duration\" class=\"duration\">{{ duration }}</span>\n </div>\n <!-- 中央插槽:内容在 header 内水平 + 垂直居中(全屏宫格放控制按钮组) -->\n <div class=\"header-center\">\n <slot name=\"center\"></slot>\n </div>\n <div class=\"window-controls\">\n <button\n class=\"win-btn grid-btn\"\n type=\"button\"\n :aria-label=\"gridMode ? '恢复默认布局' : '全屏宫格'\"\n :title=\"gridMode ? '恢复默认布局' : '全屏宫格'\"\n @click=\"emit('toggleGrid')\"\n v-html=\"gridMode ? GridExitIcon : GridEnterIcon\"\n ></button>\n <button\n v-if=\"!gridMode\"\n class=\"win-btn minimize-btn\"\n type=\"button\"\n aria-label=\"最小化\"\n title=\"最小化\"\n @click=\"emit('minimize')\"\n v-html=\"MinimizeIcon\"\n ></button>\n <button\n class=\"win-btn close-btn\"\n type=\"button\"\n aria-label=\"关闭会议\"\n title=\"关闭\"\n @click=\"emit('close')\"\n v-html=\"CloseIcon\"\n ></button>\n </div>\n </header>\n</template>\n\n<style scoped>\n.meeting-header {\n flex: 0 0 auto;\n position: relative; /* 中央插槽绝对定位的锚点 */\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 6px 0;\n border-bottom: 1px solid var(--im-meeting-border, rgba(255, 255, 255, 0.08));\n cursor: move;\n}\n/* 中央插槽:相对整个 header 水平居中 + 垂直居中(不受左右两列宽度影响) */\n.header-center {\n position: absolute;\n left: 50%;\n top: 50%;\n transform: translate(-50%, -50%);\n}\n.title-area {\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 0;\n}\n.title {\n font-size: 15px;\n font-weight: 600;\n color: var(--im-meeting-text-primary, #fff);\n margin: 0;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.duration {\n font-size: 12px;\n color: var(--im-meeting-text-tertiary, rgba(255, 255, 255, 0.55));\n font-variant-numeric: tabular-nums;\n}\n.window-controls {\n display: flex;\n align-items: center;\n gap: 6px;\n}\n.win-btn {\n width: 24px;\n height: 24px;\n border: none;\n background: transparent;\n color: var(--im-meeting-text-secondary, rgba(255, 255, 255, 0.85));\n cursor: pointer;\n border-radius: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n transition:\n background 0.2s,\n color 0.2s;\n}\n.win-btn:hover {\n background: rgba(255, 255, 255, 0.1);\n color: var(--im-meeting-text-primary, #fff);\n}\n.win-btn :deep(svg) {\n width: 18px;\n height: 18px;\n}\n.win-btn.close-btn:hover {\n background: #e53935;\n color: #fff;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { MinimizeIcon, CloseIcon, GridEnterIcon, GridExitIcon } from './icons'\n\ndefineProps<{\n title: string\n duration?: string\n // 全屏宫格模式:图标变为「退出宫格」,且隐藏最小化按钮(与全屏互斥)\n gridMode?: boolean\n}>()\n\nconst emit = defineEmits<{\n (e: 'close'): void\n (e: 'minimize'): void\n (e: 'toggleGrid'): void\n}>()\n</script>\n\n<template>\n <header class=\"meeting-header\">\n <div class=\"title-area\">\n <h1 class=\"title\">{{ title }}</h1>\n <span v-if=\"duration\" class=\"duration\">{{ duration }}</span>\n </div>\n <!-- 中央插槽:内容在 header 内水平 + 垂直居中(全屏宫格放控制按钮组) -->\n <div class=\"header-center\">\n <slot name=\"center\"></slot>\n </div>\n <div class=\"window-controls\">\n <button\n class=\"win-btn grid-btn\"\n type=\"button\"\n :aria-label=\"gridMode ? '恢复默认布局' : '全屏宫格'\"\n :title=\"gridMode ? '恢复默认布局' : '全屏宫格'\"\n @click=\"emit('toggleGrid')\"\n v-html=\"gridMode ? GridExitIcon : GridEnterIcon\"\n ></button>\n <button\n v-if=\"!gridMode\"\n class=\"win-btn minimize-btn\"\n type=\"button\"\n aria-label=\"最小化\"\n title=\"最小化\"\n @click=\"emit('minimize')\"\n v-html=\"MinimizeIcon\"\n ></button>\n <button\n class=\"win-btn close-btn\"\n type=\"button\"\n aria-label=\"关闭会议\"\n title=\"关闭\"\n @click=\"emit('close')\"\n v-html=\"CloseIcon\"\n ></button>\n </div>\n </header>\n</template>\n\n<style scoped>\n.meeting-header {\n flex: 0 0 auto;\n position: relative; /* 中央插槽绝对定位的锚点 */\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 6px 0;\n border-bottom: 1px solid var(--im-meeting-border, rgba(255, 255, 255, 0.08));\n cursor: move;\n}\n/* 中央插槽:相对整个 header 水平居中 + 垂直居中(不受左右两列宽度影响) */\n.header-center {\n position: absolute;\n left: 50%;\n top: 50%;\n transform: translate(-50%, -50%);\n}\n.title-area {\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 0;\n}\n.title {\n font-size: 15px;\n font-weight: 600;\n color: var(--im-meeting-text-primary, #fff);\n margin: 0;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.duration {\n font-size: 12px;\n color: var(--im-meeting-text-tertiary, rgba(255, 255, 255, 0.55));\n font-variant-numeric: tabular-nums;\n}\n.window-controls {\n display: flex;\n align-items: center;\n gap: 6px;\n}\n.win-btn {\n width: 24px;\n height: 24px;\n border: none;\n background: transparent;\n color: var(--im-meeting-text-secondary, rgba(255, 255, 255, 0.85));\n cursor: pointer;\n border-radius: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n transition:\n background 0.2s,\n color 0.2s;\n}\n.win-btn:hover {\n background: rgba(255, 255, 255, 0.1);\n color: var(--im-meeting-text-primary, #fff);\n}\n.win-btn :deep(svg) {\n width: 18px;\n height: 18px;\n}\n.win-btn.close-btn:hover {\n background: #e53935;\n color: #fff;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'\nimport Icon from '../Icon.vue'\nimport { SpeakingIndicator } from './icons'\n\nconst props = defineProps<{\n identity: string\n name: string\n muted: boolean\n // 摄像头状态:false 时显示「摄像头已关」遮罩(传入方负责排除未入会成员)\n cameraOn?: boolean\n speaking: boolean\n isHost: boolean\n isSelf?: boolean\n // 自己的麦克风不可用(无设备/无权限/被占用):状态图标置灰(仅 isSelf 时生效)\n micUnavailable?: boolean\n onVideoElReady?: (identity: string, el: HTMLVideoElement | null) => void\n}>()\n\nconst videoEl = ref<HTMLVideoElement | null>(null)\n\n// 初始 mount + identity 切换(主持人转让后)都要通知父组件重新 attach\nfunction notify() {\n props.onVideoElReady?.(props.identity, videoEl.value)\n}\n\nonMounted(notify)\nonBeforeUnmount(() => {\n props.onVideoElReady?.(props.identity, null)\n})\nwatch(\n () => props.identity,\n () => {\n nextTick(notify)\n },\n)\n</script>\n\n<template>\n <section class=\"main-video\" aria-label=\"主视频画面\">\n <video ref=\"videoEl\" autoplay muted playsinline class=\"tile-video\"></video>\n <!-- 摄像头已关遮罩:盖住冻结帧(LiveKit mute 后 video 停在最后一帧) -->\n <div v-if=\"cameraOn === false\" class=\"camera-off-mask\">\n <Icon name=\"camera-off\" :size=\"28\" color=\"rgba(255, 255, 255, 0.45)\" />\n <span class=\"camera-off-text\">摄像头已关</span>\n </div>\n <div v-if=\"!name\" class=\"video-placeholder\">主视频画面</div>\n <div class=\"video-overlay\">\n <div class=\"video-meta-grid\">\n <div class=\"corner-tl\">\n <span class=\"participant-name\">{{ isSelf ? '我' : name || '等待主持人加入' }}</span>\n </div>\n <div v-if=\"speaking\" class=\"corner-tr\" v-html=\"SpeakingIndicator\"></div>\n <div class=\"corner-br\">\n <Icon\n :name=\"muted || (isSelf && micUnavailable) ? 'mic-off' : 'mic-on'\"\n :size=\"18\"\n :color=\"\n isSelf && micUnavailable ? 'rgba(255, 255, 255, 0.45)' : muted ? '#ef4444' : '#fff'\n \"\n :title=\"isSelf && micUnavailable ? '麦克风不可用' : muted ? '麦克风已关' : '麦克风已开'\"\n />\n </div>\n </div>\n </div>\n </section>\n</template>\n\n<style scoped>\n.main-video {\n position: relative;\n width: 100%;\n height: 100%;\n background: var(--im-meeting-bg-video, #000);\n border-radius: 0;\n overflow: hidden;\n}\n.tile-video {\n width: 100%;\n height: 100%;\n object-fit: cover;\n background: var(--im-meeting-bg-video, #000);\n}\n.camera-off-mask {\n position: absolute;\n inset: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 8px;\n background: var(--im-meeting-bg-video, #000);\n pointer-events: none;\n}\n.camera-off-text {\n font-size: 13px;\n color: rgba(255, 255, 255, 0.45);\n}\n.video-placeholder {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--im-meeting-text-secondary, rgba(255, 255, 255, 0.85));\n font-size: 14px;\n}\n.video-overlay {\n position: absolute;\n inset: 0;\n pointer-events: none;\n padding: var(--im-meeting-overlay-gap, 5px);\n}\n.video-meta-grid {\n position: absolute;\n inset: var(--im-meeting-overlay-gap, 5px);\n display: grid;\n grid-template-columns: 1fr auto;\n grid-template-rows: auto 1fr auto;\n pointer-events: none;\n}\n.corner-tl {\n grid-area: 3 / 1 / 4 / 2;\n align-self: end;\n justify-self: start;\n}\n.corner-tr {\n grid-area: 1 / 2 / 2 / 3;\n align-self: start;\n justify-self: end;\n}\n.corner-br {\n grid-area: 3 / 2 / 4 / 3;\n align-self: end;\n justify-self: end;\n}\n.participant-name {\n font-size: 14px;\n font-weight: 500;\n color: var(--im-meeting-text-primary, #fff);\n text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n max-width: 60%;\n}\n.corner-br {\n filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5));\n}\n.main-video :deep(.speaking-indicator) {\n display: inline-flex;\n align-items: center;\n gap: 3px;\n font-size: 11px;\n font-weight: 500;\n color: var(--im-meeting-speaking, #22c55e);\n background: rgba(34, 197, 94, 0.15);\n border: 1px solid rgba(34, 197, 94, 0.5);\n border-radius: 0;\n padding: 2px 6px;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);\n backdrop-filter: blur(2px);\n}\n.main-video :deep(.wave-bar) {\n width: 2px;\n background: var(--im-meeting-speaking, #22c55e);\n border-radius: 0;\n animation: wave 1s ease-in-out infinite;\n}\n.main-video :deep(.wave-bar:nth-child(1)) {\n height: 6px;\n animation-delay: 0s;\n}\n.main-video :deep(.wave-bar:nth-child(2)) {\n height: 10px;\n animation-delay: 0.15s;\n}\n.main-video :deep(.wave-bar:nth-child(3)) {\n height: 6px;\n animation-delay: 0.3s;\n}\n@keyframes wave {\n 0%,\n 100% {\n transform: scaleY(1);\n opacity: 0.7;\n }\n 50% {\n transform: scaleY(1.6);\n opacity: 1;\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'\nimport Icon from '../Icon.vue'\nimport { SpeakingIndicator } from './icons'\n\nconst props = defineProps<{\n identity: string\n name: string\n muted: boolean\n // 摄像头状态:false 时显示「摄像头已关」遮罩(传入方负责排除未入会成员)\n cameraOn?: boolean\n speaking: boolean\n isHost: boolean\n isSelf?: boolean\n // 自己的麦克风不可用(无设备/无权限/被占用):状态图标置灰(仅 isSelf 时生效)\n micUnavailable?: boolean\n onVideoElReady?: (identity: string, el: HTMLVideoElement | null) => void\n}>()\n\nconst videoEl = ref<HTMLVideoElement | null>(null)\n\n// 初始 mount + identity 切换(主持人转让后)都要通知父组件重新 attach\nfunction notify() {\n props.onVideoElReady?.(props.identity, videoEl.value)\n}\n\nonMounted(notify)\nonBeforeUnmount(() => {\n props.onVideoElReady?.(props.identity, null)\n})\nwatch(\n () => props.identity,\n () => {\n nextTick(notify)\n },\n)\n</script>\n\n<template>\n <section class=\"main-video\" aria-label=\"主视频画面\">\n <video ref=\"videoEl\" autoplay muted playsinline class=\"tile-video\"></video>\n <!-- 摄像头已关遮罩:盖住冻结帧(LiveKit mute 后 video 停在最后一帧) -->\n <div v-if=\"cameraOn === false\" class=\"camera-off-mask\">\n <Icon name=\"camera-off\" :size=\"28\" color=\"rgba(255, 255, 255, 0.45)\" />\n <span class=\"camera-off-text\">摄像头已关</span>\n </div>\n <div v-if=\"!name\" class=\"video-placeholder\">主视频画面</div>\n <div class=\"video-overlay\">\n <div class=\"video-meta-grid\">\n <div class=\"corner-tl\">\n <span class=\"participant-name\">{{ isSelf ? '我' : name || '等待主持人加入' }}</span>\n </div>\n <div v-if=\"speaking\" class=\"corner-tr\" v-html=\"SpeakingIndicator\"></div>\n <div class=\"corner-br\">\n <Icon\n :name=\"muted || (isSelf && micUnavailable) ? 'mic-off' : 'mic-on'\"\n :size=\"18\"\n :color=\"\n isSelf && micUnavailable ? 'rgba(255, 255, 255, 0.45)' : muted ? '#ef4444' : '#fff'\n \"\n :title=\"isSelf && micUnavailable ? '麦克风不可用' : muted ? '麦克风已关' : '麦克风已开'\"\n />\n </div>\n </div>\n </div>\n </section>\n</template>\n\n<style scoped>\n.main-video {\n position: relative;\n width: 100%;\n height: 100%;\n background: var(--im-meeting-bg-video, #000);\n border-radius: 0;\n overflow: hidden;\n}\n.tile-video {\n width: 100%;\n height: 100%;\n object-fit: cover;\n background: var(--im-meeting-bg-video, #000);\n}\n.camera-off-mask {\n position: absolute;\n inset: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 8px;\n background: var(--im-meeting-bg-video, #000);\n pointer-events: none;\n}\n.camera-off-text {\n font-size: 13px;\n color: rgba(255, 255, 255, 0.45);\n}\n.video-placeholder {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--im-meeting-text-secondary, rgba(255, 255, 255, 0.85));\n font-size: 14px;\n}\n.video-overlay {\n position: absolute;\n inset: 0;\n pointer-events: none;\n padding: var(--im-meeting-overlay-gap, 5px);\n}\n.video-meta-grid {\n position: absolute;\n inset: var(--im-meeting-overlay-gap, 5px);\n display: grid;\n grid-template-columns: 1fr auto;\n grid-template-rows: auto 1fr auto;\n pointer-events: none;\n}\n.corner-tl {\n grid-area: 3 / 1 / 4 / 2;\n align-self: end;\n justify-self: start;\n}\n.corner-tr {\n grid-area: 1 / 2 / 2 / 3;\n align-self: start;\n justify-self: end;\n}\n.corner-br {\n grid-area: 3 / 2 / 4 / 3;\n align-self: end;\n justify-self: end;\n}\n.participant-name {\n font-size: 14px;\n font-weight: 500;\n color: var(--im-meeting-text-primary, #fff);\n text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n max-width: 60%;\n}\n.corner-br {\n filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5));\n}\n.main-video :deep(.speaking-indicator) {\n display: inline-flex;\n align-items: center;\n gap: 3px;\n font-size: 11px;\n font-weight: 500;\n color: var(--im-meeting-speaking, #22c55e);\n background: rgba(34, 197, 94, 0.15);\n border: 1px solid rgba(34, 197, 94, 0.5);\n border-radius: 0;\n padding: 2px 6px;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);\n backdrop-filter: blur(2px);\n}\n.main-video :deep(.wave-bar) {\n width: 2px;\n background: var(--im-meeting-speaking, #22c55e);\n border-radius: 0;\n animation: wave 1s ease-in-out infinite;\n}\n.main-video :deep(.wave-bar:nth-child(1)) {\n height: 6px;\n animation-delay: 0s;\n}\n.main-video :deep(.wave-bar:nth-child(2)) {\n height: 10px;\n animation-delay: 0.15s;\n}\n.main-video :deep(.wave-bar:nth-child(3)) {\n height: 6px;\n animation-delay: 0.3s;\n}\n@keyframes wave {\n 0%,\n 100% {\n transform: scaleY(1);\n opacity: 0.7;\n }\n 50% {\n transform: scaleY(1.6);\n opacity: 1;\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { ref, onMounted, onBeforeUnmount } from 'vue'\nimport Icon from '../Icon.vue'\nimport { SpeakingIndicator, HostBadge } from './icons'\n\nconst props = defineProps<{\n identity: string\n name: string\n muted: boolean\n // 摄像头状态:false 时显示「摄像头已关」遮罩(传入方负责排除未入会成员)\n cameraOn?: boolean\n speaking: boolean\n isHost: boolean\n isSelf?: boolean\n // 主持人标签放视频框左上角(全屏宫格模式用);默认在左下角与昵称同行\n hostBadgeTop?: boolean\n // 自己的麦克风不可用(无设备/无权限/被占用):状态图标置灰(仅 isSelf 时生效,红色保留给主动闭麦)\n micUnavailable?: boolean\n // 挂载/卸载时回调,把 video 元素交回父组件——避开 v-for + 函数 ref 的不稳定问题\n onVideoElReady?: (identity: string, el: HTMLVideoElement | null) => void\n}>()\n\nconst emit = defineEmits<{\n (e: 'click', identity: string): void\n}>()\n\nconst videoEl = ref<HTMLVideoElement | null>(null)\n\nonMounted(() => {\n if (videoEl.value) {\n props.onVideoElReady?.(props.identity, videoEl.value)\n }\n})\n\nonBeforeUnmount(() => {\n props.onVideoElReady?.(props.identity, null)\n})\n</script>\n\n<template>\n <div\n class=\"small-video\"\n role=\"img\"\n :aria-label=\"`${isSelf ? '我' : name} 的视频画面`\"\n @click=\"emit('click', identity)\"\n >\n <video ref=\"videoEl\" autoplay muted playsinline class=\"tile-video\"></video>\n <!-- 摄像头已关遮罩:盖住冻结帧(LiveKit mute 后 video 停在最后一帧) -->\n <div v-if=\"cameraOn === false\" class=\"camera-off-mask\">\n <Icon name=\"camera-off\" :size=\"22\" color=\"rgba(255, 255, 255, 0.45)\" />\n <span class=\"camera-off-text\">摄像头已关</span>\n </div>\n <div class=\"video-overlay\">\n <div class=\"video-meta-grid\">\n <!-- 左上角主持人标签(全屏宫格):与左下角昵称同 inset(5px)对齐 -->\n <div v-if=\"isHost && hostBadgeTop\" class=\"corner-host-top\">\n <span class=\"host-badge-inline\" v-html=\"HostBadge\"></span>\n </div>\n <div class=\"corner-tl\">\n <span class=\"participant-name\">{{ isSelf ? '我' : name }}</span>\n <span v-if=\"isHost && !hostBadgeTop\" class=\"host-badge-inline\" v-html=\"HostBadge\"></span>\n </div>\n <div v-if=\"speaking\" class=\"corner-tr\" v-html=\"SpeakingIndicator\"></div>\n <div class=\"corner-br\">\n <Icon\n :name=\"muted || (isSelf && micUnavailable) ? 'mic-off' : 'mic-on'\"\n :size=\"18\"\n :color=\"\n isSelf && micUnavailable ? 'rgba(255, 255, 255, 0.45)' : muted ? '#ef4444' : '#fff'\n \"\n :title=\"isSelf && micUnavailable ? '麦克风不可用' : muted ? '麦克风已关' : '麦克风已开'\"\n />\n </div>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.small-video {\n position: relative;\n background: var(--im-meeting-bg-video, #000);\n border-radius: 0;\n overflow: hidden;\n flex: 0 0 auto;\n cursor: pointer;\n}\n.tile-video {\n width: 100%;\n height: 100%;\n object-fit: cover;\n background: var(--im-meeting-bg-video, #000);\n}\n.camera-off-mask {\n position: absolute;\n inset: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 6px;\n background: var(--im-meeting-bg-video, #000);\n pointer-events: none;\n}\n.camera-off-text {\n font-size: 11px;\n color: rgba(255, 255, 255, 0.45);\n}\n.video-placeholder {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n color: rgba(255, 255, 255, 0.4);\n font-size: 12px;\n pointer-events: none;\n}\n.video-overlay {\n position: absolute;\n inset: 0;\n pointer-events: none;\n padding: var(--im-meeting-overlay-gap, 5px);\n}\n.video-meta-grid {\n position: absolute;\n inset: var(--im-meeting-overlay-gap, 5px);\n display: grid;\n grid-template-columns: 1fr auto;\n grid-template-rows: auto 1fr auto;\n pointer-events: none;\n}\n.corner-tl {\n grid-area: 3 / 1 / 4 / 2;\n align-self: end;\n justify-self: start;\n display: flex;\n align-items: center;\n gap: 4px;\n max-width: 100%;\n}\n.corner-tr {\n grid-area: 1 / 2 / 2 / 3;\n align-self: start;\n justify-self: end;\n}\n/* 左上角主持人标签:与左下角昵称(corner-tl,同 inset 5px)边距一致 */\n.corner-host-top {\n grid-area: 1 / 1 / 2 / 2;\n align-self: start;\n justify-self: start;\n}\n.corner-br {\n grid-area: 3 / 2 / 4 / 3;\n align-self: end;\n justify-self: end;\n}\n.participant-name {\n font-size: 12px;\n font-weight: 500;\n color: var(--im-meeting-text-primary, #fff);\n text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n max-width: 100%;\n}\n.corner-br {\n filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5));\n}\n.small-video :deep(.speaking-indicator) {\n display: inline-flex;\n align-items: center;\n gap: 3px;\n font-size: 10px;\n font-weight: 500;\n color: var(--im-meeting-speaking, #22c55e);\n background: rgba(34, 197, 94, 0.15);\n border: 1px solid rgba(34, 197, 94, 0.5);\n border-radius: 0;\n padding: 1px 4px;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);\n}\n.small-video :deep(.wave-bar) {\n width: 2px;\n background: var(--im-meeting-speaking, #22c55e);\n border-radius: 0;\n animation: wave-sm 1s ease-in-out infinite;\n}\n.small-video :deep(.wave-bar:nth-child(1)) {\n height: 4px;\n animation-delay: 0s;\n}\n.small-video :deep(.wave-bar:nth-child(2)) {\n height: 7px;\n animation-delay: 0.15s;\n}\n.small-video :deep(.wave-bar:nth-child(3)) {\n height: 4px;\n animation-delay: 0.3s;\n}\n.host-badge-inline :deep(.host-badge) {\n font-size: 9px;\n color: #fbbf24;\n background: rgba(251, 191, 36, 0.15);\n border: 1px solid rgba(251, 191, 36, 0.5);\n padding: 1px 4px;\n white-space: nowrap;\n}\n@keyframes wave-sm {\n 0%,\n 100% {\n transform: scaleY(1);\n opacity: 0.7;\n }\n 50% {\n transform: scaleY(1.6);\n opacity: 1;\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { ref, onMounted, onBeforeUnmount } from 'vue'\nimport Icon from '../Icon.vue'\nimport { SpeakingIndicator, HostBadge } from './icons'\n\nconst props = defineProps<{\n identity: string\n name: string\n muted: boolean\n // 摄像头状态:false 时显示「摄像头已关」遮罩(传入方负责排除未入会成员)\n cameraOn?: boolean\n speaking: boolean\n isHost: boolean\n isSelf?: boolean\n // 主持人标签放视频框左上角(全屏宫格模式用);默认在左下角与昵称同行\n hostBadgeTop?: boolean\n // 自己的麦克风不可用(无设备/无权限/被占用):状态图标置灰(仅 isSelf 时生效,红色保留给主动闭麦)\n micUnavailable?: boolean\n // 挂载/卸载时回调,把 video 元素交回父组件——避开 v-for + 函数 ref 的不稳定问题\n onVideoElReady?: (identity: string, el: HTMLVideoElement | null) => void\n}>()\n\nconst emit = defineEmits<{\n (e: 'click', identity: string): void\n}>()\n\nconst videoEl = ref<HTMLVideoElement | null>(null)\n\nonMounted(() => {\n if (videoEl.value) {\n props.onVideoElReady?.(props.identity, videoEl.value)\n }\n})\n\nonBeforeUnmount(() => {\n props.onVideoElReady?.(props.identity, null)\n})\n</script>\n\n<template>\n <div\n class=\"small-video\"\n role=\"img\"\n :aria-label=\"`${isSelf ? '我' : name} 的视频画面`\"\n @click=\"emit('click', identity)\"\n >\n <video ref=\"videoEl\" autoplay muted playsinline class=\"tile-video\"></video>\n <!-- 摄像头已关遮罩:盖住冻结帧(LiveKit mute 后 video 停在最后一帧) -->\n <div v-if=\"cameraOn === false\" class=\"camera-off-mask\">\n <Icon name=\"camera-off\" :size=\"22\" color=\"rgba(255, 255, 255, 0.45)\" />\n <span class=\"camera-off-text\">摄像头已关</span>\n </div>\n <div class=\"video-overlay\">\n <div class=\"video-meta-grid\">\n <!-- 左上角主持人标签(全屏宫格):与左下角昵称同 inset(5px)对齐 -->\n <div v-if=\"isHost && hostBadgeTop\" class=\"corner-host-top\">\n <span class=\"host-badge-inline\" v-html=\"HostBadge\"></span>\n </div>\n <div class=\"corner-tl\">\n <span class=\"participant-name\">{{ isSelf ? '我' : name }}</span>\n <span v-if=\"isHost && !hostBadgeTop\" class=\"host-badge-inline\" v-html=\"HostBadge\"></span>\n </div>\n <div v-if=\"speaking\" class=\"corner-tr\" v-html=\"SpeakingIndicator\"></div>\n <div class=\"corner-br\">\n <Icon\n :name=\"muted || (isSelf && micUnavailable) ? 'mic-off' : 'mic-on'\"\n :size=\"18\"\n :color=\"\n isSelf && micUnavailable ? 'rgba(255, 255, 255, 0.45)' : muted ? '#ef4444' : '#fff'\n \"\n :title=\"isSelf && micUnavailable ? '麦克风不可用' : muted ? '麦克风已关' : '麦克风已开'\"\n />\n </div>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.small-video {\n position: relative;\n background: var(--im-meeting-bg-video, #000);\n border-radius: 0;\n overflow: hidden;\n flex: 0 0 auto;\n cursor: pointer;\n}\n.tile-video {\n width: 100%;\n height: 100%;\n object-fit: cover;\n background: var(--im-meeting-bg-video, #000);\n}\n.camera-off-mask {\n position: absolute;\n inset: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 6px;\n background: var(--im-meeting-bg-video, #000);\n pointer-events: none;\n}\n.camera-off-text {\n font-size: 11px;\n color: rgba(255, 255, 255, 0.45);\n}\n.video-placeholder {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n color: rgba(255, 255, 255, 0.4);\n font-size: 12px;\n pointer-events: none;\n}\n.video-overlay {\n position: absolute;\n inset: 0;\n pointer-events: none;\n padding: var(--im-meeting-overlay-gap, 5px);\n}\n.video-meta-grid {\n position: absolute;\n inset: var(--im-meeting-overlay-gap, 5px);\n display: grid;\n grid-template-columns: 1fr auto;\n grid-template-rows: auto 1fr auto;\n pointer-events: none;\n}\n.corner-tl {\n grid-area: 3 / 1 / 4 / 2;\n align-self: end;\n justify-self: start;\n display: flex;\n align-items: center;\n gap: 4px;\n max-width: 100%;\n}\n.corner-tr {\n grid-area: 1 / 2 / 2 / 3;\n align-self: start;\n justify-self: end;\n}\n/* 左上角主持人标签:与左下角昵称(corner-tl,同 inset 5px)边距一致 */\n.corner-host-top {\n grid-area: 1 / 1 / 2 / 2;\n align-self: start;\n justify-self: start;\n}\n.corner-br {\n grid-area: 3 / 2 / 4 / 3;\n align-self: end;\n justify-self: end;\n}\n.participant-name {\n font-size: 12px;\n font-weight: 500;\n color: var(--im-meeting-text-primary, #fff);\n text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n max-width: 100%;\n}\n.corner-br {\n filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5));\n}\n.small-video :deep(.speaking-indicator) {\n display: inline-flex;\n align-items: center;\n gap: 3px;\n font-size: 10px;\n font-weight: 500;\n color: var(--im-meeting-speaking, #22c55e);\n background: rgba(34, 197, 94, 0.15);\n border: 1px solid rgba(34, 197, 94, 0.5);\n border-radius: 0;\n padding: 1px 4px;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);\n}\n.small-video :deep(.wave-bar) {\n width: 2px;\n background: var(--im-meeting-speaking, #22c55e);\n border-radius: 0;\n animation: wave-sm 1s ease-in-out infinite;\n}\n.small-video :deep(.wave-bar:nth-child(1)) {\n height: 4px;\n animation-delay: 0s;\n}\n.small-video :deep(.wave-bar:nth-child(2)) {\n height: 7px;\n animation-delay: 0.15s;\n}\n.small-video :deep(.wave-bar:nth-child(3)) {\n height: 4px;\n animation-delay: 0.3s;\n}\n.host-badge-inline :deep(.host-badge) {\n font-size: 9px;\n color: #fbbf24;\n background: rgba(251, 191, 36, 0.15);\n border: 1px solid rgba(251, 191, 36, 0.5);\n padding: 1px 4px;\n white-space: nowrap;\n}\n@keyframes wave-sm {\n 0%,\n 100% {\n transform: scaleY(1);\n opacity: 0.7;\n }\n 50% {\n transform: scaleY(1.6);\n opacity: 1;\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { ref } from 'vue'\nimport SmallVideoTile from './SmallVideoTile.vue'\nimport type { MeetingMember } from '@quitting/contract'\n\nconst props = defineProps<{\n members: MeetingMember[]\n // 自己的 identity(tile 据此把昵称显示为「我」)\n selfIdentity?: string\n // 自己的麦克风不可用(tile 状态图标置灰)\n selfMicUnavailable?: boolean\n // 每个小视频 tile 挂载/卸载时回调(去重,identity 不变不会重复触发)\n onTileMount?: (identity: string, el: HTMLVideoElement | null) => void\n}>()\n\nconst emit = defineEmits<{\n (e: 'tileClick', identity: string): void\n}>()\n\n// 暴露 listEl 给父组件做布局计算(设置 16:9 tile 尺寸)\nconst listEl = ref<HTMLElement | null>(null)\ndefineExpose({ listEl })\n\n// 闭包稳定:不会因 parent re-render 反复触发\nfunction handleTileVideoReady(identity: string, el: HTMLVideoElement | null) {\n props.onTileMount?.(identity, el)\n}\n</script>\n\n<template>\n <div ref=\"listEl\" class=\"sidebar-list\">\n <SmallVideoTile\n v-for=\"m in members\"\n :key=\"m.identity\"\n :identity=\"m.identity\"\n :name=\"m.name\"\n :muted=\"m.muted\"\n :camera-on=\"m.joined && m.cameraOn\"\n :speaking=\"m.speaking\"\n :is-host=\"m.isHost\"\n :is-self=\"m.identity === (selfIdentity || '')\"\n :mic-unavailable=\"m.identity === (selfIdentity || '') && !!selfMicUnavailable\"\n :on-video-el-ready=\"handleTileVideoReady\"\n @click=\"(id: string) => emit('tileClick', id)\"\n />\n </div>\n</template>\n\n<style scoped>\n.sidebar-list {\n flex: 1 1 0;\n min-height: 0;\n overflow-y: auto;\n overflow-x: hidden;\n display: flex;\n flex-direction: column;\n gap: 8px;\n /* 隐藏滚动条但保留滚动功能 */\n scrollbar-width: none;\n}\n.sidebar-list::-webkit-scrollbar {\n display: none;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { ref } from 'vue'\nimport SmallVideoTile from './SmallVideoTile.vue'\nimport type { MeetingMember } from '@quitting/contract'\n\nconst props = defineProps<{\n members: MeetingMember[]\n // 自己的 identity(tile 据此把昵称显示为「我」)\n selfIdentity?: string\n // 自己的麦克风不可用(tile 状态图标置灰)\n selfMicUnavailable?: boolean\n // 每个小视频 tile 挂载/卸载时回调(去重,identity 不变不会重复触发)\n onTileMount?: (identity: string, el: HTMLVideoElement | null) => void\n}>()\n\nconst emit = defineEmits<{\n (e: 'tileClick', identity: string): void\n}>()\n\n// 暴露 listEl 给父组件做布局计算(设置 16:9 tile 尺寸)\nconst listEl = ref<HTMLElement | null>(null)\ndefineExpose({ listEl })\n\n// 闭包稳定:不会因 parent re-render 反复触发\nfunction handleTileVideoReady(identity: string, el: HTMLVideoElement | null) {\n props.onTileMount?.(identity, el)\n}\n</script>\n\n<template>\n <div ref=\"listEl\" class=\"sidebar-list\">\n <SmallVideoTile\n v-for=\"m in members\"\n :key=\"m.identity\"\n :identity=\"m.identity\"\n :name=\"m.name\"\n :muted=\"m.muted\"\n :camera-on=\"m.joined && m.cameraOn\"\n :speaking=\"m.speaking\"\n :is-host=\"m.isHost\"\n :is-self=\"m.identity === (selfIdentity || '')\"\n :mic-unavailable=\"m.identity === (selfIdentity || '') && !!selfMicUnavailable\"\n :on-video-el-ready=\"handleTileVideoReady\"\n @click=\"(id: string) => emit('tileClick', id)\"\n />\n </div>\n</template>\n\n<style scoped>\n.sidebar-list {\n flex: 1 1 0;\n min-height: 0;\n overflow-y: auto;\n overflow-x: hidden;\n display: flex;\n flex-direction: column;\n gap: 8px;\n /* 隐藏滚动条但保留滚动功能 */\n scrollbar-width: none;\n}\n.sidebar-list::-webkit-scrollbar {\n display: none;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { ref, computed, onMounted, onBeforeUnmount } from 'vue'\nimport Icon from '../Icon.vue'\nimport type { MeetingMember } from '@quitting/contract'\n\nconst props = defineProps<{\n member: MeetingMember\n // 当前用户是否主持人(决定能否触发转主持人/踢人/再次邀请)\n canPromote: boolean\n canKick: boolean\n canReinvite?: boolean\n // 自己的 identity:成员列表中自己显示「我」而非昵称\n selfIdentity?: string\n // 自己的麦克风/摄像头不可用(无设备/无权限/被占用):对应状态图标置灰而非红/白\n selfMicUnavailable?: boolean\n selfCameraUnavailable?: boolean\n}>()\n\n// 是否是自己那一行\nconst isSelf = computed(() => props.member.identity === (props.selfIdentity || ''))\n\n// 设备不可用判定:自己用本地检测(更及时),他人用后端推断字段\n// (已入会但从未发布对应 track = 无设备/无权限/被占用;本端检测结果无法跨端传递)\nconst micUnavailable = computed(\n () =>\n (isSelf.value && !!props.selfMicUnavailable) ||\n (!isSelf.value && !!props.member.micUnavailable),\n)\nconst cameraUnavailable = computed(\n () =>\n (isSelf.value && !!props.selfCameraUnavailable) ||\n (!isSelf.value && !!props.member.cameraUnavailable),\n)\n\nconst emit = defineEmits<{\n (e: 'transferHost', uid: string): void\n (e: 'kick', uid: string): void\n (e: 'reinvite', uid: string): void\n}>()\n\n// 三点菜单展开状态\nconst menuOpen = ref(false)\n// 鼠标离开关闭定时器\nlet closeTimer: ReturnType<typeof setTimeout> | null = null\nconst CLOSE_DELAY = 500\n\n// 入会(online + joined)才允许设为主持人;离线/等待加入不显示此菜单项\nconst canShowPromote = () =>\n props.canPromote && props.member.online && props.member.joined && !props.member.isHost\n// 入会后(无论在线还是暂时离线)都允许踢出;已离会不需要再踢\nconst canShowKick = () => props.canKick && !props.member.isHost && props.member.status !== 'left'\n// 再次邀请:等待加入(status=invited)或已离会(status=left)允许\nconst canShowReinvite = () =>\n !!props.canReinvite &&\n !props.member.isHost &&\n (props.member.status === 'invited' || props.member.status === 'left')\n\nfunction toggleMenu() {\n if (menuOpen.value) {\n closeMenu()\n } else {\n menuOpen.value = true\n }\n}\nfunction closeMenu() {\n menuOpen.value = false\n if (closeTimer) {\n clearTimeout(closeTimer)\n closeTimer = null\n }\n}\n// 鼠标进入按钮或菜单:取消关闭定时器\nfunction onMouseEnter() {\n if (closeTimer) {\n clearTimeout(closeTimer)\n closeTimer = null\n }\n}\n// 鼠标离开按钮或菜单:500ms 后关闭\nfunction onMouseLeave() {\n if (!menuOpen.value) return\n if (closeTimer) clearTimeout(closeTimer)\n closeTimer = setTimeout(() => {\n menuOpen.value = false\n closeTimer = null\n }, CLOSE_DELAY)\n}\nfunction onPromote() {\n closeMenu()\n emit('transferHost', props.member.identity)\n}\nfunction onKick() {\n closeMenu()\n emit('kick', props.member.identity)\n}\nfunction onReinvite() {\n closeMenu()\n emit('reinvite', props.member.identity)\n}\n\n// 点击外部关闭菜单\nfunction onDocClick(e: MouseEvent) {\n if (!menuOpen.value) return\n const target = e.target as HTMLElement\n if (!target.closest('.member-item-row')) {\n closeMenu()\n }\n}\nonMounted(() => {\n document.addEventListener('click', onDocClick)\n})\nonBeforeUnmount(() => {\n document.removeEventListener('click', onDocClick)\n if (closeTimer) clearTimeout(closeTimer)\n})\n</script>\n\n<template>\n <div\n class=\"member-item\"\n :class=\"{\n offline: !member.online && member.status !== 'left',\n waiting: member.online && !member.joined,\n left: member.status === 'left',\n }\"\n >\n <div class=\"member-row\">\n <div class=\"member-main\">\n <div class=\"member-name\">\n {{ isSelf ? '我' : member.name }}\n <span v-if=\"member.isHost\" class=\"host-tag\">主持人</span>\n </div>\n </div>\n <div class=\"member-status\">\n <span v-if=\"member.status === 'left'\" class=\"status-badge\">已离会</span>\n <span v-else-if=\"!member.online\" class=\"status-badge\">离线</span>\n <span v-else-if=\"!member.joined\" class=\"status-badge\">等待加入</span>\n <span v-else-if=\"member.speaking\" class=\"status-badge speaking\">正在发言</span>\n </div>\n </div>\n <div class=\"member-row member-item-row\">\n <div class=\"member-main\">\n <div class=\"member-dept\">{{ member.dept || '—' }}</div>\n </div>\n <div class=\"member-status\">\n <!-- 三点菜单按钮(左侧) -->\n <button\n v-if=\"canShowPromote() || canShowKick() || canShowReinvite()\"\n class=\"member-menu\"\n type=\"button\"\n aria-label=\"更多操作\"\n @click.stop=\"toggleMenu\"\n @mouseenter=\"onMouseEnter\"\n @mouseleave=\"onMouseLeave\"\n >\n <Icon name=\"more\" :size=\"14\" />\n </button>\n <!-- 下拉菜单 -->\n <div\n v-if=\"menuOpen\"\n class=\"member-menu-popover\"\n @mouseenter=\"onMouseEnter\"\n @mouseleave=\"onMouseLeave\"\n >\n <button v-if=\"canShowReinvite()\" class=\"menu-item\" type=\"button\" @click.stop=\"onReinvite\">\n 再次邀请\n </button>\n <button v-if=\"canShowPromote()\" class=\"menu-item\" type=\"button\" @click.stop=\"onPromote\">\n 设为主持人\n </button>\n <button v-if=\"canShowKick()\" class=\"menu-item\" type=\"button\" @click.stop=\"onKick\">\n 离会\n </button>\n </div>\n <!-- 摄像头/麦克风状态图标(最右侧):仅入会成员显示。\n 图标统一:不可用或关闭 → 带斜杠 off 图标;不可用置灰(自己用本地检测,他人用后端推断字段),\n 主动闭麦红色,开启亮白 -->\n <Icon\n v-if=\"member.joined\"\n :name=\"cameraUnavailable || !member.cameraOn ? 'camera-off' : 'camera-on'\"\n :size=\"14\"\n :color=\"\n cameraUnavailable || !member.cameraOn\n ? 'rgba(255, 255, 255, 0.45)'\n : 'rgba(255, 255, 255, 0.85)'\n \"\n :title=\"\n cameraUnavailable ? '摄像头不可用' : member.cameraOn ? '摄像头已开' : '摄像头已关'\n \"\n />\n <Icon\n v-if=\"member.joined\"\n :name=\"micUnavailable || member.muted ? 'mic-off' : 'mic-on'\"\n :size=\"14\"\n :color=\"\n micUnavailable\n ? 'rgba(255, 255, 255, 0.45)'\n : member.muted\n ? '#ef4444'\n : 'rgba(255, 255, 255, 0.85)'\n \"\n :title=\"micUnavailable ? '麦克风不可用' : member.muted ? '麦克风已关' : '麦克风已开'\"\n />\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.member-item {\n position: relative;\n display: flex;\n flex-direction: column;\n justify-content: center;\n gap: 6px;\n height: 56px;\n padding: 0 10px;\n background: rgba(255, 255, 255, 0.03);\n border: 1px solid var(--im-meeting-border, rgba(255, 255, 255, 0.08));\n border-radius: 0;\n}\n/* 离线/等待加入:仅文字透明度降低,边框/按钮/菜单正常显示 */\n/* 整行不再 opacity,避免影响 ⋯ 按钮 + 悬浮菜单可读性 */\n.member-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n min-height: 0;\n}\n.member-main {\n flex: 1 1 0;\n min-width: 0;\n}\n.member-name {\n /* 入会成员昵称:从中亮白降到中等亮度的暖灰,避免与背景对比过强 */\n font-size: 13px;\n font-weight: 500;\n color: rgba(255, 255, 255, 0.78);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n display: flex;\n align-items: center;\n gap: 6px;\n}\n.member-item.offline .member-name,\n.member-item.waiting .member-name,\n.member-item.left .member-name {\n color: var(--im-meeting-text-tertiary, rgba(255, 255, 255, 0.55));\n}\n.host-tag {\n font-size: 10px;\n color: #fbbf24;\n background: rgba(251, 191, 36, 0.15);\n border: 1px solid rgba(251, 191, 36, 0.5);\n padding: 1px 4px;\n white-space: nowrap;\n}\n.member-dept {\n /* 部门颜色同样降低亮度 */\n font-size: 11px;\n color: rgba(255, 255, 255, 0.55);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.member-item.offline .member-dept,\n.member-item.waiting .member-dept,\n.member-item.left .member-dept {\n color: var(--im-meeting-text-tertiary, rgba(255, 255, 255, 0.55));\n}\n.member-status {\n display: flex;\n align-items: center;\n gap: 6px;\n flex: 0 0 auto;\n}\n/* 状态标签:无圆角、无边框(仅靠字色区分) */\n.status-badge {\n font-size: 10px;\n color: var(--im-meeting-text-tertiary, rgba(255, 255, 255, 0.55));\n padding: 1px 5px;\n white-space: nowrap;\n border-radius: 0;\n background: transparent;\n}\n.status-badge.speaking {\n color: var(--im-meeting-speaking, #22c55e);\n}\n.member-menu {\n width: 22px;\n height: 22px;\n border: none;\n background: transparent;\n color: var(--im-meeting-text-secondary, rgba(255, 255, 255, 0.85));\n cursor: pointer;\n border-radius: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n}\n.member-menu:hover {\n background: rgba(255, 255, 255, 0.08);\n color: var(--im-meeting-text-primary, #fff);\n}\n/* 下拉菜单:弹在三点菜单按钮左侧(麦克图标现在在最右) */\n.member-menu-popover {\n position: absolute;\n top: calc(50% - 12px);\n right: 56px;\n z-index: 5;\n min-width: 96px;\n background: #1e293b;\n border: 1px solid var(--im-meeting-border, rgba(255, 255, 255, 0.08));\n border-radius: 0;\n display: flex;\n flex-direction: column;\n padding: 2px 0;\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);\n}\n.menu-item {\n border: none;\n background: transparent;\n /* 离会与设为主持人颜色一致——不再用红色突出 danger */\n color: var(--im-meeting-text-secondary, rgba(255, 255, 255, 0.85));\n font-size: 12px;\n padding: 6px 12px;\n text-align: left;\n cursor: pointer;\n border-radius: 0;\n}\n.menu-item:hover {\n background: rgba(255, 255, 255, 0.08);\n color: var(--im-meeting-text-primary, #fff);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { ref, computed, onMounted, onBeforeUnmount } from 'vue'\nimport Icon from '../Icon.vue'\nimport type { MeetingMember } from '@quitting/contract'\n\nconst props = defineProps<{\n member: MeetingMember\n // 当前用户是否主持人(决定能否触发转主持人/踢人/再次邀请)\n canPromote: boolean\n canKick: boolean\n canReinvite?: boolean\n // 自己的 identity:成员列表中自己显示「我」而非昵称\n selfIdentity?: string\n // 自己的麦克风/摄像头不可用(无设备/无权限/被占用):对应状态图标置灰而非红/白\n selfMicUnavailable?: boolean\n selfCameraUnavailable?: boolean\n}>()\n\n// 是否是自己那一行\nconst isSelf = computed(() => props.member.identity === (props.selfIdentity || ''))\n\n// 设备不可用判定:自己用本地检测(更及时),他人用后端推断字段\n// (已入会但从未发布对应 track = 无设备/无权限/被占用;本端检测结果无法跨端传递)\nconst micUnavailable = computed(\n () =>\n (isSelf.value && !!props.selfMicUnavailable) ||\n (!isSelf.value && !!props.member.micUnavailable),\n)\nconst cameraUnavailable = computed(\n () =>\n (isSelf.value && !!props.selfCameraUnavailable) ||\n (!isSelf.value && !!props.member.cameraUnavailable),\n)\n\nconst emit = defineEmits<{\n (e: 'transferHost', uid: string): void\n (e: 'kick', uid: string): void\n (e: 'reinvite', uid: string): void\n}>()\n\n// 三点菜单展开状态\nconst menuOpen = ref(false)\n// 鼠标离开关闭定时器\nlet closeTimer: ReturnType<typeof setTimeout> | null = null\nconst CLOSE_DELAY = 500\n\n// 入会(online + joined)才允许设为主持人;离线/等待加入不显示此菜单项\nconst canShowPromote = () =>\n props.canPromote && props.member.online && props.member.joined && !props.member.isHost\n// 入会后(无论在线还是暂时离线)都允许踢出;已离会不需要再踢\nconst canShowKick = () => props.canKick && !props.member.isHost && props.member.status !== 'left'\n// 再次邀请:等待加入(status=invited)或已离会(status=left)允许\nconst canShowReinvite = () =>\n !!props.canReinvite &&\n !props.member.isHost &&\n (props.member.status === 'invited' || props.member.status === 'left')\n\nfunction toggleMenu() {\n if (menuOpen.value) {\n closeMenu()\n } else {\n menuOpen.value = true\n }\n}\nfunction closeMenu() {\n menuOpen.value = false\n if (closeTimer) {\n clearTimeout(closeTimer)\n closeTimer = null\n }\n}\n// 鼠标进入按钮或菜单:取消关闭定时器\nfunction onMouseEnter() {\n if (closeTimer) {\n clearTimeout(closeTimer)\n closeTimer = null\n }\n}\n// 鼠标离开按钮或菜单:500ms 后关闭\nfunction onMouseLeave() {\n if (!menuOpen.value) return\n if (closeTimer) clearTimeout(closeTimer)\n closeTimer = setTimeout(() => {\n menuOpen.value = false\n closeTimer = null\n }, CLOSE_DELAY)\n}\nfunction onPromote() {\n closeMenu()\n emit('transferHost', props.member.identity)\n}\nfunction onKick() {\n closeMenu()\n emit('kick', props.member.identity)\n}\nfunction onReinvite() {\n closeMenu()\n emit('reinvite', props.member.identity)\n}\n\n// 点击外部关闭菜单\nfunction onDocClick(e: MouseEvent) {\n if (!menuOpen.value) return\n const target = e.target as HTMLElement\n if (!target.closest('.member-item-row')) {\n closeMenu()\n }\n}\nonMounted(() => {\n document.addEventListener('click', onDocClick)\n})\nonBeforeUnmount(() => {\n document.removeEventListener('click', onDocClick)\n if (closeTimer) clearTimeout(closeTimer)\n})\n</script>\n\n<template>\n <div\n class=\"member-item\"\n :class=\"{\n offline: !member.online && member.status !== 'left',\n waiting: member.online && !member.joined,\n left: member.status === 'left',\n }\"\n >\n <div class=\"member-row\">\n <div class=\"member-main\">\n <div class=\"member-name\">\n {{ isSelf ? '我' : member.name }}\n <span v-if=\"member.isHost\" class=\"host-tag\">主持人</span>\n </div>\n </div>\n <div class=\"member-status\">\n <span v-if=\"member.status === 'left'\" class=\"status-badge\">已离会</span>\n <span v-else-if=\"!member.online\" class=\"status-badge\">离线</span>\n <span v-else-if=\"!member.joined\" class=\"status-badge\">等待加入</span>\n <span v-else-if=\"member.speaking\" class=\"status-badge speaking\">正在发言</span>\n </div>\n </div>\n <div class=\"member-row member-item-row\">\n <div class=\"member-main\">\n <div class=\"member-dept\">{{ member.dept || '—' }}</div>\n </div>\n <div class=\"member-status\">\n <!-- 三点菜单按钮(左侧) -->\n <button\n v-if=\"canShowPromote() || canShowKick() || canShowReinvite()\"\n class=\"member-menu\"\n type=\"button\"\n aria-label=\"更多操作\"\n @click.stop=\"toggleMenu\"\n @mouseenter=\"onMouseEnter\"\n @mouseleave=\"onMouseLeave\"\n >\n <Icon name=\"more\" :size=\"14\" />\n </button>\n <!-- 下拉菜单 -->\n <div\n v-if=\"menuOpen\"\n class=\"member-menu-popover\"\n @mouseenter=\"onMouseEnter\"\n @mouseleave=\"onMouseLeave\"\n >\n <button v-if=\"canShowReinvite()\" class=\"menu-item\" type=\"button\" @click.stop=\"onReinvite\">\n 再次邀请\n </button>\n <button v-if=\"canShowPromote()\" class=\"menu-item\" type=\"button\" @click.stop=\"onPromote\">\n 设为主持人\n </button>\n <button v-if=\"canShowKick()\" class=\"menu-item\" type=\"button\" @click.stop=\"onKick\">\n 离会\n </button>\n </div>\n <!-- 摄像头/麦克风状态图标(最右侧):仅入会成员显示。\n 图标统一:不可用或关闭 → 带斜杠 off 图标;不可用置灰(自己用本地检测,他人用后端推断字段),\n 主动闭麦红色,开启亮白 -->\n <Icon\n v-if=\"member.joined\"\n :name=\"cameraUnavailable || !member.cameraOn ? 'camera-off' : 'camera-on'\"\n :size=\"14\"\n :color=\"\n cameraUnavailable || !member.cameraOn\n ? 'rgba(255, 255, 255, 0.45)'\n : 'rgba(255, 255, 255, 0.85)'\n \"\n :title=\"\n cameraUnavailable ? '摄像头不可用' : member.cameraOn ? '摄像头已开' : '摄像头已关'\n \"\n />\n <Icon\n v-if=\"member.joined\"\n :name=\"micUnavailable || member.muted ? 'mic-off' : 'mic-on'\"\n :size=\"14\"\n :color=\"\n micUnavailable\n ? 'rgba(255, 255, 255, 0.45)'\n : member.muted\n ? '#ef4444'\n : 'rgba(255, 255, 255, 0.85)'\n \"\n :title=\"micUnavailable ? '麦克风不可用' : member.muted ? '麦克风已关' : '麦克风已开'\"\n />\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.member-item {\n position: relative;\n display: flex;\n flex-direction: column;\n justify-content: center;\n gap: 6px;\n height: 56px;\n padding: 0 10px;\n background: rgba(255, 255, 255, 0.03);\n border: 1px solid var(--im-meeting-border, rgba(255, 255, 255, 0.08));\n border-radius: 0;\n}\n/* 离线/等待加入:仅文字透明度降低,边框/按钮/菜单正常显示 */\n/* 整行不再 opacity,避免影响 ⋯ 按钮 + 悬浮菜单可读性 */\n.member-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n min-height: 0;\n}\n.member-main {\n flex: 1 1 0;\n min-width: 0;\n}\n.member-name {\n /* 入会成员昵称:从中亮白降到中等亮度的暖灰,避免与背景对比过强 */\n font-size: 13px;\n font-weight: 500;\n color: rgba(255, 255, 255, 0.78);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n display: flex;\n align-items: center;\n gap: 6px;\n}\n.member-item.offline .member-name,\n.member-item.waiting .member-name,\n.member-item.left .member-name {\n color: var(--im-meeting-text-tertiary, rgba(255, 255, 255, 0.55));\n}\n.host-tag {\n font-size: 10px;\n color: #fbbf24;\n background: rgba(251, 191, 36, 0.15);\n border: 1px solid rgba(251, 191, 36, 0.5);\n padding: 1px 4px;\n white-space: nowrap;\n}\n.member-dept {\n /* 部门颜色同样降低亮度 */\n font-size: 11px;\n color: rgba(255, 255, 255, 0.55);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.member-item.offline .member-dept,\n.member-item.waiting .member-dept,\n.member-item.left .member-dept {\n color: var(--im-meeting-text-tertiary, rgba(255, 255, 255, 0.55));\n}\n.member-status {\n display: flex;\n align-items: center;\n gap: 6px;\n flex: 0 0 auto;\n}\n/* 状态标签:无圆角、无边框(仅靠字色区分) */\n.status-badge {\n font-size: 10px;\n color: var(--im-meeting-text-tertiary, rgba(255, 255, 255, 0.55));\n padding: 1px 5px;\n white-space: nowrap;\n border-radius: 0;\n background: transparent;\n}\n.status-badge.speaking {\n color: var(--im-meeting-speaking, #22c55e);\n}\n.member-menu {\n width: 22px;\n height: 22px;\n border: none;\n background: transparent;\n color: var(--im-meeting-text-secondary, rgba(255, 255, 255, 0.85));\n cursor: pointer;\n border-radius: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n}\n.member-menu:hover {\n background: rgba(255, 255, 255, 0.08);\n color: var(--im-meeting-text-primary, #fff);\n}\n/* 下拉菜单:弹在三点菜单按钮左侧(麦克图标现在在最右) */\n.member-menu-popover {\n position: absolute;\n top: calc(50% - 12px);\n right: 56px;\n z-index: 5;\n min-width: 96px;\n background: #1e293b;\n border: 1px solid var(--im-meeting-border, rgba(255, 255, 255, 0.08));\n border-radius: 0;\n display: flex;\n flex-direction: column;\n padding: 2px 0;\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);\n}\n.menu-item {\n border: none;\n background: transparent;\n /* 离会与设为主持人颜色一致——不再用红色突出 danger */\n color: var(--im-meeting-text-secondary, rgba(255, 255, 255, 0.85));\n font-size: 12px;\n padding: 6px 12px;\n text-align: left;\n cursor: pointer;\n border-radius: 0;\n}\n.menu-item:hover {\n background: rgba(255, 255, 255, 0.08);\n color: var(--im-meeting-text-primary, #fff);\n}\n</style>\n","<script setup lang=\"ts\">\nimport MemberItem from './MemberItem.vue'\nimport type { MeetingMember } from '@quitting/contract'\n\ndefineProps<{\n members: MeetingMember[]\n canPromote: boolean\n canKick: boolean\n // 主持人权限:允许「再次邀请」status=invited/left 的成员\n canReinvite?: boolean\n // 自己的 identity 与设备不可用状态(透传给 MemberItem:自己显示「我」、图标置灰)\n selfIdentity?: string\n selfMicUnavailable?: boolean\n selfCameraUnavailable?: boolean\n}>()\n\nconst emit = defineEmits<{\n (e: 'transferHost', uid: string): void\n (e: 'kick', uid: string): void\n (e: 'reinvite', uid: string): void\n}>()\n</script>\n\n<template>\n <div class=\"member-list\">\n <MemberItem\n v-for=\"m in members\"\n :key=\"m.identity\"\n :member=\"m\"\n :can-promote=\"canPromote\"\n :can-kick=\"canKick\"\n :can-reinvite=\"canReinvite\"\n :self-identity=\"selfIdentity\"\n :self-mic-unavailable=\"selfMicUnavailable\"\n :self-camera-unavailable=\"selfCameraUnavailable\"\n @transfer-host=\"(uid: string) => emit('transferHost', uid)\"\n @kick=\"(uid: string) => emit('kick', uid)\"\n @reinvite=\"(uid: string) => emit('reinvite', uid)\"\n />\n </div>\n</template>\n\n<style scoped>\n.member-list {\n flex: 1 1 0;\n min-height: 0;\n overflow-y: auto;\n overflow-x: hidden;\n display: flex;\n flex-direction: column;\n gap: 8px;\n /* 隐藏滚动条但保留滚动功能 */\n scrollbar-width: none;\n}\n.member-list::-webkit-scrollbar {\n display: none;\n}\n</style>\n","<script setup lang=\"ts\">\nimport MemberItem from './MemberItem.vue'\nimport type { MeetingMember } from '@quitting/contract'\n\ndefineProps<{\n members: MeetingMember[]\n canPromote: boolean\n canKick: boolean\n // 主持人权限:允许「再次邀请」status=invited/left 的成员\n canReinvite?: boolean\n // 自己的 identity 与设备不可用状态(透传给 MemberItem:自己显示「我」、图标置灰)\n selfIdentity?: string\n selfMicUnavailable?: boolean\n selfCameraUnavailable?: boolean\n}>()\n\nconst emit = defineEmits<{\n (e: 'transferHost', uid: string): void\n (e: 'kick', uid: string): void\n (e: 'reinvite', uid: string): void\n}>()\n</script>\n\n<template>\n <div class=\"member-list\">\n <MemberItem\n v-for=\"m in members\"\n :key=\"m.identity\"\n :member=\"m\"\n :can-promote=\"canPromote\"\n :can-kick=\"canKick\"\n :can-reinvite=\"canReinvite\"\n :self-identity=\"selfIdentity\"\n :self-mic-unavailable=\"selfMicUnavailable\"\n :self-camera-unavailable=\"selfCameraUnavailable\"\n @transfer-host=\"(uid: string) => emit('transferHost', uid)\"\n @kick=\"(uid: string) => emit('kick', uid)\"\n @reinvite=\"(uid: string) => emit('reinvite', uid)\"\n />\n </div>\n</template>\n\n<style scoped>\n.member-list {\n flex: 1 1 0;\n min-height: 0;\n overflow-y: auto;\n overflow-x: hidden;\n display: flex;\n flex-direction: column;\n gap: 8px;\n /* 隐藏滚动条但保留滚动功能 */\n scrollbar-width: none;\n}\n.member-list::-webkit-scrollbar {\n display: none;\n}\n</style>\n","<script setup lang=\"ts\">\nimport Icon from '../Icon.vue'\n\ndefineProps<{\n activeTab: 'video' | 'member'\n // 是否显示「+」添加参会人员按钮——仅主持人可邀请,默认不显示\n isHost?: boolean\n}>()\n\nconst emit = defineEmits<{\n (e: 'update:activeTab', tab: 'video' | 'member'): void\n (e: 'addMember'): void\n}>()\n</script>\n\n<template>\n <div class=\"tab-bar\">\n <button\n class=\"tab-btn\"\n :class=\"{ active: activeTab === 'video' }\"\n type=\"button\"\n @click=\"emit('update:activeTab', 'video')\"\n >\n 视频列表\n </button>\n <button\n class=\"tab-btn\"\n :class=\"{ active: activeTab === 'member', 'before-add': true }\"\n type=\"button\"\n @click=\"emit('update:activeTab', 'member')\"\n >\n 成员列表\n </button>\n <template v-if=\"isHost\">\n <span class=\"tab-divider\" aria-hidden=\"true\"></span>\n <button class=\"add-member-btn\" type=\"button\" title=\"添加参会人员\" @click=\"emit('addMember')\">\n <Icon name=\"plus\" :size=\"14\" />\n </button>\n </template>\n </div>\n</template>\n\n<style scoped>\n.tab-bar {\n flex: 0 0 auto;\n display: flex;\n flex-direction: row;\n align-items: stretch;\n /* 整个 tab-bar 撑满父级宽度,与下方列表对齐 */\n width: 100%;\n gap: 0;\n}\n.tab-btn {\n flex: 1 1 0;\n padding: 0 8px;\n height: 28px;\n line-height: 28px;\n background: var(--im-meeting-tab-inactive, #111827);\n border: 1px solid var(--im-meeting-border, rgba(255, 255, 255, 0.08));\n border-right: none;\n color: var(--im-meeting-text-secondary, rgba(255, 255, 255, 0.85));\n font-size: 12px;\n cursor: pointer;\n transition:\n background 0.2s,\n color 0.2s;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n border-radius: 0;\n min-width: 0;\n}\n/* 选中 tab 与 + 按钮之间需要可见分割线:两个 #1e293b 背景连成一片时 1px 边框看不出来\n 用一个 2px 深色实体分割条嵌在中间,颜色对比足够明显 */\n.tab-btn.before-add {\n border-right: none;\n}\n.tab-divider {\n flex: 0 0 auto;\n width: 2px;\n background: #0b1220;\n border: none;\n align-self: stretch;\n}\n.tab-btn.active {\n background: var(--im-meeting-tab-active, #1e293b);\n color: var(--im-meeting-text-primary, #fff);\n}\n.tab-btn:hover:not(.active) {\n background: #1f2937;\n color: var(--im-meeting-text-primary, #fff);\n}\n/* 添加按钮:始终 28×28 正方形;Icon 由 flex 居中 */\n.add-member-btn {\n flex: 0 0 28px;\n width: 28px;\n height: 28px;\n border: 1px solid var(--im-meeting-border, rgba(255, 255, 255, 0.08));\n border-left: none;\n background: var(--im-meeting-tab-active, #1e293b);\n color: var(--im-meeting-text-primary, #fff);\n cursor: pointer;\n border-radius: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n margin-left: -1px;\n box-sizing: border-box;\n}\n.add-member-btn:hover {\n background: #334155;\n}\n</style>\n","<script setup lang=\"ts\">\nimport Icon from '../Icon.vue'\n\ndefineProps<{\n activeTab: 'video' | 'member'\n // 是否显示「+」添加参会人员按钮——仅主持人可邀请,默认不显示\n isHost?: boolean\n}>()\n\nconst emit = defineEmits<{\n (e: 'update:activeTab', tab: 'video' | 'member'): void\n (e: 'addMember'): void\n}>()\n</script>\n\n<template>\n <div class=\"tab-bar\">\n <button\n class=\"tab-btn\"\n :class=\"{ active: activeTab === 'video' }\"\n type=\"button\"\n @click=\"emit('update:activeTab', 'video')\"\n >\n 视频列表\n </button>\n <button\n class=\"tab-btn\"\n :class=\"{ active: activeTab === 'member', 'before-add': true }\"\n type=\"button\"\n @click=\"emit('update:activeTab', 'member')\"\n >\n 成员列表\n </button>\n <template v-if=\"isHost\">\n <span class=\"tab-divider\" aria-hidden=\"true\"></span>\n <button class=\"add-member-btn\" type=\"button\" title=\"添加参会人员\" @click=\"emit('addMember')\">\n <Icon name=\"plus\" :size=\"14\" />\n </button>\n </template>\n </div>\n</template>\n\n<style scoped>\n.tab-bar {\n flex: 0 0 auto;\n display: flex;\n flex-direction: row;\n align-items: stretch;\n /* 整个 tab-bar 撑满父级宽度,与下方列表对齐 */\n width: 100%;\n gap: 0;\n}\n.tab-btn {\n flex: 1 1 0;\n padding: 0 8px;\n height: 28px;\n line-height: 28px;\n background: var(--im-meeting-tab-inactive, #111827);\n border: 1px solid var(--im-meeting-border, rgba(255, 255, 255, 0.08));\n border-right: none;\n color: var(--im-meeting-text-secondary, rgba(255, 255, 255, 0.85));\n font-size: 12px;\n cursor: pointer;\n transition:\n background 0.2s,\n color 0.2s;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n border-radius: 0;\n min-width: 0;\n}\n/* 选中 tab 与 + 按钮之间需要可见分割线:两个 #1e293b 背景连成一片时 1px 边框看不出来\n 用一个 2px 深色实体分割条嵌在中间,颜色对比足够明显 */\n.tab-btn.before-add {\n border-right: none;\n}\n.tab-divider {\n flex: 0 0 auto;\n width: 2px;\n background: #0b1220;\n border: none;\n align-self: stretch;\n}\n.tab-btn.active {\n background: var(--im-meeting-tab-active, #1e293b);\n color: var(--im-meeting-text-primary, #fff);\n}\n.tab-btn:hover:not(.active) {\n background: #1f2937;\n color: var(--im-meeting-text-primary, #fff);\n}\n/* 添加按钮:始终 28×28 正方形;Icon 由 flex 居中 */\n.add-member-btn {\n flex: 0 0 28px;\n width: 28px;\n height: 28px;\n border: 1px solid var(--im-meeting-border, rgba(255, 255, 255, 0.08));\n border-left: none;\n background: var(--im-meeting-tab-active, #1e293b);\n color: var(--im-meeting-text-primary, #fff);\n cursor: pointer;\n border-radius: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n margin-left: -1px;\n box-sizing: border-box;\n}\n.add-member-btn:hover {\n background: #334155;\n}\n</style>\n","<script setup lang=\"ts\">\nimport Icon from '../Icon.vue'\n\n// 会议控制按钮组(麦克风/摄像头/扬声器/挂断)\n// 默认模式放在窗口底部、全屏宫格模式放在 meeting-header 中央——两处复用同一组件,\n// 保证样式与行为完全一致。状态与动作由父组件持有,本组件只做展示与事件转发\n\ndefineProps<{\n micEnabled: boolean\n cameraEnabled: boolean\n speakerMuted: boolean\n // 设备不可用(无设备/无权限/被占用):按钮置灰禁用——红色仅表示主动关闭\n micUnavailable?: boolean\n cameraUnavailable?: boolean\n}>()\n\nconst emit = defineEmits<{\n (e: 'toggleMic'): void\n (e: 'toggleCamera'): void\n (e: 'toggleSpeaker'): void\n (e: 'hangup'): void\n}>()\n</script>\n\n<template>\n <div class=\"meeting-controls\">\n <button\n class=\"ctrl-btn\"\n :class=\"{ off: !micEnabled && !micUnavailable, unavailable: micUnavailable }\"\n type=\"button\"\n :disabled=\"micUnavailable\"\n :title=\"micUnavailable ? '麦克风不可用' : micEnabled ? '麦克风已开' : '麦克风已关'\"\n @click=\"emit('toggleMic')\"\n >\n <Icon :name=\"micEnabled && !micUnavailable ? 'mic-on' : 'mic-off'\" :size=\"22\" />\n </button>\n <button\n class=\"ctrl-btn\"\n :class=\"{ off: !cameraEnabled && !cameraUnavailable, unavailable: cameraUnavailable }\"\n type=\"button\"\n :disabled=\"cameraUnavailable\"\n :title=\"cameraUnavailable ? '摄像头不可用' : cameraEnabled ? '摄像头已开' : '摄像头已关'\"\n @click=\"emit('toggleCamera')\"\n >\n <Icon :name=\"cameraEnabled && !cameraUnavailable ? 'camera-on' : 'camera-off'\" :size=\"22\" />\n </button>\n <button\n class=\"ctrl-btn\"\n :class=\"{ off: speakerMuted }\"\n type=\"button\"\n :title=\"speakerMuted ? '扬声器已关' : '扬声器已开'\"\n @click=\"emit('toggleSpeaker')\"\n >\n <Icon :name=\"speakerMuted ? 'sound-off' : 'sound-on'\" :size=\"22\" />\n </button>\n <button class=\"ctrl-btn hangup\" type=\"button\" title=\"离开会议\" @click=\"emit('hangup')\">\n <Icon name=\"hangup\" :size=\"22\" />\n </button>\n </div>\n</template>\n\n<style scoped>\n.meeting-controls {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 16px;\n}\n.ctrl-btn {\n width: 32px;\n height: 32px;\n /* 无边框无背景:仅图标 */\n border: none;\n background: transparent;\n color: var(--im-meeting-text-primary, #fff);\n cursor: pointer;\n border-radius: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n transition:\n transform 0.1s,\n opacity 0.15s;\n opacity: 0.85;\n}\n.ctrl-btn:hover {\n opacity: 1;\n}\n.ctrl-btn:active {\n transform: scale(0.92);\n}\n/* 开关关闭状态:图标变红(仅主动关闭;设备不可用走 .unavailable 置灰) */\n.ctrl-btn.off {\n color: #ef4444;\n opacity: 1;\n}\n/* 设备不可用:置灰 + 禁点 */\n.ctrl-btn.unavailable {\n color: rgba(255, 255, 255, 0.45);\n opacity: 1;\n cursor: not-allowed;\n}\n/* 挂断按钮:图标红色(无红色背景块) */\n.ctrl-btn.hangup {\n color: #ef4444;\n}\n.ctrl-btn.hangup:hover {\n color: #f45f5b;\n}\n</style>\n","<script setup lang=\"ts\">\nimport Icon from '../Icon.vue'\n\n// 会议控制按钮组(麦克风/摄像头/扬声器/挂断)\n// 默认模式放在窗口底部、全屏宫格模式放在 meeting-header 中央——两处复用同一组件,\n// 保证样式与行为完全一致。状态与动作由父组件持有,本组件只做展示与事件转发\n\ndefineProps<{\n micEnabled: boolean\n cameraEnabled: boolean\n speakerMuted: boolean\n // 设备不可用(无设备/无权限/被占用):按钮置灰禁用——红色仅表示主动关闭\n micUnavailable?: boolean\n cameraUnavailable?: boolean\n}>()\n\nconst emit = defineEmits<{\n (e: 'toggleMic'): void\n (e: 'toggleCamera'): void\n (e: 'toggleSpeaker'): void\n (e: 'hangup'): void\n}>()\n</script>\n\n<template>\n <div class=\"meeting-controls\">\n <button\n class=\"ctrl-btn\"\n :class=\"{ off: !micEnabled && !micUnavailable, unavailable: micUnavailable }\"\n type=\"button\"\n :disabled=\"micUnavailable\"\n :title=\"micUnavailable ? '麦克风不可用' : micEnabled ? '麦克风已开' : '麦克风已关'\"\n @click=\"emit('toggleMic')\"\n >\n <Icon :name=\"micEnabled && !micUnavailable ? 'mic-on' : 'mic-off'\" :size=\"22\" />\n </button>\n <button\n class=\"ctrl-btn\"\n :class=\"{ off: !cameraEnabled && !cameraUnavailable, unavailable: cameraUnavailable }\"\n type=\"button\"\n :disabled=\"cameraUnavailable\"\n :title=\"cameraUnavailable ? '摄像头不可用' : cameraEnabled ? '摄像头已开' : '摄像头已关'\"\n @click=\"emit('toggleCamera')\"\n >\n <Icon :name=\"cameraEnabled && !cameraUnavailable ? 'camera-on' : 'camera-off'\" :size=\"22\" />\n </button>\n <button\n class=\"ctrl-btn\"\n :class=\"{ off: speakerMuted }\"\n type=\"button\"\n :title=\"speakerMuted ? '扬声器已关' : '扬声器已开'\"\n @click=\"emit('toggleSpeaker')\"\n >\n <Icon :name=\"speakerMuted ? 'sound-off' : 'sound-on'\" :size=\"22\" />\n </button>\n <button class=\"ctrl-btn hangup\" type=\"button\" title=\"离开会议\" @click=\"emit('hangup')\">\n <Icon name=\"hangup\" :size=\"22\" />\n </button>\n </div>\n</template>\n\n<style scoped>\n.meeting-controls {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 16px;\n}\n.ctrl-btn {\n width: 32px;\n height: 32px;\n /* 无边框无背景:仅图标 */\n border: none;\n background: transparent;\n color: var(--im-meeting-text-primary, #fff);\n cursor: pointer;\n border-radius: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n transition:\n transform 0.1s,\n opacity 0.15s;\n opacity: 0.85;\n}\n.ctrl-btn:hover {\n opacity: 1;\n}\n.ctrl-btn:active {\n transform: scale(0.92);\n}\n/* 开关关闭状态:图标变红(仅主动关闭;设备不可用走 .unavailable 置灰) */\n.ctrl-btn.off {\n color: #ef4444;\n opacity: 1;\n}\n/* 设备不可用:置灰 + 禁点 */\n.ctrl-btn.unavailable {\n color: rgba(255, 255, 255, 0.45);\n opacity: 1;\n cursor: not-allowed;\n}\n/* 挂断按钮:图标红色(无红色背景块) */\n.ctrl-btn.hangup {\n color: #ef4444;\n}\n.ctrl-btn.hangup:hover {\n color: #f45f5b;\n}\n</style>\n","<script setup lang=\"ts\">\n// 通用确认弹窗:深色主题、零圆角、水平居中\ndefineProps<{\n title?: string\n message: string\n confirmText?: string\n cancelText?: string\n danger?: boolean\n}>()\n\nconst emit = defineEmits<{\n (e: 'confirm'): void\n (e: 'cancel'): void\n}>()\n</script>\n\n<template>\n <div class=\"confirm-mask\" @click.self=\"emit('cancel')\">\n <div class=\"confirm-dialog\">\n <div v-if=\"title\" class=\"confirm-title\">{{ title }}</div>\n <div class=\"confirm-message\">{{ message }}</div>\n <div class=\"confirm-actions\">\n <button class=\"confirm-btn cancel\" type=\"button\" @click=\"emit('cancel')\">\n {{ cancelText || '取消' }}\n </button>\n <button class=\"confirm-btn ok\" :class=\"{ danger }\" type=\"button\" @click=\"emit('confirm')\">\n {{ confirmText || '确认' }}\n </button>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.confirm-mask {\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.5);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 99999;\n}\n.confirm-dialog {\n min-width: 320px;\n max-width: 440px;\n background: #0f172a;\n border: 1px solid rgba(255, 255, 255, 0.08);\n border-radius: 0;\n padding: 20px;\n color: #fff;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6);\n}\n.confirm-title {\n font-size: 15px;\n font-weight: 600;\n margin-bottom: 8px;\n}\n.confirm-message {\n font-size: 13px;\n color: rgba(255, 255, 255, 0.85);\n line-height: 1.5;\n margin-bottom: 16px;\n}\n.confirm-actions {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n}\n.confirm-btn {\n padding: 6px 14px;\n font-size: 12px;\n border: 1px solid rgba(255, 255, 255, 0.15);\n background: rgba(255, 255, 255, 0.08);\n color: rgba(255, 255, 255, 0.85);\n cursor: pointer;\n border-radius: 0;\n}\n.confirm-btn:hover {\n background: rgba(255, 255, 255, 0.15);\n color: #fff;\n}\n.confirm-btn.ok {\n background: #1e293b;\n color: #fff;\n}\n.confirm-btn.ok.danger {\n background: #e53935;\n border-color: #e53935;\n}\n.confirm-btn.ok.danger:hover {\n background: #f45f5b;\n}\n</style>\n","<script setup lang=\"ts\">\n// 通用确认弹窗:深色主题、零圆角、水平居中\ndefineProps<{\n title?: string\n message: string\n confirmText?: string\n cancelText?: string\n danger?: boolean\n}>()\n\nconst emit = defineEmits<{\n (e: 'confirm'): void\n (e: 'cancel'): void\n}>()\n</script>\n\n<template>\n <div class=\"confirm-mask\" @click.self=\"emit('cancel')\">\n <div class=\"confirm-dialog\">\n <div v-if=\"title\" class=\"confirm-title\">{{ title }}</div>\n <div class=\"confirm-message\">{{ message }}</div>\n <div class=\"confirm-actions\">\n <button class=\"confirm-btn cancel\" type=\"button\" @click=\"emit('cancel')\">\n {{ cancelText || '取消' }}\n </button>\n <button class=\"confirm-btn ok\" :class=\"{ danger }\" type=\"button\" @click=\"emit('confirm')\">\n {{ confirmText || '确认' }}\n </button>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.confirm-mask {\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.5);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 99999;\n}\n.confirm-dialog {\n min-width: 320px;\n max-width: 440px;\n background: #0f172a;\n border: 1px solid rgba(255, 255, 255, 0.08);\n border-radius: 0;\n padding: 20px;\n color: #fff;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6);\n}\n.confirm-title {\n font-size: 15px;\n font-weight: 600;\n margin-bottom: 8px;\n}\n.confirm-message {\n font-size: 13px;\n color: rgba(255, 255, 255, 0.85);\n line-height: 1.5;\n margin-bottom: 16px;\n}\n.confirm-actions {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n}\n.confirm-btn {\n padding: 6px 14px;\n font-size: 12px;\n border: 1px solid rgba(255, 255, 255, 0.15);\n background: rgba(255, 255, 255, 0.08);\n color: rgba(255, 255, 255, 0.85);\n cursor: pointer;\n border-radius: 0;\n}\n.confirm-btn:hover {\n background: rgba(255, 255, 255, 0.15);\n color: #fff;\n}\n.confirm-btn.ok {\n background: #1e293b;\n color: #fff;\n}\n.confirm-btn.ok.danger {\n background: #e53935;\n border-color: #e53935;\n}\n.confirm-btn.ok.danger:hover {\n background: #f45f5b;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { ref, onMounted, onBeforeUnmount, computed } from 'vue'\nimport Icon from './Icon.vue'\n\n// 会议结束独立提示浮窗:固定定位在屏幕右下角,不依赖会议主窗口存在\n// 用于解散/被踢/结束/主动离开等场景,显示原因 + 倒计时 + 进度条\nconst props = withDefaults(\n defineProps<{\n reason?: string\n duration?: number // 倒计时秒数\n }>(),\n {\n reason: '会议已结束',\n duration: 5,\n },\n)\n\nconst emit = defineEmits<{ (e: 'close'): void }>()\n\nconst remaining = ref(props.duration)\nlet timer: ReturnType<typeof setInterval> | null = null\n\nconst displayText = computed(() => `${props.reason}(${remaining.value}s)`)\n\nonMounted(() => {\n timer = setInterval(() => {\n remaining.value--\n if (remaining.value <= 0) {\n if (timer) clearInterval(timer)\n timer = null\n emit('close')\n }\n }, 1000)\n})\n\nonBeforeUnmount(() => {\n if (timer) clearInterval(timer)\n})\n</script>\n\n<template>\n <div class=\"ended-toast\">\n <div class=\"ended-toast-card\">\n <span class=\"ended-toast-text\">{{ displayText }}</span>\n <button class=\"ended-toast-close\" type=\"button\" title=\"立即关闭\" @click=\"emit('close')\">\n <Icon name=\"close\" :size=\"14\" />\n </button>\n </div>\n <!-- 倒计时进度条 -->\n <div class=\"ended-toast-progress\">\n <div\n class=\"ended-toast-progress-bar\"\n :style=\"{ animationDuration: `${props.duration}s` }\"\n ></div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.ended-toast {\n position: fixed;\n right: 20px;\n bottom: 20px;\n z-index: 10001;\n width: 240px;\n background: var(--im-meeting-bg-panel, #0f172a);\n border: 1px solid var(--im-meeting-border, rgba(255, 255, 255, 0.12));\n border-radius: 0;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);\n color: #fff;\n overflow: hidden;\n}\n.ended-toast-card {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n padding: 10px 12px;\n}\n.ended-toast-text {\n font-size: 13px;\n font-weight: 500;\n color: #fff;\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.ended-toast-close {\n width: 22px;\n height: 22px;\n flex: 0 0 auto;\n border: 1px solid rgba(255, 255, 255, 0.2);\n background: rgba(255, 255, 255, 0.08);\n color: #fff;\n cursor: pointer;\n border-radius: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n}\n.ended-toast-close:hover {\n background: rgba(255, 255, 255, 0.18);\n}\n.ended-toast-progress {\n height: 2px;\n background: rgba(255, 255, 255, 0.1);\n overflow: hidden;\n}\n.ended-toast-progress-bar {\n height: 100%;\n background: #e53935;\n width: 100%;\n transform-origin: left;\n animation: ended-toast-shrink linear forwards;\n}\n@keyframes ended-toast-shrink {\n from {\n transform: scaleX(1);\n }\n to {\n transform: scaleX(0);\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { ref, onMounted, onBeforeUnmount, computed } from 'vue'\nimport Icon from './Icon.vue'\n\n// 会议结束独立提示浮窗:固定定位在屏幕右下角,不依赖会议主窗口存在\n// 用于解散/被踢/结束/主动离开等场景,显示原因 + 倒计时 + 进度条\nconst props = withDefaults(\n defineProps<{\n reason?: string\n duration?: number // 倒计时秒数\n }>(),\n {\n reason: '会议已结束',\n duration: 5,\n },\n)\n\nconst emit = defineEmits<{ (e: 'close'): void }>()\n\nconst remaining = ref(props.duration)\nlet timer: ReturnType<typeof setInterval> | null = null\n\nconst displayText = computed(() => `${props.reason}(${remaining.value}s)`)\n\nonMounted(() => {\n timer = setInterval(() => {\n remaining.value--\n if (remaining.value <= 0) {\n if (timer) clearInterval(timer)\n timer = null\n emit('close')\n }\n }, 1000)\n})\n\nonBeforeUnmount(() => {\n if (timer) clearInterval(timer)\n})\n</script>\n\n<template>\n <div class=\"ended-toast\">\n <div class=\"ended-toast-card\">\n <span class=\"ended-toast-text\">{{ displayText }}</span>\n <button class=\"ended-toast-close\" type=\"button\" title=\"立即关闭\" @click=\"emit('close')\">\n <Icon name=\"close\" :size=\"14\" />\n </button>\n </div>\n <!-- 倒计时进度条 -->\n <div class=\"ended-toast-progress\">\n <div\n class=\"ended-toast-progress-bar\"\n :style=\"{ animationDuration: `${props.duration}s` }\"\n ></div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.ended-toast {\n position: fixed;\n right: 20px;\n bottom: 20px;\n z-index: 10001;\n width: 240px;\n background: var(--im-meeting-bg-panel, #0f172a);\n border: 1px solid var(--im-meeting-border, rgba(255, 255, 255, 0.12));\n border-radius: 0;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);\n color: #fff;\n overflow: hidden;\n}\n.ended-toast-card {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n padding: 10px 12px;\n}\n.ended-toast-text {\n font-size: 13px;\n font-weight: 500;\n color: #fff;\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.ended-toast-close {\n width: 22px;\n height: 22px;\n flex: 0 0 auto;\n border: 1px solid rgba(255, 255, 255, 0.2);\n background: rgba(255, 255, 255, 0.08);\n color: #fff;\n cursor: pointer;\n border-radius: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n}\n.ended-toast-close:hover {\n background: rgba(255, 255, 255, 0.18);\n}\n.ended-toast-progress {\n height: 2px;\n background: rgba(255, 255, 255, 0.1);\n overflow: hidden;\n}\n.ended-toast-progress-bar {\n height: 100%;\n background: #e53935;\n width: 100%;\n transform-origin: left;\n animation: ended-toast-shrink linear forwards;\n}\n@keyframes ended-toast-shrink {\n from {\n transform: scaleX(1);\n }\n to {\n transform: scaleX(0);\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport type { PickerUser } from '../types'\n\n// 选人列表(无 mask 的纯列表区):工具栏(计数/全选)+ 多选列表 + 空态\n// 被 UserPickerDialog(邀请参会)与 MeetingCreateDialog(发起会议)复用\n// 自定义行渲染:#user-item 插槽(scope: { user, selected, toggle })\nconst props = defineProps<{\n users: PickerUser[]\n loading?: boolean\n selected?: string[]\n emptyText?: string\n}>()\n\nconst emit = defineEmits<{ (e: 'update:selected', uids: string[]): void }>()\n\nconst slots = defineSlots<{\n 'user-item'?(scope: { user: PickerUser; selected: boolean; toggle: () => void }): unknown\n}>()\n\nconst selectedSet = computed(() => new Set(props.selected || []))\n\nfunction toggleOne(uid: string) {\n const next = new Set(selectedSet.value)\n if (next.has(uid)) next.delete(uid)\n else next.add(uid)\n emit('update:selected', [...next])\n}\n\nconst allSelected = computed(\n () => props.users.length > 0 && selectedSet.value.size === props.users.length,\n)\n\nfunction toggleAll() {\n emit('update:selected', allSelected.value ? [] : props.users.map((u) => u.uid))\n}\n</script>\n\n<template>\n <div class=\"im-picker-list\">\n <div class=\"im-picker-toolbar\">\n <span class=\"im-picker-label\">选择人员</span>\n <div class=\"im-picker-toolbar-right\">\n <span class=\"im-picker-counter\">已选 {{ selectedSet.size }} / {{ users.length }} 人</span>\n <button\n v-if=\"users.length > 0\"\n class=\"im-picker-select-all\"\n type=\"button\"\n @click=\"toggleAll\"\n >\n {{ allSelected ? '取消全选' : '全选' }}\n </button>\n </div>\n </div>\n\n <div v-if=\"loading\" class=\"im-picker-empty\">加载中...</div>\n <div v-else-if=\"users.length === 0\" class=\"im-picker-empty\">\n {{ emptyText || '暂无可选人员' }}\n </div>\n <ul v-else class=\"im-picker-items\">\n <li\n v-for=\"u in users\"\n :key=\"u.uid\"\n :class=\"['im-picker-item', { active: selectedSet.has(u.uid) }]\"\n >\n <slot\n name=\"user-item\"\n :user=\"u\"\n :selected=\"selectedSet.has(u.uid)\"\n :toggle=\"() => toggleOne(u.uid)\"\n >\n <button class=\"im-picker-item-btn\" type=\"button\" @click=\"toggleOne(u.uid)\">\n <input\n type=\"checkbox\"\n :checked=\"selectedSet.has(u.uid)\"\n tabindex=\"-1\"\n @click.stop=\"toggleOne(u.uid)\"\n />\n <div class=\"im-picker-avatar\">{{ u.name[0] }}</div>\n <div class=\"im-picker-info\">\n <div class=\"im-picker-name\">{{ u.name }}</div>\n <div v-if=\"u.dept\" class=\"im-picker-dept\">{{ u.dept }}</div>\n </div>\n </button>\n </slot>\n </li>\n </ul>\n </div>\n</template>\n\n<style scoped>\n.im-picker-list {\n /* 默认主题变量:业务方在 :root/祖先覆盖即可定制,无需 :deep() */\n --im-picker-primary: #667eea;\n --im-picker-primary-hover: #5568d3;\n --im-picker-primary-weak: #eef2ff;\n --im-picker-text: #1a1a1a;\n --im-picker-text-secondary: #999;\n --im-picker-border: #f0f0f0;\n --im-picker-bg-hover: #f5f7fa;\n --im-picker-radius: 6px;\n\n display: flex;\n flex-direction: column;\n min-height: 0;\n}\n.im-picker-toolbar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-bottom: 8px;\n}\n.im-picker-label {\n font-size: 12px;\n color: var(--im-picker-text-secondary);\n}\n.im-picker-toolbar-right {\n display: flex;\n align-items: center;\n gap: 12px;\n}\n.im-picker-counter {\n font-size: 12px;\n color: var(--im-picker-text-secondary);\n}\n.im-picker-select-all {\n padding: 4px 10px;\n font-size: 12px;\n border: 1px solid var(--im-picker-primary);\n background: transparent;\n color: var(--im-picker-primary);\n border-radius: 4px;\n cursor: pointer;\n transition: all 0.2s;\n}\n.im-picker-select-all:hover {\n background: var(--im-picker-primary);\n color: #fff;\n}\n.im-picker-empty {\n text-align: center;\n padding: 32px 0;\n color: var(--im-picker-text-secondary);\n font-size: 13px;\n border: 1px solid var(--im-picker-border);\n border-radius: var(--im-picker-radius);\n}\n.im-picker-items {\n list-style: none;\n padding: 0;\n margin: 0;\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n border: 1px solid var(--im-picker-border);\n border-radius: var(--im-picker-radius);\n}\n.im-picker-item:last-child .im-picker-item-btn {\n border-bottom: none;\n}\n.im-picker-item.active .im-picker-item-btn {\n background: var(--im-picker-primary-weak);\n}\n.im-picker-item-btn {\n display: flex;\n align-items: center;\n gap: 12px;\n width: 100%;\n padding: 10px 12px;\n border: none;\n border-bottom: 1px solid #f7f7f7;\n background: transparent;\n cursor: pointer;\n text-align: left;\n transition: background 0.15s;\n font: inherit;\n}\n.im-picker-item-btn:hover {\n background: var(--im-picker-bg-hover);\n}\n.im-picker-item input[type='checkbox'] {\n width: 16px;\n height: 16px;\n flex-shrink: 0;\n cursor: pointer;\n accent-color: var(--im-picker-primary);\n pointer-events: none;\n}\n.im-picker-avatar {\n width: 32px;\n height: 32px;\n border-radius: 50%;\n background: var(--im-picker-primary);\n color: #fff;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 13px;\n font-weight: 600;\n flex-shrink: 0;\n}\n.im-picker-info {\n flex: 1;\n min-width: 0;\n}\n.im-picker-name {\n font-size: 14px;\n color: var(--im-picker-text);\n font-weight: 500;\n}\n.im-picker-dept {\n font-size: 11px;\n color: var(--im-picker-text-secondary);\n margin-top: 2px;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport type { PickerUser } from '../types'\n\n// 选人列表(无 mask 的纯列表区):工具栏(计数/全选)+ 多选列表 + 空态\n// 被 UserPickerDialog(邀请参会)与 MeetingCreateDialog(发起会议)复用\n// 自定义行渲染:#user-item 插槽(scope: { user, selected, toggle })\nconst props = defineProps<{\n users: PickerUser[]\n loading?: boolean\n selected?: string[]\n emptyText?: string\n}>()\n\nconst emit = defineEmits<{ (e: 'update:selected', uids: string[]): void }>()\n\nconst slots = defineSlots<{\n 'user-item'?(scope: { user: PickerUser; selected: boolean; toggle: () => void }): unknown\n}>()\n\nconst selectedSet = computed(() => new Set(props.selected || []))\n\nfunction toggleOne(uid: string) {\n const next = new Set(selectedSet.value)\n if (next.has(uid)) next.delete(uid)\n else next.add(uid)\n emit('update:selected', [...next])\n}\n\nconst allSelected = computed(\n () => props.users.length > 0 && selectedSet.value.size === props.users.length,\n)\n\nfunction toggleAll() {\n emit('update:selected', allSelected.value ? [] : props.users.map((u) => u.uid))\n}\n</script>\n\n<template>\n <div class=\"im-picker-list\">\n <div class=\"im-picker-toolbar\">\n <span class=\"im-picker-label\">选择人员</span>\n <div class=\"im-picker-toolbar-right\">\n <span class=\"im-picker-counter\">已选 {{ selectedSet.size }} / {{ users.length }} 人</span>\n <button\n v-if=\"users.length > 0\"\n class=\"im-picker-select-all\"\n type=\"button\"\n @click=\"toggleAll\"\n >\n {{ allSelected ? '取消全选' : '全选' }}\n </button>\n </div>\n </div>\n\n <div v-if=\"loading\" class=\"im-picker-empty\">加载中...</div>\n <div v-else-if=\"users.length === 0\" class=\"im-picker-empty\">\n {{ emptyText || '暂无可选人员' }}\n </div>\n <ul v-else class=\"im-picker-items\">\n <li\n v-for=\"u in users\"\n :key=\"u.uid\"\n :class=\"['im-picker-item', { active: selectedSet.has(u.uid) }]\"\n >\n <slot\n name=\"user-item\"\n :user=\"u\"\n :selected=\"selectedSet.has(u.uid)\"\n :toggle=\"() => toggleOne(u.uid)\"\n >\n <button class=\"im-picker-item-btn\" type=\"button\" @click=\"toggleOne(u.uid)\">\n <input\n type=\"checkbox\"\n :checked=\"selectedSet.has(u.uid)\"\n tabindex=\"-1\"\n @click.stop=\"toggleOne(u.uid)\"\n />\n <div class=\"im-picker-avatar\">{{ u.name[0] }}</div>\n <div class=\"im-picker-info\">\n <div class=\"im-picker-name\">{{ u.name }}</div>\n <div v-if=\"u.dept\" class=\"im-picker-dept\">{{ u.dept }}</div>\n </div>\n </button>\n </slot>\n </li>\n </ul>\n </div>\n</template>\n\n<style scoped>\n.im-picker-list {\n /* 默认主题变量:业务方在 :root/祖先覆盖即可定制,无需 :deep() */\n --im-picker-primary: #667eea;\n --im-picker-primary-hover: #5568d3;\n --im-picker-primary-weak: #eef2ff;\n --im-picker-text: #1a1a1a;\n --im-picker-text-secondary: #999;\n --im-picker-border: #f0f0f0;\n --im-picker-bg-hover: #f5f7fa;\n --im-picker-radius: 6px;\n\n display: flex;\n flex-direction: column;\n min-height: 0;\n}\n.im-picker-toolbar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-bottom: 8px;\n}\n.im-picker-label {\n font-size: 12px;\n color: var(--im-picker-text-secondary);\n}\n.im-picker-toolbar-right {\n display: flex;\n align-items: center;\n gap: 12px;\n}\n.im-picker-counter {\n font-size: 12px;\n color: var(--im-picker-text-secondary);\n}\n.im-picker-select-all {\n padding: 4px 10px;\n font-size: 12px;\n border: 1px solid var(--im-picker-primary);\n background: transparent;\n color: var(--im-picker-primary);\n border-radius: 4px;\n cursor: pointer;\n transition: all 0.2s;\n}\n.im-picker-select-all:hover {\n background: var(--im-picker-primary);\n color: #fff;\n}\n.im-picker-empty {\n text-align: center;\n padding: 32px 0;\n color: var(--im-picker-text-secondary);\n font-size: 13px;\n border: 1px solid var(--im-picker-border);\n border-radius: var(--im-picker-radius);\n}\n.im-picker-items {\n list-style: none;\n padding: 0;\n margin: 0;\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n border: 1px solid var(--im-picker-border);\n border-radius: var(--im-picker-radius);\n}\n.im-picker-item:last-child .im-picker-item-btn {\n border-bottom: none;\n}\n.im-picker-item.active .im-picker-item-btn {\n background: var(--im-picker-primary-weak);\n}\n.im-picker-item-btn {\n display: flex;\n align-items: center;\n gap: 12px;\n width: 100%;\n padding: 10px 12px;\n border: none;\n border-bottom: 1px solid #f7f7f7;\n background: transparent;\n cursor: pointer;\n text-align: left;\n transition: background 0.15s;\n font: inherit;\n}\n.im-picker-item-btn:hover {\n background: var(--im-picker-bg-hover);\n}\n.im-picker-item input[type='checkbox'] {\n width: 16px;\n height: 16px;\n flex-shrink: 0;\n cursor: pointer;\n accent-color: var(--im-picker-primary);\n pointer-events: none;\n}\n.im-picker-avatar {\n width: 32px;\n height: 32px;\n border-radius: 50%;\n background: var(--im-picker-primary);\n color: #fff;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 13px;\n font-weight: 600;\n flex-shrink: 0;\n}\n.im-picker-info {\n flex: 1;\n min-width: 0;\n}\n.im-picker-name {\n font-size: 14px;\n color: var(--im-picker-text);\n font-weight: 500;\n}\n.im-picker-dept {\n font-size: 11px;\n color: var(--im-picker-text-secondary);\n margin-top: 2px;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { ref, onMounted } from 'vue'\nimport type { ImClient } from '@quitting/core'\nimport { getImClient } from '../client-access'\nimport UserPickerList from './UserPickerList.vue'\nimport type { PickerUser } from '../types'\n\n// 邀请参会选人弹窗:mask + 头部 + UserPickerList + 底部按钮\n// 数据源默认 client.users.list()(uid 为 externalId);业务方可传 loader 完全自定义\n// 自定义行渲染:#user-item 插槽(透传给 UserPickerList)\nconst props = defineProps<{\n title?: string\n // 排除自己(externalId)\n myUid?: string\n // 排除列表(如已在会议中的成员,externalId)\n excludeUids?: string[]\n // 自定义数据源;缺省走 SDK 用户目录 client.users.list()\n loader?: () => Promise<PickerUser[]>\n confirmText?: string\n emptyText?: string\n onClose?: () => void\n}>()\n\nconst emit = defineEmits<{\n (e: 'confirm', selectedUids: string[]): void\n (e: 'cancel'): void\n (e: 'close'): void\n}>()\n\nconst slots = defineSlots<{\n 'user-item'?(scope: { user: PickerUser; selected: boolean; toggle: () => void }): unknown\n}>()\n\nconst users = ref<PickerUser[]>([])\nconst selected = ref<string[]>([])\nconst loading = ref(true)\n\nfunction getClient(): ImClient | undefined {\n return getImClient()\n}\n\n// 默认数据源:SDK 用户目录(externalId/name/dept),排除自己 + excludeUids\nasync function loadDefault(): Promise<PickerUser[]> {\n const client = getClient()\n if (!client?.users) return []\n const list = await client.users.list()\n // 自己不在候选里:resolve 支持内部 uid 反查(client.uid 是 UUID)\n const me = await client.users.resolve(client.uid).catch(() => undefined)\n const exclude = new Set<string>(props.excludeUids || [])\n if (me) exclude.add(me.externalId)\n if (props.myUid) exclude.add(props.myUid)\n return list\n .filter((u) => !exclude.has(u.externalId))\n .map((u) => ({ uid: u.externalId, name: u.name, dept: u.deptName || undefined }))\n}\n\nonMounted(async () => {\n try {\n users.value = props.loader ? await props.loader() : await loadDefault()\n } catch (err) {\n console.warn('[im] 选人列表加载失败', err)\n users.value = []\n } finally {\n loading.value = false\n }\n})\n\nfunction confirm() {\n if (selected.value.length === 0) return\n emit('confirm', [...selected.value])\n emit('close')\n}\n\nfunction cancel() {\n emit('cancel')\n emit('close')\n}\n</script>\n\n<template>\n <div class=\"im-picker-mask\" @click.self=\"cancel\">\n <div class=\"im-picker-dialog\">\n <div class=\"im-picker-header\">\n <h2 class=\"im-picker-title\">{{ title || '邀请参会' }}</h2>\n <button class=\"im-picker-close\" type=\"button\" aria-label=\"关闭\" @click=\"cancel\">✕</button>\n </div>\n <div class=\"im-picker-body\">\n <UserPickerList\n v-model:selected=\"selected\"\n :users=\"users\"\n :loading=\"loading\"\n :empty-text=\"emptyText || '所有成员都已在会议中'\"\n >\n <template v-if=\"slots['user-item']\" #user-item=\"scope\">\n <slot name=\"user-item\" v-bind=\"scope\" />\n </template>\n </UserPickerList>\n </div>\n <div class=\"im-picker-footer\">\n <button class=\"im-picker-btn cancel\" type=\"button\" @click=\"cancel\">取消</button>\n <button\n class=\"im-picker-btn confirm\"\n type=\"button\"\n :disabled=\"selected.length === 0\"\n @click=\"confirm\"\n >\n {{ confirmText || '邀请' }}\n </button>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.im-picker-mask {\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.45);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 100001;\n}\n.im-picker-dialog {\n /* 主题变量(与 UserPickerList 一致;:root 覆盖一处即可全局生效) */\n --im-picker-primary: #667eea;\n --im-picker-primary-hover: #5568d3;\n --im-picker-primary-weak: #eef2ff;\n\n width: 480px;\n max-height: 80vh;\n background: #fff;\n border-radius: 12px;\n box-shadow: 0 16px 48px rgba(0, 0, 0, 0.2);\n display: flex;\n flex-direction: column;\n overflow: hidden;\n}\n.im-picker-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 16px 20px;\n border-bottom: 1px solid #f0f0f0;\n}\n.im-picker-title {\n margin: 0;\n font-size: 16px;\n font-weight: 600;\n color: #1a1a1a;\n}\n.im-picker-close {\n width: 28px;\n height: 28px;\n border: none;\n background: transparent;\n color: #999;\n font-size: 16px;\n cursor: pointer;\n border-radius: 4px;\n}\n.im-picker-close:hover {\n background: #f5f7fa;\n color: #1a1a1a;\n}\n.im-picker-body {\n flex: 1;\n min-height: 0;\n display: flex;\n flex-direction: column;\n padding: 14px 20px;\n}\n.im-picker-body .im-picker-list {\n flex: 1;\n min-height: 0;\n}\n.im-picker-body :deep(.im-picker-items) {\n max-height: 320px;\n}\n.im-picker-footer {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n padding: 14px 20px;\n border-top: 1px solid #f0f0f0;\n}\n.im-picker-btn {\n padding: 8px 18px;\n font-size: 13px;\n border-radius: 6px;\n cursor: pointer;\n transition: all 0.2s;\n border: none;\n}\n.im-picker-btn.cancel {\n background: #f5f7fa;\n color: #666;\n}\n.im-picker-btn.cancel:hover {\n background: #eef0f4;\n}\n.im-picker-btn.confirm {\n background: var(--im-picker-primary, #667eea);\n color: #fff;\n}\n.im-picker-btn.confirm:hover:not(:disabled) {\n background: var(--im-picker-primary-hover, #5568d3);\n}\n.im-picker-btn.confirm:disabled {\n background: #bbb;\n cursor: not-allowed;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { ref, onMounted } from 'vue'\nimport type { ImClient } from '@quitting/core'\nimport { getImClient } from '../client-access'\nimport UserPickerList from './UserPickerList.vue'\nimport type { PickerUser } from '../types'\n\n// 邀请参会选人弹窗:mask + 头部 + UserPickerList + 底部按钮\n// 数据源默认 client.users.list()(uid 为 externalId);业务方可传 loader 完全自定义\n// 自定义行渲染:#user-item 插槽(透传给 UserPickerList)\nconst props = defineProps<{\n title?: string\n // 排除自己(externalId)\n myUid?: string\n // 排除列表(如已在会议中的成员,externalId)\n excludeUids?: string[]\n // 自定义数据源;缺省走 SDK 用户目录 client.users.list()\n loader?: () => Promise<PickerUser[]>\n confirmText?: string\n emptyText?: string\n onClose?: () => void\n}>()\n\nconst emit = defineEmits<{\n (e: 'confirm', selectedUids: string[]): void\n (e: 'cancel'): void\n (e: 'close'): void\n}>()\n\nconst slots = defineSlots<{\n 'user-item'?(scope: { user: PickerUser; selected: boolean; toggle: () => void }): unknown\n}>()\n\nconst users = ref<PickerUser[]>([])\nconst selected = ref<string[]>([])\nconst loading = ref(true)\n\nfunction getClient(): ImClient | undefined {\n return getImClient()\n}\n\n// 默认数据源:SDK 用户目录(externalId/name/dept),排除自己 + excludeUids\nasync function loadDefault(): Promise<PickerUser[]> {\n const client = getClient()\n if (!client?.users) return []\n const list = await client.users.list()\n // 自己不在候选里:resolve 支持内部 uid 反查(client.uid 是 UUID)\n const me = await client.users.resolve(client.uid).catch(() => undefined)\n const exclude = new Set<string>(props.excludeUids || [])\n if (me) exclude.add(me.externalId)\n if (props.myUid) exclude.add(props.myUid)\n return list\n .filter((u) => !exclude.has(u.externalId))\n .map((u) => ({ uid: u.externalId, name: u.name, dept: u.deptName || undefined }))\n}\n\nonMounted(async () => {\n try {\n users.value = props.loader ? await props.loader() : await loadDefault()\n } catch (err) {\n console.warn('[im] 选人列表加载失败', err)\n users.value = []\n } finally {\n loading.value = false\n }\n})\n\nfunction confirm() {\n if (selected.value.length === 0) return\n emit('confirm', [...selected.value])\n emit('close')\n}\n\nfunction cancel() {\n emit('cancel')\n emit('close')\n}\n</script>\n\n<template>\n <div class=\"im-picker-mask\" @click.self=\"cancel\">\n <div class=\"im-picker-dialog\">\n <div class=\"im-picker-header\">\n <h2 class=\"im-picker-title\">{{ title || '邀请参会' }}</h2>\n <button class=\"im-picker-close\" type=\"button\" aria-label=\"关闭\" @click=\"cancel\">✕</button>\n </div>\n <div class=\"im-picker-body\">\n <UserPickerList\n v-model:selected=\"selected\"\n :users=\"users\"\n :loading=\"loading\"\n :empty-text=\"emptyText || '所有成员都已在会议中'\"\n >\n <template v-if=\"slots['user-item']\" #user-item=\"scope\">\n <slot name=\"user-item\" v-bind=\"scope\" />\n </template>\n </UserPickerList>\n </div>\n <div class=\"im-picker-footer\">\n <button class=\"im-picker-btn cancel\" type=\"button\" @click=\"cancel\">取消</button>\n <button\n class=\"im-picker-btn confirm\"\n type=\"button\"\n :disabled=\"selected.length === 0\"\n @click=\"confirm\"\n >\n {{ confirmText || '邀请' }}\n </button>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.im-picker-mask {\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.45);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 100001;\n}\n.im-picker-dialog {\n /* 主题变量(与 UserPickerList 一致;:root 覆盖一处即可全局生效) */\n --im-picker-primary: #667eea;\n --im-picker-primary-hover: #5568d3;\n --im-picker-primary-weak: #eef2ff;\n\n width: 480px;\n max-height: 80vh;\n background: #fff;\n border-radius: 12px;\n box-shadow: 0 16px 48px rgba(0, 0, 0, 0.2);\n display: flex;\n flex-direction: column;\n overflow: hidden;\n}\n.im-picker-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 16px 20px;\n border-bottom: 1px solid #f0f0f0;\n}\n.im-picker-title {\n margin: 0;\n font-size: 16px;\n font-weight: 600;\n color: #1a1a1a;\n}\n.im-picker-close {\n width: 28px;\n height: 28px;\n border: none;\n background: transparent;\n color: #999;\n font-size: 16px;\n cursor: pointer;\n border-radius: 4px;\n}\n.im-picker-close:hover {\n background: #f5f7fa;\n color: #1a1a1a;\n}\n.im-picker-body {\n flex: 1;\n min-height: 0;\n display: flex;\n flex-direction: column;\n padding: 14px 20px;\n}\n.im-picker-body .im-picker-list {\n flex: 1;\n min-height: 0;\n}\n.im-picker-body :deep(.im-picker-items) {\n max-height: 320px;\n}\n.im-picker-footer {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n padding: 14px 20px;\n border-top: 1px solid #f0f0f0;\n}\n.im-picker-btn {\n padding: 8px 18px;\n font-size: 13px;\n border-radius: 6px;\n cursor: pointer;\n transition: all 0.2s;\n border: none;\n}\n.im-picker-btn.cancel {\n background: #f5f7fa;\n color: #666;\n}\n.im-picker-btn.cancel:hover {\n background: #eef0f4;\n}\n.im-picker-btn.confirm {\n background: var(--im-picker-primary, #667eea);\n color: #fff;\n}\n.im-picker-btn.confirm:hover:not(:disabled) {\n background: var(--im-picker-primary-hover, #5568d3);\n}\n.im-picker-btn.confirm:disabled {\n background: #bbb;\n cursor: not-allowed;\n}\n</style>\n","<script setup lang=\"ts\">\nimport {\n ref,\n onMounted,\n onUnmounted,\n computed,\n nextTick,\n watch,\n useSlots,\n getCurrentInstance,\n} from 'vue'\nimport vDraggable from '../directives/draggable'\nimport { mountComponent } from '../mount'\nimport { ImToastApi as toast } from '../toast'\nimport MeetingHeader from './meeting/MeetingHeader.vue'\nimport MainVideoTile from './meeting/MainVideoTile.vue'\nimport VideoListPanel from './meeting/VideoListPanel.vue'\nimport MemberListPanel from './meeting/MemberListPanel.vue'\nimport SidebarTabs from './meeting/SidebarTabs.vue'\nimport SmallVideoTile from './meeting/SmallVideoTile.vue'\nimport MeetingControlBar from './meeting/MeetingControlBar.vue'\nimport ConfirmDialog from './ConfirmDialog.vue'\nimport MeetingEndedToast from './MeetingEndedToast.vue'\nimport UserPickerDialog from './UserPickerDialog.vue'\nimport { MaximizeIcon, CloseIcon } from './meeting/icons'\nimport type {\n MeetingMember,\n MeetingHostChangedEvent,\n MeetingDissolvedEvent,\n} from '@quitting/contract'\n\n// 会议窗口:左主视频(默认主持人) + 右侧栏 Tab(视频列表/成员列表) + 底部成员状态条\n// 布局严格 16:9,所有组件无圆角;样式对齐 design/video-conference-layout.html\n// 视频挂载沿用 CallSession/MeetingSession 的 attach 模式:通过 onVideoElReady 把元素交回 SDK\n\nconst props = defineProps<{\n mode: 'host' | 'participant'\n title: string\n // 窗口高度(CSS 值),由调用方决定。支持 \"80vh\" / \"600px\" / \"100%\" 等\n // position: fixed 下百分比相对 viewport;宽度由 layout() 按 16:9 推算\n height?: string\n onClose?: () => void\n // 用户信息解析器:uid → { name, dept? }\n // 后端 ListMembers 在 user 表无对应记录时会回退为 uid(如 demo mock 用户),\n // 调用方可传此函数让 SDK UI 显示业务侧维护的 name/dept;返回 undefined 则保留后端原值\n resolveUser?: (uid: string) => { name: string; dept?: string } | undefined\n // 禁用内置邀请弹窗(点击「+」只抛 addMember 事件,不弹内置选人窗)\n disableDefaultInvite?: boolean\n}>()\n\nconst emit = defineEmits<{\n (e: 'close'): void\n // 添加成员按钮被点击。默认行为按优先级退让:\n // 传了 #invite-panel 插槽 → 渲染插槽(完全自定义)\n // 传了 @add-member 监听 → 仅抛事件(不弹内置选人窗,由外部接管)\n // 都没有 → 抛事件 + 弹内置 UserPickerDialog(批量 reinvite)\n (e: 'addMember'): void\n}>()\n\nconst slots = useSlots()\n// 自定义邀请面板插槽签名(运行时按名取用;类型仅作文档)\n// scope: members=当前成员列表 / excludeUids=已在会成员 / invite(uids)=批量邀请 / close()=收起面板\n\n// 自定义邀请面板开关(#invite-panel 插槽渲染层)\nconst invitePanelOpen = ref(false)\n// 当前会议成员 externalId 列表(插槽 scope 用:选人时排除已在会成员)\nconst memberIdentities = computed(() => members.value.map((m) => m.identity))\n\nconst micEnabled = ref(true)\nconst cameraEnabled = ref(true)\nconst speakerMuted = ref(false)\nconst hasCamera = ref(true)\nconst hasMic = ref(true)\n// 设备不可用(无设备/无权限/被占用):按钮与自己的状态图标置灰,与「主动关闭」(红色)区分\nconst micUnavailable = ref(false)\nconst cameraUnavailable = ref(false)\nconst deviceErrors = ref<string[]>([])\n// 通用操作提示(reinvite 失败等),复用 device-warn 样式,3 秒自动消失\nconst actionError = ref('')\nlet actionErrorTimer: ReturnType<typeof setTimeout> | null = null\nfunction pushToast(msg: string) {\n actionError.value = msg\n if (actionErrorTimer) clearTimeout(actionErrorTimer)\n actionErrorTimer = setTimeout(() => (actionError.value = ''), 3000)\n}\nconst duration = ref(0)\nconst endedReason = ref('')\nconst activeTab = ref<'video' | 'member'>('video')\nconst members = ref<MeetingMember[]>([])\nconst hostIdentity = ref<string>('')\n// 最小化状态:true 时隐藏主窗口内容,显示可拖拽的小浮条(LiveKit 会话不中断)\nconst minimized = ref(false)\n// 全屏宫格模式:true 时覆盖整个视口的 4 列宫格布局(LiveKit 会话不中断)\nconst gridMode = ref(false)\n\n// 元素引用\nconst contentEl = ref<HTMLElement | null>(null)\nconst mainVideoWrap = ref<HTMLElement | null>(null)\nconst sidebarEl = ref<HTMLElement | null>(null)\nconst tabBarWrap = ref<HTMLElement | null>(null)\nconst videoListPanelRef = ref<InstanceType<typeof VideoListPanel> | null>(null)\nconst mainVideoRef = ref<InstanceType<typeof MainVideoTile> | null>(null)\n\n// 视频 attach:identity → 元素;保留 self/remote 区分\nlet selfVideoEl: HTMLVideoElement | null = null\nlet mainVideoIdentity: string | null = null\nlet mainVideoEl: HTMLVideoElement | null = null\nconst participantVideoEls = new Map<string, HTMLVideoElement>()\n// 全屏宫格的视频元素池:identity → 元素(track 后到时补挂载用)\nconst gridVideoEls = new Map<string, HTMLVideoElement>()\n\nlet timer: ReturnType<typeof setInterval> | null = null\nlet resizeObserver: ResizeObserver | null = null\n\nfunction getClient() {\n return (window as any).__imClient\n}\n\nfunction getSession() {\n return getClient()?.meeting?.currentSession\n}\n\n// 主视频参与者:当前 hostIdentity 对应的 member(可能未入会)\nconst mainVideoMember = computed<MeetingMember | null>(() => {\n const m = members.value.find((x) => x.identity === hostIdentity.value)\n return m || null\n})\n\n// 当前用户是否是「当前主持人」——动态判断,主持人转让后自动更新\n// mode prop 在创建时固化(host/participant),转让后原主持人 mode 仍是 host 但已无权限,\n// 新主持人 mode 是 participant 却应有权限。改用 identity === hostIdentity 解决菜单错乱。\nconst isCurrentHost = computed(() => {\n const sess = getSession()\n return !!sess && sess.identity === hostIdentity.value\n})\n\n// 正在发言的成员(最小化浮条显示用)——与成员列表 speaking 判定一致\nconst speakingMembers = computed(() =>\n members.value.filter((m) => m.speaking && m.joined && m.online),\n)\n// 发言人展示文本:≤3 个全显示(顿号分隔);>3 个显示第一个 + \"...等N人\"\nconst speakerDisplayText = computed(() => {\n const list = speakingMembers.value\n if (list.length === 0) return ''\n if (list.length <= 3) return list.map((m) => m.name).join('、')\n return `${list[0].name}...等${list.length}人`\n})\n\n// 小视频列表:除了主视频那位,其他在线成员(排序规则与所有布局统一)\nconst smallVideoMembers = computed<MeetingMember[]>(() => {\n return sortMembers(\n members.value.filter((m) => m.identity !== hostIdentity.value && (m.online || m.joined)),\n )\n})\n\n// 全屏宫格成员:所有在线/已入会成员(含主持人和自己),排序规则与所有布局统一\nconst gridMembers = computed<MeetingMember[]>(() => {\n return sortMembers(members.value.filter((m) => m.online || m.joined))\n})\n\n// 全屏宫格视频元素就绪:与 onSmallVideoReady 同模式(self 走本地 attach,其余走远端)\nfunction onGridVideoReady(identity: string, el: HTMLVideoElement | null) {\n const session = getSession()\n if (!session) return\n if (el) {\n gridVideoEls.set(identity, el)\n if (identity === session.identity) {\n session.attachLocalVideo(el)\n } else {\n session.attachParticipantVideo(identity, el)\n }\n } else {\n gridVideoEls.delete(identity)\n }\n}\n\n// 小视频列表变化后必须重新 layout:\n// mount 时 members 还是空 → small-video 没渲染 → layout() 跳过尺寸应用\n// 之后 refreshMembers 完成、small-video 渲染,需要重新 layout 把 inline style 设上\nwatch(smallVideoMembers, () => {\n nextTick(layout)\n})\n\n// 排序后的成员列表(成员列表 tab / 全屏宫格 / 小视频列表三处布局统一使用)\n// 排序规则:主持人最前 → 其余按加入时间升序(joinedAt)→ 未加入的按邀请序 → 已离会最后。\n// 不再按发言状态排序——speaking 仅用于 tile 角标与高亮,不影响位置\nfunction getStatusPriority(m: MeetingMember, hostUid: string): number {\n if (m.identity === hostUid) return 0\n if (m.status === 'left') return 3\n if (m.joinedAt > 0) return 1\n return 2\n}\nfunction sortMembers(list: MeetingMember[]): MeetingMember[] {\n const hostUid = hostIdentity.value\n return list.slice().sort((a, b) => {\n const pa = getStatusPriority(a, hostUid)\n const pb = getStatusPriority(b, hostUid)\n if (pa !== pb) return pa - pb\n // 同组内按加入时间;均为 0(未加入)时退化为邀请序\n if (a.joinedAt !== b.joinedAt) return a.joinedAt - b.joinedAt\n return (a.inviteOrder || 0) - (b.inviteOrder || 0)\n })\n}\nconst sortedMembers = computed(() => sortMembers(members.value))\n\nasync function refreshMembers(force = false) {\n const client = getClient()\n if (!client?.meeting) return\n try {\n const list = await client.meeting.getMembers(force)\n // 若调用方提供 resolveUser,用业务侧 name/dept 覆盖后端返回(demo 模式下后端回退 uid)\n if (props.resolveUser) {\n members.value = list.map((m: MeetingMember) => {\n const u = props.resolveUser?.(m.identity)\n if (!u) return m\n return { ...m, name: u.name, dept: u.dept ?? m.dept }\n })\n } else {\n members.value = list\n }\n } catch (err) {\n console.warn('[im] refreshMembers 失败', err)\n }\n}\n\nasync function reattachAllVideos() {\n const session = getSession()\n if (!session) return\n // 主视频:self 时走 attachLocalVideo,否则 attachParticipantVideo\n if (mainVideoIdentity && mainVideoEl) {\n if (mainVideoIdentity === session.identity) {\n session.attachLocalVideo(mainVideoEl)\n } else {\n session.attachParticipantVideo(mainVideoIdentity, mainVideoEl)\n }\n }\n}\n\n// 主视频元素就绪\nasync function onMainVideoReady(identity: string, el: HTMLVideoElement | null) {\n const session = getSession()\n if (!session) return\n if (el) {\n mainVideoIdentity = identity\n mainVideoEl = el\n if (identity === session.identity) {\n session.attachLocalVideo(el)\n } else {\n session.attachParticipantVideo(identity, el)\n }\n } else {\n mainVideoIdentity = null\n mainVideoEl = null\n }\n}\n\n// 小视频元素就绪\nfunction onSmallVideoReady(identity: string, el: HTMLVideoElement | null) {\n const session = getSession()\n if (!session) return\n if (el) {\n participantVideoEls.set(identity, el)\n if (identity === session.identity) {\n // 自己的 tile:attach 本地视频(非主持人在小视频列表里看不到自己就是因为这里跳过了)\n session.attachLocalVideo(el)\n } else {\n session.attachParticipantVideo(identity, el)\n }\n } else {\n participantVideoEls.delete(identity)\n }\n}\n\nasync function syncDeviceCapability() {\n try {\n const devices = await navigator.mediaDevices.enumerateDevices()\n hasCamera.value = devices.some((d) => d.kind === 'videoinput')\n hasMic.value = devices.some((d) => d.kind === 'audioinput')\n if (!hasCamera.value) cameraEnabled.value = false\n if (!hasMic.value) micEnabled.value = false\n // 无设备 = 不可用(按钮置灰而非隐藏)\n micUnavailable.value = !hasMic.value\n cameraUnavailable.value = !hasCamera.value\n // 无权限 = 不可用:permissions.query best-effort(Safari 等不支持会抛错,忽略——\n // 开启失败路径由 devices 事件的 errors 兜底)\n try {\n const p = await navigator.permissions.query({ name: 'microphone' })\n if (p.state === 'denied') micUnavailable.value = true\n } catch {\n /* 忽略 */\n }\n try {\n const p = await navigator.permissions.query({ name: 'camera' })\n if (p.state === 'denied') cameraUnavailable.value = true\n } catch {\n /* 忽略 */\n }\n } catch (e) {\n console.warn('[im] enumerateDevices 失败', e)\n }\n}\n\nfunction triggerLayout() {\n nextTick(layout)\n}\n\n// 布局计算:从 design/video-conference-layout.html layout() 函数搬\n// 所有尺寸(mwW/sidebarW/mainW/smallW)无条件计算,避免小视频列表为空时窗口宽度塌陷\nfunction layout() {\n const content = contentEl.value\n const mainVideo = mainVideoWrap.value\n const sidebar = sidebarEl.value\n const videoListEl = videoListPanelRef.value?.listEl as HTMLElement | undefined\n const items = videoListEl?.querySelectorAll<HTMLElement>('.small-video') ?? []\n if (!content || !mainVideo || !sidebar) return\n\n // 1. 主视频 16:9,高度 = content 高度\n const contentH = content.clientHeight\n if (contentH <= 0) return\n const mainH = contentH\n const mainW = (mainH * 16) / 9\n\n // 2. 小视频 16:9,3 个 + 3 个 gap 填满 sidebar 可用高度\n const smallGap = 8\n const tabBarH = tabBarWrap.value?.offsetHeight ?? 28\n const smallH = (mainH - tabBarH - smallGap * 3) / 3\n const smallW = (smallH * 16) / 9\n\n // 3. sidebar 宽度 = 小视频宽(不再保留自定义滚动条空间,列表与 tab 等宽)\n const sidebarW = smallW\n\n // 4. 窗口总宽度(padding + main + gap + sidebar + padding)—— 左右 padding 对称\n const padding = 10\n const gap = 10\n const mwW = padding + mainW + gap + sidebarW + padding\n\n // 5. 应用到 DOM(items 为空也照常设,保持窗口宽度稳定)\n const mw = document.getElementById('meetingWindow') as HTMLElement | null\n if (mw) {\n mw.style.width = mwW + 'px'\n }\n\n mainVideo.style.width = mainW + 'px'\n mainVideo.style.height = mainH + 'px'\n\n // sidebar 宽度 = smallW,与 list 完全对齐\n sidebar.style.width = sidebarW + 'px'\n sidebar.style.height = contentH + 'px'\n\n if (videoListEl) {\n // list 撑满 sidebar,与 tab-bar 等宽\n videoListEl.style.width = '100%'\n videoListEl.style.height = contentH - tabBarH - smallGap + 'px'\n }\n\n items.forEach((item) => {\n // 小视频保持 16:9\n item.style.width = smallW + 'px'\n // 不取整:Math.floor 会让 3 个 small 累计损失 ~2px,导致最后 small 底部与主视频底部错位\n item.style.height = smallH + 'px'\n })\n}\n\n// ============ 设备/会议控制 ============\nasync function toggleMic() {\n if (micUnavailable.value) return\n const session = getSession()\n if (!session) return\n const next = !micEnabled.value\n try {\n await session.setMicEnabled(next)\n micEnabled.value = next\n // 本端自己不走远端 trackMuted 事件,手动更新 members 里自己那行\n // (members[].muted 语义 = 已闭麦,与 micEnabled 相反),与远端事件路径合流\n const self = members.value.find((m) => m.identity === session.identity)\n if (self && self.muted !== !next) {\n self.muted = !next\n members.value = [...members.value]\n }\n } catch (err) {\n console.warn('[im] setMicEnabled 失败,UI 不切换', err)\n }\n}\n\nasync function toggleCamera() {\n if (cameraUnavailable.value) return\n const session = getSession()\n if (!session) return\n const next = !cameraEnabled.value\n try {\n await session.setCameraEnabled(next)\n cameraEnabled.value = next\n // 本端自己不走远端 trackMuted 事件,手动更新 members 里自己那行\n // (与远端事件路径合流,其他端/本端列表与遮罩立即反映)\n const self = members.value.find((m) => m.identity === session.identity)\n if (self && self.cameraOn !== next) {\n self.cameraOn = next\n members.value = [...members.value]\n }\n nextTick(reattachAllVideos)\n } catch (err) {\n console.warn('[im] setCameraEnabled 失败,UI 不切换', err)\n }\n}\n\nasync function toggleSpeaker() {\n const session = getSession()\n if (!session) return\n speakerMuted.value = !speakerMuted.value\n await session.setSpeakerMuted(speakerMuted.value)\n}\n\n// 是否显示「解散会议」确认弹窗\nconst showDissolveConfirm = ref(false)\nconst dissolvePending = ref(false)\n\nasync function leaveMeeting() {\n const client = getClient()\n if (!client) return\n // 当前主持人才能解散;非主持人(含已转让的前主持人)直接走普通 leave\n if (!isCurrentHost.value) {\n await client.meeting?.leave()\n // 主窗口立即隐藏,独立倒计时 toast 提示后由 toast 触发 close\n endedReason.value = '已离开'\n return\n }\n // host:查房内参与者数量\n // - count > 1:房内还有其他人,弹\"解散会议\"确认(让 host 明确选择 Leave 还是 Dissolve)\n // - count <= 1:只剩 host 自己——直接 dissolve,避免遗留僵尸会议让离线 invitee 上线后收到 stale 邀请\n // 之前的 bug:count=1 时走 leave,只改 host 自己 invite.status=left,meeting.status 仍 active,\n // 其他 invitee 的 status 仍 invited → pending-invites 返回 stale 邀请\n let count = 1\n try {\n count = await client.meeting.getParticipantCount()\n } catch (err) {\n console.warn('[im] 查参与者数量失败,按 1 处理(自动解散)', err)\n }\n if (count > 1) {\n showDissolveConfirm.value = true\n return\n }\n dissolvePending.value = true\n try {\n await client.meeting.dissolve()\n endedReason.value = '会议已解散'\n } catch (err) {\n // dissolve 失败兜底:后端 Leave 内部也会做同样的自动 dissolve 兜底,但前端先失败说明网络层有问题\n console.error('[im] host 单独时解散失败,回退到 leave', err)\n endedReason.value = '已离开'\n await client.meeting?.leave().catch(() => {})\n } finally {\n dissolvePending.value = false\n }\n}\n\n// 确认解散\nasync function confirmDissolve() {\n const client = getClient()\n if (!client) return\n showDissolveConfirm.value = false\n dissolvePending.value = true\n try {\n await client.meeting.dissolve()\n // 解散成功:endedReason 让 ended-overlay 显示;broadcast 到达后 SDK 也会触发 ended 事件\n endedReason.value = '会议已解散'\n } catch (err) {\n console.error('[im] 解散失败', err)\n endedReason.value = '解散失败'\n } finally {\n dissolvePending.value = false\n }\n}\n\nfunction cancelDissolve() {\n showDissolveConfirm.value = false\n}\n\nasync function kickParticipant(uid: string) {\n const session = getSession()\n if (!session) return\n try {\n await session.kick(getClient().transport, uid)\n await refreshMembers(true)\n } catch (err) {\n console.warn('[im] kickParticipant 失败', err)\n }\n}\n\nasync function transferHost(uid: string) {\n const session = getSession()\n if (!session) return\n try {\n await session.transferHost(getClient().transport, uid)\n hostIdentity.value = uid\n await refreshMembers(true)\n } catch (err) {\n console.warn('[im] transferHost 失败', err)\n }\n}\n\n// 主持人再次邀请某成员(等待加入/已离会状态)\nasync function reinviteMember(uid: string) {\n const session = getSession()\n if (!session) return\n try {\n await getClient().meeting.reinvite(session.meetingId, uid)\n await refreshMembers(true)\n } catch (err: unknown) {\n // 后端错误码映射成中文提示,避免静默失败:\n // target_busy=对方正在通话/会议或有待处理邀请;not_host=主持人已转让\n const msg = err instanceof Error ? err.message : ''\n if (msg.includes('对方正忙') || msg.includes('target_busy')) {\n pushToast('对方正忙,稍后再试')\n } else if (msg.includes('无权') || msg.includes('not_host')) {\n pushToast('仅主持人可邀请成员')\n } else if (msg.includes('meeting_ended')) {\n pushToast('会议已结束')\n } else {\n pushToast('再次邀请失败,请重试')\n }\n console.warn('[im] reinvite 失败', err)\n }\n}\n\n// uid → 展示名(内置邀请流程 toast 用;优先 SDK 用户目录,回退 uid 本身)\nasync function nameOf(uid: string): Promise<string> {\n const dir = getClient()?.users\n if (dir?.resolve) {\n const u = await dir.resolve(uid).catch(() => undefined)\n if (u?.name) return u.name\n }\n return uid\n}\n\n// 批量邀请入会:内置默认流程与 #invite-panel 插槽的 invite() 共用\n// 单条失败不阻塞其他;busy/其他失败分类提示\nasync function inviteUids(uids: string[]) {\n const client = getClient()\n const session = getSession()\n if (!client || !session || uids.length === 0) return\n const busyUids: string[] = []\n const failedUids: string[] = []\n await Promise.all(\n uids.map((uid) =>\n client.meeting.reinvite(session.meetingId, uid).catch((e: unknown) => {\n const msg = e instanceof Error ? e.message : ''\n if (msg.includes('对方正忙') || msg.includes('target_busy')) busyUids.push(uid)\n else failedUids.push(uid)\n }),\n ),\n )\n if (busyUids.length > 0) {\n const names = (await Promise.all(busyUids.map(nameOf))).join('、')\n toast.error(`${names} 正忙,未能邀请加入会议`)\n }\n if (failedUids.length > 0) {\n const names = (await Promise.all(failedUids.map(nameOf))).join('、')\n toast.error(`${names} 邀请失败,请重试`)\n }\n refreshMembers(true)\n}\n\n// 内置默认邀请流程:拉当前成员(含已邀请未加入)→ 选人弹窗 → 批量 reinvite\nasync function openDefaultInvite() {\n const client = getClient()\n const session = getSession()\n if (!client || !session) return\n let excludeUids: string[] = []\n try {\n const list = await client.meeting.getMembers(true)\n excludeUids = list.map((m: MeetingMember) => m.identity)\n } catch (err) {\n console.warn('[im] 拉取会议成员失败,邀请弹窗不排除已入会成员', err)\n }\n mountComponent(UserPickerDialog, {\n title: '邀请参会',\n excludeUids,\n emptyText: '所有成员都已在会议中',\n onConfirm: (uids: string[]) => {\n inviteUids(uids)\n },\n })\n}\n\n// 点击「+」添加成员:插槽 > 外部事件监听 > 内置默认弹窗(退让式)\nfunction onAddMemberClick() {\n emit('addMember')\n if (slots['invite-panel']) {\n invitePanelOpen.value = !invitePanelOpen.value\n return\n }\n // 外部传了 @add-member 监听 → 仅抛事件,不弹内置选人窗(保持旧边界行为)\n const instance = getCurrentInstance()\n if (instance?.vnode.props?.onAddMember) return\n if (props.disableDefaultInvite) return\n openDefaultInvite()\n}\n\n// 监听 hostChanged 广播:切换主视频\nfunction onHostChangedEvent(e: MeetingHostChangedEvent) {\n const session = getSession()\n if (!session || e.meetingId !== session.meetingId) return\n hostIdentity.value = e.hostUid\n // 主视频元素会被 v-if 重新挂载,nextTick 后 reattach\n nextTick(() => {\n refreshMembers(true)\n })\n}\n\n// 监听解散广播:主持人解散后所有参与人收到,立即隐藏视频窗口,只留独立倒计时提示。\n// 不依赖 session 'ended' 事件(它要等 sess.end() 的 async disconnect 完成才触发,有延迟)。\nfunction onDissolvedEvent(e: MeetingDissolvedEvent) {\n const session = getSession()\n if (!session || e.meetingId !== session.meetingId) return\n if (!endedReason.value) endedReason.value = '会议已解散'\n if (timer) clearInterval(timer)\n}\n\n// 监听被踢广播:立即隐藏视频窗口\nfunction onKickedEvent(e: { meetingId: string; by: string }) {\n const session = getSession()\n if (!session || e.meetingId !== session.meetingId) return\n if (!endedReason.value) endedReason.value = '已被移出会议'\n if (timer) clearInterval(timer)\n}\n\nfunction formatDuration(s: number) {\n const h = Math.floor(s / 3600)\n const m = Math.floor((s % 3600) / 60)\n .toString()\n .padStart(2, '0')\n const sec = (s % 60).toString().padStart(2, '0')\n return h > 0 ? `${h}:${m}:${sec}` : `${m}:${sec}`\n}\n\nonMounted(async () => {\n await syncDeviceCapability()\n const client = getClient()\n if (!client) return\n const session = client.meeting?.currentSession\n if (!session) return\n\n hostIdentity.value = session.hostIdentity || session.identity\n\n session.onSession('connected', () => {\n refreshMembers()\n nextTick(layout)\n timer = setInterval(() => {\n duration.value++\n // 每 10 秒刷一次成员列表(muted/online 实时态)\n // 后端 presence scanner 缓存也是 10s 刷新,更高频率纯浪费;muted 等变化由事件即时触发 force refresh\n if (duration.value % 10 === 0) refreshMembers()\n }, 1000)\n })\n session.onSession('participantJoined', () => {\n refreshMembers(true)\n })\n session.onSession('participantLeft', () => {\n refreshMembers(true)\n })\n session.onSession('participantTrackSubscribed', (info: { kind: string; identity: string }) => {\n if (info.kind !== 'video') return\n // 优先用小视频池的元素\n const el = participantVideoEls.get(info.identity)\n if (el) {\n session.attachParticipantVideo(info.identity, el)\n }\n // 全屏宫格池的元素同样补挂载(track.attach 支持同一 track 挂多个元素)\n const gridEl = gridVideoEls.get(info.identity)\n if (gridEl) {\n session.attachParticipantVideo(info.identity, gridEl)\n }\n // 主视频位置的 track 流入:reattach 主视频(mainVideoEl 不在 participantVideoEls 里)\n if (mainVideoIdentity === info.identity) {\n nextTick(reattachAllVideos)\n }\n })\n // 发言态:LiveKit 给全量列表,覆盖式更新 members[].speaking\n // refreshMembers 每 10 秒会用后端 speaking=false 覆盖,下个 activeSpeakersChanged 事件会再次纠正——可接受\n // 就地修改成员字段,仅在状态变化时替换数组引用触发响应式\n session.onSession('activeSpeakersChanged', (info: { identities: string[] }) => {\n const set = new Set(info.identities)\n let changed = false\n for (const m of members.value) {\n const speaking = set.has(m.identity)\n if (m.speaking !== speaking) {\n m.speaking = speaking\n changed = true\n }\n }\n if (changed) members.value = [...members.value]\n })\n // 远端麦克风/摄像头开/关:毫秒级事件就地更新 members[].muted / cameraOn\n // (此前仅靠 10s 轮询后端快照;轮询保留作断线/丢事件后的兜底校正)\n session.onSession(\n 'participantMuteChanged',\n (info: { identity: string; muted: boolean; kind: 'audio' | 'video' }) => {\n const m = members.value.find((x) => x.identity === info.identity)\n if (!m) return\n let changed = false\n if (info.kind === 'audio') {\n if (m.muted !== info.muted) {\n m.muted = info.muted\n changed = true\n }\n } else {\n // video 事件 muted=true 表示摄像头关闭;cameraOn 语义相反\n if (m.cameraOn !== !info.muted) {\n m.cameraOn = !info.muted\n changed = true\n }\n }\n if (changed) members.value = [...members.value]\n },\n )\n session.onSession('devices', (info: { mic: boolean; camera: boolean; errors: string[] }) => {\n if (info.errors && info.errors.length > 0) {\n deviceErrors.value = info.errors\n setTimeout(() => (deviceErrors.value = []), 3000)\n }\n // 同步设备实际开启状态(enableDevices 可能 mic/camera 开启失败,UI 需据此更新按钮)\n if (!info.mic) micEnabled.value = false\n if (!info.camera) cameraEnabled.value = false\n if (info.camera) nextTick(reattachAllVideos)\n // 开启失败(无权限/被占用)= 不可用置灰;本次开启成功则恢复(如权限后来被授予)\n // errors 条目带 \"mic:\"/\"camera:\" 前缀(core enableDevices 约定),可精确区分设备\n if (info.errors.some((e) => e.startsWith('mic:'))) micUnavailable.value = true\n else if (info.mic) micUnavailable.value = false\n if (info.errors.some((e) => e.startsWith('camera:'))) cameraUnavailable.value = true\n else if (info.camera) cameraUnavailable.value = false\n })\n session.onSession('ended', () => {\n // 解散/被踢/对端断开都会触发——独立倒计时浮窗提示,5s 后自动关或用户手动关\n if (!endedReason.value) endedReason.value = '会议已结束'\n if (timer) clearInterval(timer)\n })\n\n // race 修复:mount 之前可能已 connected\n if (session.isConnected) {\n refreshMembers()\n nextTick(layout)\n timer = setInterval(() => {\n duration.value++\n if (duration.value % 10 === 0) refreshMembers()\n }, 1000)\n }\n\n // 监听 host 转让广播(所有客户端)\n client.on('meeting:hostChanged', onHostChangedEvent)\n // 监听解散/被踢广播:参与人端立即隐藏视频窗口(不等 sess.end 的 async 完成)\n client.on('meeting:dissolved', onDissolvedEvent)\n client.on('meeting:kicked', onKickedEvent)\n\n // 布局双保险:ResizeObserver + window resize\n if (contentEl.value) {\n resizeObserver = new ResizeObserver(() => layout())\n resizeObserver.observe(contentEl.value)\n }\n window.addEventListener('resize', layout)\n nextTick(layout)\n})\n\nonUnmounted(() => {\n if (timer) clearInterval(timer)\n if (actionErrorTimer) clearTimeout(actionErrorTimer)\n if (resizeObserver) {\n resizeObserver.disconnect()\n resizeObserver = null\n }\n window.removeEventListener('resize', layout)\n const client = getClient()\n client?.off?.('meeting:hostChanged', onHostChangedEvent)\n client?.off?.('meeting:dissolved', onDissolvedEvent)\n client?.off?.('meeting:kicked', onKickedEvent)\n})\n</script>\n\n<template>\n <div\n id=\"meetingWindow\"\n v-show=\"!minimized && !endedReason\"\n v-draggable=\"'.meeting-header'\"\n class=\"meeting-window\"\n :style=\"{ height: height || '80vh' }\"\n >\n <MeetingHeader\n :title=\"title\"\n :duration=\"formatDuration(duration)\"\n :grid-mode=\"gridMode\"\n @toggle-grid=\"gridMode = !gridMode\"\n @close=\"leaveMeeting\"\n @minimize=\"minimized = true\"\n />\n\n <div v-if=\"deviceErrors.length\" class=\"device-warn\">\n {{ deviceErrors.join('; ') }}\n </div>\n <div v-if=\"actionError\" class=\"device-warn\">\n {{ actionError }}\n </div>\n\n <div ref=\"contentEl\" class=\"content\">\n <!-- 主视频:当前主持人 -->\n <div ref=\"mainVideoWrap\">\n <MainVideoTile\n v-if=\"mainVideoMember\"\n ref=\"mainVideoRef\"\n :identity=\"mainVideoMember.identity\"\n :name=\"mainVideoMember.name\"\n :muted=\"mainVideoMember.muted\"\n :camera-on=\"mainVideoMember.cameraOn\"\n :speaking=\"mainVideoMember.speaking\"\n :is-host=\"mainVideoMember.isHost\"\n :is-self=\"mainVideoMember.identity === (getSession()?.identity || '')\"\n :mic-unavailable=\"micUnavailable\"\n :on-video-el-ready=\"onMainVideoReady\"\n />\n </div>\n\n <aside ref=\"sidebarEl\" class=\"sidebar\">\n <div ref=\"tabBarWrap\">\n <SidebarTabs\n v-model:active-tab=\"activeTab\"\n :is-host=\"isCurrentHost\"\n @add-member=\"onAddMemberClick\"\n />\n </div>\n <!-- 视频列表 tab -->\n <VideoListPanel\n v-show=\"activeTab === 'video'\"\n ref=\"videoListPanelRef\"\n :members=\"smallVideoMembers\"\n :self-identity=\"getSession()?.identity || ''\"\n :self-mic-unavailable=\"micUnavailable\"\n :on-tile-mount=\"onSmallVideoReady\"\n />\n <!-- 成员列表 tab -->\n <MemberListPanel\n v-show=\"activeTab === 'member'\"\n :members=\"sortedMembers\"\n :can-promote=\"isCurrentHost\"\n :can-kick=\"isCurrentHost\"\n :can-reinvite=\"isCurrentHost\"\n :self-identity=\"getSession()?.identity || ''\"\n :self-mic-unavailable=\"micUnavailable\"\n :self-camera-unavailable=\"cameraUnavailable\"\n @transfer-host=\"(uid: string) => transferHost(uid)\"\n @kick=\"(uid: string) => kickParticipant(uid)\"\n @reinvite=\"(uid: string) => reinviteMember(uid)\"\n />\n </aside>\n </div>\n\n <!-- 底部控制按钮区:按钮组抽为 MeetingControlBar(全屏宫格的 header 中央复用同一组件) -->\n <div class=\"bottom-controls\">\n <MeetingControlBar\n :mic-enabled=\"micEnabled\"\n :camera-enabled=\"cameraEnabled\"\n :speaker-muted=\"speakerMuted\"\n :mic-unavailable=\"micUnavailable\"\n :camera-unavailable=\"cameraUnavailable\"\n @toggle-mic=\"toggleMic\"\n @toggle-camera=\"toggleCamera\"\n @toggle-speaker=\"toggleSpeaker\"\n @hangup=\"leaveMeeting\"\n />\n </div>\n </div>\n\n <!-- 全屏宫格模式:覆盖整个视口的 4 列 × N 行宫格。\n tile 固定 16:9,高度按可视高度 1/4 推算(减 3 条 5px 间隔)——\n 4 行时正好垂直铺满,超出 4 行出现纵向滚动条;宫格整体水平居中,左右留白。\n 复用 MeetingHeader(图标自动变为「退出宫格」+ 隐藏最小化)与 SmallVideoTile。\n 会议结束/被踢/解散时随 endedReason 自动退出 -->\n <div v-if=\"gridMode && !endedReason\" class=\"fs-grid-overlay\">\n <MeetingHeader\n :title=\"title\"\n :duration=\"formatDuration(duration)\"\n :grid-mode=\"true\"\n @toggle-grid=\"gridMode = false\"\n @close=\"leaveMeeting\"\n >\n <!-- 控制按钮组:与默认模式底部同一组件,放在 header 水平/垂直居中位 -->\n <template #center>\n <MeetingControlBar\n :mic-enabled=\"micEnabled\"\n :camera-enabled=\"cameraEnabled\"\n :speaker-muted=\"speakerMuted\"\n :mic-unavailable=\"micUnavailable\"\n :camera-unavailable=\"cameraUnavailable\"\n @toggle-mic=\"toggleMic\"\n @toggle-camera=\"toggleCamera\"\n @toggle-speaker=\"toggleSpeaker\"\n @hangup=\"leaveMeeting\"\n />\n </template>\n </MeetingHeader>\n <div class=\"fs-grid-scroll\">\n <div class=\"fs-grid\">\n <SmallVideoTile\n v-for=\"m in gridMembers\"\n :key=\"m.identity\"\n :identity=\"m.identity\"\n :name=\"m.name\"\n :muted=\"m.muted\"\n :camera-on=\"m.joined && m.cameraOn\"\n :speaking=\"m.speaking\"\n :is-host=\"m.isHost\"\n :is-self=\"m.identity === (getSession()?.identity || '')\"\n :host-badge-top=\"true\"\n :mic-unavailable=\"micUnavailable\"\n :on-video-el-ready=\"onGridVideoReady\"\n />\n </div>\n </div>\n </div>\n\n <!-- 解散会议确认弹窗:独立于主窗口,最小化态下也能显示 -->\n <ConfirmDialog\n v-if=\"showDissolveConfirm\"\n title=\"解散会议\"\n message=\"会议中还有其他成员,确认要解散会议吗?所有成员将被强制离会。\"\n confirm-text=\"解散会议\"\n cancel-text=\"取消\"\n :danger=\"true\"\n @confirm=\"confirmDissolve\"\n @cancel=\"cancelDissolve\"\n />\n\n <!-- 自定义邀请面板:业务方传 #invite-panel 时替代内置选人弹窗。\n 插槽内容渲染在根级(无容器包裹),定位/遮罩完全由插槽内容自定 -->\n <slot\n v-if=\"invitePanelOpen\"\n name=\"invite-panel\"\n :members=\"members\"\n :exclude-uids=\"memberIdentities\"\n :invite=\"inviteUids\"\n :close=\"() => (invitePanelOpen = false)\"\n />\n\n <!-- 会议结束/离开倒计时提示:独立浮窗,不依赖主窗口 -->\n <MeetingEndedToast\n v-if=\"endedReason\"\n :reason=\"endedReason\"\n :duration=\"5\"\n @close=\"emit('close')\"\n />\n\n <!-- 最小化浮条:可拖拽,单行显示发言人/会议名称 + 时长 + 还原/关闭按钮 -->\n <div v-if=\"minimized && !endedReason\" class=\"mini-bar\" v-draggable=\"'.mini-bar-body'\">\n <div class=\"mini-bar-body\">\n <!-- 有人发言时显示声波动效 + 发言人;无人发言显示会议名称(同一位置切换) -->\n <span v-if=\"speakerDisplayText\" class=\"speaking-indicator\">\n <span class=\"wave-bar\"></span>\n <span class=\"wave-bar\"></span>\n <span class=\"wave-bar\"></span>\n </span>\n <span class=\"mini-main-text\" :class=\"{ speaking: !!speakerDisplayText }\">\n {{ speakerDisplayText || title }}\n </span>\n <span class=\"mini-duration\">{{ formatDuration(duration) }}</span>\n </div>\n <div class=\"mini-bar-actions\">\n <button\n class=\"mini-btn\"\n type=\"button\"\n aria-label=\"还原窗口\"\n title=\"还原窗口\"\n v-html=\"MaximizeIcon\"\n @click=\"minimized = false\"\n ></button>\n <button\n class=\"mini-btn mini-close\"\n type=\"button\"\n aria-label=\"离开会议\"\n title=\"离开会议\"\n v-html=\"CloseIcon\"\n @click=\"leaveMeeting\"\n ></button>\n </div>\n </div>\n</template>\n\n<style scoped>\n.meeting-window {\n position: fixed;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n /* height 由 prop 控制(inline style 注入),未传时 fallback 80vh */\n width: var(--mw-w, min(1200px, 95vw));\n background: var(--im-meeting-bg-panel, #0f172a);\n display: flex;\n flex-direction: column;\n overflow: hidden;\n margin: 0 auto;\n padding: 10px;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);\n color: var(--im-meeting-text-primary, #fff);\n z-index: 9999;\n border-radius: 0;\n /* 与 design/video-conference-layout.html :root 变量一一对应 */\n --im-meeting-bg-body: #0b1220;\n --im-meeting-bg-panel: #0f172a;\n --im-meeting-bg-video: #000000;\n --im-meeting-text-primary: #ffffff;\n --im-meeting-text-secondary: rgba(255, 255, 255, 0.85);\n --im-meeting-text-tertiary: rgba(255, 255, 255, 0.55);\n --im-meeting-border: rgba(255, 255, 255, 0.08);\n --im-meeting-scrollbar: rgba(255, 255, 255, 0.15);\n --im-meeting-scrollbar-hover: rgba(255, 255, 255, 0.35);\n --im-meeting-tab-active: #1e293b;\n --im-meeting-tab-inactive: #111827;\n --im-meeting-speaking: #22c55e;\n --im-meeting-online: #22c55e;\n --im-meeting-offline: #6b7280;\n --im-meeting-overlay-gap: 5px;\n}\n.content {\n flex: 1 1 0;\n min-height: 0;\n display: flex;\n flex-direction: row;\n gap: 10px;\n padding: 0;\n overflow: hidden;\n margin-top: 5px;\n}\n.content > div:first-child {\n flex: 0 0 auto;\n}\n.sidebar {\n flex: 0 0 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n /* 比会议窗口背景 #0f172a 稍深一点的暗色,让空列表区域不显得空洞 */\n background: #0b1220;\n /* tab-bar 与 list 之间的 8px 间距由 sidebar flex gap 直接管理,\n 不依赖子组件内部 margin(避免 BFC 包含 margin 导致 offsetHeight 多算 8px) */\n gap: 8px;\n overflow: hidden;\n}\n/* 所有子节点撑满 sidebar 宽度:tab-bar / video list / member list 三者左右对齐 */\n.sidebar > * {\n width: 100%;\n}\n.sidebar > div:first-child {\n flex: 0 0 auto;\n}\n/* 设备失败提示 */\n.device-warn {\n position: absolute;\n top: 44px;\n left: 50%;\n transform: translateX(-50%);\n background: rgba(212, 160, 23, 0.92);\n color: var(--im-meeting-text-primary, #fff);\n font-size: 12px;\n padding: 4px 12px;\n border-radius: 0;\n z-index: 6;\n pointer-events: none;\n max-width: 80%;\n text-align: center;\n}\n/* 底部控制按钮区:水平居中、常驻显示、紧凑高度 */\n.bottom-controls {\n flex: 0 0 auto;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 16px;\n padding: 6px 0 2px;\n z-index: 4;\n}\n\n/* ============ 全屏宫格模式 ============ */\n.fs-grid-overlay {\n position: fixed;\n inset: 0;\n z-index: 10000; /* 盖过会议窗口(9999) */\n background: var(--im-meeting-bg-body, #0b1220);\n display: flex;\n flex-direction: column;\n padding: 10px;\n color: var(--im-meeting-text-primary, #fff);\n}\n/* 全屏态的 header 不可拖拽(拖拽指令只绑在主窗口上) */\n.fs-grid-overlay :deep(.meeting-header) {\n cursor: default;\n}\n.fs-grid-scroll {\n flex: 1 1 0;\n min-height: 0;\n margin-top: 5px;\n overflow-y: auto;\n overflow-x: hidden;\n /* 容器查询单位:tile 高度按可视高度的 1/4 推算(减 3 条 5px 间隔);\n 窄屏(竖屏)时按容器宽度反推收缩,避免水平溢出 */\n container-type: size;\n scrollbar-width: thin;\n scrollbar-color: var(--im-meeting-scrollbar, rgba(255, 255, 255, 0.15)) transparent;\n}\n.fs-grid-scroll::-webkit-scrollbar {\n width: 8px;\n}\n.fs-grid-scroll::-webkit-scrollbar-thumb {\n background: var(--im-meeting-scrollbar, rgba(255, 255, 255, 0.15));\n}\n.fs-grid-scroll::-webkit-scrollbar-thumb:hover {\n background: var(--im-meeting-scrollbar-hover, rgba(255, 255, 255, 0.35));\n}\n/* 宫格:4 列,tile 16:9,间隔 5px;整体宽度 fit-content + margin auto 实现水平居中留白 */\n.fs-grid {\n --fs-tile-h: min(calc((100cqh - 15px) / 4), calc((100cqw - 15px) * 9 / 64));\n display: grid;\n grid-template-columns: repeat(4, calc(var(--fs-tile-h) * 16 / 9));\n grid-auto-rows: var(--fs-tile-h);\n gap: 5px;\n width: fit-content;\n margin: 0 auto;\n}\n.fs-grid :deep(.small-video) {\n width: 100%;\n height: 100%;\n}\n\n/* ============ 最小化浮条 ============ */\n.mini-bar {\n position: fixed;\n right: 20px;\n bottom: 20px;\n z-index: 9999;\n display: flex;\n align-items: stretch;\n max-width: 320px;\n min-width: 220px;\n background: var(--im-meeting-bg-panel, #0f172a);\n border: 1px solid var(--im-meeting-border, rgba(255, 255, 255, 0.12));\n border-radius: 0;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);\n color: var(--im-meeting-text-primary, #fff);\n overflow: hidden;\n}\n/* 拖拽区:单行——发言人/会议名称 + 时长 */\n.mini-bar-body {\n flex: 1 1 auto;\n min-width: 0;\n padding: 8px 10px;\n cursor: move;\n display: flex;\n align-items: center;\n gap: 6px;\n}\n/* 主体文本:有人发言时显示发言人(绿色),无人发言显示会议名称(白色) */\n.mini-main-text {\n font-size: 13px;\n font-weight: 500;\n color: var(--im-meeting-text-primary, #fff);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n flex: 1 1 auto;\n min-width: 0;\n}\n.mini-main-text.speaking {\n color: var(--im-meeting-speaking, #22c55e);\n font-weight: 600;\n}\n.mini-duration {\n font-size: 11px;\n color: var(--im-meeting-text-tertiary, rgba(255, 255, 255, 0.55));\n font-variant-numeric: tabular-nums;\n flex: 0 0 auto;\n}\n/* 声波动效:复用 MainVideoTile 的 wave-bar 三柱动效 + 颜色变量 */\n.mini-bar-body .speaking-indicator {\n display: inline-flex;\n align-items: flex-end;\n gap: 2px;\n flex: 0 0 auto;\n}\n.mini-bar-body .wave-bar {\n width: 2px;\n background: var(--im-meeting-speaking, #22c55e);\n animation: mini-wave 1s ease-in-out infinite;\n}\n.mini-bar-body .wave-bar:nth-child(1) {\n height: 6px;\n animation-delay: 0s;\n}\n.mini-bar-body .wave-bar:nth-child(2) {\n height: 10px;\n animation-delay: 0.15s;\n}\n.mini-bar-body .wave-bar:nth-child(3) {\n height: 6px;\n animation-delay: 0.3s;\n}\n@keyframes mini-wave {\n 0%,\n 100% {\n transform: scaleY(1);\n opacity: 0.7;\n }\n 50% {\n transform: scaleY(1.6);\n opacity: 1;\n }\n}\n/* 操作按钮区 */\n.mini-bar-actions {\n flex: 0 0 auto;\n display: flex;\n align-items: center;\n gap: 2px;\n padding: 0 4px;\n border-left: 1px solid var(--im-meeting-border, rgba(255, 255, 255, 0.08));\n}\n.mini-btn {\n width: 26px;\n height: 100%;\n min-height: 36px;\n border: none;\n background: transparent;\n color: var(--im-meeting-text-secondary, rgba(255, 255, 255, 0.85));\n cursor: pointer;\n border-radius: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n transition:\n background 0.2s,\n color 0.2s;\n}\n.mini-btn:hover {\n background: rgba(255, 255, 255, 0.1);\n color: var(--im-meeting-text-primary, #fff);\n}\n.mini-btn :deep(svg) {\n width: 16px;\n height: 16px;\n}\n.mini-btn.mini-close:hover {\n background: #e53935;\n color: #fff;\n}\n</style>\n","<script setup lang=\"ts\">\nimport {\n ref,\n onMounted,\n onUnmounted,\n computed,\n nextTick,\n watch,\n useSlots,\n getCurrentInstance,\n} from 'vue'\nimport vDraggable from '../directives/draggable'\nimport { mountComponent } from '../mount'\nimport { ImToastApi as toast } from '../toast'\nimport MeetingHeader from './meeting/MeetingHeader.vue'\nimport MainVideoTile from './meeting/MainVideoTile.vue'\nimport VideoListPanel from './meeting/VideoListPanel.vue'\nimport MemberListPanel from './meeting/MemberListPanel.vue'\nimport SidebarTabs from './meeting/SidebarTabs.vue'\nimport SmallVideoTile from './meeting/SmallVideoTile.vue'\nimport MeetingControlBar from './meeting/MeetingControlBar.vue'\nimport ConfirmDialog from './ConfirmDialog.vue'\nimport MeetingEndedToast from './MeetingEndedToast.vue'\nimport UserPickerDialog from './UserPickerDialog.vue'\nimport { MaximizeIcon, CloseIcon } from './meeting/icons'\nimport type {\n MeetingMember,\n MeetingHostChangedEvent,\n MeetingDissolvedEvent,\n} from '@quitting/contract'\n\n// 会议窗口:左主视频(默认主持人) + 右侧栏 Tab(视频列表/成员列表) + 底部成员状态条\n// 布局严格 16:9,所有组件无圆角;样式对齐 design/video-conference-layout.html\n// 视频挂载沿用 CallSession/MeetingSession 的 attach 模式:通过 onVideoElReady 把元素交回 SDK\n\nconst props = defineProps<{\n mode: 'host' | 'participant'\n title: string\n // 窗口高度(CSS 值),由调用方决定。支持 \"80vh\" / \"600px\" / \"100%\" 等\n // position: fixed 下百分比相对 viewport;宽度由 layout() 按 16:9 推算\n height?: string\n onClose?: () => void\n // 用户信息解析器:uid → { name, dept? }\n // 后端 ListMembers 在 user 表无对应记录时会回退为 uid(如 demo mock 用户),\n // 调用方可传此函数让 SDK UI 显示业务侧维护的 name/dept;返回 undefined 则保留后端原值\n resolveUser?: (uid: string) => { name: string; dept?: string } | undefined\n // 禁用内置邀请弹窗(点击「+」只抛 addMember 事件,不弹内置选人窗)\n disableDefaultInvite?: boolean\n}>()\n\nconst emit = defineEmits<{\n (e: 'close'): void\n // 添加成员按钮被点击。默认行为按优先级退让:\n // 传了 #invite-panel 插槽 → 渲染插槽(完全自定义)\n // 传了 @add-member 监听 → 仅抛事件(不弹内置选人窗,由外部接管)\n // 都没有 → 抛事件 + 弹内置 UserPickerDialog(批量 reinvite)\n (e: 'addMember'): void\n}>()\n\nconst slots = useSlots()\n// 自定义邀请面板插槽签名(运行时按名取用;类型仅作文档)\n// scope: members=当前成员列表 / excludeUids=已在会成员 / invite(uids)=批量邀请 / close()=收起面板\n\n// 自定义邀请面板开关(#invite-panel 插槽渲染层)\nconst invitePanelOpen = ref(false)\n// 当前会议成员 externalId 列表(插槽 scope 用:选人时排除已在会成员)\nconst memberIdentities = computed(() => members.value.map((m) => m.identity))\n\nconst micEnabled = ref(true)\nconst cameraEnabled = ref(true)\nconst speakerMuted = ref(false)\nconst hasCamera = ref(true)\nconst hasMic = ref(true)\n// 设备不可用(无设备/无权限/被占用):按钮与自己的状态图标置灰,与「主动关闭」(红色)区分\nconst micUnavailable = ref(false)\nconst cameraUnavailable = ref(false)\nconst deviceErrors = ref<string[]>([])\n// 通用操作提示(reinvite 失败等),复用 device-warn 样式,3 秒自动消失\nconst actionError = ref('')\nlet actionErrorTimer: ReturnType<typeof setTimeout> | null = null\nfunction pushToast(msg: string) {\n actionError.value = msg\n if (actionErrorTimer) clearTimeout(actionErrorTimer)\n actionErrorTimer = setTimeout(() => (actionError.value = ''), 3000)\n}\nconst duration = ref(0)\nconst endedReason = ref('')\nconst activeTab = ref<'video' | 'member'>('video')\nconst members = ref<MeetingMember[]>([])\nconst hostIdentity = ref<string>('')\n// 最小化状态:true 时隐藏主窗口内容,显示可拖拽的小浮条(LiveKit 会话不中断)\nconst minimized = ref(false)\n// 全屏宫格模式:true 时覆盖整个视口的 4 列宫格布局(LiveKit 会话不中断)\nconst gridMode = ref(false)\n\n// 元素引用\nconst contentEl = ref<HTMLElement | null>(null)\nconst mainVideoWrap = ref<HTMLElement | null>(null)\nconst sidebarEl = ref<HTMLElement | null>(null)\nconst tabBarWrap = ref<HTMLElement | null>(null)\nconst videoListPanelRef = ref<InstanceType<typeof VideoListPanel> | null>(null)\nconst mainVideoRef = ref<InstanceType<typeof MainVideoTile> | null>(null)\n\n// 视频 attach:identity → 元素;保留 self/remote 区分\nlet selfVideoEl: HTMLVideoElement | null = null\nlet mainVideoIdentity: string | null = null\nlet mainVideoEl: HTMLVideoElement | null = null\nconst participantVideoEls = new Map<string, HTMLVideoElement>()\n// 全屏宫格的视频元素池:identity → 元素(track 后到时补挂载用)\nconst gridVideoEls = new Map<string, HTMLVideoElement>()\n\nlet timer: ReturnType<typeof setInterval> | null = null\nlet resizeObserver: ResizeObserver | null = null\n\nfunction getClient() {\n return (window as any).__imClient\n}\n\nfunction getSession() {\n return getClient()?.meeting?.currentSession\n}\n\n// 主视频参与者:当前 hostIdentity 对应的 member(可能未入会)\nconst mainVideoMember = computed<MeetingMember | null>(() => {\n const m = members.value.find((x) => x.identity === hostIdentity.value)\n return m || null\n})\n\n// 当前用户是否是「当前主持人」——动态判断,主持人转让后自动更新\n// mode prop 在创建时固化(host/participant),转让后原主持人 mode 仍是 host 但已无权限,\n// 新主持人 mode 是 participant 却应有权限。改用 identity === hostIdentity 解决菜单错乱。\nconst isCurrentHost = computed(() => {\n const sess = getSession()\n return !!sess && sess.identity === hostIdentity.value\n})\n\n// 正在发言的成员(最小化浮条显示用)——与成员列表 speaking 判定一致\nconst speakingMembers = computed(() =>\n members.value.filter((m) => m.speaking && m.joined && m.online),\n)\n// 发言人展示文本:≤3 个全显示(顿号分隔);>3 个显示第一个 + \"...等N人\"\nconst speakerDisplayText = computed(() => {\n const list = speakingMembers.value\n if (list.length === 0) return ''\n if (list.length <= 3) return list.map((m) => m.name).join('、')\n return `${list[0].name}...等${list.length}人`\n})\n\n// 小视频列表:除了主视频那位,其他在线成员(排序规则与所有布局统一)\nconst smallVideoMembers = computed<MeetingMember[]>(() => {\n return sortMembers(\n members.value.filter((m) => m.identity !== hostIdentity.value && (m.online || m.joined)),\n )\n})\n\n// 全屏宫格成员:所有在线/已入会成员(含主持人和自己),排序规则与所有布局统一\nconst gridMembers = computed<MeetingMember[]>(() => {\n return sortMembers(members.value.filter((m) => m.online || m.joined))\n})\n\n// 全屏宫格视频元素就绪:与 onSmallVideoReady 同模式(self 走本地 attach,其余走远端)\nfunction onGridVideoReady(identity: string, el: HTMLVideoElement | null) {\n const session = getSession()\n if (!session) return\n if (el) {\n gridVideoEls.set(identity, el)\n if (identity === session.identity) {\n session.attachLocalVideo(el)\n } else {\n session.attachParticipantVideo(identity, el)\n }\n } else {\n gridVideoEls.delete(identity)\n }\n}\n\n// 小视频列表变化后必须重新 layout:\n// mount 时 members 还是空 → small-video 没渲染 → layout() 跳过尺寸应用\n// 之后 refreshMembers 完成、small-video 渲染,需要重新 layout 把 inline style 设上\nwatch(smallVideoMembers, () => {\n nextTick(layout)\n})\n\n// 排序后的成员列表(成员列表 tab / 全屏宫格 / 小视频列表三处布局统一使用)\n// 排序规则:主持人最前 → 其余按加入时间升序(joinedAt)→ 未加入的按邀请序 → 已离会最后。\n// 不再按发言状态排序——speaking 仅用于 tile 角标与高亮,不影响位置\nfunction getStatusPriority(m: MeetingMember, hostUid: string): number {\n if (m.identity === hostUid) return 0\n if (m.status === 'left') return 3\n if (m.joinedAt > 0) return 1\n return 2\n}\nfunction sortMembers(list: MeetingMember[]): MeetingMember[] {\n const hostUid = hostIdentity.value\n return list.slice().sort((a, b) => {\n const pa = getStatusPriority(a, hostUid)\n const pb = getStatusPriority(b, hostUid)\n if (pa !== pb) return pa - pb\n // 同组内按加入时间;均为 0(未加入)时退化为邀请序\n if (a.joinedAt !== b.joinedAt) return a.joinedAt - b.joinedAt\n return (a.inviteOrder || 0) - (b.inviteOrder || 0)\n })\n}\nconst sortedMembers = computed(() => sortMembers(members.value))\n\nasync function refreshMembers(force = false) {\n const client = getClient()\n if (!client?.meeting) return\n try {\n const list = await client.meeting.getMembers(force)\n // 若调用方提供 resolveUser,用业务侧 name/dept 覆盖后端返回(demo 模式下后端回退 uid)\n if (props.resolveUser) {\n members.value = list.map((m: MeetingMember) => {\n const u = props.resolveUser?.(m.identity)\n if (!u) return m\n return { ...m, name: u.name, dept: u.dept ?? m.dept }\n })\n } else {\n members.value = list\n }\n } catch (err) {\n console.warn('[im] refreshMembers 失败', err)\n }\n}\n\nasync function reattachAllVideos() {\n const session = getSession()\n if (!session) return\n // 主视频:self 时走 attachLocalVideo,否则 attachParticipantVideo\n if (mainVideoIdentity && mainVideoEl) {\n if (mainVideoIdentity === session.identity) {\n session.attachLocalVideo(mainVideoEl)\n } else {\n session.attachParticipantVideo(mainVideoIdentity, mainVideoEl)\n }\n }\n}\n\n// 主视频元素就绪\nasync function onMainVideoReady(identity: string, el: HTMLVideoElement | null) {\n const session = getSession()\n if (!session) return\n if (el) {\n mainVideoIdentity = identity\n mainVideoEl = el\n if (identity === session.identity) {\n session.attachLocalVideo(el)\n } else {\n session.attachParticipantVideo(identity, el)\n }\n } else {\n mainVideoIdentity = null\n mainVideoEl = null\n }\n}\n\n// 小视频元素就绪\nfunction onSmallVideoReady(identity: string, el: HTMLVideoElement | null) {\n const session = getSession()\n if (!session) return\n if (el) {\n participantVideoEls.set(identity, el)\n if (identity === session.identity) {\n // 自己的 tile:attach 本地视频(非主持人在小视频列表里看不到自己就是因为这里跳过了)\n session.attachLocalVideo(el)\n } else {\n session.attachParticipantVideo(identity, el)\n }\n } else {\n participantVideoEls.delete(identity)\n }\n}\n\nasync function syncDeviceCapability() {\n try {\n const devices = await navigator.mediaDevices.enumerateDevices()\n hasCamera.value = devices.some((d) => d.kind === 'videoinput')\n hasMic.value = devices.some((d) => d.kind === 'audioinput')\n if (!hasCamera.value) cameraEnabled.value = false\n if (!hasMic.value) micEnabled.value = false\n // 无设备 = 不可用(按钮置灰而非隐藏)\n micUnavailable.value = !hasMic.value\n cameraUnavailable.value = !hasCamera.value\n // 无权限 = 不可用:permissions.query best-effort(Safari 等不支持会抛错,忽略——\n // 开启失败路径由 devices 事件的 errors 兜底)\n try {\n const p = await navigator.permissions.query({ name: 'microphone' })\n if (p.state === 'denied') micUnavailable.value = true\n } catch {\n /* 忽略 */\n }\n try {\n const p = await navigator.permissions.query({ name: 'camera' })\n if (p.state === 'denied') cameraUnavailable.value = true\n } catch {\n /* 忽略 */\n }\n } catch (e) {\n console.warn('[im] enumerateDevices 失败', e)\n }\n}\n\nfunction triggerLayout() {\n nextTick(layout)\n}\n\n// 布局计算:从 design/video-conference-layout.html layout() 函数搬\n// 所有尺寸(mwW/sidebarW/mainW/smallW)无条件计算,避免小视频列表为空时窗口宽度塌陷\nfunction layout() {\n const content = contentEl.value\n const mainVideo = mainVideoWrap.value\n const sidebar = sidebarEl.value\n const videoListEl = videoListPanelRef.value?.listEl as HTMLElement | undefined\n const items = videoListEl?.querySelectorAll<HTMLElement>('.small-video') ?? []\n if (!content || !mainVideo || !sidebar) return\n\n // 1. 主视频 16:9,高度 = content 高度\n const contentH = content.clientHeight\n if (contentH <= 0) return\n const mainH = contentH\n const mainW = (mainH * 16) / 9\n\n // 2. 小视频 16:9,3 个 + 3 个 gap 填满 sidebar 可用高度\n const smallGap = 8\n const tabBarH = tabBarWrap.value?.offsetHeight ?? 28\n const smallH = (mainH - tabBarH - smallGap * 3) / 3\n const smallW = (smallH * 16) / 9\n\n // 3. sidebar 宽度 = 小视频宽(不再保留自定义滚动条空间,列表与 tab 等宽)\n const sidebarW = smallW\n\n // 4. 窗口总宽度(padding + main + gap + sidebar + padding)—— 左右 padding 对称\n const padding = 10\n const gap = 10\n const mwW = padding + mainW + gap + sidebarW + padding\n\n // 5. 应用到 DOM(items 为空也照常设,保持窗口宽度稳定)\n const mw = document.getElementById('meetingWindow') as HTMLElement | null\n if (mw) {\n mw.style.width = mwW + 'px'\n }\n\n mainVideo.style.width = mainW + 'px'\n mainVideo.style.height = mainH + 'px'\n\n // sidebar 宽度 = smallW,与 list 完全对齐\n sidebar.style.width = sidebarW + 'px'\n sidebar.style.height = contentH + 'px'\n\n if (videoListEl) {\n // list 撑满 sidebar,与 tab-bar 等宽\n videoListEl.style.width = '100%'\n videoListEl.style.height = contentH - tabBarH - smallGap + 'px'\n }\n\n items.forEach((item) => {\n // 小视频保持 16:9\n item.style.width = smallW + 'px'\n // 不取整:Math.floor 会让 3 个 small 累计损失 ~2px,导致最后 small 底部与主视频底部错位\n item.style.height = smallH + 'px'\n })\n}\n\n// ============ 设备/会议控制 ============\nasync function toggleMic() {\n if (micUnavailable.value) return\n const session = getSession()\n if (!session) return\n const next = !micEnabled.value\n try {\n await session.setMicEnabled(next)\n micEnabled.value = next\n // 本端自己不走远端 trackMuted 事件,手动更新 members 里自己那行\n // (members[].muted 语义 = 已闭麦,与 micEnabled 相反),与远端事件路径合流\n const self = members.value.find((m) => m.identity === session.identity)\n if (self && self.muted !== !next) {\n self.muted = !next\n members.value = [...members.value]\n }\n } catch (err) {\n console.warn('[im] setMicEnabled 失败,UI 不切换', err)\n }\n}\n\nasync function toggleCamera() {\n if (cameraUnavailable.value) return\n const session = getSession()\n if (!session) return\n const next = !cameraEnabled.value\n try {\n await session.setCameraEnabled(next)\n cameraEnabled.value = next\n // 本端自己不走远端 trackMuted 事件,手动更新 members 里自己那行\n // (与远端事件路径合流,其他端/本端列表与遮罩立即反映)\n const self = members.value.find((m) => m.identity === session.identity)\n if (self && self.cameraOn !== next) {\n self.cameraOn = next\n members.value = [...members.value]\n }\n nextTick(reattachAllVideos)\n } catch (err) {\n console.warn('[im] setCameraEnabled 失败,UI 不切换', err)\n }\n}\n\nasync function toggleSpeaker() {\n const session = getSession()\n if (!session) return\n speakerMuted.value = !speakerMuted.value\n await session.setSpeakerMuted(speakerMuted.value)\n}\n\n// 是否显示「解散会议」确认弹窗\nconst showDissolveConfirm = ref(false)\nconst dissolvePending = ref(false)\n\nasync function leaveMeeting() {\n const client = getClient()\n if (!client) return\n // 当前主持人才能解散;非主持人(含已转让的前主持人)直接走普通 leave\n if (!isCurrentHost.value) {\n await client.meeting?.leave()\n // 主窗口立即隐藏,独立倒计时 toast 提示后由 toast 触发 close\n endedReason.value = '已离开'\n return\n }\n // host:查房内参与者数量\n // - count > 1:房内还有其他人,弹\"解散会议\"确认(让 host 明确选择 Leave 还是 Dissolve)\n // - count <= 1:只剩 host 自己——直接 dissolve,避免遗留僵尸会议让离线 invitee 上线后收到 stale 邀请\n // 之前的 bug:count=1 时走 leave,只改 host 自己 invite.status=left,meeting.status 仍 active,\n // 其他 invitee 的 status 仍 invited → pending-invites 返回 stale 邀请\n let count = 1\n try {\n count = await client.meeting.getParticipantCount()\n } catch (err) {\n console.warn('[im] 查参与者数量失败,按 1 处理(自动解散)', err)\n }\n if (count > 1) {\n showDissolveConfirm.value = true\n return\n }\n dissolvePending.value = true\n try {\n await client.meeting.dissolve()\n endedReason.value = '会议已解散'\n } catch (err) {\n // dissolve 失败兜底:后端 Leave 内部也会做同样的自动 dissolve 兜底,但前端先失败说明网络层有问题\n console.error('[im] host 单独时解散失败,回退到 leave', err)\n endedReason.value = '已离开'\n await client.meeting?.leave().catch(() => {})\n } finally {\n dissolvePending.value = false\n }\n}\n\n// 确认解散\nasync function confirmDissolve() {\n const client = getClient()\n if (!client) return\n showDissolveConfirm.value = false\n dissolvePending.value = true\n try {\n await client.meeting.dissolve()\n // 解散成功:endedReason 让 ended-overlay 显示;broadcast 到达后 SDK 也会触发 ended 事件\n endedReason.value = '会议已解散'\n } catch (err) {\n console.error('[im] 解散失败', err)\n endedReason.value = '解散失败'\n } finally {\n dissolvePending.value = false\n }\n}\n\nfunction cancelDissolve() {\n showDissolveConfirm.value = false\n}\n\nasync function kickParticipant(uid: string) {\n const session = getSession()\n if (!session) return\n try {\n await session.kick(getClient().transport, uid)\n await refreshMembers(true)\n } catch (err) {\n console.warn('[im] kickParticipant 失败', err)\n }\n}\n\nasync function transferHost(uid: string) {\n const session = getSession()\n if (!session) return\n try {\n await session.transferHost(getClient().transport, uid)\n hostIdentity.value = uid\n await refreshMembers(true)\n } catch (err) {\n console.warn('[im] transferHost 失败', err)\n }\n}\n\n// 主持人再次邀请某成员(等待加入/已离会状态)\nasync function reinviteMember(uid: string) {\n const session = getSession()\n if (!session) return\n try {\n await getClient().meeting.reinvite(session.meetingId, uid)\n await refreshMembers(true)\n } catch (err: unknown) {\n // 后端错误码映射成中文提示,避免静默失败:\n // target_busy=对方正在通话/会议或有待处理邀请;not_host=主持人已转让\n const msg = err instanceof Error ? err.message : ''\n if (msg.includes('对方正忙') || msg.includes('target_busy')) {\n pushToast('对方正忙,稍后再试')\n } else if (msg.includes('无权') || msg.includes('not_host')) {\n pushToast('仅主持人可邀请成员')\n } else if (msg.includes('meeting_ended')) {\n pushToast('会议已结束')\n } else {\n pushToast('再次邀请失败,请重试')\n }\n console.warn('[im] reinvite 失败', err)\n }\n}\n\n// uid → 展示名(内置邀请流程 toast 用;优先 SDK 用户目录,回退 uid 本身)\nasync function nameOf(uid: string): Promise<string> {\n const dir = getClient()?.users\n if (dir?.resolve) {\n const u = await dir.resolve(uid).catch(() => undefined)\n if (u?.name) return u.name\n }\n return uid\n}\n\n// 批量邀请入会:内置默认流程与 #invite-panel 插槽的 invite() 共用\n// 单条失败不阻塞其他;busy/其他失败分类提示\nasync function inviteUids(uids: string[]) {\n const client = getClient()\n const session = getSession()\n if (!client || !session || uids.length === 0) return\n const busyUids: string[] = []\n const failedUids: string[] = []\n await Promise.all(\n uids.map((uid) =>\n client.meeting.reinvite(session.meetingId, uid).catch((e: unknown) => {\n const msg = e instanceof Error ? e.message : ''\n if (msg.includes('对方正忙') || msg.includes('target_busy')) busyUids.push(uid)\n else failedUids.push(uid)\n }),\n ),\n )\n if (busyUids.length > 0) {\n const names = (await Promise.all(busyUids.map(nameOf))).join('、')\n toast.error(`${names} 正忙,未能邀请加入会议`)\n }\n if (failedUids.length > 0) {\n const names = (await Promise.all(failedUids.map(nameOf))).join('、')\n toast.error(`${names} 邀请失败,请重试`)\n }\n refreshMembers(true)\n}\n\n// 内置默认邀请流程:拉当前成员(含已邀请未加入)→ 选人弹窗 → 批量 reinvite\nasync function openDefaultInvite() {\n const client = getClient()\n const session = getSession()\n if (!client || !session) return\n let excludeUids: string[] = []\n try {\n const list = await client.meeting.getMembers(true)\n excludeUids = list.map((m: MeetingMember) => m.identity)\n } catch (err) {\n console.warn('[im] 拉取会议成员失败,邀请弹窗不排除已入会成员', err)\n }\n mountComponent(UserPickerDialog, {\n title: '邀请参会',\n excludeUids,\n emptyText: '所有成员都已在会议中',\n onConfirm: (uids: string[]) => {\n inviteUids(uids)\n },\n })\n}\n\n// 点击「+」添加成员:插槽 > 外部事件监听 > 内置默认弹窗(退让式)\nfunction onAddMemberClick() {\n emit('addMember')\n if (slots['invite-panel']) {\n invitePanelOpen.value = !invitePanelOpen.value\n return\n }\n // 外部传了 @add-member 监听 → 仅抛事件,不弹内置选人窗(保持旧边界行为)\n const instance = getCurrentInstance()\n if (instance?.vnode.props?.onAddMember) return\n if (props.disableDefaultInvite) return\n openDefaultInvite()\n}\n\n// 监听 hostChanged 广播:切换主视频\nfunction onHostChangedEvent(e: MeetingHostChangedEvent) {\n const session = getSession()\n if (!session || e.meetingId !== session.meetingId) return\n hostIdentity.value = e.hostUid\n // 主视频元素会被 v-if 重新挂载,nextTick 后 reattach\n nextTick(() => {\n refreshMembers(true)\n })\n}\n\n// 监听解散广播:主持人解散后所有参与人收到,立即隐藏视频窗口,只留独立倒计时提示。\n// 不依赖 session 'ended' 事件(它要等 sess.end() 的 async disconnect 完成才触发,有延迟)。\nfunction onDissolvedEvent(e: MeetingDissolvedEvent) {\n const session = getSession()\n if (!session || e.meetingId !== session.meetingId) return\n if (!endedReason.value) endedReason.value = '会议已解散'\n if (timer) clearInterval(timer)\n}\n\n// 监听被踢广播:立即隐藏视频窗口\nfunction onKickedEvent(e: { meetingId: string; by: string }) {\n const session = getSession()\n if (!session || e.meetingId !== session.meetingId) return\n if (!endedReason.value) endedReason.value = '已被移出会议'\n if (timer) clearInterval(timer)\n}\n\nfunction formatDuration(s: number) {\n const h = Math.floor(s / 3600)\n const m = Math.floor((s % 3600) / 60)\n .toString()\n .padStart(2, '0')\n const sec = (s % 60).toString().padStart(2, '0')\n return h > 0 ? `${h}:${m}:${sec}` : `${m}:${sec}`\n}\n\nonMounted(async () => {\n await syncDeviceCapability()\n const client = getClient()\n if (!client) return\n const session = client.meeting?.currentSession\n if (!session) return\n\n hostIdentity.value = session.hostIdentity || session.identity\n\n session.onSession('connected', () => {\n refreshMembers()\n nextTick(layout)\n timer = setInterval(() => {\n duration.value++\n // 每 10 秒刷一次成员列表(muted/online 实时态)\n // 后端 presence scanner 缓存也是 10s 刷新,更高频率纯浪费;muted 等变化由事件即时触发 force refresh\n if (duration.value % 10 === 0) refreshMembers()\n }, 1000)\n })\n session.onSession('participantJoined', () => {\n refreshMembers(true)\n })\n session.onSession('participantLeft', () => {\n refreshMembers(true)\n })\n session.onSession('participantTrackSubscribed', (info: { kind: string; identity: string }) => {\n if (info.kind !== 'video') return\n // 优先用小视频池的元素\n const el = participantVideoEls.get(info.identity)\n if (el) {\n session.attachParticipantVideo(info.identity, el)\n }\n // 全屏宫格池的元素同样补挂载(track.attach 支持同一 track 挂多个元素)\n const gridEl = gridVideoEls.get(info.identity)\n if (gridEl) {\n session.attachParticipantVideo(info.identity, gridEl)\n }\n // 主视频位置的 track 流入:reattach 主视频(mainVideoEl 不在 participantVideoEls 里)\n if (mainVideoIdentity === info.identity) {\n nextTick(reattachAllVideos)\n }\n })\n // 发言态:LiveKit 给全量列表,覆盖式更新 members[].speaking\n // refreshMembers 每 10 秒会用后端 speaking=false 覆盖,下个 activeSpeakersChanged 事件会再次纠正——可接受\n // 就地修改成员字段,仅在状态变化时替换数组引用触发响应式\n session.onSession('activeSpeakersChanged', (info: { identities: string[] }) => {\n const set = new Set(info.identities)\n let changed = false\n for (const m of members.value) {\n const speaking = set.has(m.identity)\n if (m.speaking !== speaking) {\n m.speaking = speaking\n changed = true\n }\n }\n if (changed) members.value = [...members.value]\n })\n // 远端麦克风/摄像头开/关:毫秒级事件就地更新 members[].muted / cameraOn\n // (此前仅靠 10s 轮询后端快照;轮询保留作断线/丢事件后的兜底校正)\n session.onSession(\n 'participantMuteChanged',\n (info: { identity: string; muted: boolean; kind: 'audio' | 'video' }) => {\n const m = members.value.find((x) => x.identity === info.identity)\n if (!m) return\n let changed = false\n if (info.kind === 'audio') {\n if (m.muted !== info.muted) {\n m.muted = info.muted\n changed = true\n }\n } else {\n // video 事件 muted=true 表示摄像头关闭;cameraOn 语义相反\n if (m.cameraOn !== !info.muted) {\n m.cameraOn = !info.muted\n changed = true\n }\n }\n if (changed) members.value = [...members.value]\n },\n )\n session.onSession('devices', (info: { mic: boolean; camera: boolean; errors: string[] }) => {\n if (info.errors && info.errors.length > 0) {\n deviceErrors.value = info.errors\n setTimeout(() => (deviceErrors.value = []), 3000)\n }\n // 同步设备实际开启状态(enableDevices 可能 mic/camera 开启失败,UI 需据此更新按钮)\n if (!info.mic) micEnabled.value = false\n if (!info.camera) cameraEnabled.value = false\n if (info.camera) nextTick(reattachAllVideos)\n // 开启失败(无权限/被占用)= 不可用置灰;本次开启成功则恢复(如权限后来被授予)\n // errors 条目带 \"mic:\"/\"camera:\" 前缀(core enableDevices 约定),可精确区分设备\n if (info.errors.some((e) => e.startsWith('mic:'))) micUnavailable.value = true\n else if (info.mic) micUnavailable.value = false\n if (info.errors.some((e) => e.startsWith('camera:'))) cameraUnavailable.value = true\n else if (info.camera) cameraUnavailable.value = false\n })\n session.onSession('ended', () => {\n // 解散/被踢/对端断开都会触发——独立倒计时浮窗提示,5s 后自动关或用户手动关\n if (!endedReason.value) endedReason.value = '会议已结束'\n if (timer) clearInterval(timer)\n })\n\n // race 修复:mount 之前可能已 connected\n if (session.isConnected) {\n refreshMembers()\n nextTick(layout)\n timer = setInterval(() => {\n duration.value++\n if (duration.value % 10 === 0) refreshMembers()\n }, 1000)\n }\n\n // 监听 host 转让广播(所有客户端)\n client.on('meeting:hostChanged', onHostChangedEvent)\n // 监听解散/被踢广播:参与人端立即隐藏视频窗口(不等 sess.end 的 async 完成)\n client.on('meeting:dissolved', onDissolvedEvent)\n client.on('meeting:kicked', onKickedEvent)\n\n // 布局双保险:ResizeObserver + window resize\n if (contentEl.value) {\n resizeObserver = new ResizeObserver(() => layout())\n resizeObserver.observe(contentEl.value)\n }\n window.addEventListener('resize', layout)\n nextTick(layout)\n})\n\nonUnmounted(() => {\n if (timer) clearInterval(timer)\n if (actionErrorTimer) clearTimeout(actionErrorTimer)\n if (resizeObserver) {\n resizeObserver.disconnect()\n resizeObserver = null\n }\n window.removeEventListener('resize', layout)\n const client = getClient()\n client?.off?.('meeting:hostChanged', onHostChangedEvent)\n client?.off?.('meeting:dissolved', onDissolvedEvent)\n client?.off?.('meeting:kicked', onKickedEvent)\n})\n</script>\n\n<template>\n <div\n id=\"meetingWindow\"\n v-show=\"!minimized && !endedReason\"\n v-draggable=\"'.meeting-header'\"\n class=\"meeting-window\"\n :style=\"{ height: height || '80vh' }\"\n >\n <MeetingHeader\n :title=\"title\"\n :duration=\"formatDuration(duration)\"\n :grid-mode=\"gridMode\"\n @toggle-grid=\"gridMode = !gridMode\"\n @close=\"leaveMeeting\"\n @minimize=\"minimized = true\"\n />\n\n <div v-if=\"deviceErrors.length\" class=\"device-warn\">\n {{ deviceErrors.join('; ') }}\n </div>\n <div v-if=\"actionError\" class=\"device-warn\">\n {{ actionError }}\n </div>\n\n <div ref=\"contentEl\" class=\"content\">\n <!-- 主视频:当前主持人 -->\n <div ref=\"mainVideoWrap\">\n <MainVideoTile\n v-if=\"mainVideoMember\"\n ref=\"mainVideoRef\"\n :identity=\"mainVideoMember.identity\"\n :name=\"mainVideoMember.name\"\n :muted=\"mainVideoMember.muted\"\n :camera-on=\"mainVideoMember.cameraOn\"\n :speaking=\"mainVideoMember.speaking\"\n :is-host=\"mainVideoMember.isHost\"\n :is-self=\"mainVideoMember.identity === (getSession()?.identity || '')\"\n :mic-unavailable=\"micUnavailable\"\n :on-video-el-ready=\"onMainVideoReady\"\n />\n </div>\n\n <aside ref=\"sidebarEl\" class=\"sidebar\">\n <div ref=\"tabBarWrap\">\n <SidebarTabs\n v-model:active-tab=\"activeTab\"\n :is-host=\"isCurrentHost\"\n @add-member=\"onAddMemberClick\"\n />\n </div>\n <!-- 视频列表 tab -->\n <VideoListPanel\n v-show=\"activeTab === 'video'\"\n ref=\"videoListPanelRef\"\n :members=\"smallVideoMembers\"\n :self-identity=\"getSession()?.identity || ''\"\n :self-mic-unavailable=\"micUnavailable\"\n :on-tile-mount=\"onSmallVideoReady\"\n />\n <!-- 成员列表 tab -->\n <MemberListPanel\n v-show=\"activeTab === 'member'\"\n :members=\"sortedMembers\"\n :can-promote=\"isCurrentHost\"\n :can-kick=\"isCurrentHost\"\n :can-reinvite=\"isCurrentHost\"\n :self-identity=\"getSession()?.identity || ''\"\n :self-mic-unavailable=\"micUnavailable\"\n :self-camera-unavailable=\"cameraUnavailable\"\n @transfer-host=\"(uid: string) => transferHost(uid)\"\n @kick=\"(uid: string) => kickParticipant(uid)\"\n @reinvite=\"(uid: string) => reinviteMember(uid)\"\n />\n </aside>\n </div>\n\n <!-- 底部控制按钮区:按钮组抽为 MeetingControlBar(全屏宫格的 header 中央复用同一组件) -->\n <div class=\"bottom-controls\">\n <MeetingControlBar\n :mic-enabled=\"micEnabled\"\n :camera-enabled=\"cameraEnabled\"\n :speaker-muted=\"speakerMuted\"\n :mic-unavailable=\"micUnavailable\"\n :camera-unavailable=\"cameraUnavailable\"\n @toggle-mic=\"toggleMic\"\n @toggle-camera=\"toggleCamera\"\n @toggle-speaker=\"toggleSpeaker\"\n @hangup=\"leaveMeeting\"\n />\n </div>\n </div>\n\n <!-- 全屏宫格模式:覆盖整个视口的 4 列 × N 行宫格。\n tile 固定 16:9,高度按可视高度 1/4 推算(减 3 条 5px 间隔)——\n 4 行时正好垂直铺满,超出 4 行出现纵向滚动条;宫格整体水平居中,左右留白。\n 复用 MeetingHeader(图标自动变为「退出宫格」+ 隐藏最小化)与 SmallVideoTile。\n 会议结束/被踢/解散时随 endedReason 自动退出 -->\n <div v-if=\"gridMode && !endedReason\" class=\"fs-grid-overlay\">\n <MeetingHeader\n :title=\"title\"\n :duration=\"formatDuration(duration)\"\n :grid-mode=\"true\"\n @toggle-grid=\"gridMode = false\"\n @close=\"leaveMeeting\"\n >\n <!-- 控制按钮组:与默认模式底部同一组件,放在 header 水平/垂直居中位 -->\n <template #center>\n <MeetingControlBar\n :mic-enabled=\"micEnabled\"\n :camera-enabled=\"cameraEnabled\"\n :speaker-muted=\"speakerMuted\"\n :mic-unavailable=\"micUnavailable\"\n :camera-unavailable=\"cameraUnavailable\"\n @toggle-mic=\"toggleMic\"\n @toggle-camera=\"toggleCamera\"\n @toggle-speaker=\"toggleSpeaker\"\n @hangup=\"leaveMeeting\"\n />\n </template>\n </MeetingHeader>\n <div class=\"fs-grid-scroll\">\n <div class=\"fs-grid\">\n <SmallVideoTile\n v-for=\"m in gridMembers\"\n :key=\"m.identity\"\n :identity=\"m.identity\"\n :name=\"m.name\"\n :muted=\"m.muted\"\n :camera-on=\"m.joined && m.cameraOn\"\n :speaking=\"m.speaking\"\n :is-host=\"m.isHost\"\n :is-self=\"m.identity === (getSession()?.identity || '')\"\n :host-badge-top=\"true\"\n :mic-unavailable=\"micUnavailable\"\n :on-video-el-ready=\"onGridVideoReady\"\n />\n </div>\n </div>\n </div>\n\n <!-- 解散会议确认弹窗:独立于主窗口,最小化态下也能显示 -->\n <ConfirmDialog\n v-if=\"showDissolveConfirm\"\n title=\"解散会议\"\n message=\"会议中还有其他成员,确认要解散会议吗?所有成员将被强制离会。\"\n confirm-text=\"解散会议\"\n cancel-text=\"取消\"\n :danger=\"true\"\n @confirm=\"confirmDissolve\"\n @cancel=\"cancelDissolve\"\n />\n\n <!-- 自定义邀请面板:业务方传 #invite-panel 时替代内置选人弹窗。\n 插槽内容渲染在根级(无容器包裹),定位/遮罩完全由插槽内容自定 -->\n <slot\n v-if=\"invitePanelOpen\"\n name=\"invite-panel\"\n :members=\"members\"\n :exclude-uids=\"memberIdentities\"\n :invite=\"inviteUids\"\n :close=\"() => (invitePanelOpen = false)\"\n />\n\n <!-- 会议结束/离开倒计时提示:独立浮窗,不依赖主窗口 -->\n <MeetingEndedToast\n v-if=\"endedReason\"\n :reason=\"endedReason\"\n :duration=\"5\"\n @close=\"emit('close')\"\n />\n\n <!-- 最小化浮条:可拖拽,单行显示发言人/会议名称 + 时长 + 还原/关闭按钮 -->\n <div v-if=\"minimized && !endedReason\" class=\"mini-bar\" v-draggable=\"'.mini-bar-body'\">\n <div class=\"mini-bar-body\">\n <!-- 有人发言时显示声波动效 + 发言人;无人发言显示会议名称(同一位置切换) -->\n <span v-if=\"speakerDisplayText\" class=\"speaking-indicator\">\n <span class=\"wave-bar\"></span>\n <span class=\"wave-bar\"></span>\n <span class=\"wave-bar\"></span>\n </span>\n <span class=\"mini-main-text\" :class=\"{ speaking: !!speakerDisplayText }\">\n {{ speakerDisplayText || title }}\n </span>\n <span class=\"mini-duration\">{{ formatDuration(duration) }}</span>\n </div>\n <div class=\"mini-bar-actions\">\n <button\n class=\"mini-btn\"\n type=\"button\"\n aria-label=\"还原窗口\"\n title=\"还原窗口\"\n v-html=\"MaximizeIcon\"\n @click=\"minimized = false\"\n ></button>\n <button\n class=\"mini-btn mini-close\"\n type=\"button\"\n aria-label=\"离开会议\"\n title=\"离开会议\"\n v-html=\"CloseIcon\"\n @click=\"leaveMeeting\"\n ></button>\n </div>\n </div>\n</template>\n\n<style scoped>\n.meeting-window {\n position: fixed;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n /* height 由 prop 控制(inline style 注入),未传时 fallback 80vh */\n width: var(--mw-w, min(1200px, 95vw));\n background: var(--im-meeting-bg-panel, #0f172a);\n display: flex;\n flex-direction: column;\n overflow: hidden;\n margin: 0 auto;\n padding: 10px;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);\n color: var(--im-meeting-text-primary, #fff);\n z-index: 9999;\n border-radius: 0;\n /* 与 design/video-conference-layout.html :root 变量一一对应 */\n --im-meeting-bg-body: #0b1220;\n --im-meeting-bg-panel: #0f172a;\n --im-meeting-bg-video: #000000;\n --im-meeting-text-primary: #ffffff;\n --im-meeting-text-secondary: rgba(255, 255, 255, 0.85);\n --im-meeting-text-tertiary: rgba(255, 255, 255, 0.55);\n --im-meeting-border: rgba(255, 255, 255, 0.08);\n --im-meeting-scrollbar: rgba(255, 255, 255, 0.15);\n --im-meeting-scrollbar-hover: rgba(255, 255, 255, 0.35);\n --im-meeting-tab-active: #1e293b;\n --im-meeting-tab-inactive: #111827;\n --im-meeting-speaking: #22c55e;\n --im-meeting-online: #22c55e;\n --im-meeting-offline: #6b7280;\n --im-meeting-overlay-gap: 5px;\n}\n.content {\n flex: 1 1 0;\n min-height: 0;\n display: flex;\n flex-direction: row;\n gap: 10px;\n padding: 0;\n overflow: hidden;\n margin-top: 5px;\n}\n.content > div:first-child {\n flex: 0 0 auto;\n}\n.sidebar {\n flex: 0 0 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n /* 比会议窗口背景 #0f172a 稍深一点的暗色,让空列表区域不显得空洞 */\n background: #0b1220;\n /* tab-bar 与 list 之间的 8px 间距由 sidebar flex gap 直接管理,\n 不依赖子组件内部 margin(避免 BFC 包含 margin 导致 offsetHeight 多算 8px) */\n gap: 8px;\n overflow: hidden;\n}\n/* 所有子节点撑满 sidebar 宽度:tab-bar / video list / member list 三者左右对齐 */\n.sidebar > * {\n width: 100%;\n}\n.sidebar > div:first-child {\n flex: 0 0 auto;\n}\n/* 设备失败提示 */\n.device-warn {\n position: absolute;\n top: 44px;\n left: 50%;\n transform: translateX(-50%);\n background: rgba(212, 160, 23, 0.92);\n color: var(--im-meeting-text-primary, #fff);\n font-size: 12px;\n padding: 4px 12px;\n border-radius: 0;\n z-index: 6;\n pointer-events: none;\n max-width: 80%;\n text-align: center;\n}\n/* 底部控制按钮区:水平居中、常驻显示、紧凑高度 */\n.bottom-controls {\n flex: 0 0 auto;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 16px;\n padding: 6px 0 2px;\n z-index: 4;\n}\n\n/* ============ 全屏宫格模式 ============ */\n.fs-grid-overlay {\n position: fixed;\n inset: 0;\n z-index: 10000; /* 盖过会议窗口(9999) */\n background: var(--im-meeting-bg-body, #0b1220);\n display: flex;\n flex-direction: column;\n padding: 10px;\n color: var(--im-meeting-text-primary, #fff);\n}\n/* 全屏态的 header 不可拖拽(拖拽指令只绑在主窗口上) */\n.fs-grid-overlay :deep(.meeting-header) {\n cursor: default;\n}\n.fs-grid-scroll {\n flex: 1 1 0;\n min-height: 0;\n margin-top: 5px;\n overflow-y: auto;\n overflow-x: hidden;\n /* 容器查询单位:tile 高度按可视高度的 1/4 推算(减 3 条 5px 间隔);\n 窄屏(竖屏)时按容器宽度反推收缩,避免水平溢出 */\n container-type: size;\n scrollbar-width: thin;\n scrollbar-color: var(--im-meeting-scrollbar, rgba(255, 255, 255, 0.15)) transparent;\n}\n.fs-grid-scroll::-webkit-scrollbar {\n width: 8px;\n}\n.fs-grid-scroll::-webkit-scrollbar-thumb {\n background: var(--im-meeting-scrollbar, rgba(255, 255, 255, 0.15));\n}\n.fs-grid-scroll::-webkit-scrollbar-thumb:hover {\n background: var(--im-meeting-scrollbar-hover, rgba(255, 255, 255, 0.35));\n}\n/* 宫格:4 列,tile 16:9,间隔 5px;整体宽度 fit-content + margin auto 实现水平居中留白 */\n.fs-grid {\n --fs-tile-h: min(calc((100cqh - 15px) / 4), calc((100cqw - 15px) * 9 / 64));\n display: grid;\n grid-template-columns: repeat(4, calc(var(--fs-tile-h) * 16 / 9));\n grid-auto-rows: var(--fs-tile-h);\n gap: 5px;\n width: fit-content;\n margin: 0 auto;\n}\n.fs-grid :deep(.small-video) {\n width: 100%;\n height: 100%;\n}\n\n/* ============ 最小化浮条 ============ */\n.mini-bar {\n position: fixed;\n right: 20px;\n bottom: 20px;\n z-index: 9999;\n display: flex;\n align-items: stretch;\n max-width: 320px;\n min-width: 220px;\n background: var(--im-meeting-bg-panel, #0f172a);\n border: 1px solid var(--im-meeting-border, rgba(255, 255, 255, 0.12));\n border-radius: 0;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);\n color: var(--im-meeting-text-primary, #fff);\n overflow: hidden;\n}\n/* 拖拽区:单行——发言人/会议名称 + 时长 */\n.mini-bar-body {\n flex: 1 1 auto;\n min-width: 0;\n padding: 8px 10px;\n cursor: move;\n display: flex;\n align-items: center;\n gap: 6px;\n}\n/* 主体文本:有人发言时显示发言人(绿色),无人发言显示会议名称(白色) */\n.mini-main-text {\n font-size: 13px;\n font-weight: 500;\n color: var(--im-meeting-text-primary, #fff);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n flex: 1 1 auto;\n min-width: 0;\n}\n.mini-main-text.speaking {\n color: var(--im-meeting-speaking, #22c55e);\n font-weight: 600;\n}\n.mini-duration {\n font-size: 11px;\n color: var(--im-meeting-text-tertiary, rgba(255, 255, 255, 0.55));\n font-variant-numeric: tabular-nums;\n flex: 0 0 auto;\n}\n/* 声波动效:复用 MainVideoTile 的 wave-bar 三柱动效 + 颜色变量 */\n.mini-bar-body .speaking-indicator {\n display: inline-flex;\n align-items: flex-end;\n gap: 2px;\n flex: 0 0 auto;\n}\n.mini-bar-body .wave-bar {\n width: 2px;\n background: var(--im-meeting-speaking, #22c55e);\n animation: mini-wave 1s ease-in-out infinite;\n}\n.mini-bar-body .wave-bar:nth-child(1) {\n height: 6px;\n animation-delay: 0s;\n}\n.mini-bar-body .wave-bar:nth-child(2) {\n height: 10px;\n animation-delay: 0.15s;\n}\n.mini-bar-body .wave-bar:nth-child(3) {\n height: 6px;\n animation-delay: 0.3s;\n}\n@keyframes mini-wave {\n 0%,\n 100% {\n transform: scaleY(1);\n opacity: 0.7;\n }\n 50% {\n transform: scaleY(1.6);\n opacity: 1;\n }\n}\n/* 操作按钮区 */\n.mini-bar-actions {\n flex: 0 0 auto;\n display: flex;\n align-items: center;\n gap: 2px;\n padding: 0 4px;\n border-left: 1px solid var(--im-meeting-border, rgba(255, 255, 255, 0.08));\n}\n.mini-btn {\n width: 26px;\n height: 100%;\n min-height: 36px;\n border: none;\n background: transparent;\n color: var(--im-meeting-text-secondary, rgba(255, 255, 255, 0.85));\n cursor: pointer;\n border-radius: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n transition:\n background 0.2s,\n color 0.2s;\n}\n.mini-btn:hover {\n background: rgba(255, 255, 255, 0.1);\n color: var(--im-meeting-text-primary, #fff);\n}\n.mini-btn :deep(svg) {\n width: 16px;\n height: 16px;\n}\n.mini-btn.mini-close:hover {\n background: #e53935;\n color: #fff;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { onMounted, onBeforeUnmount, ref, computed, watch } from 'vue'\nimport type { MeetingInviteEvent } from '@quitting/contract'\n// 响铃音频必须走 ESM import,Vite 才会处理资源 pipeline,运行时拿到带 hash 的真实 URL\n// 之前用 new Audio(\"./ring-callee.mp3\") 是相对页面 URL,demo public 目录没这个文件 → 404 → NotSupportedError\nimport ringCalleeUrl from '../sounds/ring-callee.mp3'\n\n// 会议邀请弹框:结构 / 样式严格对齐 design/meeting-invite-dialog.html\n// 调用方传 fromName 把 uid 翻译为业务侧用户名;不传则回退显示 invite.from\n\nconst props = defineProps<{\n invite: MeetingInviteEvent\n conflict?: boolean\n // 邀请者展示名:SDK UI 不知道 uid→name 映射,由调用方(如 demo 的 DEMO_USERS)解析后传入\n fromName?: string\n onClose?: () => void\n}>()\n\nconst emit = defineEmits<{\n (e: 'close'): void\n (e: 'accepted', session: unknown): void\n}>()\n\n// 响铃控制:默认循环播放,可点击「关闭响铃」静音\nconst ringMuted = ref(false)\nlet audioEl: HTMLAudioElement | null = null\n\n// 顶部标题:根据 variant 描述邀请者的动作\n// 邀请者展示名:外部传 fromName 优先;未传时查 SDK 用户目录(client.users),\n// 仍无则回退 invite.from——业务方零配置也能显示姓名\nconst fallbackName = ref('')\nwatch(\n () => props.invite.from,\n async (from) => {\n fallbackName.value = ''\n if (props.fromName || !from) return\n const dir = getClient()?.users\n if (!dir?.resolve) return\n const u = await dir.resolve(from).catch(() => undefined)\n if (u?.name) fallbackName.value = u.name\n },\n { immediate: true },\n)\nconst inviterName = computed(() => props.fromName || fallbackName.value || props.invite.from)\nconst titleText = computed(() => {\n const name = inviterName.value\n const variant = props.invite.variant\n if (variant === 'rejoin') return '重新加入会议'\n if (variant === 'reinvite') return `${name} 再次邀请您入会`\n return `${name}邀请您入会`\n})\n\nfunction startRing() {\n if (audioEl) {\n audioEl.muted = ringMuted.value\n audioEl.currentTime = 0\n audioEl.play().catch((err) => {\n console.warn('[im] 邀请响铃播放失败(可能是 autoplay 策略)', err)\n })\n }\n}\n\nfunction toggleMute() {\n ringMuted.value = !ringMuted.value\n if (audioEl) {\n audioEl.muted = ringMuted.value\n if (!ringMuted.value) {\n audioEl.play().catch(() => {})\n }\n }\n}\n\nfunction getClient() {\n return (window as any).__imClient\n}\n\n// 把后端错误 message 映射成中文提示;解析失败回退通用文案\n// 后端错误关键字参考 server/internal/signaling/meeting.go 各处 errors.New\nfunction friendlyError(err: any): string {\n const msg = String(err?.message || err || '')\n if (msg.includes('meeting_ended') || msg.includes('meeting_not_found')) {\n return '会议已结束'\n }\n if (msg.includes('invite_status_invalid')) {\n return '邀请已失效'\n }\n if (msg.includes('not_invited')) {\n return '您未被邀请到该会议'\n }\n if (msg.includes('kicked')) {\n return '您已被移出该会议'\n }\n // 规则1:多端并发进入,本端是后进端,已被服务端拒绝\n if (msg.includes('conflict') || msg.includes('entered elsewhere')) {\n return '已在其他设备进入,本端邀请已失效'\n }\n // 规则2:已有待处理邀请或已在会话中\n if (msg.includes('busy') || msg.includes('pending')) {\n return '对方正忙,请稍后再试'\n }\n return '网络异常,请稍后重试'\n}\n\nasync function accept() {\n const client = getClient()\n if (!client) return\n if (props.conflict) {\n if (client.call?.currentSession) {\n await client.call.end('user_left').catch(() => {})\n } else if (client.meeting?.currentSession) {\n await client.meeting.leave().catch(() => {})\n }\n }\n try {\n const session = await client.meeting.respond(\n props.invite.meetingId,\n 'accept',\n props.invite.title,\n )\n if (session) {\n emit('accepted', session)\n }\n emit('close')\n } catch (err: any) {\n console.error('[im] 会议接听失败', err)\n // 用户场景:邀请弹窗未及时收到 dissolved 事件(如断线期间),\n // 点击\"加入\"时后端拒绝——必须给明确提示,不能静默关闭\n alert(friendlyError(err))\n emit('close')\n }\n}\n\nasync function reject() {\n const client = getClient()\n // reject 在后端等价于主动离会(status=left),与离会行为一致\n // 失败仅记录日志,不阻塞关闭弹窗(reject 失败不影响他人)\n try {\n await client?.meeting?.respond(props.invite.meetingId, 'reject')\n } catch (err: any) {\n console.warn('[im] 会议拒绝失败', err)\n }\n emit('close')\n}\n\nonMounted(() => {\n // 响铃:复用 ring-callee.mp3(被叫响铃,语义匹配)\n audioEl = new Audio(ringCalleeUrl)\n audioEl.loop = true\n audioEl.preload = 'auto'\n startRing()\n})\n\nonBeforeUnmount(() => {\n if (audioEl) {\n audioEl.pause()\n audioEl = null\n }\n})\n</script>\n\n<template>\n <div class=\"invite-dialog\">\n <div class=\"dialog-title\">{{ titleText }}</div>\n <div class=\"meeting-info\">\n <span class=\"info-label\">会议名:</span>\n <span class=\"info-value\">{{ invite.title }}</span>\n </div>\n <div class=\"dialog-actions\">\n <button class=\"btn btn-ghost\" type=\"button\" @click=\"toggleMute\">\n {{ ringMuted ? '恢复响铃' : '关闭响铃' }}\n </button>\n <button class=\"btn btn-secondary\" type=\"button\" @click=\"reject\">拒绝</button>\n <button class=\"btn btn-primary\" type=\"button\" @click=\"accept\">\n {{ conflict ? '切换' : '加入' }}\n </button>\n </div>\n </div>\n</template>\n\n<style scoped>\n/* 颜色变量:同时兼容深浅背景 */\n.invite-dialog {\n --dialog-bg: rgba(255, 255, 255, 0.92);\n --dialog-bg-dark: rgba(18, 22, 32, 0.92);\n --text-primary: rgba(0, 0, 0, 0.88);\n --text-secondary: rgba(0, 0, 0, 0.55);\n --text-primary-dark: rgba(255, 255, 255, 0.92);\n --text-secondary-dark: rgba(255, 255, 255, 0.55);\n --border: rgba(0, 0, 0, 0.08);\n --border-dark: rgba(255, 255, 255, 0.1);\n --accent: #10b981;\n --accent-hover: #059669;\n --danger: #ef4444;\n --danger-hover: #dc2626;\n\n width: 260px;\n padding: 14px;\n /* 玻璃拟态:背景模糊 + 半透明白色 */\n background: var(--dialog-bg);\n backdrop-filter: blur(20px) saturate(180%);\n -webkit-backdrop-filter: blur(20px) saturate(180%);\n border: 1px solid rgba(255, 255, 255, 0.5);\n border-radius: 16px;\n box-shadow:\n 0 1px 2px rgba(0, 0, 0, 0.02),\n 0 4px 8px rgba(0, 0, 0, 0.04),\n 0 8px 16px rgba(0, 0, 0, 0.06),\n 0 16px 32px rgba(0, 0, 0, 0.08);\n color: var(--text-primary);\n position: relative;\n overflow: hidden;\n}\n\n/* 顶部柔和光条 */\n.invite-dialog::before {\n content: '';\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n height: 1px;\n background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.6), transparent);\n pointer-events: none;\n}\n\n/* 深色背景适配:用 .dark 类切换 */\n.invite-dialog.dark {\n background: var(--dialog-bg-dark);\n border-color: rgba(255, 255, 255, 0.12);\n color: var(--text-primary-dark);\n}\n.invite-dialog.dark .info-label {\n color: var(--text-secondary-dark);\n}\n.invite-dialog.dark .info-value {\n color: var(--text-primary-dark);\n}\n.invite-dialog.dark .btn-secondary {\n border-color: var(--border-dark);\n color: rgba(255, 255, 255, 0.8);\n}\n.invite-dialog.dark .btn-ghost {\n color: rgba(255, 255, 255, 0.5);\n}\n.invite-dialog.dark .btn-secondary:hover {\n background: rgba(255, 255, 255, 0.08);\n border-color: rgba(255, 255, 255, 0.2);\n}\n.invite-dialog.dark .btn-secondary.danger {\n border-color: rgba(239, 68, 68, 0.5);\n color: rgba(255, 255, 255, 0.9);\n}\n.invite-dialog.dark .btn-secondary.danger:hover {\n background: rgba(239, 68, 68, 0.15);\n border-color: rgba(239, 68, 68, 0.7);\n}\n\n.dialog-title {\n font-size: 13px;\n font-weight: 600;\n line-height: 1.4;\n margin-bottom: 8px;\n}\n\n.meeting-info {\n display: flex;\n align-items: baseline;\n gap: 4px;\n margin-bottom: 12px;\n font-size: 12px;\n}\n\n.info-label {\n color: var(--text-secondary);\n white-space: nowrap;\n}\n\n.info-value {\n color: var(--text-primary);\n font-weight: 600;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.dialog-actions {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 6px;\n}\n\n.btn {\n height: 26px;\n padding: 0 10px;\n border: 1px solid transparent;\n background: transparent;\n color: var(--text-primary);\n font-size: 12px;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.15s ease;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 8px;\n}\n\n.btn:hover {\n color: var(--text-primary);\n}\n\n.btn-primary {\n background: var(--accent);\n color: #fff;\n box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3);\n}\n.btn-primary:hover {\n background: var(--accent-hover);\n box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);\n transform: translateY(-1px);\n}\n\n.btn-secondary {\n border-color: var(--border);\n color: var(--text-primary);\n}\n.btn-secondary:hover {\n background: rgba(0, 0, 0, 0.04);\n border-color: rgba(0, 0, 0, 0.12);\n}\n\n.btn-ghost {\n padding: 0 6px;\n color: var(--text-secondary);\n}\n.btn-ghost:hover {\n color: var(--text-primary);\n background: rgba(0, 0, 0, 0.04);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { onMounted, onBeforeUnmount, ref, computed, watch } from 'vue'\nimport type { MeetingInviteEvent } from '@quitting/contract'\n// 响铃音频必须走 ESM import,Vite 才会处理资源 pipeline,运行时拿到带 hash 的真实 URL\n// 之前用 new Audio(\"./ring-callee.mp3\") 是相对页面 URL,demo public 目录没这个文件 → 404 → NotSupportedError\nimport ringCalleeUrl from '../sounds/ring-callee.mp3'\n\n// 会议邀请弹框:结构 / 样式严格对齐 design/meeting-invite-dialog.html\n// 调用方传 fromName 把 uid 翻译为业务侧用户名;不传则回退显示 invite.from\n\nconst props = defineProps<{\n invite: MeetingInviteEvent\n conflict?: boolean\n // 邀请者展示名:SDK UI 不知道 uid→name 映射,由调用方(如 demo 的 DEMO_USERS)解析后传入\n fromName?: string\n onClose?: () => void\n}>()\n\nconst emit = defineEmits<{\n (e: 'close'): void\n (e: 'accepted', session: unknown): void\n}>()\n\n// 响铃控制:默认循环播放,可点击「关闭响铃」静音\nconst ringMuted = ref(false)\nlet audioEl: HTMLAudioElement | null = null\n\n// 顶部标题:根据 variant 描述邀请者的动作\n// 邀请者展示名:外部传 fromName 优先;未传时查 SDK 用户目录(client.users),\n// 仍无则回退 invite.from——业务方零配置也能显示姓名\nconst fallbackName = ref('')\nwatch(\n () => props.invite.from,\n async (from) => {\n fallbackName.value = ''\n if (props.fromName || !from) return\n const dir = getClient()?.users\n if (!dir?.resolve) return\n const u = await dir.resolve(from).catch(() => undefined)\n if (u?.name) fallbackName.value = u.name\n },\n { immediate: true },\n)\nconst inviterName = computed(() => props.fromName || fallbackName.value || props.invite.from)\nconst titleText = computed(() => {\n const name = inviterName.value\n const variant = props.invite.variant\n if (variant === 'rejoin') return '重新加入会议'\n if (variant === 'reinvite') return `${name} 再次邀请您入会`\n return `${name}邀请您入会`\n})\n\nfunction startRing() {\n if (audioEl) {\n audioEl.muted = ringMuted.value\n audioEl.currentTime = 0\n audioEl.play().catch((err) => {\n console.warn('[im] 邀请响铃播放失败(可能是 autoplay 策略)', err)\n })\n }\n}\n\nfunction toggleMute() {\n ringMuted.value = !ringMuted.value\n if (audioEl) {\n audioEl.muted = ringMuted.value\n if (!ringMuted.value) {\n audioEl.play().catch(() => {})\n }\n }\n}\n\nfunction getClient() {\n return (window as any).__imClient\n}\n\n// 把后端错误 message 映射成中文提示;解析失败回退通用文案\n// 后端错误关键字参考 server/internal/signaling/meeting.go 各处 errors.New\nfunction friendlyError(err: any): string {\n const msg = String(err?.message || err || '')\n if (msg.includes('meeting_ended') || msg.includes('meeting_not_found')) {\n return '会议已结束'\n }\n if (msg.includes('invite_status_invalid')) {\n return '邀请已失效'\n }\n if (msg.includes('not_invited')) {\n return '您未被邀请到该会议'\n }\n if (msg.includes('kicked')) {\n return '您已被移出该会议'\n }\n // 规则1:多端并发进入,本端是后进端,已被服务端拒绝\n if (msg.includes('conflict') || msg.includes('entered elsewhere')) {\n return '已在其他设备进入,本端邀请已失效'\n }\n // 规则2:已有待处理邀请或已在会话中\n if (msg.includes('busy') || msg.includes('pending')) {\n return '对方正忙,请稍后再试'\n }\n return '网络异常,请稍后重试'\n}\n\nasync function accept() {\n const client = getClient()\n if (!client) return\n if (props.conflict) {\n if (client.call?.currentSession) {\n await client.call.end('user_left').catch(() => {})\n } else if (client.meeting?.currentSession) {\n await client.meeting.leave().catch(() => {})\n }\n }\n try {\n const session = await client.meeting.respond(\n props.invite.meetingId,\n 'accept',\n props.invite.title,\n )\n if (session) {\n emit('accepted', session)\n }\n emit('close')\n } catch (err: any) {\n console.error('[im] 会议接听失败', err)\n // 用户场景:邀请弹窗未及时收到 dissolved 事件(如断线期间),\n // 点击\"加入\"时后端拒绝——必须给明确提示,不能静默关闭\n alert(friendlyError(err))\n emit('close')\n }\n}\n\nasync function reject() {\n const client = getClient()\n // reject 在后端等价于主动离会(status=left),与离会行为一致\n // 失败仅记录日志,不阻塞关闭弹窗(reject 失败不影响他人)\n try {\n await client?.meeting?.respond(props.invite.meetingId, 'reject')\n } catch (err: any) {\n console.warn('[im] 会议拒绝失败', err)\n }\n emit('close')\n}\n\nonMounted(() => {\n // 响铃:复用 ring-callee.mp3(被叫响铃,语义匹配)\n audioEl = new Audio(ringCalleeUrl)\n audioEl.loop = true\n audioEl.preload = 'auto'\n startRing()\n})\n\nonBeforeUnmount(() => {\n if (audioEl) {\n audioEl.pause()\n audioEl = null\n }\n})\n</script>\n\n<template>\n <div class=\"invite-dialog\">\n <div class=\"dialog-title\">{{ titleText }}</div>\n <div class=\"meeting-info\">\n <span class=\"info-label\">会议名:</span>\n <span class=\"info-value\">{{ invite.title }}</span>\n </div>\n <div class=\"dialog-actions\">\n <button class=\"btn btn-ghost\" type=\"button\" @click=\"toggleMute\">\n {{ ringMuted ? '恢复响铃' : '关闭响铃' }}\n </button>\n <button class=\"btn btn-secondary\" type=\"button\" @click=\"reject\">拒绝</button>\n <button class=\"btn btn-primary\" type=\"button\" @click=\"accept\">\n {{ conflict ? '切换' : '加入' }}\n </button>\n </div>\n </div>\n</template>\n\n<style scoped>\n/* 颜色变量:同时兼容深浅背景 */\n.invite-dialog {\n --dialog-bg: rgba(255, 255, 255, 0.92);\n --dialog-bg-dark: rgba(18, 22, 32, 0.92);\n --text-primary: rgba(0, 0, 0, 0.88);\n --text-secondary: rgba(0, 0, 0, 0.55);\n --text-primary-dark: rgba(255, 255, 255, 0.92);\n --text-secondary-dark: rgba(255, 255, 255, 0.55);\n --border: rgba(0, 0, 0, 0.08);\n --border-dark: rgba(255, 255, 255, 0.1);\n --accent: #10b981;\n --accent-hover: #059669;\n --danger: #ef4444;\n --danger-hover: #dc2626;\n\n width: 260px;\n padding: 14px;\n /* 玻璃拟态:背景模糊 + 半透明白色 */\n background: var(--dialog-bg);\n backdrop-filter: blur(20px) saturate(180%);\n -webkit-backdrop-filter: blur(20px) saturate(180%);\n border: 1px solid rgba(255, 255, 255, 0.5);\n border-radius: 16px;\n box-shadow:\n 0 1px 2px rgba(0, 0, 0, 0.02),\n 0 4px 8px rgba(0, 0, 0, 0.04),\n 0 8px 16px rgba(0, 0, 0, 0.06),\n 0 16px 32px rgba(0, 0, 0, 0.08);\n color: var(--text-primary);\n position: relative;\n overflow: hidden;\n}\n\n/* 顶部柔和光条 */\n.invite-dialog::before {\n content: '';\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n height: 1px;\n background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.6), transparent);\n pointer-events: none;\n}\n\n/* 深色背景适配:用 .dark 类切换 */\n.invite-dialog.dark {\n background: var(--dialog-bg-dark);\n border-color: rgba(255, 255, 255, 0.12);\n color: var(--text-primary-dark);\n}\n.invite-dialog.dark .info-label {\n color: var(--text-secondary-dark);\n}\n.invite-dialog.dark .info-value {\n color: var(--text-primary-dark);\n}\n.invite-dialog.dark .btn-secondary {\n border-color: var(--border-dark);\n color: rgba(255, 255, 255, 0.8);\n}\n.invite-dialog.dark .btn-ghost {\n color: rgba(255, 255, 255, 0.5);\n}\n.invite-dialog.dark .btn-secondary:hover {\n background: rgba(255, 255, 255, 0.08);\n border-color: rgba(255, 255, 255, 0.2);\n}\n.invite-dialog.dark .btn-secondary.danger {\n border-color: rgba(239, 68, 68, 0.5);\n color: rgba(255, 255, 255, 0.9);\n}\n.invite-dialog.dark .btn-secondary.danger:hover {\n background: rgba(239, 68, 68, 0.15);\n border-color: rgba(239, 68, 68, 0.7);\n}\n\n.dialog-title {\n font-size: 13px;\n font-weight: 600;\n line-height: 1.4;\n margin-bottom: 8px;\n}\n\n.meeting-info {\n display: flex;\n align-items: baseline;\n gap: 4px;\n margin-bottom: 12px;\n font-size: 12px;\n}\n\n.info-label {\n color: var(--text-secondary);\n white-space: nowrap;\n}\n\n.info-value {\n color: var(--text-primary);\n font-weight: 600;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.dialog-actions {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 6px;\n}\n\n.btn {\n height: 26px;\n padding: 0 10px;\n border: 1px solid transparent;\n background: transparent;\n color: var(--text-primary);\n font-size: 12px;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.15s ease;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 8px;\n}\n\n.btn:hover {\n color: var(--text-primary);\n}\n\n.btn-primary {\n background: var(--accent);\n color: #fff;\n box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3);\n}\n.btn-primary:hover {\n background: var(--accent-hover);\n box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);\n transform: translateY(-1px);\n}\n\n.btn-secondary {\n border-color: var(--border);\n color: var(--text-primary);\n}\n.btn-secondary:hover {\n background: rgba(0, 0, 0, 0.04);\n border-color: rgba(0, 0, 0, 0.12);\n}\n\n.btn-ghost {\n padding: 0 6px;\n color: var(--text-secondary);\n}\n.btn-ghost:hover {\n color: var(--text-primary);\n background: rgba(0, 0, 0, 0.04);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { ref, computed, onMounted } from 'vue'\nimport type { ImClient } from '@quitting/core'\nimport { getImClient } from '../client-access'\nimport UserPickerList from './UserPickerList.vue'\nimport type { PickerUser } from '../types'\n\n// 发起会议弹窗:会议名输入 + UserPickerList + 底部按钮\n// 纯 UI 组件——confirm 把 (uids, title) 抛给调用方(global-methods 的 ImUI.meeting 接管建会)\n// 数据源默认 client.users.list();业务方可传 loader 完全自定义\nconst props = defineProps<{\n myUid?: string\n loader?: () => Promise<PickerUser[]>\n defaultTitle?: string\n onClose?: () => void\n}>()\n\nconst emit = defineEmits<{\n (e: 'confirm', selectedUids: string[], title: string): void\n (e: 'cancel'): void\n (e: 'close'): void\n}>()\n\nconst slots = defineSlots<{\n 'user-item'?(scope: { user: PickerUser; selected: boolean; toggle: () => void }): unknown\n}>()\n\nconst title = ref(props.defaultTitle || '')\nconst users = ref<PickerUser[]>([])\nconst selected = ref<string[]>([])\nconst loading = ref(true)\n\nconst trimmedTitle = computed(() => title.value.trim())\nconst canConfirm = computed(() => trimmedTitle.value.length > 0 && selected.value.length > 0)\n\nfunction getClient(): ImClient | undefined {\n return getImClient()\n}\n\nasync function loadDefault(): Promise<PickerUser[]> {\n const client = getClient()\n if (!client?.users) return []\n const list = await client.users.list()\n // 自己不在候选里:resolve 支持内部 uid 反查(client.uid 是 UUID)\n const me = await client.users.resolve(client.uid).catch(() => undefined)\n const exclude = new Set<string>()\n if (me) exclude.add(me.externalId)\n if (props.myUid) exclude.add(props.myUid)\n return list\n .filter((u) => !exclude.has(u.externalId))\n .map((u) => ({ uid: u.externalId, name: u.name, dept: u.deptName || undefined }))\n}\n\nonMounted(async () => {\n try {\n users.value = props.loader ? await props.loader() : await loadDefault()\n } catch (err) {\n console.warn('[im] 选人列表加载失败', err)\n users.value = []\n } finally {\n loading.value = false\n }\n})\n\nfunction confirm() {\n if (!canConfirm.value) return\n // 标题不以「会议」结尾则补全(保持与 demo 旧行为一致)\n const t = trimmedTitle.value\n emit('confirm', [...selected.value], t.endsWith('会议') ? t : `${t}会议`)\n emit('close')\n}\n\nfunction cancel() {\n emit('cancel')\n emit('close')\n}\n</script>\n\n<template>\n <div class=\"im-create-mask\" @click.self=\"cancel\">\n <div class=\"im-create-dialog\">\n <div class=\"im-create-header\">\n <h2 class=\"im-create-title\">发起会议</h2>\n <button class=\"im-create-close\" type=\"button\" aria-label=\"关闭\" @click=\"cancel\">✕</button>\n </div>\n\n <div class=\"im-create-row\">\n <label class=\"im-create-label\">会议名称</label>\n <input\n v-model=\"title\"\n class=\"im-create-input\"\n type=\"text\"\n placeholder=\"请输入会议名称\"\n maxlength=\"40\"\n />\n </div>\n\n <div class=\"im-create-row im-create-row-list\">\n <UserPickerList\n v-model:selected=\"selected\"\n :users=\"users\"\n :loading=\"loading\"\n empty-text=\"暂无其他用户\"\n >\n <template v-if=\"slots['user-item']\" #user-item=\"scope\">\n <slot name=\"user-item\" v-bind=\"scope\" />\n </template>\n </UserPickerList>\n </div>\n\n <div class=\"im-create-footer\">\n <button class=\"im-create-btn cancel\" type=\"button\" @click=\"cancel\">取消</button>\n <button\n class=\"im-create-btn confirm\"\n type=\"button\"\n :disabled=\"!canConfirm\"\n @click=\"confirm\"\n >\n 发起会议\n </button>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.im-create-mask {\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.45);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 100001;\n}\n.im-create-dialog {\n /* 主题变量(与 UserPickerList 一致;:root 覆盖一处即可全局生效) */\n --im-picker-primary: #667eea;\n --im-picker-primary-hover: #5568d3;\n\n width: 480px;\n max-height: 80vh;\n background: #fff;\n border-radius: 12px;\n box-shadow: 0 16px 48px rgba(0, 0, 0, 0.2);\n display: flex;\n flex-direction: column;\n overflow: hidden;\n}\n.im-create-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 16px 20px;\n border-bottom: 1px solid #f0f0f0;\n}\n.im-create-title {\n margin: 0;\n font-size: 16px;\n font-weight: 600;\n color: #1a1a1a;\n}\n.im-create-close {\n width: 28px;\n height: 28px;\n border: none;\n background: transparent;\n color: #999;\n font-size: 16px;\n cursor: pointer;\n border-radius: 4px;\n}\n.im-create-close:hover {\n background: #f5f7fa;\n color: #1a1a1a;\n}\n.im-create-row {\n padding: 14px 20px 0;\n}\n.im-create-label {\n display: block;\n font-size: 12px;\n color: #666;\n margin-bottom: 8px;\n}\n.im-create-input {\n width: 100%;\n padding: 8px 12px;\n border: 1px solid #ddd;\n border-radius: 6px;\n font-size: 14px;\n outline: none;\n transition: border-color 0.2s;\n}\n.im-create-input:focus {\n border-color: var(--im-picker-primary);\n}\n.im-create-row-list {\n flex: 1;\n min-height: 0;\n display: flex;\n flex-direction: column;\n padding-bottom: 14px;\n}\n.im-create-row-list .im-picker-list {\n flex: 1;\n min-height: 0;\n}\n.im-create-row-list :deep(.im-picker-items) {\n max-height: 240px;\n}\n.im-create-footer {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n padding: 14px 20px;\n border-top: 1px solid #f0f0f0;\n}\n.im-create-btn {\n padding: 8px 18px;\n font-size: 13px;\n border-radius: 6px;\n cursor: pointer;\n transition: all 0.2s;\n border: none;\n}\n.im-create-btn.cancel {\n background: #f5f7fa;\n color: #666;\n}\n.im-create-btn.cancel:hover {\n background: #eef0f4;\n}\n.im-create-btn.confirm {\n background: var(--im-picker-primary);\n color: #fff;\n}\n.im-create-btn.confirm:hover:not(:disabled) {\n background: var(--im-picker-primary-hover);\n}\n.im-create-btn.confirm:disabled {\n background: #bbb;\n cursor: not-allowed;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { ref, computed, onMounted } from 'vue'\nimport type { ImClient } from '@quitting/core'\nimport { getImClient } from '../client-access'\nimport UserPickerList from './UserPickerList.vue'\nimport type { PickerUser } from '../types'\n\n// 发起会议弹窗:会议名输入 + UserPickerList + 底部按钮\n// 纯 UI 组件——confirm 把 (uids, title) 抛给调用方(global-methods 的 ImUI.meeting 接管建会)\n// 数据源默认 client.users.list();业务方可传 loader 完全自定义\nconst props = defineProps<{\n myUid?: string\n loader?: () => Promise<PickerUser[]>\n defaultTitle?: string\n onClose?: () => void\n}>()\n\nconst emit = defineEmits<{\n (e: 'confirm', selectedUids: string[], title: string): void\n (e: 'cancel'): void\n (e: 'close'): void\n}>()\n\nconst slots = defineSlots<{\n 'user-item'?(scope: { user: PickerUser; selected: boolean; toggle: () => void }): unknown\n}>()\n\nconst title = ref(props.defaultTitle || '')\nconst users = ref<PickerUser[]>([])\nconst selected = ref<string[]>([])\nconst loading = ref(true)\n\nconst trimmedTitle = computed(() => title.value.trim())\nconst canConfirm = computed(() => trimmedTitle.value.length > 0 && selected.value.length > 0)\n\nfunction getClient(): ImClient | undefined {\n return getImClient()\n}\n\nasync function loadDefault(): Promise<PickerUser[]> {\n const client = getClient()\n if (!client?.users) return []\n const list = await client.users.list()\n // 自己不在候选里:resolve 支持内部 uid 反查(client.uid 是 UUID)\n const me = await client.users.resolve(client.uid).catch(() => undefined)\n const exclude = new Set<string>()\n if (me) exclude.add(me.externalId)\n if (props.myUid) exclude.add(props.myUid)\n return list\n .filter((u) => !exclude.has(u.externalId))\n .map((u) => ({ uid: u.externalId, name: u.name, dept: u.deptName || undefined }))\n}\n\nonMounted(async () => {\n try {\n users.value = props.loader ? await props.loader() : await loadDefault()\n } catch (err) {\n console.warn('[im] 选人列表加载失败', err)\n users.value = []\n } finally {\n loading.value = false\n }\n})\n\nfunction confirm() {\n if (!canConfirm.value) return\n // 标题不以「会议」结尾则补全(保持与 demo 旧行为一致)\n const t = trimmedTitle.value\n emit('confirm', [...selected.value], t.endsWith('会议') ? t : `${t}会议`)\n emit('close')\n}\n\nfunction cancel() {\n emit('cancel')\n emit('close')\n}\n</script>\n\n<template>\n <div class=\"im-create-mask\" @click.self=\"cancel\">\n <div class=\"im-create-dialog\">\n <div class=\"im-create-header\">\n <h2 class=\"im-create-title\">发起会议</h2>\n <button class=\"im-create-close\" type=\"button\" aria-label=\"关闭\" @click=\"cancel\">✕</button>\n </div>\n\n <div class=\"im-create-row\">\n <label class=\"im-create-label\">会议名称</label>\n <input\n v-model=\"title\"\n class=\"im-create-input\"\n type=\"text\"\n placeholder=\"请输入会议名称\"\n maxlength=\"40\"\n />\n </div>\n\n <div class=\"im-create-row im-create-row-list\">\n <UserPickerList\n v-model:selected=\"selected\"\n :users=\"users\"\n :loading=\"loading\"\n empty-text=\"暂无其他用户\"\n >\n <template v-if=\"slots['user-item']\" #user-item=\"scope\">\n <slot name=\"user-item\" v-bind=\"scope\" />\n </template>\n </UserPickerList>\n </div>\n\n <div class=\"im-create-footer\">\n <button class=\"im-create-btn cancel\" type=\"button\" @click=\"cancel\">取消</button>\n <button\n class=\"im-create-btn confirm\"\n type=\"button\"\n :disabled=\"!canConfirm\"\n @click=\"confirm\"\n >\n 发起会议\n </button>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.im-create-mask {\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.45);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 100001;\n}\n.im-create-dialog {\n /* 主题变量(与 UserPickerList 一致;:root 覆盖一处即可全局生效) */\n --im-picker-primary: #667eea;\n --im-picker-primary-hover: #5568d3;\n\n width: 480px;\n max-height: 80vh;\n background: #fff;\n border-radius: 12px;\n box-shadow: 0 16px 48px rgba(0, 0, 0, 0.2);\n display: flex;\n flex-direction: column;\n overflow: hidden;\n}\n.im-create-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 16px 20px;\n border-bottom: 1px solid #f0f0f0;\n}\n.im-create-title {\n margin: 0;\n font-size: 16px;\n font-weight: 600;\n color: #1a1a1a;\n}\n.im-create-close {\n width: 28px;\n height: 28px;\n border: none;\n background: transparent;\n color: #999;\n font-size: 16px;\n cursor: pointer;\n border-radius: 4px;\n}\n.im-create-close:hover {\n background: #f5f7fa;\n color: #1a1a1a;\n}\n.im-create-row {\n padding: 14px 20px 0;\n}\n.im-create-label {\n display: block;\n font-size: 12px;\n color: #666;\n margin-bottom: 8px;\n}\n.im-create-input {\n width: 100%;\n padding: 8px 12px;\n border: 1px solid #ddd;\n border-radius: 6px;\n font-size: 14px;\n outline: none;\n transition: border-color 0.2s;\n}\n.im-create-input:focus {\n border-color: var(--im-picker-primary);\n}\n.im-create-row-list {\n flex: 1;\n min-height: 0;\n display: flex;\n flex-direction: column;\n padding-bottom: 14px;\n}\n.im-create-row-list .im-picker-list {\n flex: 1;\n min-height: 0;\n}\n.im-create-row-list :deep(.im-picker-items) {\n max-height: 240px;\n}\n.im-create-footer {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n padding: 14px 20px;\n border-top: 1px solid #f0f0f0;\n}\n.im-create-btn {\n padding: 8px 18px;\n font-size: 13px;\n border-radius: 6px;\n cursor: pointer;\n transition: all 0.2s;\n border: none;\n}\n.im-create-btn.cancel {\n background: #f5f7fa;\n color: #666;\n}\n.im-create-btn.cancel:hover {\n background: #eef0f4;\n}\n.im-create-btn.confirm {\n background: var(--im-picker-primary);\n color: #fff;\n}\n.im-create-btn.confirm:hover:not(:disabled) {\n background: var(--im-picker-primary-hover);\n}\n.im-create-btn.confirm:disabled {\n background: #bbb;\n cursor: not-allowed;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { ref, computed, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'\nimport type { ImClient } from '@quitting/core'\nimport type { Conversation, Message } from '@quitting/contract'\nimport { getImClient } from '../client-access'\n\n// 聊天面板:与单个 peer 的 1:1 对话。\n// 两种布局模式:\n// 浮窗(默认):fixed 右下角,ImUI.chat() 弹出用\n// embedded:随容器铺满(position:static + 100%),业务方嵌入自己的布局用\n// 能力:历史拉取 + dialog 通道订阅 + 乐观渲染 + ack + 撤回 + 已读上报 +\n// Enter 发送(IME 安全)/Shift+Enter 换行/输入自动增高/表情面板/时间分隔线\n// 自定义:#message 插槽(自定义气泡)/ #input 插槽(自定义输入区)\nconst props = defineProps<{\n to: string\n // 对端展示名;未传时查 SDK 用户目录,仍无则显示 to 本身\n peerName?: string\n // 我的 externalId(左右气泡归属判定用);未传时自动查用户目录解析\n myUid?: string\n embedded?: boolean\n onClose?: () => void\n}>()\n\nconst emit = defineEmits<{ (e: 'close'): void }>()\n\ndefineSlots<{\n // 自定义消息气泡渲染(scope: message 为完整 Message,含 status)\n message?(scope: { message: Message; mine: boolean }): unknown\n // 自定义输入区(scope: send 为发送函数,业务方自管输入状态时可忽略)\n input?(scope: { send: (text: string) => void }): unknown\n}>()\n\nconst messages = ref<Message[]>([])\nconst input = ref('')\nconst conversationId = ref('')\nconst lastReadSeq = ref(0)\nconst listEl = ref<HTMLDivElement | null>(null)\nconst textareaEl = ref<HTMLTextAreaElement | null>(null)\nconst unsubscribers: Array<() => void> = []\n// 表情面板开关\nconst emojiPickerOpen = ref(false)\n// 常用 emoji 子集;业务方要更全的可通过 #input 插槽接 emoji-mart 等\nconst emojis = [\n '😀',\n '😁',\n '😂',\n '🤣',\n '😊',\n '😍',\n '🥰',\n '😘',\n '😎',\n '🤔',\n '😴',\n '😭',\n '😱',\n '🤯',\n '🤗',\n '👍',\n '👎',\n '👏',\n '🙏',\n '💪',\n '🤝',\n '✌️',\n '🤞',\n '❤️',\n '💔',\n '💕',\n '💯',\n '🔥',\n '⭐',\n '🎉',\n '🎁',\n '🚀',\n]\n\nfunction getClient(): ImClient | undefined {\n return getImClient()\n}\n\n// 对端展示名:外部传 peerName 优先;未传查用户目录\nconst fallbackName = ref('')\nwatch(\n () => props.to,\n async (to) => {\n fallbackName.value = ''\n if (props.peerName || !to) return\n const dir = getClient()?.users\n if (!dir?.resolve) return\n const u = await dir.resolve(to).catch(() => undefined)\n if (u?.name) fallbackName.value = u.name\n },\n { immediate: true },\n)\nconst displayPeerName = computed(() => props.peerName || fallbackName.value || props.to)\n\n// 解析与 to(externalId)的会话 ID:反查 peer UUID 后在会话列表里匹配 dialog 两端\n// (后端 dialogUserA/B 存的是内部 UUID,见 server signaling/message.go GetOrCreateDialog)\n// 后端会话在首条消息发送时才创建,没发过消息时返回空串\nasync function resolveConversation(client: ImClient): Promise<string> {\n try {\n const resp = await client.transport.usersByExternal([props.to])\n const peerUUID = resp.users[props.to]?.uid\n if (!peerUUID) return ''\n const convs = (await client.message.listConversations()) || []\n const me = client.uid\n const conv = convs.find(\n (c: Conversation) =>\n (c.dialogUserA === me && c.dialogUserB === peerUUID) ||\n (c.dialogUserA === peerUUID && c.dialogUserB === me),\n )\n return conv?.id || ''\n } catch (err) {\n console.warn('[im] 解析会话 ID 失败', err)\n return ''\n }\n}\n\n// dialog 通道 message publication 的原始 payload(realtime 透传给 'message' 事件,\n// 字段与 contract Message 不同:msgId/from(externalId)/msgType/ts,见后端 signaling/message.go)\ninterface DialogPublication {\n msgId?: string\n conversationId?: string\n seq?: number\n from?: string\n msgType?: string\n body?: string\n ts?: number\n}\n\n// 我的 externalId:历史消息的 senderId 是 externalId(后端 History 已转换),\n// 左右归属判定必须用它而非内部 uid。prop 优先,缺省查用户目录反查\nconst myExternalId = ref(props.myUid || '')\n\nonMounted(async () => {\n const client = getClient()\n if (!client) return\n\n if (!props.myUid) {\n const me = await client.users?.resolve(client.uid).catch(() => undefined)\n if (me?.externalId) myExternalId.value = me.externalId\n }\n // 订阅与 peer 的 dialog 通道——SDK 不自动订阅,不订阅则对端回复永远收不到\n client.message\n .openDialog(props.to)\n .catch((err: unknown) => console.warn('[im] openDialog 失败', err))\n\n // 已有会话则直接拉历史;没有(首次聊天)时发送成功后再解析\n conversationId.value = await resolveConversation(client)\n if (conversationId.value) {\n await loadHistory()\n }\n\n unsubscribers.push(\n client.on('message', (raw) => {\n const p = raw as unknown as DialogPublication\n // 会话过滤:conversationId 已知时精确匹配;未知(首次聊天/解析中)用 from 兜底,\n // 否则首条对端消息(自己还没发过、会话还没建)会被丢弃\n const belongs = conversationId.value\n ? p.conversationId === conversationId.value\n : p.from === props.to\n if (!belongs) return\n // 去重:自己发的消息 send() 已乐观插入并被回填真实 msgId\n if (!p.msgId || messages.value.some((m) => m.id === p.msgId)) return\n messages.value.push({\n id: p.msgId,\n appId: '',\n conversationId: p.conversationId || conversationId.value,\n seq: Number(p.seq) || 0,\n // 注意:来自 publication 的 senderId 是 externalId;自己的乐观消息也统一用\n // externalId(myExternalId),与后端 History 口径一致,isMine 判定两侧统一\n senderId: p.from || '',\n clientMid: p.msgId,\n type: (p.msgType as Message['type']) || 'text',\n body: p.body,\n status: 'sent',\n createdAt: p.ts || Date.now(),\n })\n scrollToBottom()\n // 自动 ACK(消息必达三保险的客户端侧)\n client.message.ack(p.msgId).catch(() => {})\n }),\n )\n unsubscribers.push(\n client.on('message:revoked', (e: { messageId: string }) => {\n const m = messages.value.find((x) => x.id === e.messageId)\n if (m) m.status = 'revoked'\n }),\n )\n})\n\nonBeforeUnmount(() => {\n unsubscribers.forEach((fn) => fn())\n getClient()?.message?.closeDialog?.(props.to)\n})\n\nasync function send() {\n const body = input.value.trim()\n if (!body) return\n input.value = ''\n resetTextareaHeight()\n emojiPickerOpen.value = false\n await sendText(body)\n}\n\n// #input 插槽的 scope.send:自定义输入区可调它发送任意文本\nasync function sendText(body: string) {\n const client = getClient()\n if (!client || !body) return\n // 乐观渲染:先本地加一条 status=sending(senderId 用 externalId,与历史消息口径一致)\n const tempId = 'temp-' + Date.now()\n messages.value.push({\n id: tempId,\n appId: '',\n conversationId: conversationId.value,\n seq: 0,\n senderId: myExternalId.value || client.uid,\n clientMid: tempId,\n type: 'text',\n body,\n status: 'sending',\n createdAt: Date.now(),\n })\n scrollToBottom()\n try {\n const resp = await client.message.send({ to: props.to, type: 'text', body })\n // 更新本地消息为已发送\n const m = messages.value.find((x) => x.id === tempId)\n if (m) {\n m.id = resp.msgId\n m.seq = resp.seq\n m.status = 'sent'\n }\n lastReadSeq.value = resp.seq\n // 首条消息发送后服务端才建会话——此时解析 conversationId 供后续收消息过滤\n if (!conversationId.value) {\n conversationId.value = await resolveConversation(client)\n if (m) m.conversationId = conversationId.value\n }\n } catch (err) {\n const m = messages.value.find((x) => x.id === tempId)\n if (m) m.status = 'failed'\n }\n}\n\nasync function loadHistory() {\n const client = getClient()\n if (!client || !conversationId.value) return\n const msgs = await client.message.history(conversationId.value, 0, 50)\n // 按 seq 升序渲染(后端升序返回,这里兜底排序保证展示稳定)\n messages.value = [...msgs].sort((a, b) => a.seq - b.seq)\n if (msgs.length > 0) {\n lastReadSeq.value = msgs[msgs.length - 1].seq\n await client.message.markRead(conversationId.value, lastReadSeq.value)\n }\n scrollToBottom()\n}\n\nfunction scrollToBottom() {\n nextTick(() => {\n if (listEl.value) listEl.value.scrollTop = listEl.value.scrollHeight\n })\n}\n\n// ===== 输入区交互 =====\n\n// textarea 自动增高:最大高度 96px(约 4 行)\nfunction autoResize() {\n const el = textareaEl.value\n if (!el) return\n el.style.height = 'auto'\n el.style.height = Math.min(el.scrollHeight, 96) + 'px'\n}\n\nfunction resetTextareaHeight() {\n if (textareaEl.value) {\n textareaEl.value.style.height = 'auto'\n }\n}\n\n// Enter 发送 / Shift+Enter 换行;IME 输入法 composing 状态(中文中间态)回车不触发发送\nfunction onInputKeyDown(e: KeyboardEvent) {\n if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) {\n e.preventDefault()\n send()\n }\n}\n\n// 表情插入到光标位置(避免用户在中间编辑时被推到末尾)\nfunction insertEmoji(emoji: string) {\n const el = textareaEl.value\n if (!el) {\n input.value += emoji\n return\n }\n const start = el.selectionStart ?? input.value.length\n const end = el.selectionEnd ?? input.value.length\n input.value = input.value.slice(0, start) + emoji + input.value.slice(end)\n nextTick(() => {\n el.focus()\n const pos = start + emoji.length\n el.setSelectionRange(pos, pos)\n autoResize()\n })\n}\n\n// ===== 渲染辅助 =====\n\nconst sortedMessages = computed(() => [...messages.value].sort((a, b) => a.seq - b.seq))\n\n// 时间分隔线:相邻消息间隔 > 5 分钟插入分隔元素\ntype FlatItem =\n | { type: 'time'; ts: number; key: string }\n | { type: 'msg'; msg: Message; key: string }\n\nconst flatList = computed<FlatItem[]>(() => {\n const list: FlatItem[] = []\n const sorted = sortedMessages.value\n const GAP_MS = 5 * 60 * 1000\n let lastTs = 0\n for (const m of sorted) {\n if (m.createdAt - lastTs > GAP_MS || lastTs === 0) {\n list.push({ type: 'time', ts: m.createdAt, key: `t-${m.id}` })\n }\n list.push({ type: 'msg', msg: m, key: m.id })\n lastTs = m.createdAt\n }\n return list\n})\n\nfunction fmtTime(ts: number): string {\n const d = new Date(ts)\n const hh = d.getHours().toString().padStart(2, '0')\n const mm = d.getMinutes().toString().padStart(2, '0')\n return `${hh}:${mm}`\n}\n\n// 分隔线完整时间:今天 HH:mm;昨天 HH:mm;更早 M月D日 HH:mm\nfunction fmtTimeFull(ts: number): string {\n const d = new Date(ts)\n const now = new Date()\n const hm = fmtTime(ts)\n if (d.toDateString() === now.toDateString()) return hm\n const yest = new Date(now)\n yest.setDate(now.getDate() - 1)\n if (d.toDateString() === yest.toDateString()) return `昨天 ${hm}`\n return `${d.getMonth() + 1}月${d.getDate()}日 ${hm}`\n}\n\nfunction isMine(m: Message): boolean {\n // 统一按 externalId 判定(历史消息/回推消息的 senderId 均为 externalId)\n const me = myExternalId.value\n return !!me && m.senderId === me\n}\n</script>\n\n<template>\n <div :class=\"['im-chat-panel', { 'im-chat-panel--embedded': embedded }]\">\n <div class=\"im-chat-header\">\n <span class=\"im-chat-name\">{{ displayPeerName }}</span>\n <button v-if=\"!embedded\" class=\"im-chat-close\" @click=\"emit('close')\">×</button>\n </div>\n <div ref=\"listEl\" class=\"im-chat-messages\">\n <div v-if=\"sortedMessages.length === 0\" class=\"im-chat-empty\">还没有消息,发个招呼吧 👋</div>\n <template v-for=\"item in flatList\" :key=\"item.key\">\n <div v-if=\"item.type === 'time'\" class=\"im-chat-time-sep\">\n {{ fmtTimeFull(item.ts) }}\n </div>\n <div v-else :class=\"['im-chat-msg', isMine(item.msg) ? 'mine' : 'theirs']\">\n <!-- 自定义气泡插槽:业务方处理富文本/媒体等渲染 -->\n <slot name=\"message\" :message=\"item.msg\" :mine=\"isMine(item.msg)\">\n <div class=\"im-chat-bubble\">\n <span v-if=\"item.msg.status === 'revoked'\" class=\"im-chat-revoked\">[已撤回]</span>\n <span v-else>{{ item.msg.body }}</span>\n <span v-if=\"item.msg.status === 'failed'\" class=\"im-chat-fail\">!发送失败</span>\n </div>\n </slot>\n <div class=\"im-chat-meta\">{{ fmtTime(item.msg.createdAt) }}</div>\n </div>\n </template>\n </div>\n <!-- 自定义输入区插槽:不传则用默认(表情 + textarea + 发送按钮) -->\n <slot name=\"input\" :send=\"sendText\">\n <div class=\"im-chat-input\">\n <button class=\"im-chat-emoji-btn\" title=\"表情\" @click=\"emojiPickerOpen = !emojiPickerOpen\">\n 😊\n </button>\n <textarea\n ref=\"textareaEl\"\n v-model=\"input\"\n rows=\"1\"\n placeholder=\"输入消息,Enter 发送,Shift+Enter 换行\"\n @keydown=\"onInputKeyDown\"\n @input=\"autoResize\"\n ></textarea>\n <button :disabled=\"!input.trim()\" @click=\"send\">发送</button>\n <!-- 表情面板:定位相对 .im-chat-input -->\n <div v-if=\"emojiPickerOpen\" class=\"im-chat-emoji-picker\">\n <button v-for=\"e in emojis\" :key=\"e\" class=\"im-chat-emoji-item\" @click=\"insertEmoji(e)\">\n {{ e }}\n </button>\n </div>\n </div>\n </slot>\n </div>\n</template>\n\n<style scoped>\n.im-chat-panel {\n /* 主题变量:业务方在 :root/祖先覆盖即可定制配色 */\n --im-chat-primary: #667eea;\n --im-chat-bg: #f5f7fa;\n --im-chat-bubble-mine: #95ec69;\n --im-chat-bubble-peer: #fff;\n\n position: fixed;\n bottom: 16px;\n right: 16px;\n width: 360px;\n height: 480px;\n background: #fff;\n border-radius: 8px;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);\n display: flex;\n flex-direction: column;\n z-index: 9999;\n font-family: -apple-system, 'Segoe UI', Roboto, 'PingFang SC', 'Microsoft YaHei', sans-serif;\n}\n/* 内嵌模式:随容器铺满,阴影/圆角交给外层容器 */\n.im-chat-panel--embedded {\n position: static;\n flex: 1;\n min-height: 0;\n width: 100%;\n height: 100%;\n border-radius: 0;\n box-shadow: none;\n z-index: auto;\n}\n.im-chat-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 12px 16px;\n border-bottom: 1px solid #eee;\n font-weight: 600;\n}\n.im-chat-name {\n font-size: 15px;\n}\n.im-chat-close {\n background: none;\n border: none;\n cursor: pointer;\n font-size: 20px;\n color: #999;\n}\n.im-chat-messages {\n flex: 1;\n overflow-y: auto;\n padding: 12px 16px;\n background: var(--im-chat-bg);\n}\n.im-chat-empty {\n color: #aaa;\n text-align: center;\n padding-top: 40px;\n font-size: 13px;\n}\n.im-chat-time-sep {\n text-align: center;\n font-size: 11px;\n color: #999;\n background: rgba(0, 0, 0, 0.05);\n border-radius: 4px;\n padding: 2px 8px;\n margin: 12px auto;\n width: fit-content;\n}\n.im-chat-msg {\n margin-bottom: 12px;\n display: flex;\n flex-direction: column;\n}\n.im-chat-msg.mine {\n align-items: flex-end;\n}\n.im-chat-msg.theirs {\n align-items: flex-start;\n}\n.im-chat-bubble {\n max-width: 70%;\n padding: 8px 12px;\n border-radius: 12px;\n font-size: 14px;\n word-break: break-word;\n}\n.im-chat-msg.mine .im-chat-bubble {\n background: var(--im-chat-bubble-mine);\n color: #1a1a1a;\n}\n.im-chat-msg.theirs .im-chat-bubble {\n background: var(--im-chat-bubble-peer);\n color: #1a1a1a;\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);\n}\n.im-chat-revoked {\n color: #999;\n font-style: italic;\n font-size: 12px;\n}\n.im-chat-fail {\n color: #ff4d4f;\n font-size: 11px;\n margin-left: 4px;\n}\n.im-chat-meta {\n font-size: 11px;\n color: #bbb;\n margin-top: 2px;\n}\n.im-chat-input {\n position: relative;\n display: flex;\n align-items: flex-end;\n padding: 10px 12px;\n border-top: 1px solid #eee;\n background: #fff;\n}\n.im-chat-emoji-btn {\n width: 32px;\n height: 32px;\n margin-right: 8px;\n border: none;\n background: transparent;\n font-size: 20px;\n cursor: pointer;\n border-radius: 4px;\n}\n.im-chat-emoji-btn:hover {\n background: #f0f0f0;\n}\n.im-chat-input textarea {\n flex: 1;\n padding: 8px 12px;\n border: 1px solid #ddd;\n border-radius: 4px;\n outline: none;\n font-size: 14px;\n font-family: inherit;\n resize: none;\n line-height: 1.4;\n min-height: 36px;\n max-height: 96px;\n overflow-y: auto;\n}\n.im-chat-input textarea:focus {\n border-color: var(--im-chat-primary);\n}\n.im-chat-input button {\n margin-left: 8px;\n padding: 0 16px;\n height: 36px;\n background: var(--im-chat-primary);\n color: #fff;\n border: none;\n border-radius: 4px;\n cursor: pointer;\n font-size: 13px;\n}\n.im-chat-input button:disabled {\n background: #bbb;\n cursor: not-allowed;\n}\n/* 表情面板:定位在输入框上方,4 行 × 8 列网格 */\n.im-chat-emoji-picker {\n position: absolute;\n bottom: 60px;\n left: 12px;\n right: 12px;\n background: #fff;\n border: 1px solid #eee;\n border-radius: 8px;\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);\n padding: 8px;\n display: grid;\n grid-template-columns: repeat(8, 1fr);\n gap: 4px;\n z-index: 10;\n}\n.im-chat-emoji-item {\n font-size: 20px;\n border: none;\n background: transparent;\n cursor: pointer;\n padding: 6px;\n border-radius: 4px;\n}\n.im-chat-emoji-item:hover {\n background: #f0f0f0;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { ref, computed, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'\nimport type { ImClient } from '@quitting/core'\nimport type { Conversation, Message } from '@quitting/contract'\nimport { getImClient } from '../client-access'\n\n// 聊天面板:与单个 peer 的 1:1 对话。\n// 两种布局模式:\n// 浮窗(默认):fixed 右下角,ImUI.chat() 弹出用\n// embedded:随容器铺满(position:static + 100%),业务方嵌入自己的布局用\n// 能力:历史拉取 + dialog 通道订阅 + 乐观渲染 + ack + 撤回 + 已读上报 +\n// Enter 发送(IME 安全)/Shift+Enter 换行/输入自动增高/表情面板/时间分隔线\n// 自定义:#message 插槽(自定义气泡)/ #input 插槽(自定义输入区)\nconst props = defineProps<{\n to: string\n // 对端展示名;未传时查 SDK 用户目录,仍无则显示 to 本身\n peerName?: string\n // 我的 externalId(左右气泡归属判定用);未传时自动查用户目录解析\n myUid?: string\n embedded?: boolean\n onClose?: () => void\n}>()\n\nconst emit = defineEmits<{ (e: 'close'): void }>()\n\ndefineSlots<{\n // 自定义消息气泡渲染(scope: message 为完整 Message,含 status)\n message?(scope: { message: Message; mine: boolean }): unknown\n // 自定义输入区(scope: send 为发送函数,业务方自管输入状态时可忽略)\n input?(scope: { send: (text: string) => void }): unknown\n}>()\n\nconst messages = ref<Message[]>([])\nconst input = ref('')\nconst conversationId = ref('')\nconst lastReadSeq = ref(0)\nconst listEl = ref<HTMLDivElement | null>(null)\nconst textareaEl = ref<HTMLTextAreaElement | null>(null)\nconst unsubscribers: Array<() => void> = []\n// 表情面板开关\nconst emojiPickerOpen = ref(false)\n// 常用 emoji 子集;业务方要更全的可通过 #input 插槽接 emoji-mart 等\nconst emojis = [\n '😀',\n '😁',\n '😂',\n '🤣',\n '😊',\n '😍',\n '🥰',\n '😘',\n '😎',\n '🤔',\n '😴',\n '😭',\n '😱',\n '🤯',\n '🤗',\n '👍',\n '👎',\n '👏',\n '🙏',\n '💪',\n '🤝',\n '✌️',\n '🤞',\n '❤️',\n '💔',\n '💕',\n '💯',\n '🔥',\n '⭐',\n '🎉',\n '🎁',\n '🚀',\n]\n\nfunction getClient(): ImClient | undefined {\n return getImClient()\n}\n\n// 对端展示名:外部传 peerName 优先;未传查用户目录\nconst fallbackName = ref('')\nwatch(\n () => props.to,\n async (to) => {\n fallbackName.value = ''\n if (props.peerName || !to) return\n const dir = getClient()?.users\n if (!dir?.resolve) return\n const u = await dir.resolve(to).catch(() => undefined)\n if (u?.name) fallbackName.value = u.name\n },\n { immediate: true },\n)\nconst displayPeerName = computed(() => props.peerName || fallbackName.value || props.to)\n\n// 解析与 to(externalId)的会话 ID:反查 peer UUID 后在会话列表里匹配 dialog 两端\n// (后端 dialogUserA/B 存的是内部 UUID,见 server signaling/message.go GetOrCreateDialog)\n// 后端会话在首条消息发送时才创建,没发过消息时返回空串\nasync function resolveConversation(client: ImClient): Promise<string> {\n try {\n const resp = await client.transport.usersByExternal([props.to])\n const peerUUID = resp.users[props.to]?.uid\n if (!peerUUID) return ''\n const convs = (await client.message.listConversations()) || []\n const me = client.uid\n const conv = convs.find(\n (c: Conversation) =>\n (c.dialogUserA === me && c.dialogUserB === peerUUID) ||\n (c.dialogUserA === peerUUID && c.dialogUserB === me),\n )\n return conv?.id || ''\n } catch (err) {\n console.warn('[im] 解析会话 ID 失败', err)\n return ''\n }\n}\n\n// dialog 通道 message publication 的原始 payload(realtime 透传给 'message' 事件,\n// 字段与 contract Message 不同:msgId/from(externalId)/msgType/ts,见后端 signaling/message.go)\ninterface DialogPublication {\n msgId?: string\n conversationId?: string\n seq?: number\n from?: string\n msgType?: string\n body?: string\n ts?: number\n}\n\n// 我的 externalId:历史消息的 senderId 是 externalId(后端 History 已转换),\n// 左右归属判定必须用它而非内部 uid。prop 优先,缺省查用户目录反查\nconst myExternalId = ref(props.myUid || '')\n\nonMounted(async () => {\n const client = getClient()\n if (!client) return\n\n if (!props.myUid) {\n const me = await client.users?.resolve(client.uid).catch(() => undefined)\n if (me?.externalId) myExternalId.value = me.externalId\n }\n // 订阅与 peer 的 dialog 通道——SDK 不自动订阅,不订阅则对端回复永远收不到\n client.message\n .openDialog(props.to)\n .catch((err: unknown) => console.warn('[im] openDialog 失败', err))\n\n // 已有会话则直接拉历史;没有(首次聊天)时发送成功后再解析\n conversationId.value = await resolveConversation(client)\n if (conversationId.value) {\n await loadHistory()\n }\n\n unsubscribers.push(\n client.on('message', (raw) => {\n const p = raw as unknown as DialogPublication\n // 会话过滤:conversationId 已知时精确匹配;未知(首次聊天/解析中)用 from 兜底,\n // 否则首条对端消息(自己还没发过、会话还没建)会被丢弃\n const belongs = conversationId.value\n ? p.conversationId === conversationId.value\n : p.from === props.to\n if (!belongs) return\n // 去重:自己发的消息 send() 已乐观插入并被回填真实 msgId\n if (!p.msgId || messages.value.some((m) => m.id === p.msgId)) return\n messages.value.push({\n id: p.msgId,\n appId: '',\n conversationId: p.conversationId || conversationId.value,\n seq: Number(p.seq) || 0,\n // 注意:来自 publication 的 senderId 是 externalId;自己的乐观消息也统一用\n // externalId(myExternalId),与后端 History 口径一致,isMine 判定两侧统一\n senderId: p.from || '',\n clientMid: p.msgId,\n type: (p.msgType as Message['type']) || 'text',\n body: p.body,\n status: 'sent',\n createdAt: p.ts || Date.now(),\n })\n scrollToBottom()\n // 自动 ACK(消息必达三保险的客户端侧)\n client.message.ack(p.msgId).catch(() => {})\n }),\n )\n unsubscribers.push(\n client.on('message:revoked', (e: { messageId: string }) => {\n const m = messages.value.find((x) => x.id === e.messageId)\n if (m) m.status = 'revoked'\n }),\n )\n})\n\nonBeforeUnmount(() => {\n unsubscribers.forEach((fn) => fn())\n getClient()?.message?.closeDialog?.(props.to)\n})\n\nasync function send() {\n const body = input.value.trim()\n if (!body) return\n input.value = ''\n resetTextareaHeight()\n emojiPickerOpen.value = false\n await sendText(body)\n}\n\n// #input 插槽的 scope.send:自定义输入区可调它发送任意文本\nasync function sendText(body: string) {\n const client = getClient()\n if (!client || !body) return\n // 乐观渲染:先本地加一条 status=sending(senderId 用 externalId,与历史消息口径一致)\n const tempId = 'temp-' + Date.now()\n messages.value.push({\n id: tempId,\n appId: '',\n conversationId: conversationId.value,\n seq: 0,\n senderId: myExternalId.value || client.uid,\n clientMid: tempId,\n type: 'text',\n body,\n status: 'sending',\n createdAt: Date.now(),\n })\n scrollToBottom()\n try {\n const resp = await client.message.send({ to: props.to, type: 'text', body })\n // 更新本地消息为已发送\n const m = messages.value.find((x) => x.id === tempId)\n if (m) {\n m.id = resp.msgId\n m.seq = resp.seq\n m.status = 'sent'\n }\n lastReadSeq.value = resp.seq\n // 首条消息发送后服务端才建会话——此时解析 conversationId 供后续收消息过滤\n if (!conversationId.value) {\n conversationId.value = await resolveConversation(client)\n if (m) m.conversationId = conversationId.value\n }\n } catch (err) {\n const m = messages.value.find((x) => x.id === tempId)\n if (m) m.status = 'failed'\n }\n}\n\nasync function loadHistory() {\n const client = getClient()\n if (!client || !conversationId.value) return\n const msgs = await client.message.history(conversationId.value, 0, 50)\n // 按 seq 升序渲染(后端升序返回,这里兜底排序保证展示稳定)\n messages.value = [...msgs].sort((a, b) => a.seq - b.seq)\n if (msgs.length > 0) {\n lastReadSeq.value = msgs[msgs.length - 1].seq\n await client.message.markRead(conversationId.value, lastReadSeq.value)\n }\n scrollToBottom()\n}\n\nfunction scrollToBottom() {\n nextTick(() => {\n if (listEl.value) listEl.value.scrollTop = listEl.value.scrollHeight\n })\n}\n\n// ===== 输入区交互 =====\n\n// textarea 自动增高:最大高度 96px(约 4 行)\nfunction autoResize() {\n const el = textareaEl.value\n if (!el) return\n el.style.height = 'auto'\n el.style.height = Math.min(el.scrollHeight, 96) + 'px'\n}\n\nfunction resetTextareaHeight() {\n if (textareaEl.value) {\n textareaEl.value.style.height = 'auto'\n }\n}\n\n// Enter 发送 / Shift+Enter 换行;IME 输入法 composing 状态(中文中间态)回车不触发发送\nfunction onInputKeyDown(e: KeyboardEvent) {\n if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) {\n e.preventDefault()\n send()\n }\n}\n\n// 表情插入到光标位置(避免用户在中间编辑时被推到末尾)\nfunction insertEmoji(emoji: string) {\n const el = textareaEl.value\n if (!el) {\n input.value += emoji\n return\n }\n const start = el.selectionStart ?? input.value.length\n const end = el.selectionEnd ?? input.value.length\n input.value = input.value.slice(0, start) + emoji + input.value.slice(end)\n nextTick(() => {\n el.focus()\n const pos = start + emoji.length\n el.setSelectionRange(pos, pos)\n autoResize()\n })\n}\n\n// ===== 渲染辅助 =====\n\nconst sortedMessages = computed(() => [...messages.value].sort((a, b) => a.seq - b.seq))\n\n// 时间分隔线:相邻消息间隔 > 5 分钟插入分隔元素\ntype FlatItem =\n | { type: 'time'; ts: number; key: string }\n | { type: 'msg'; msg: Message; key: string }\n\nconst flatList = computed<FlatItem[]>(() => {\n const list: FlatItem[] = []\n const sorted = sortedMessages.value\n const GAP_MS = 5 * 60 * 1000\n let lastTs = 0\n for (const m of sorted) {\n if (m.createdAt - lastTs > GAP_MS || lastTs === 0) {\n list.push({ type: 'time', ts: m.createdAt, key: `t-${m.id}` })\n }\n list.push({ type: 'msg', msg: m, key: m.id })\n lastTs = m.createdAt\n }\n return list\n})\n\nfunction fmtTime(ts: number): string {\n const d = new Date(ts)\n const hh = d.getHours().toString().padStart(2, '0')\n const mm = d.getMinutes().toString().padStart(2, '0')\n return `${hh}:${mm}`\n}\n\n// 分隔线完整时间:今天 HH:mm;昨天 HH:mm;更早 M月D日 HH:mm\nfunction fmtTimeFull(ts: number): string {\n const d = new Date(ts)\n const now = new Date()\n const hm = fmtTime(ts)\n if (d.toDateString() === now.toDateString()) return hm\n const yest = new Date(now)\n yest.setDate(now.getDate() - 1)\n if (d.toDateString() === yest.toDateString()) return `昨天 ${hm}`\n return `${d.getMonth() + 1}月${d.getDate()}日 ${hm}`\n}\n\nfunction isMine(m: Message): boolean {\n // 统一按 externalId 判定(历史消息/回推消息的 senderId 均为 externalId)\n const me = myExternalId.value\n return !!me && m.senderId === me\n}\n</script>\n\n<template>\n <div :class=\"['im-chat-panel', { 'im-chat-panel--embedded': embedded }]\">\n <div class=\"im-chat-header\">\n <span class=\"im-chat-name\">{{ displayPeerName }}</span>\n <button v-if=\"!embedded\" class=\"im-chat-close\" @click=\"emit('close')\">×</button>\n </div>\n <div ref=\"listEl\" class=\"im-chat-messages\">\n <div v-if=\"sortedMessages.length === 0\" class=\"im-chat-empty\">还没有消息,发个招呼吧 👋</div>\n <template v-for=\"item in flatList\" :key=\"item.key\">\n <div v-if=\"item.type === 'time'\" class=\"im-chat-time-sep\">\n {{ fmtTimeFull(item.ts) }}\n </div>\n <div v-else :class=\"['im-chat-msg', isMine(item.msg) ? 'mine' : 'theirs']\">\n <!-- 自定义气泡插槽:业务方处理富文本/媒体等渲染 -->\n <slot name=\"message\" :message=\"item.msg\" :mine=\"isMine(item.msg)\">\n <div class=\"im-chat-bubble\">\n <span v-if=\"item.msg.status === 'revoked'\" class=\"im-chat-revoked\">[已撤回]</span>\n <span v-else>{{ item.msg.body }}</span>\n <span v-if=\"item.msg.status === 'failed'\" class=\"im-chat-fail\">!发送失败</span>\n </div>\n </slot>\n <div class=\"im-chat-meta\">{{ fmtTime(item.msg.createdAt) }}</div>\n </div>\n </template>\n </div>\n <!-- 自定义输入区插槽:不传则用默认(表情 + textarea + 发送按钮) -->\n <slot name=\"input\" :send=\"sendText\">\n <div class=\"im-chat-input\">\n <button class=\"im-chat-emoji-btn\" title=\"表情\" @click=\"emojiPickerOpen = !emojiPickerOpen\">\n 😊\n </button>\n <textarea\n ref=\"textareaEl\"\n v-model=\"input\"\n rows=\"1\"\n placeholder=\"输入消息,Enter 发送,Shift+Enter 换行\"\n @keydown=\"onInputKeyDown\"\n @input=\"autoResize\"\n ></textarea>\n <button :disabled=\"!input.trim()\" @click=\"send\">发送</button>\n <!-- 表情面板:定位相对 .im-chat-input -->\n <div v-if=\"emojiPickerOpen\" class=\"im-chat-emoji-picker\">\n <button v-for=\"e in emojis\" :key=\"e\" class=\"im-chat-emoji-item\" @click=\"insertEmoji(e)\">\n {{ e }}\n </button>\n </div>\n </div>\n </slot>\n </div>\n</template>\n\n<style scoped>\n.im-chat-panel {\n /* 主题变量:业务方在 :root/祖先覆盖即可定制配色 */\n --im-chat-primary: #667eea;\n --im-chat-bg: #f5f7fa;\n --im-chat-bubble-mine: #95ec69;\n --im-chat-bubble-peer: #fff;\n\n position: fixed;\n bottom: 16px;\n right: 16px;\n width: 360px;\n height: 480px;\n background: #fff;\n border-radius: 8px;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);\n display: flex;\n flex-direction: column;\n z-index: 9999;\n font-family: -apple-system, 'Segoe UI', Roboto, 'PingFang SC', 'Microsoft YaHei', sans-serif;\n}\n/* 内嵌模式:随容器铺满,阴影/圆角交给外层容器 */\n.im-chat-panel--embedded {\n position: static;\n flex: 1;\n min-height: 0;\n width: 100%;\n height: 100%;\n border-radius: 0;\n box-shadow: none;\n z-index: auto;\n}\n.im-chat-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 12px 16px;\n border-bottom: 1px solid #eee;\n font-weight: 600;\n}\n.im-chat-name {\n font-size: 15px;\n}\n.im-chat-close {\n background: none;\n border: none;\n cursor: pointer;\n font-size: 20px;\n color: #999;\n}\n.im-chat-messages {\n flex: 1;\n overflow-y: auto;\n padding: 12px 16px;\n background: var(--im-chat-bg);\n}\n.im-chat-empty {\n color: #aaa;\n text-align: center;\n padding-top: 40px;\n font-size: 13px;\n}\n.im-chat-time-sep {\n text-align: center;\n font-size: 11px;\n color: #999;\n background: rgba(0, 0, 0, 0.05);\n border-radius: 4px;\n padding: 2px 8px;\n margin: 12px auto;\n width: fit-content;\n}\n.im-chat-msg {\n margin-bottom: 12px;\n display: flex;\n flex-direction: column;\n}\n.im-chat-msg.mine {\n align-items: flex-end;\n}\n.im-chat-msg.theirs {\n align-items: flex-start;\n}\n.im-chat-bubble {\n max-width: 70%;\n padding: 8px 12px;\n border-radius: 12px;\n font-size: 14px;\n word-break: break-word;\n}\n.im-chat-msg.mine .im-chat-bubble {\n background: var(--im-chat-bubble-mine);\n color: #1a1a1a;\n}\n.im-chat-msg.theirs .im-chat-bubble {\n background: var(--im-chat-bubble-peer);\n color: #1a1a1a;\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);\n}\n.im-chat-revoked {\n color: #999;\n font-style: italic;\n font-size: 12px;\n}\n.im-chat-fail {\n color: #ff4d4f;\n font-size: 11px;\n margin-left: 4px;\n}\n.im-chat-meta {\n font-size: 11px;\n color: #bbb;\n margin-top: 2px;\n}\n.im-chat-input {\n position: relative;\n display: flex;\n align-items: flex-end;\n padding: 10px 12px;\n border-top: 1px solid #eee;\n background: #fff;\n}\n.im-chat-emoji-btn {\n width: 32px;\n height: 32px;\n margin-right: 8px;\n border: none;\n background: transparent;\n font-size: 20px;\n cursor: pointer;\n border-radius: 4px;\n}\n.im-chat-emoji-btn:hover {\n background: #f0f0f0;\n}\n.im-chat-input textarea {\n flex: 1;\n padding: 8px 12px;\n border: 1px solid #ddd;\n border-radius: 4px;\n outline: none;\n font-size: 14px;\n font-family: inherit;\n resize: none;\n line-height: 1.4;\n min-height: 36px;\n max-height: 96px;\n overflow-y: auto;\n}\n.im-chat-input textarea:focus {\n border-color: var(--im-chat-primary);\n}\n.im-chat-input button {\n margin-left: 8px;\n padding: 0 16px;\n height: 36px;\n background: var(--im-chat-primary);\n color: #fff;\n border: none;\n border-radius: 4px;\n cursor: pointer;\n font-size: 13px;\n}\n.im-chat-input button:disabled {\n background: #bbb;\n cursor: not-allowed;\n}\n/* 表情面板:定位在输入框上方,4 行 × 8 列网格 */\n.im-chat-emoji-picker {\n position: absolute;\n bottom: 60px;\n left: 12px;\n right: 12px;\n background: #fff;\n border: 1px solid #eee;\n border-radius: 8px;\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);\n padding: 8px;\n display: grid;\n grid-template-columns: repeat(8, 1fr);\n gap: 4px;\n z-index: 10;\n}\n.im-chat-emoji-item {\n font-size: 20px;\n border: none;\n background: transparent;\n cursor: pointer;\n padding: 6px;\n border-radius: 4px;\n}\n.im-chat-emoji-item:hover {\n background: #f0f0f0;\n}\n</style>\n","import type { App } from 'vue'\n\nimport type { CallSession, ImClient } from '@quitting/core'\nimport type { CallType } from '@quitting/contract'\n\nimport { setRootApp, mountComponent, unmountRef, type MountedRef } from './mount'\nimport { setImClient } from './client-access'\nimport { ImToastApi as toast } from './toast'\nimport { friendlyApiError } from './error-text'\nimport CallWindow from './components/CallWindow.vue'\nimport CallDialog from './components/CallDialog.vue'\nimport MeetWindow from './components/MeetWindow.vue'\nimport MeetingInvite from './components/MeetingInvite.vue'\nimport MeetingCreateDialog from './components/MeetingCreateDialog.vue'\nimport ChatPanel from './components/ChatPanel.vue'\n\n// 全局方法:业务方零编码接入,调 ImUI.chat/call/meeting 即弹窗\n// 对方收到邀请自动弹 CallDialog(incoming)/MeetingInvite(由本模块监听 client 事件触发)\n//\n// 接线口径与 apps/demo 的 App.vue 一致(该链路已端到端验证):\n// 主叫:init 成功 → CallDialog(outgoing) → call:accepted → sess.connect()+enableDevices → CallWindow\n// 被叫:call:invite → CallDialog(incoming) → accepted → CallWindow\n// 会议:create 成功 → MeetWindow(host);被邀 → MeetingInvite → accepted → MeetWindow(participant)\n//\n// autoPopups=false 时关闭自动弹窗监听(业务方完全自定义 UI),ImUI.* 主动方法仍可用\n\nlet client: ImClient\n// setupGlobalOptions 传入的定制项(ImUI.* 主动方法弹窗时复用,如 resolveUser)\nlet uiOptions: SetupUIOptions | undefined\n\n// 会议邀请弹窗引用:按 meetingId 索引,dissolved/kicked 事件来时按 meetingId 清弹窗\ninterface MountedInvite {\n subApp: App\n div: HTMLDivElement\n}\nconst mountedInvites = new Map<string, MountedInvite>()\n\n// 通话等待/响铃弹窗是否挂着(忙线判断用)。\n// 不用 client.sessionState——它是登录时的一次性快照,之后不更新(见 core/index.ts 注释)\nlet callRingingMounted = false\n\nfunction callUiBusy(): boolean {\n return callRingingMounted || !!client.call?.currentSession || !!client.meeting?.currentSession\n}\n\n// 会议邀请弹窗堆叠容器:fixed 右上角、多条纵向排列。\n// MeetingInvite 组件自身是 position:relative(260px 卡片),不带定位——\n// 旧 demo 靠外层 .meeting-invites-stack 容器定位;动态 mount 必须提供同款容器,\n// 否则弹窗渲染在 body 文档流末尾(视口外),表现为\"收不到邀请\"\nfunction ensureInviteStack(): HTMLElement {\n let el = document.querySelector<HTMLElement>('div.im-invite-stack')\n if (el) return el\n el = document.createElement('div')\n el.className = 'im-invite-stack'\n el.style.position = 'fixed'\n el.style.top = '24px'\n el.style.right = '24px'\n el.style.zIndex = '10000'\n el.style.display = 'flex'\n el.style.flexDirection = 'column'\n el.style.gap = '12px'\n el.style.pointerEvents = 'none'\n document.body.appendChild(el)\n return el\n}\n\n// uid → 展示名(业务方未维护映射时的兜底:SDK 用户目录)\nasync function nameOf(uid: string): Promise<string> {\n const dir = client.users\n if (dir?.resolve) {\n const u = await dir.resolve(uid).catch(() => undefined)\n if (u?.name) return u.name\n }\n return uid\n}\n\nexport interface SetupUIOptions {\n autoPopups?: boolean\n // uid → { name, dept? } 自定义解析器:透传给自动弹出的 MeetWindow\n // (后端成员表 name 不全/有业务侧别名时用;缺省走 SDK 用户目录)\n resolveUser?: (uid: string) => { name: string; dept?: string } | undefined\n}\n\nexport function setupGlobalMethods(imClient: ImClient, vueApp: App, opts?: SetupUIOptions) {\n client = imClient\n uiOptions = opts\n setRootApp(vueApp)\n\n // 注入到 window,供 Vue 组件内部 getClient() 拿到\n // 严格 provide/inject 模式可由业务方自行实现,这里用全局引用简化跨组件访问\n setImClient(imClient)\n\n // ===== 监听邀请事件,自动弹窗(autoPopups=false 可整体关闭) =====\n if (opts?.autoPopups !== false) {\n client.on('call:invite', (e: { callId: string; from: string; callType: CallType }) => {\n if (callUiBusy()) {\n // 会话冲突:对新 callId 直接回 busy。注意不能走 client.call.end('busy')——\n // 它在 currentSession 存在(正在通话/呼叫中)时会挂断当前这路通话\n client.transport.callEnd(e.callId, 'busy').catch(() => {})\n return\n }\n callRingingMounted = true\n mountComponent(CallDialog, {\n mode: 'incoming',\n peerUid: e.from,\n callType: e.callType,\n callId: e.callId,\n onAccepted: (session: unknown) => {\n // 被叫接听成功:accept 内部已完成 connect,直接弹通话窗口\n const sess = session as CallSession\n mountComponent(CallWindow, { mode: 'callee', callType: sess.callType })\n },\n onClose: () => {\n callRingingMounted = false\n },\n })\n })\n\n client.on('meeting:invite', async (e) => {\n // 已在同一会议中:忽略(避免重复弹)\n if (client.meeting?.currentSession) return\n // 异步校验后端 pending:Centrifugo recovery 重放 stale publication 时丢弃\n try {\n const resp = await client.transport.meetingPendingInvites()\n const valid = (resp.invites || []).some((p) => p.meetingId === e.meetingId)\n if (!valid) {\n console.warn('[im-ui] stale meeting_invite 丢弃(会议已结束)', e.meetingId)\n return\n }\n } catch (err) {\n console.warn('[im-ui] invite 校验失败,默认透传', err)\n }\n // 同 meetingId 已弹过:先清旧的(避免堆叠),再 mount 新的(覆盖 reinvite 场景)\n unmountRef(mountedInvites.get(e.meetingId) || null)\n mountedInvites.delete(e.meetingId)\n const ref = mountComponent(\n MeetingInvite,\n {\n invite: e,\n conflict: callUiBusy(),\n onAccepted: () => {\n // 接听成功:弹会议窗口(accept 内部已建 currentSession)\n const sess = client.meeting?.currentSession\n mountComponent(MeetWindow, {\n mode: 'participant',\n title: sess?.title || '会议',\n resolveUser: opts?.resolveUser,\n })\n },\n onClose: () => {\n mountedInvites.delete(e.meetingId)\n },\n },\n // 挂进 fixed 右上角堆叠容器(MeetingInvite 自身无定位)\n ensureInviteStack(),\n )\n mountedInvites.set(e.meetingId, ref)\n })\n\n // 会议解散:清对应邀请弹窗(MeetWindow 自身监听 dissolved 显示结束态,无需在此处理)\n client.on('meeting:dissolved', (e: { meetingId: string }) => {\n unmountRef(mountedInvites.get(e.meetingId) || null)\n mountedInvites.delete(e.meetingId)\n })\n\n // 被主持人踢出:清对应邀请弹窗(MeetWindow 自身监听 kicked 显示结束态)\n client.on('meeting:kicked', (e: { meetingId: string }) => {\n unmountRef(mountedInvites.get(e.meetingId) || null)\n mountedInvites.delete(e.meetingId)\n })\n }\n\n client.on('session:kicked', (e) => {\n alert(\n `您的账号在其他 ${e.clientType === 4 ? 'Web' : e.clientType === 1 ? 'PC' : '移动端'} 登录,已强制下线`,\n )\n window.location.reload()\n })\n\n // beforeunload 兜底:通话/会议中关闭或刷新页面时直发挂断/离会。\n // 必须用 sendBeacon(keepalive)——普通 fetch 在 unload 时被取消,会留僵尸记录\n window.addEventListener('beforeunload', () => {\n const callSess = client?.call?.currentSession\n if (callSess) {\n client.transport.sendBeacon('/call/end', { callId: callSess.callId, reason: 'user_left' })\n }\n const meetSess = client?.meeting?.currentSession\n if (meetSess) {\n client.transport.sendBeacon(`/meeting/${meetSess.meetingId}/leave`, {})\n }\n })\n}\n\n// 暴露给业务方的全局方法\nexport const ImUI = {\n // 弹出与某用户的聊天框\n chat(userId: string) {\n mountComponent(ChatPanel, { to: userId })\n },\n\n // 发起 1:1 通话(主叫)\n async call(userId: string, type: CallType = 'video') {\n let sess: CallSession | null = null\n try {\n // 先 init 成功再弹窗:init 失败(对方忙/不存在)时不该出现死窗口\n sess = await client.call.init(userId, type)\n } catch (err) {\n toast.error(friendlyApiError(err, '发起通话失败'))\n return\n }\n callRingingMounted = true\n let dialog: MountedRef | null = null\n dialog = mountComponent(CallDialog, {\n mode: 'outgoing',\n peerUid: userId,\n callType: type,\n onConnected: () => {\n // 被叫已接听:主叫 connect + 开设备 → 关等待窗、弹通话窗口\n unmountRef(dialog)\n dialog = null\n callRingingMounted = false\n sess!\n .connect()\n .then(() => {\n sess!.enableDevices().catch((err) => console.warn('[im] 主叫 enableDevices 异常', err))\n mountComponent(CallWindow, { mode: 'caller', callType: type })\n })\n .catch((err) => {\n console.error('[im] 主叫 connect 失败', err)\n toast.error('连接建立失败,通话已取消')\n client.call.end('user_left').catch(() => {})\n })\n },\n })\n },\n\n // 发起会议(主持人)。\n // 传 invitees:直接建会;不传:弹内置选人弹窗(MeetingCreateDialog),确认后建会\n // 选人弹窗默认排除自己(内部 resolve(client.uid),无需外部传 myUid)\n async meeting(opts?: { invitees?: string[]; title?: string }) {\n if (!opts?.invitees) {\n // 内置选人弹窗 → confirm 后递归走建会流程\n mountComponent(MeetingCreateDialog, {\n onConfirm: (uids: string[], title: string) => {\n ImUI.meeting({ invitees: uids, title })\n },\n })\n return\n }\n const invitees = opts.invitees\n const title = opts.title || '即时会议'\n let result: { busyInvitees?: string[] }\n try {\n result = await client.meeting.create(invitees, title)\n } catch (err) {\n toast.error(friendlyApiError(err, '创建会议失败'))\n return\n }\n const busyList = result.busyInvitees || []\n if (invitees.length > 0 && busyList.length >= invitees.length) {\n // 全员繁忙:会议已在 server 创建,主持人主动 leave 清理,不开会议窗口\n client.meeting.currentSession?.leave(client.transport).catch(() => {})\n const names = (await Promise.all(busyList.map(nameOf))).join('、')\n toast.error(`${names} 均正忙,会议未能创建`)\n return\n }\n // create 成功后 currentSession 已建立,MeetWindow 挂载时才能拿到\n mountComponent(MeetWindow, { mode: 'host', title, resolveUser: uiOptions?.resolveUser })\n if (busyList.length > 0) {\n const names = (await Promise.all(busyList.map(nameOf))).join('、')\n toast.info(`${names} 正忙,未能邀请加入会议`)\n }\n },\n}\n","<script setup lang=\"ts\">\nimport { ref, onMounted, onBeforeUnmount } from 'vue'\nimport type { CallType } from '@quitting/contract'\nimport vDraggable from '../directives/draggable'\n\n// 主叫呼叫等待弹窗(不显示视频)\n// 在 SDK CallController.init 调用后、被叫接听前显示\n// 主叫收到 call:accepted → emit('connected') → 父组件切换到 CallWindow\n// 主叫收到 call:rejected → 显示\"对方拒接\" 1.5s 后 emit('close')\n// 主叫收到 call:busy → 显示\"对方繁忙\" 1.5s 后 emit('close')\n// 主叫收到 call:no_answer → 显示\"无人接听\" 1.5s 后 emit('close')\n// 主叫点取消 → 调 client.call.end('cancelled') + emit('close')\n\nconst props = defineProps<{\n calleeUid: string\n calleeName?: string\n callType: CallType\n // 可选:超时秒数(默认 30s),到时显示\"无人接听\"\n timeoutSec?: number\n}>()\n\nconst emit = defineEmits<{\n (e: 'close'): void\n (e: 'connected'): void\n}>()\n\nconst status = ref<'ringing' | 'rejected' | 'busy' | 'no_answer' | 'cancelled'>('ringing')\nconst statusText = ref('')\nconst unsubscribers: Array<() => void> = []\nlet timeoutTimer: ReturnType<typeof setTimeout> | null = null\n\nfunction getClient() {\n return (window as any).__imClient\n}\n\nfunction setStatusAndClose(s: typeof status.value, text: string, delay = 1500) {\n status.value = s\n statusText.value = text\n setTimeout(() => emit('close'), delay)\n}\n\nasync function cancel() {\n // 主叫主动取消:currentSession 存在时 SDK 走 hangup,否则用 callId 兜底\n status.value = 'cancelled'\n statusText.value = '已取消'\n const client = getClient()\n const callId = client?.call?.currentSession?.callId\n await client?.call?.end('cancelled', callId).catch(() => {})\n setTimeout(() => emit('close'), 800)\n}\n\nonMounted(() => {\n const client = getClient()\n if (!client) {\n emit('close')\n return\n }\n\n // 监听被叫响应——全部登记 unsubscribers,卸载时统一清理\n // (只保存 accepted 一个的话,多次呼叫后残留的 stale handler 会驱动已卸载组件)\n unsubscribers.push(\n client.on('call:accepted', () => {\n statusText.value = '对方已接听'\n setTimeout(() => emit('connected'), 300)\n }),\n )\n unsubscribers.push(\n client.on('call:rejected', () => {\n setStatusAndClose('rejected', '对方拒接')\n }),\n )\n unsubscribers.push(\n client.on('call:busy', () => {\n setStatusAndClose('busy', '对方繁忙')\n }),\n )\n unsubscribers.push(\n client.on('call:no_answer', () => {\n setStatusAndClose('no_answer', '无人接听')\n }),\n )\n // 极少情况:主叫取消后后端回推 call_cancelled 给自己——直接关\n unsubscribers.push(\n client.on('call:cancelled', () => {\n setStatusAndClose('cancelled', '已取消', 500)\n }),\n )\n\n const sec = props.timeoutSec ?? 30\n timeoutTimer = setTimeout(() => {\n if (status.value === 'ringing') {\n setStatusAndClose('no_answer', '无人接听')\n }\n }, sec * 1000)\n})\n\nonBeforeUnmount(() => {\n unsubscribers.forEach((fn) => fn())\n if (timeoutTimer) clearTimeout(timeoutTimer)\n})\n</script>\n\n<template>\n <div v-draggable=\"'.call-out-header'\" class=\"call-out-window\">\n <div class=\"call-out-header\">\n <span>{{ callType === 'video' ? '视频通话' : '语音通话' }}</span>\n </div>\n <div class=\"call-out-body\">\n <div :class=\"['call-out-avatar', status === 'ringing' ? 'ringing' : status]\">\n {{ (calleeName || calleeUid)[0].toUpperCase() }}\n </div>\n <div class=\"call-out-name\">{{ calleeName || calleeUid }}</div>\n <div class=\"call-out-status\">\n <span v-if=\"status === 'ringing'\" class=\"ringing-dots\">\n 呼叫中<span class=\"dot\">·</span><span class=\"dot\">·</span><span class=\"dot\">·</span>\n </span>\n <span v-else :class=\"status\">{{ statusText }}</span>\n </div>\n </div>\n <div class=\"call-out-actions\">\n <button v-if=\"status === 'ringing'\" class=\"cancel-btn\" @click=\"cancel\">取消</button>\n </div>\n </div>\n</template>\n\n<style scoped>\n.call-out-window {\n position: fixed;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n width: 320px;\n background: #0f172a;\n border-radius: 0;\n padding: 24px;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);\n color: #fff;\n z-index: 9999;\n text-align: center;\n}\n.call-out-header {\n cursor: move;\n font-size: 13px;\n color: #999;\n padding-bottom: 12px;\n border-bottom: 1px solid #333;\n margin-bottom: 16px;\n}\n.call-out-avatar {\n width: 80px;\n height: 80px;\n border-radius: 50%;\n background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n color: #fff;\n font-size: 36px;\n font-weight: 600;\n line-height: 80px;\n margin: 0 auto 12px;\n position: relative;\n}\n.call-out-avatar.ringing::before {\n content: '';\n position: absolute;\n inset: -8px;\n border: 2px solid #667eea;\n border-radius: 50%;\n animation: pulse 1.5s ease-out infinite;\n}\n.call-out-avatar.rejected,\n.call-out-avatar.cancelled {\n background: #888;\n}\n.call-out-avatar.busy,\n.call-out-avatar.no_answer {\n background: #d4a017;\n}\n@keyframes pulse {\n 0% {\n transform: scale(1);\n opacity: 0.8;\n }\n 100% {\n transform: scale(1.4);\n opacity: 0;\n }\n}\n.call-out-name {\n font-size: 16px;\n font-weight: 600;\n margin-bottom: 8px;\n}\n.call-out-status {\n font-size: 13px;\n color: #999;\n min-height: 20px;\n}\n.call-out-status .rejected,\n.call-out-status .cancelled {\n color: #888;\n}\n.call-out-status .busy,\n.call-out-status .no_answer {\n color: #d4a017;\n}\n.ringing-dots .dot {\n display: inline-block;\n animation: dot-bounce 1.4s infinite;\n}\n.ringing-dots .dot:nth-child(2) {\n animation-delay: 0.2s;\n}\n.ringing-dots .dot:nth-child(3) {\n animation-delay: 0.4s;\n}\n@keyframes dot-bounce {\n 0%,\n 60%,\n 100% {\n opacity: 0.3;\n }\n 30% {\n opacity: 1;\n }\n}\n.call-out-actions {\n margin-top: 24px;\n min-height: 40px;\n}\n.cancel-btn {\n padding: 8px 32px;\n background: #ef4444;\n color: #fff;\n border: none;\n border-radius: 0;\n cursor: pointer;\n font-size: 13px;\n transition: opacity 0.1s;\n}\n.cancel-btn:active {\n opacity: 0.8;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { ref, onMounted, onBeforeUnmount } from 'vue'\nimport type { CallType } from '@quitting/contract'\nimport vDraggable from '../directives/draggable'\n\n// 主叫呼叫等待弹窗(不显示视频)\n// 在 SDK CallController.init 调用后、被叫接听前显示\n// 主叫收到 call:accepted → emit('connected') → 父组件切换到 CallWindow\n// 主叫收到 call:rejected → 显示\"对方拒接\" 1.5s 后 emit('close')\n// 主叫收到 call:busy → 显示\"对方繁忙\" 1.5s 后 emit('close')\n// 主叫收到 call:no_answer → 显示\"无人接听\" 1.5s 后 emit('close')\n// 主叫点取消 → 调 client.call.end('cancelled') + emit('close')\n\nconst props = defineProps<{\n calleeUid: string\n calleeName?: string\n callType: CallType\n // 可选:超时秒数(默认 30s),到时显示\"无人接听\"\n timeoutSec?: number\n}>()\n\nconst emit = defineEmits<{\n (e: 'close'): void\n (e: 'connected'): void\n}>()\n\nconst status = ref<'ringing' | 'rejected' | 'busy' | 'no_answer' | 'cancelled'>('ringing')\nconst statusText = ref('')\nconst unsubscribers: Array<() => void> = []\nlet timeoutTimer: ReturnType<typeof setTimeout> | null = null\n\nfunction getClient() {\n return (window as any).__imClient\n}\n\nfunction setStatusAndClose(s: typeof status.value, text: string, delay = 1500) {\n status.value = s\n statusText.value = text\n setTimeout(() => emit('close'), delay)\n}\n\nasync function cancel() {\n // 主叫主动取消:currentSession 存在时 SDK 走 hangup,否则用 callId 兜底\n status.value = 'cancelled'\n statusText.value = '已取消'\n const client = getClient()\n const callId = client?.call?.currentSession?.callId\n await client?.call?.end('cancelled', callId).catch(() => {})\n setTimeout(() => emit('close'), 800)\n}\n\nonMounted(() => {\n const client = getClient()\n if (!client) {\n emit('close')\n return\n }\n\n // 监听被叫响应——全部登记 unsubscribers,卸载时统一清理\n // (只保存 accepted 一个的话,多次呼叫后残留的 stale handler 会驱动已卸载组件)\n unsubscribers.push(\n client.on('call:accepted', () => {\n statusText.value = '对方已接听'\n setTimeout(() => emit('connected'), 300)\n }),\n )\n unsubscribers.push(\n client.on('call:rejected', () => {\n setStatusAndClose('rejected', '对方拒接')\n }),\n )\n unsubscribers.push(\n client.on('call:busy', () => {\n setStatusAndClose('busy', '对方繁忙')\n }),\n )\n unsubscribers.push(\n client.on('call:no_answer', () => {\n setStatusAndClose('no_answer', '无人接听')\n }),\n )\n // 极少情况:主叫取消后后端回推 call_cancelled 给自己——直接关\n unsubscribers.push(\n client.on('call:cancelled', () => {\n setStatusAndClose('cancelled', '已取消', 500)\n }),\n )\n\n const sec = props.timeoutSec ?? 30\n timeoutTimer = setTimeout(() => {\n if (status.value === 'ringing') {\n setStatusAndClose('no_answer', '无人接听')\n }\n }, sec * 1000)\n})\n\nonBeforeUnmount(() => {\n unsubscribers.forEach((fn) => fn())\n if (timeoutTimer) clearTimeout(timeoutTimer)\n})\n</script>\n\n<template>\n <div v-draggable=\"'.call-out-header'\" class=\"call-out-window\">\n <div class=\"call-out-header\">\n <span>{{ callType === 'video' ? '视频通话' : '语音通话' }}</span>\n </div>\n <div class=\"call-out-body\">\n <div :class=\"['call-out-avatar', status === 'ringing' ? 'ringing' : status]\">\n {{ (calleeName || calleeUid)[0].toUpperCase() }}\n </div>\n <div class=\"call-out-name\">{{ calleeName || calleeUid }}</div>\n <div class=\"call-out-status\">\n <span v-if=\"status === 'ringing'\" class=\"ringing-dots\">\n 呼叫中<span class=\"dot\">·</span><span class=\"dot\">·</span><span class=\"dot\">·</span>\n </span>\n <span v-else :class=\"status\">{{ statusText }}</span>\n </div>\n </div>\n <div class=\"call-out-actions\">\n <button v-if=\"status === 'ringing'\" class=\"cancel-btn\" @click=\"cancel\">取消</button>\n </div>\n </div>\n</template>\n\n<style scoped>\n.call-out-window {\n position: fixed;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n width: 320px;\n background: #0f172a;\n border-radius: 0;\n padding: 24px;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);\n color: #fff;\n z-index: 9999;\n text-align: center;\n}\n.call-out-header {\n cursor: move;\n font-size: 13px;\n color: #999;\n padding-bottom: 12px;\n border-bottom: 1px solid #333;\n margin-bottom: 16px;\n}\n.call-out-avatar {\n width: 80px;\n height: 80px;\n border-radius: 50%;\n background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n color: #fff;\n font-size: 36px;\n font-weight: 600;\n line-height: 80px;\n margin: 0 auto 12px;\n position: relative;\n}\n.call-out-avatar.ringing::before {\n content: '';\n position: absolute;\n inset: -8px;\n border: 2px solid #667eea;\n border-radius: 50%;\n animation: pulse 1.5s ease-out infinite;\n}\n.call-out-avatar.rejected,\n.call-out-avatar.cancelled {\n background: #888;\n}\n.call-out-avatar.busy,\n.call-out-avatar.no_answer {\n background: #d4a017;\n}\n@keyframes pulse {\n 0% {\n transform: scale(1);\n opacity: 0.8;\n }\n 100% {\n transform: scale(1.4);\n opacity: 0;\n }\n}\n.call-out-name {\n font-size: 16px;\n font-weight: 600;\n margin-bottom: 8px;\n}\n.call-out-status {\n font-size: 13px;\n color: #999;\n min-height: 20px;\n}\n.call-out-status .rejected,\n.call-out-status .cancelled {\n color: #888;\n}\n.call-out-status .busy,\n.call-out-status .no_answer {\n color: #d4a017;\n}\n.ringing-dots .dot {\n display: inline-block;\n animation: dot-bounce 1.4s infinite;\n}\n.ringing-dots .dot:nth-child(2) {\n animation-delay: 0.2s;\n}\n.ringing-dots .dot:nth-child(3) {\n animation-delay: 0.4s;\n}\n@keyframes dot-bounce {\n 0%,\n 60%,\n 100% {\n opacity: 0.3;\n }\n 30% {\n opacity: 1;\n }\n}\n.call-out-actions {\n margin-top: 24px;\n min-height: 40px;\n}\n.cancel-btn {\n padding: 8px 32px;\n background: #ef4444;\n color: #fff;\n border: none;\n border-radius: 0;\n cursor: pointer;\n font-size: 13px;\n transition: opacity 0.1s;\n}\n.cancel-btn:active {\n opacity: 0.8;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { ref, onMounted, onBeforeUnmount } from 'vue'\nimport type { CallInviteEvent } from '@quitting/contract'\n// 被叫响铃中循环播放 callee.mp3\nimport ringCalleeUrl from '../sounds/ring-callee.mp3'\nimport Icon from './Icon.vue'\n\nconst props = defineProps<{\n invite: CallInviteEvent\n onClose?: () => void\n}>()\n\nconst emit = defineEmits<{\n (e: 'close'): void\n // 接听成功后携带 session 通知父组件渲染 CallWindow\n // 不能依赖父组件 watch currentSession——ImClient 不是 reactive,watch 不会触发\n (e: 'accepted', session: unknown): void\n}>()\n\nconst ringAudio = ref<HTMLAudioElement | null>(null)\n\nfunction getClient() {\n return (window as any).__imClient\n}\n\nfunction startRing() {\n if (!ringAudio.value) return\n ringAudio.value.currentTime = 0\n ringAudio.value.play().catch(() => {\n // 自动播放策略可能拒绝,不报错\n })\n}\n\nfunction stopRing() {\n if (!ringAudio.value) return\n ringAudio.value.pause()\n ringAudio.value.currentTime = 0\n}\n\nconst unsubscribers: Array<() => void> = []\n\nonMounted(() => {\n startRing()\n // 主叫取消 / 无人接听超时,被叫端响铃都应自动关\n // no_answer 必须监听:后端超时会同时通知主叫和被叫,只等 cancelled 会永不停止响铃\n const client = getClient()\n if (client) {\n unsubscribers.push(\n client.on('call:cancelled', () => {\n stopRing()\n emit('close')\n }),\n )\n unsubscribers.push(\n client.on('call:no_answer', () => {\n stopRing()\n emit('close')\n }),\n )\n }\n})\n\nonBeforeUnmount(() => {\n stopRing()\n unsubscribers.forEach((fn) => fn())\n})\n\nasync function accept() {\n stopRing()\n const client = getClient()\n try {\n const session = await client?.call?.accept(props.invite.callId, props.invite.callType)\n emit('accepted', session)\n emit('close')\n } catch (err: any) {\n // 规则1:多端并发进入,本端是后进端被拒(另一端已接听)\n const msg = String(err?.message || err || '')\n if (msg.includes('conflict') || msg.includes('entered elsewhere')) {\n alert('已在其他设备接听,本端通话已关闭')\n } else {\n alert('网络异常,请稍后重试')\n }\n emit('close')\n }\n}\n\nasync function reject() {\n stopRing()\n const client = getClient()\n // 被叫拒接:currentSession 为 null(还没 accept),必须显式传 callId 让 SDK 调后端 /call/end\n await client?.call?.end('rejected', props.invite.callId)\n emit('close')\n}\n</script>\n\n<template>\n <div class=\"im-incoming-ring\">\n <audio ref=\"ringAudio\" :src=\"ringCalleeUrl\" loop preload=\"auto\" />\n <div class=\"im-ring-content\">\n <div class=\"im-ring-avatar\"><Icon name=\"answer\" :size=\"24\" color=\"#fff\" /></div>\n <div class=\"im-ring-info\">\n <div class=\"im-ring-name\">{{ invite.from }}</div>\n <div class=\"im-ring-type\">\n {{ invite.callType === 'video' ? '视频通话' : '语音通话' }}\n </div>\n </div>\n <div class=\"im-ring-actions\">\n <button class=\"im-reject\" @click=\"reject\">拒接</button>\n <button class=\"im-accept\" @click=\"accept\">接听</button>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.im-incoming-ring {\n position: fixed;\n top: 24px;\n right: 24px;\n background: #0f172a;\n border-radius: 0;\n padding: 16px;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);\n z-index: 10000;\n animation: slide-in 0.3s ease;\n}\n@keyframes slide-in {\n from {\n transform: translateX(120%);\n }\n to {\n transform: translateX(0);\n }\n}\n.im-ring-content {\n display: flex;\n align-items: center;\n gap: 12px;\n min-width: 280px;\n}\n.im-ring-avatar {\n width: 48px;\n height: 48px;\n border-radius: 50%;\n background: #52c41a;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 24px;\n}\n.im-ring-info {\n flex: 1;\n}\n.im-ring-name {\n font-weight: 600;\n font-size: 15px;\n color: rgba(255, 255, 255, 0.85);\n}\n.im-ring-type {\n font-size: 12px;\n color: rgba(255, 255, 255, 0.55);\n margin-top: 2px;\n}\n.im-ring-actions {\n display: flex;\n gap: 8px;\n}\n.im-ring-actions button {\n padding: 6px 14px;\n border: none;\n border-radius: 0;\n cursor: pointer;\n font-size: 13px;\n color: #fff;\n}\n.im-reject {\n background: #ef4444;\n}\n.im-accept {\n background: #52c41a;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { ref, onMounted, onBeforeUnmount } from 'vue'\nimport type { CallInviteEvent } from '@quitting/contract'\n// 被叫响铃中循环播放 callee.mp3\nimport ringCalleeUrl from '../sounds/ring-callee.mp3'\nimport Icon from './Icon.vue'\n\nconst props = defineProps<{\n invite: CallInviteEvent\n onClose?: () => void\n}>()\n\nconst emit = defineEmits<{\n (e: 'close'): void\n // 接听成功后携带 session 通知父组件渲染 CallWindow\n // 不能依赖父组件 watch currentSession——ImClient 不是 reactive,watch 不会触发\n (e: 'accepted', session: unknown): void\n}>()\n\nconst ringAudio = ref<HTMLAudioElement | null>(null)\n\nfunction getClient() {\n return (window as any).__imClient\n}\n\nfunction startRing() {\n if (!ringAudio.value) return\n ringAudio.value.currentTime = 0\n ringAudio.value.play().catch(() => {\n // 自动播放策略可能拒绝,不报错\n })\n}\n\nfunction stopRing() {\n if (!ringAudio.value) return\n ringAudio.value.pause()\n ringAudio.value.currentTime = 0\n}\n\nconst unsubscribers: Array<() => void> = []\n\nonMounted(() => {\n startRing()\n // 主叫取消 / 无人接听超时,被叫端响铃都应自动关\n // no_answer 必须监听:后端超时会同时通知主叫和被叫,只等 cancelled 会永不停止响铃\n const client = getClient()\n if (client) {\n unsubscribers.push(\n client.on('call:cancelled', () => {\n stopRing()\n emit('close')\n }),\n )\n unsubscribers.push(\n client.on('call:no_answer', () => {\n stopRing()\n emit('close')\n }),\n )\n }\n})\n\nonBeforeUnmount(() => {\n stopRing()\n unsubscribers.forEach((fn) => fn())\n})\n\nasync function accept() {\n stopRing()\n const client = getClient()\n try {\n const session = await client?.call?.accept(props.invite.callId, props.invite.callType)\n emit('accepted', session)\n emit('close')\n } catch (err: any) {\n // 规则1:多端并发进入,本端是后进端被拒(另一端已接听)\n const msg = String(err?.message || err || '')\n if (msg.includes('conflict') || msg.includes('entered elsewhere')) {\n alert('已在其他设备接听,本端通话已关闭')\n } else {\n alert('网络异常,请稍后重试')\n }\n emit('close')\n }\n}\n\nasync function reject() {\n stopRing()\n const client = getClient()\n // 被叫拒接:currentSession 为 null(还没 accept),必须显式传 callId 让 SDK 调后端 /call/end\n await client?.call?.end('rejected', props.invite.callId)\n emit('close')\n}\n</script>\n\n<template>\n <div class=\"im-incoming-ring\">\n <audio ref=\"ringAudio\" :src=\"ringCalleeUrl\" loop preload=\"auto\" />\n <div class=\"im-ring-content\">\n <div class=\"im-ring-avatar\"><Icon name=\"answer\" :size=\"24\" color=\"#fff\" /></div>\n <div class=\"im-ring-info\">\n <div class=\"im-ring-name\">{{ invite.from }}</div>\n <div class=\"im-ring-type\">\n {{ invite.callType === 'video' ? '视频通话' : '语音通话' }}\n </div>\n </div>\n <div class=\"im-ring-actions\">\n <button class=\"im-reject\" @click=\"reject\">拒接</button>\n <button class=\"im-accept\" @click=\"accept\">接听</button>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.im-incoming-ring {\n position: fixed;\n top: 24px;\n right: 24px;\n background: #0f172a;\n border-radius: 0;\n padding: 16px;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);\n z-index: 10000;\n animation: slide-in 0.3s ease;\n}\n@keyframes slide-in {\n from {\n transform: translateX(120%);\n }\n to {\n transform: translateX(0);\n }\n}\n.im-ring-content {\n display: flex;\n align-items: center;\n gap: 12px;\n min-width: 280px;\n}\n.im-ring-avatar {\n width: 48px;\n height: 48px;\n border-radius: 50%;\n background: #52c41a;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 24px;\n}\n.im-ring-info {\n flex: 1;\n}\n.im-ring-name {\n font-weight: 600;\n font-size: 15px;\n color: rgba(255, 255, 255, 0.85);\n}\n.im-ring-type {\n font-size: 12px;\n color: rgba(255, 255, 255, 0.55);\n margin-top: 2px;\n}\n.im-ring-actions {\n display: flex;\n gap: 8px;\n}\n.im-ring-actions button {\n padding: 6px 14px;\n border: none;\n border-radius: 0;\n cursor: pointer;\n font-size: 13px;\n color: #fff;\n}\n.im-reject {\n background: #ef4444;\n}\n.im-accept {\n background: #52c41a;\n}\n</style>\n","import type { App, Plugin } from 'vue'\nimport type { ImClient } from '@quitting/core'\n\nimport { setupGlobalMethods } from './global-methods'\nimport Icon from './components/Icon.vue'\nimport CallWindow from './components/CallWindow.vue'\nimport CallDialog from './components/CallDialog.vue'\nimport CallOutgoing from './components/CallOutgoing.vue'\nimport MeetWindow from './components/MeetWindow.vue'\nimport IncomingRing from './components/IncomingRing.vue'\nimport MeetingInvite from './components/MeetingInvite.vue'\nimport ChatPanel from './components/ChatPanel.vue'\nimport UserPickerList from './components/UserPickerList.vue'\nimport UserPickerDialog from './components/UserPickerDialog.vue'\nimport MeetingCreateDialog from './components/MeetingCreateDialog.vue'\n\n// 半 UI SDK 插件\n// 用法(业务方 main.ts):\n// import ImUI from '@quitting/ui'\n// import '@quitting/ui/style.css'\n// app.use(ImUI, { client: imClient })\n//\n// 之后任何业务组件都可调:\n// ImUI.chat('user-1')\n// ImUI.call('user-1', 'video')\n// ImUI.meeting({ invitees: ['user-1','user-2'], title: '周会' })\n// ImUI.meeting() // 不传参会人 → 弹内置选人窗\n//\n// client 在登录后才创建时(无法 main.ts 装插件),登录后手动调:\n// import { setupImUI } from '@quitting/ui'\n// setupImUI(client, getCurrentInstance()!.appContext.app)\nexport interface ImUIOptions {\n client: ImClient\n // 关闭自动弹窗监听(call:invite / meeting:invite 等不再自动弹窗),\n // 业务方完全自定义 UI 时置 false;ImUI.* 主动方法不受影响。默认 true\n autoPopups?: boolean\n // uid → { name, dept? } 自定义解析器:透传给自动弹出的 MeetWindow\n // (后端成员表 name 不全/有业务侧别名时用;缺省走 SDK 用户目录)\n resolveUser?: (uid: string) => { name: string; dept?: string } | undefined\n}\n\nexport const ImUIPlugin: Plugin = {\n install(app: App, options: ImUIOptions) {\n if (!options?.client) {\n throw new Error('ImUI 安装时必须传入 client(已初始化的 ImClient)')\n }\n // 注册全局组件\n app.component('ImIcon', Icon)\n app.component('ImCallWindow', CallWindow)\n app.component('ImCallDialog', CallDialog)\n app.component('ImCallOutgoing', CallOutgoing)\n app.component('ImMeetWindow', MeetWindow)\n app.component('ImIncomingRing', IncomingRing)\n app.component('ImMeetingInvite', MeetingInvite)\n app.component('ImChatPanel', ChatPanel)\n app.component('ImUserPickerList', UserPickerList)\n app.component('ImUserPickerDialog', UserPickerDialog)\n app.component('ImMeetingCreateDialog', MeetingCreateDialog)\n\n // 注入全局方法 + 自动监听邀请事件\n setupGlobalMethods(options.client, app, {\n autoPopups: options.autoPopups,\n resolveUser: options.resolveUser,\n })\n },\n}\n\n// 手动初始化(client 延迟创建场景):等价于 app.use(ImUIPlugin, { client })\nexport function setupImUI(\n client: ImClient,\n app: App,\n opts?: {\n autoPopups?: boolean\n resolveUser?: (uid: string) => { name: string; dept?: string } | undefined\n },\n) {\n setupGlobalMethods(client, app, opts)\n}\n\nexport default ImUIPlugin\n\n// 重新导出组件供按需引入\nexport {\n Icon,\n CallWindow,\n CallDialog,\n CallOutgoing,\n MeetWindow,\n IncomingRing,\n MeetingInvite,\n ChatPanel,\n UserPickerList,\n UserPickerDialog,\n MeetingCreateDialog,\n}\nexport { ImUI } from './global-methods'\n// toast:ImUI 链路错误/提示的默认出口,业务方也可直接用(如 toast.error('...'))\nexport { ImToastApi as ImToast } from './toast'\n// ApiError → 用户可读文案的统一映射(自定义错误处理场景用)\nexport { friendlyApiError } from './error-text'\nexport type { PickerUser } from './types'\n"],"mappings":"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"}
1
+ {"version":3,"file":"im-ui.es.js","names":[],"sources":["../src/mount.ts","../src/client-access.ts","../src/components/ImToast.vue","../src/components/ImToast.vue","../src/toast.ts","../src/error-text.ts","../src/directives/draggable.ts","../src/components/Icon.vue","../src/components/Icon.vue","../src/components/CallWindow.vue","../src/components/CallWindow.vue","../src/sounds/ring-caller.mp3","../src/sounds/ring-callee.mp3","../src/components/CallDialog.vue","../src/components/CallDialog.vue","../src/components/meeting/icons.ts","../src/components/meeting/MeetingHeader.vue","../src/components/meeting/MeetingHeader.vue","../src/components/meeting/MainVideoTile.vue","../src/components/meeting/MainVideoTile.vue","../src/components/meeting/SmallVideoTile.vue","../src/components/meeting/SmallVideoTile.vue","../src/components/meeting/VideoListPanel.vue","../src/components/meeting/VideoListPanel.vue","../src/components/meeting/MemberItem.vue","../src/components/meeting/MemberItem.vue","../src/components/meeting/MemberListPanel.vue","../src/components/meeting/MemberListPanel.vue","../src/components/meeting/SidebarTabs.vue","../src/components/meeting/SidebarTabs.vue","../src/components/meeting/MeetingControlBar.vue","../src/components/meeting/MeetingControlBar.vue","../src/components/ConfirmDialog.vue","../src/components/ConfirmDialog.vue","../src/components/MeetingEndedToast.vue","../src/components/MeetingEndedToast.vue","../src/components/UserPickerList.vue","../src/components/UserPickerList.vue","../src/components/UserPickerDialog.vue","../src/components/UserPickerDialog.vue","../src/components/MeetWindow.vue","../src/components/MeetWindow.vue","../src/components/MeetingInvite.vue","../src/components/MeetingInvite.vue","../src/components/MeetingCreateDialog.vue","../src/components/MeetingCreateDialog.vue","../src/components/ChatPanel.vue","../src/components/ChatPanel.vue","../src/global-methods.ts","../src/components/CallOutgoing.vue","../src/components/CallOutgoing.vue","../src/components/IncomingRing.vue","../src/components/IncomingRing.vue","../src/index.ts"],"sourcesContent":["import { createApp, h, type App, type Component } from 'vue'\n\n// 动态 mount 工具:把弹窗组件挂到独立 DOM(不依赖业务方在 template 里放组件)\n// global-methods 的自动弹窗、MeetWindow 内置邀请弹窗共用\n\nexport interface MountedRef {\n subApp: App\n div: HTMLDivElement\n}\n\n// 主 app 实例:setupGlobalMethods 时注册,动态子应用继承其全局配置(组件/插件)\n// 未注册(业务方直接在 template 用组件、未装插件)时子应用不继承,组件自包含不受影响\nlet rootApp: App | null = null\n\nexport function setRootApp(app: App | null) {\n rootApp = app\n}\n\n// props 里的 onXxx(onConfirm/onAccepted 等)透传为组件事件监听;\n// onClose 由本函数接管负责清理,调用方可额外传 onClose 做状态回置\nexport function mountComponent(\n component: Component,\n props: Record<string, unknown>,\n mountPoint?: HTMLElement,\n): MountedRef {\n const { onClose: onCloseProp, ...rest } = props\n const onClose = onCloseProp as (() => void) | undefined\n const div = document.createElement('div')\n div.classList.add('im-ui-root')\n // 挂进 pointer-events:none 的容器(如邀请弹窗堆叠层)时子树保持可交互;\n // 挂 body 时 auto 即默认值,无影响\n div.style.pointerEvents = 'auto'\n ;(mountPoint || document.body).appendChild(div)\n const subApp = createApp({\n render: () =>\n h(component, {\n ...rest,\n onClose: () => {\n onClose?.()\n subApp.unmount()\n div.remove()\n },\n }),\n })\n if (rootApp) {\n // 继承主 app 的全局组件/插件\n subApp.config = rootApp.config\n }\n subApp.mount(div)\n return { subApp, div }\n}\n\nexport function unmountRef(ref: MountedRef | null) {\n if (!ref) return\n ref.subApp.unmount()\n ref.div.remove()\n}\n","import type { ImClient } from '@quitting/core'\n\n// window.__imClient 的类型化访问。\n// setupGlobalMethods 注入实例;UI 组件内部 getClient() 从这里读取。\n// (历史组件各自用 (window as any).__imClient,新代码统一走本模块)\ndeclare global {\n interface Window {\n __imClient?: ImClient\n }\n}\n\nexport function getImClient(): ImClient | undefined {\n // eslint-disable-next-line eslint/no-underscore-dangle -- 全局键名沿用历史约定(组件 getClient 依赖)\n return window.__imClient\n}\n\nexport function setImClient(c: ImClient | undefined) {\n // eslint-disable-next-line eslint/no-underscore-dangle -- 同上\n window.__imClient = c\n}\n","<script setup lang=\"ts\">\n// 全局 toast 宿主:渲染 toast.ts 维护的消息列表(fixed 顶部居中,多条向下堆叠)\n// 样式变量 --im-toast-* 可在 :root/祖先节点覆盖(无需 :deep() 穿透 scoped)\nexport interface ToastItem {\n id: number\n message: string\n type: 'info' | 'success' | 'error'\n}\n\ndefineProps<{ items: ToastItem[] }>()\n</script>\n\n<template>\n <div class=\"im-toast-host\">\n <div v-for=\"t in items\" :key=\"t.id\" :class=\"['im-toast', `im-toast-${t.type}`]\" role=\"status\">\n {{ t.message }}\n </div>\n </div>\n</template>\n\n<style scoped>\n.im-toast-host {\n /* 宿主仅定位,颜色/尺寸全部走变量;默认值业务方可整体覆盖 */\n --im-toast-bg: #323232;\n --im-toast-bg-success: #2e7d32;\n --im-toast-bg-error: #d32f2f;\n --im-toast-color: #fff;\n --im-toast-radius: 6px;\n --im-toast-font-size: 14px;\n --im-toast-z: 100000;\n\n position: fixed;\n top: 24px;\n left: 50%;\n transform: translateX(-50%);\n z-index: var(--im-toast-z);\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 8px;\n pointer-events: none;\n}\n.im-toast {\n max-width: 70vw;\n padding: 9px 18px;\n border-radius: var(--im-toast-radius);\n background: var(--im-toast-bg);\n color: var(--im-toast-color);\n font-size: var(--im-toast-font-size);\n line-height: 1.5;\n word-break: break-all;\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.18);\n animation: im-toast-in 0.2s ease-out;\n}\n.im-toast-success {\n background: var(--im-toast-bg-success);\n}\n.im-toast-error {\n background: var(--im-toast-bg-error);\n}\n@keyframes im-toast-in {\n from {\n opacity: 0;\n transform: translateY(-8px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n</style>\n","<script setup lang=\"ts\">\n// 全局 toast 宿主:渲染 toast.ts 维护的消息列表(fixed 顶部居中,多条向下堆叠)\n// 样式变量 --im-toast-* 可在 :root/祖先节点覆盖(无需 :deep() 穿透 scoped)\nexport interface ToastItem {\n id: number\n message: string\n type: 'info' | 'success' | 'error'\n}\n\ndefineProps<{ items: ToastItem[] }>()\n</script>\n\n<template>\n <div class=\"im-toast-host\">\n <div v-for=\"t in items\" :key=\"t.id\" :class=\"['im-toast', `im-toast-${t.type}`]\" role=\"status\">\n {{ t.message }}\n </div>\n </div>\n</template>\n\n<style scoped>\n.im-toast-host {\n /* 宿主仅定位,颜色/尺寸全部走变量;默认值业务方可整体覆盖 */\n --im-toast-bg: #323232;\n --im-toast-bg-success: #2e7d32;\n --im-toast-bg-error: #d32f2f;\n --im-toast-color: #fff;\n --im-toast-radius: 6px;\n --im-toast-font-size: 14px;\n --im-toast-z: 100000;\n\n position: fixed;\n top: 24px;\n left: 50%;\n transform: translateX(-50%);\n z-index: var(--im-toast-z);\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 8px;\n pointer-events: none;\n}\n.im-toast {\n max-width: 70vw;\n padding: 9px 18px;\n border-radius: var(--im-toast-radius);\n background: var(--im-toast-bg);\n color: var(--im-toast-color);\n font-size: var(--im-toast-font-size);\n line-height: 1.5;\n word-break: break-all;\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.18);\n animation: im-toast-in 0.2s ease-out;\n}\n.im-toast-success {\n background: var(--im-toast-bg-success);\n}\n.im-toast-error {\n background: var(--im-toast-bg-error);\n}\n@keyframes im-toast-in {\n from {\n opacity: 0;\n transform: translateY(-8px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n</style>\n","import { reactive } from 'vue'\nimport { createApp, h } from 'vue'\n\nimport ImToast, { type ToastItem } from './components/ImToast.vue'\n\n// 轻量 toast:首次调用时往 body 动态挂一个宿主(.im-toast-root),之后只维护响应式列表\n// 用法:toast.error('对方正忙') / toast.success('已发送') / ImToast.info(...)\n// 颜色/层级定制::root 覆盖 --im-toast-* 变量(见 ImToast.vue)\n\nconst state = reactive<{ items: ToastItem[] }>({ items: [] })\nlet hostMounted = false\nlet seq = 0\n\nfunction ensureHost() {\n if (hostMounted) return\n const div = document.createElement('div')\n div.className = 'im-toast-root'\n document.body.appendChild(div)\n createApp({ render: () => h(ImToast, { items: state.items }) }).mount(div)\n hostMounted = true\n}\n\nfunction show(message: string, type: ToastItem['type'], duration: number) {\n ensureHost()\n const id = ++seq\n state.items.push({ id, message, type })\n setTimeout(() => {\n const idx = state.items.findIndex((t) => t.id === id)\n if (idx >= 0) state.items.splice(idx, 1)\n }, duration)\n}\n\nfunction info(message: string, duration = 3000) {\n show(message, 'info', duration)\n}\nfunction success(message: string, duration = 3000) {\n show(message, 'success', duration)\n}\nfunction error(message: string, duration = 4000) {\n show(message, 'error', duration)\n}\n\n// 业务方入口:ImToast.error(...);与组件名不冲突(导出为值而非组件)\nexport const ImToastApi = { info, success, error }\n","import { ApiError, ApiErrorCode } from '@quitting/core'\n\n// ApiError → 用户可读文案的统一映射(demo 三处红条逻辑收编于此)\n// 业务方不满足默认文案时可自行 catch 后用 toast 显示自定义文案\n\nexport function friendlyApiError(err: unknown, fallback = '请求失败'): string {\n if (err instanceof ApiError) {\n switch (err.code) {\n case ApiErrorCode.Busy:\n return '对方正忙,请稍后再试'\n case ApiErrorCode.Conflict:\n return '操作冲突,请重试'\n case ApiErrorCode.NotFound:\n return '用户不存在'\n case ApiErrorCode.Unauth:\n case ApiErrorCode.SessionKicked:\n return '登录状态已失效,请重新登录'\n case ApiErrorCode.Forbidden:\n return '无权执行该操作'\n default:\n return err.message || fallback\n }\n }\n return '网络异常,请稍后重试'\n}\n","import type { Directive, DirectiveBinding } from 'vue'\n\n// v-draggable:让元素可在视口内拖拽(基于 position: fixed)\n//\n// 用法:\n// <div v-draggable>整块都可拖</div>\n// <div v-draggable=\"'.header'\">只有 .header 子元素是拖拽句柄</div>\n//\n// 与 im-web/src/directives/draggable.ts 的差异:\n// - 基于 viewport(fixed)而非父元素(absolute)—— 通话窗口可挪到屏幕任意位置\n// - 用 addEventListener 而非覆盖 onmousedown,可与其他指令/事件共存\n// - unmounted 时清理 listener,避免泄漏\n// - 支持指定拖拽句柄(header),避免整块内容都触发拖拽(如输入框)\n//\n// 边界约束:元素中心点不超出视口;初始位置由元素 CSS 决定(不强制 left/top)\n\ninterface DragState {\n startX: number\n startY: number\n origLeft: number\n origTop: number\n}\n\nfunction getHandle(el: HTMLElement, binding: DirectiveBinding): HTMLElement {\n const selector = binding.value as string | undefined\n if (!selector) return el\n const handle = el.querySelector<HTMLElement>(selector)\n return handle || el\n}\n\nfunction onStart(e: MouseEvent, el: HTMLElement, state: DragState) {\n // 输入控件不触发拖拽(避免与 textarea/input select 冲突)\n const tag = (e.target as HTMLElement).tagName\n if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return\n\n const rect = el.getBoundingClientRect()\n state.startX = e.clientX\n state.startY = e.clientY\n state.origLeft = rect.left\n state.origTop = rect.top\n // 切到 fixed 并锁定当前坐标,避免后续 CSS 影响跳变\n // 关键:清掉 transform(如居中用的 translate(-50%, -50%)),否则 left/top 会双重偏移\n el.style.position = 'fixed'\n el.style.transform = 'none'\n el.style.left = `${rect.left}px`\n el.style.top = `${rect.top}px`\n el.style.right = 'auto'\n el.style.bottom = 'auto'\n el.style.margin = '0'\n el.classList.add('v-dragging')\n\n const onMove = (ev: MouseEvent) => {\n const dx = ev.clientX - state.startX\n const dy = ev.clientY - state.startY\n const newLeft = state.origLeft + dx\n const newTop = state.origTop + dy\n // 视口边界约束(保证元素至少有 50px 可见)\n const maxX = window.innerWidth - 50\n const maxY = window.innerHeight - 50\n const minX = 50 - el.offsetWidth\n const minY = 0\n el.style.left = `${Math.min(Math.max(newLeft, minX), maxX)}px`\n el.style.top = `${Math.min(Math.max(newTop, minY), maxY)}px`\n }\n const onUp = () => {\n document.removeEventListener('mousemove', onMove)\n document.removeEventListener('mouseup', onUp)\n el.classList.remove('v-dragging')\n }\n document.addEventListener('mousemove', onMove)\n document.addEventListener('mouseup', onUp)\n}\n\nconst draggable: Directive<HTMLElement> = {\n mounted(el, binding) {\n const handle = getHandle(el, binding)\n handle.style.cursor = 'move'\n const state: DragState = { startX: 0, startY: 0, origLeft: 0, origTop: 0 }\n const handler = (e: MouseEvent) => onStart(e, el, state)\n handle.addEventListener('mousedown', handler)\n // 存到 el 上方便 unmounted 清理\n ;(el as any).__dragHandle__ = handle\n ;(el as any).__dragHandler__ = handler\n },\n unmounted(el) {\n const handle = (el as any).__dragHandle__ as HTMLElement | undefined\n const handler = (el as any).__dragHandler__ as ((e: MouseEvent) => void) | undefined\n if (handle && handler) {\n handle.removeEventListener('mousedown', handler)\n }\n },\n}\n\nexport default draggable\n","<script setup lang=\"ts\">\n// 通用图标组件:SVG 路径内联在 ICON_PATHS 常量里,无运行时文件加载\n// 这样通过 npm 安装到其他项目后无需额外配置即可正常显示\n//\n// 用法:\n// <Icon name=\"mic-on\" :size=\"18\" />\n// <Icon name=\"mic-off\" :size=\"18\" color=\"#ef4444\" />\n//\n// 颜色继承 currentColor(父元素的 color CSS 属性),或显式传 color prop\n\n// 所有图标统一 viewBox=\"0 0 1024 1024\",方便对齐\n// fill 默认移除(继承 currentColor),少数图标有特殊 fill 在 path 上保留\nconst ICON_PATHS = {\n // 麦克风开启\n 'mic-on': `<path d=\"M514.986667 112c80.170667 0 145.109333 62.08 145.109333 138.666667v277.248c0 76.586667-64.938667 138.624-145.066667 138.624-80.213333 0-145.152-62.08-145.152-138.624V250.666667c0-76.586667 64.981333-138.666667 145.109334-138.666667z m236.970666 411.221333c0 118.485333-93.013333 215.893333-212.608 228.992v135.637334a24.149333 24.149333 0 0 1-48.341333 0v-135.04c-122.709333-10.24-219.008-109.013333-219.008-229.546667v-96.426667a24.704 24.704 0 0 1 49.408 0v109.184c0 98.56 83.712 169.088 190.592 169.088 106.837333 0 196.309333-92.8 196.309333-169.088V423.936a21.845333 21.845333 0 0 1 43.690667 0v99.285333z\"/>`,\n // 麦克风关闭\n 'mic-off': `<path d=\"M656.384 559.232L369.877333 272.725333v-22.058666c0-76.586667 64.981333-138.666667 145.109334-138.666667 80.170667 0 145.109333 62.08 145.109333 138.666667v277.248c0 10.794667-1.28 21.248-3.712 31.317333z m-84.693333 96.341333a150.528 150.528 0 0 1-56.704 10.965334c-80.128 0-145.066667-62.08-145.066667-138.624v-74.154667l201.770667 201.813333z m153.898666-27.136l-29.610666-29.568a165.973333 165.973333 0 0 0 12.373333-62.848V423.936a21.845333 21.845333 0 0 1 43.648 0v99.285333c0 37.930667-9.557333 73.685333-26.453333 105.216z m-89.173333 91.904a245.290667 245.290667 0 0 1-97.066667 31.872v135.637334a24.149333 24.149333 0 0 1-48.341333 0v-135.04c-122.709333-10.24-219.008-109.013333-219.008-229.546667v-96.426667a24.704 24.704 0 0 1 49.408 0v109.184c0 98.56 86.613333 178.474667 193.450667 178.474667 33.92 0 65.834667-8.064 93.568-22.186667l28.032 28.032zM145.28 183.893333l45.226667-45.226666 678.826666 678.826666-45.226666 45.226667z\"/>`,\n // 摄像头开启\n 'camera-on': `<path d=\"M873.770667 314.922667c19.797333-12.202667 37.632-2.048 37.632 18.304v335.232c0 24.362667-15.872 32.512-37.632 18.261333l-112.938667-67.029333c-19.797333-12.202667-37.632-26.453333-37.632-46.72V424.618667c0-18.261333 17.834667-30.464 37.632-42.666667l112.938667-67.029333zM207.232 288h391.936c43.562667 0 79.232 32 79.232 71.125333v305.749334c0 39.125333-35.669333 71.125333-79.232 71.125333H207.232C163.669333 736 128 704 128 664.874667V359.125333C128 320 163.669333 288 207.232 288z\"/>`,\n // 摄像头关闭\n 'camera-off': `<path d=\"M873.770667 314.922667c19.797333-12.202667 37.632-2.048 37.632 18.304v335.232c0 24.362667-15.872 32.512-37.632 18.261333l-112.938667-67.029333c-19.797333-12.202667-37.632-26.453333-37.632-46.72V424.618667c0-18.261333 17.834667-30.464 37.632-42.666667l112.938667-67.029333zM385.152 288h214.016c43.562667 0 79.232 32 79.232 71.125333v222.122667L385.152 288z m256.042667 437.077333a85.333333 85.333333 0 0 1-42.026667 10.922667H207.232C163.669333 736 128 704 128 664.874667V359.125333c0-38.186667 34.005333-69.632 76.16-71.082666l437.034667 437.034666zM145.28 183.893333l45.226667-45.226666 678.826666 678.826666-45.226666 45.226667z\"/>`,\n // 声音(扬声器开启)\n 'sound-on': `<path d=\"M767.317333 767.018667a33.237333 33.237333 0 0 1-20.565333 6.656 34.56 34.56 0 0 1-25.386667-10.709334 34.986667 34.986667 0 0 1-10.666666-25.557333 35.2 35.2 0 0 1 13.994666-28.501333 5.76 5.76 0 0 0 1.450667-1.109334l0.725333-0.725333c13.738667-9.386667 26.453333-20.48 38.272-33.28 11.733333-12.842667 22.058667-26.794667 30.848-41.813333 8.832-15.061333 15.701333-31.146667 20.608-48.128a190.293333 190.293333 0 0 0 7.338667-52.906667c0-37.034667-9.301333-70.826667-27.946667-101.418667a250.794667 250.794667 0 0 0-69.845333-75.52 34.730667 34.730667 0 0 1-15.445333-29.568 34.986667 34.986667 0 0 1 10.666666-25.557333 34.56 34.56 0 0 1 25.386667-10.709333c8.32 0 15.658667 2.688 22.058667 8.106666 18.133333 12.842667 35.029333 27.776 50.730666 44.8 15.658667 17.066667 29.141333 35.541333 40.448 55.509334 11.264 19.968 20.053333 41.344 26.453334 64 6.4 22.698667 9.557333 46.165333 9.557333 70.314666 0 24.704-3.2 48.384-9.557333 71.082667a285.653333 285.653333 0 0 1-26.837334 64.384 305.450667 305.450667 0 0 1-92.288 100.693333v-0.042666z m-41.173333-332.330667c10.282667 14.336 18.517333 28.757333 24.661333 43.306667 6.101333 14.549333 9.173333 32.213333 9.173334 52.906666 0 19.754667-3.2 37.546667-9.557334 53.333334-6.4 15.786667-14.464 30.08-24.277333 42.88-3.925333 5.461333-8.106667 10.24-12.501333 14.464a23.552 23.552 0 0 1-16.896 6.272 29.866667 29.866667 0 0 1-22.442667-9.6 32.085333 32.085333 0 0 1-9.173333-22.954667 33.706667 33.706667 0 0 1 2.218666-11.861333h-0.768c2.474667-5.418667 5.418667-10.581333 8.832-15.530667 3.413333-4.949333 6.741333-9.984 9.941334-15.146667a84.48 84.48 0 0 0 8.106666-17.792c2.176-6.656 3.285333-14.677333 3.285334-24.064 0-8.874667-1.109333-16.64-3.328-23.296a94.208 94.208 0 0 0-8.106667-18.133333 240.170667 240.170667 0 0 0-9.898667-15.530667 67.072 67.072 0 0 1-8.106666-15.573333 29.994667 29.994667 0 0 1-2.176-11.818667c0-8.874667 3.072-16.512 9.173333-22.954666a29.866667 29.866667 0 0 1 22.442667-9.6c6.826667 0 12.501333 2.090667 16.896 6.272 4.437333 4.181333 8.576 9.002667 12.501333 14.421333z m-237.482667 439.637333a9078.869333 9078.869333 0 0 0-157.354666-139.136 10554.453333 10554.453333 0 0 0-83.797334-70.314666 177.408 177.408 0 0 1-16.213333 0.725333 125.781333 125.781333 0 0 1-13.952 0.768h-12.501333c-13.226667 0-24.021333-4.096-32.341334-12.202667a39.424 39.424 0 0 1-12.501333-29.269333v-186.453333h0.725333l-0.725333-0.768c0-13.824 4.266667-25.173333 12.885333-34.048a42.666667 42.666667 0 0 1 32-13.312h41.856a13012.906667 13012.906667 0 0 1 122.794667-103.637334c15.701333-13.312 31.146667-26.282667 46.336-38.826666 15.189333-12.586667 28.928-24.064 41.173333-34.432 12.245333-10.368 22.570667-18.986667 30.890667-25.898667 8.32-6.912 13.482667-11.093333 15.445333-12.586667 14.677333-10.88 26.581333-15.786667 35.626667-14.805333 9.088 0.981333 16.213333 5.418667 21.333333 13.312s8.704 17.493333 10.666667 28.885333c1.962667 11.349333 2.944 21.674667 2.944 31.061334v599.466666c0 9.386667-1.706667 19.413333-5.12 29.994667-3.456 10.624-8.746667 19.029333-15.829333 25.173333-7.125333 6.186667-16.042667 8.746667-26.88 7.765334-10.752-0.981333-23.253333-8.106667-37.461334-21.461334z\"/>`,\n // 静音(扬声器关闭)\n 'sound-off': `<path d=\"M767.872 670.72c10.624-11.946667 20.010667-24.917333 28.16-38.784 8.789333-15.018667 15.658667-31.061333 20.565333-48.085333a190.293333 190.293333 0 0 0 7.338667-52.906667c0-37.034667-9.301333-70.826667-27.946667-101.418667a250.794667 250.794667 0 0 0-69.845333-75.52 34.730667 34.730667 0 0 1-15.445333-29.568 34.986667 34.986667 0 0 1 10.666666-25.557333 34.56 34.56 0 0 1 25.386667-10.709333c8.32 0 15.658667 2.688 22.058667 8.106666 18.133333 12.842667 35.029333 27.776 50.730666 44.8 15.658667 17.066667 29.141333 35.541333 40.448 55.509334 11.264 19.968 20.053333 41.344 26.453334 64 6.4 22.698667 9.557333 46.165333 9.557333 70.314666 0 24.704-3.2 48.384-9.557333 71.082667a285.653333 285.653333 0 0 1-26.837334 64.384 304.64 304.64 0 0 1-40.533333 55.552l-51.2-51.2zM725.333333 628.224l-46.08-46.08c2.090667-3.072 4.096-6.229333 6.058667-9.386667a84.48 84.48 0 0 0 8.106667-17.792c2.176-6.656 3.285333-14.677333 3.285333-24.064 0-8.874667-1.109333-16.64-3.328-23.296a94.208 94.208 0 0 0-8.106667-18.133333 240.170667 240.170667 0 0 0-9.898666-15.530667 67.072 67.072 0 0 1-8.106667-15.573333 29.994667 29.994667 0 0 1-2.176-11.818667c0-8.874667 3.072-16.512 9.173333-22.954666a29.866667 29.866667 0 0 1 22.442667-9.6c6.826667 0 12.501333 2.090667 16.896 6.272 4.437333 4.181333 8.576 9.002667 12.501333 14.421333 10.282667 14.336 18.517333 28.757333 24.661334 43.306667 6.101333 14.549333 9.173333 32.213333 9.173333 52.906666 0 19.754667-3.2 37.546667-9.557333 53.333334A183.168 183.168 0 0 1 725.333333 628.181333zM278.997333 362.837333l294.954667 294.997334v175.061333c0 9.386667-1.706667 19.370667-5.12 29.952-3.456 10.624-8.746667 19.029333-15.829333 25.173333-7.125333 6.186667-16.042667 8.746667-26.88 7.765334-10.752-0.981333-23.253333-8.106667-37.461334-21.461334a9078.869333 9078.869333 0 0 0-157.354666-139.136 10554.453333 10554.453333 0 0 0-83.797334-70.314666 177.408 177.408 0 0 1-16.213333 0.725333 125.781333 125.781333 0 0 1-13.952 0.768h-12.501333c-13.226667 0-24.021333-4.096-32.341334-12.202667a39.424 39.424 0 0 1-12.501333-29.269333v-186.453333h0.725333l-0.725333-0.768c0-13.824 4.266667-25.173333 12.885333-34.048a42.666667 42.666667 0 0 1 32-13.312h41.856l32.256-27.434667z m98.261334-82.688c13.056-11.050667 25.898667-21.802667 38.613333-32.341333 15.189333-12.544 28.928-24.021333 41.173333-34.389333 12.245333-10.368 22.570667-18.986667 30.890667-25.898667 8.32-6.912 13.482667-11.093333 15.445333-12.586667 14.677333-10.88 26.581333-15.786667 35.626667-14.805333 9.088 0.981333 16.213333 5.418667 21.333333 13.312s8.704 17.493333 10.666667 28.885333c1.962667 11.349333 2.944 21.674667 2.944 31.061334v243.413333l-196.693333-196.693333zM145.28 183.893333l45.226667-45.226666 678.826666 678.826666-45.226666 45.226667z\"/>`,\n // 接听(电话听筒)\n answer: `<path d=\"M864.597333 647.338667l11.52 11.562666c0.341333 0.682667 0.597333 1.408 1.066667 2.005334 11.52 14.762667 14.848 32.213333 10.325333 52.266666-3.285333 14.421333-9.216 28.16-16.597333 41.386667-6.613333 11.861333-13.994667 23.381333-21.034667 34.986667l-9.045333 9.088c-5.845333 3.541333-11.477333 7.594667-17.493333 10.453333-15.018667 7.04-30.421333 8.533333-45.781334 8.490667-20.906667-0.042667-40.96-3.498667-60.970666-7.210667-32.810667-6.101333-64.896-13.952-94.933334-26.88-42.154667-18.133333-79.658667-42.922667-115.2-70.528a987.818667 987.818667 0 0 1-78.08-68.010667c-0.682667-0.682667-1.536-1.152-2.346666-1.706666l-44.373334-44.373334c-0.682667-0.938667-1.322667-1.962667-2.133333-2.816-19.072-22.058667-38.656-43.690667-57.216-66.218666a620.117333 620.117333 0 0 1-60.928-88.746667C236.245333 396.928 216.32 350.421333 212.608 296.362667c-1.450667-20.693333-4.224-41.088-2.389333-62.336 1.237333-14.677333 4.053333-29.184 11.52-43.008 2.048-3.797333 4.522667-7.381333 6.826666-11.050667l9.045334-9.045333c8.32-5.248 16.554667-10.538667 24.917333-15.616 13.909333-8.490667 27.733333-17.109333 42.965333-22.442667 15.957333-5.632 30.634667-4.778667 43.605334 3.2 4.906667 3.029333 9.429333 6.613333 14.122666 9.941333l8.789334 8.789334c0.768 1.066667 1.493333 2.133333 2.304 3.157333 4.266667 5.12 8.746667 10.026667 12.714666 15.36 23.637333 31.616 42.325333 66.56 58.581334 103.082667 8.448 19.029333 6.186667 38.741333-6.826667 58.154666-6.101333 9.045333-13.226667 17.621333-20.736 25.642667-14.506667 15.488-19.84 32.213333-15.616 50.56 1.536 6.570667 3.754667 12.928 6.570667 18.901333 9.258667 19.754667 22.826667 36.266667 37.76 51.626667 27.178667 27.861333 54.869333 55.338667 82.389333 82.901333 15.488 15.488 31.530667 30.378667 50.133333 42.24 11.52 7.381333 23.68 13.525333 37.973334 15.445334 14.08 1.962667 27.562667-1.365333 39.808-11.264 6.442667-5.205333 12.330667-11.136 18.645333-16.512 22.784-19.285333 46.506667-25.642667 71.850667-15.914667 37.546667 14.506667 71.253333 34.645333 102.826666 58.197333 4.778667 3.584 9.472 7.338667 14.208 10.965334z\"/>`,\n // 挂断\n hangup: `<path d=\"M68.820008 642.540165a22.883151 22.883151 0 0 0 13.903686 21.04864c2.800052 1.158641 5.793203 1.737961 8.689804 1.737961l216.37612-0.38621a22.786601 22.786601 0 0 0 22.786601-22.786601l0-87.670471c11.682955-13.807125 25.779752-25.296981 41.517949-33.986785a274.115036 274.115036 0 0 1 140.002394-34.276445l0.482771 0a270.832225 270.832225 0 0 1 139.616184 34.083335c15.641647 8.786365 29.641883 20.276209 41.228289 34.083346l0 87.1877a22.69004 22.69004 0 0 0 22.59349 22.786601l194.072289-0.28966 22.30383 0a22.59349 22.59349 0 0 0 22.690051-22.786601l0.09655-49.531883a22.207281 22.207281 0 0 0-0.67587-5.310432l-0.965541-3.958692a150.140499 150.140499 0 0 0-11.586405-46.828392c-43.352459-100.898276-222.748643-176.982342-417.303691-176.982342l-0.965541 0a868.01487 868.01487 0 0 0-229.024605 32.152275c-76.566836 21.72451-137.105804 52.138824-170.609819 85.64285-17.765829 17.283058-32.152275 37.752367-42.290379 60.442418-8.207045 18.441688-12.551947 38.428237-12.841607 58.607897l-0.09655 47.021491z\"/>`,\n // 举手\n handup: `<path d=\"M166.997333 665.6H150.613333c-0.725333-0.213333-1.450667-0.597333-2.133333-0.682667-18.602667-2.261333-33.28-12.245333-44.288-29.653333a155.818667 155.818667 0 0 1-17.536-41.002667c-3.712-13.056-6.613333-26.410667-9.898667-39.594666v-12.842667c1.621333-6.613333 2.730667-13.482667 4.992-19.754667 5.632-15.573333 15.488-27.52 26.368-38.4 14.805333-14.72 31.402667-26.453333 48.213334-37.973333 27.477333-18.901333 55.765333-36.010667 86.144-48.128 42.624-16.981333 86.613333-25.941333 131.285333-31.573333 34.346667-4.266667 68.778667-6.4 103.296-7.125334 0.981333 0 1.92-0.298667 2.901333-0.469333h62.72c1.152 0.170667 2.346667 0.426667 3.498667 0.512 29.098667 2.090667 58.24 3.541333 87.296 6.314667 35.712 3.413333 70.997333 10.112 105.856 19.712 48.981333 13.44 95.957333 32.256 136.832 67.84 15.616 13.653333 32 26.069333 45.781333 42.368 9.472 11.264 17.749333 23.466667 22.229334 38.570666 1.28 4.138667 2.005333 8.405333 2.986666 12.629334v12.8c-2.176 9.6-4.266667 19.2-6.570666 28.714666-3.84 15.786667-7.552 31.701333-14.506667 46.208-7.338667 15.274667-18.304 25.002667-33.109333 28.586667-5.632 1.28-11.349333 1.962667-17.066667 2.944h-12.373333c-1.28-0.213333-2.56-0.469333-3.84-0.597333-6.656-0.64-13.269333-0.938667-19.882667-1.877334-39.082667-5.632-77.013333-17.152-114.346667-31.488-19.413333-7.466667-31.701333-22.997333-36.266666-45.952a205.098667 205.098667 0 0 1-3.456-32.810666c-0.682667-21.162667-8.746667-36.778667-24.746667-46.762667a94.72 94.72 0 0 0-17.92-8.704c-20.565333-7.424-41.856-9.514667-63.232-9.813333-38.954667-0.469333-77.952-0.341333-116.906667-0.341334-21.888 0-43.776 0.810667-65.365333 5.546667-13.312 2.944-26.24 7.210667-37.717333 15.914667-11.349333 8.618667-18.517333 20.48-20.181334 36.181333-0.853333 8.192-0.853333 16.554667-1.493333 24.789333-2.474667 29.781333-14.805333 51.072-39.594667 62.08-36.736 16.298667-74.88 25.898667-113.834666 31.573334-5.973333 0.853333-11.861333 1.493333-17.792 2.261333z\"/>`,\n // 关闭(叉)\n close: `<path d=\"M649.179 512l212.839-212.84c37.881-37.881 37.881-99.298 0-137.179s-99.298-37.881-137.179 0L512 374.821l-212.839-212.84c-37.881-37.881-99.298-37.881-137.179 0s-37.881 99.298 0 137.179L374.821 512 161.982 724.84c-37.881 37.881-37.881 99.297 0 137.179 18.94 18.94 43.765 28.41 68.589 28.41 24.825 0 49.649-9.47 68.589-28.41L512 649.179l212.839 212.84c18.94 18.94 43.765 28.41 68.589 28.41s49.649-9.47 68.59-28.41c37.881-37.882 37.881-99.298 0-137.179L649.179 512z\"/>`,\n // 用户\n user: `<path d=\"M0 0h1024v1024H0V0z m614.8608 664.439467c-2.973013-30.685867-1.983147-53.4528-1.983147-82.158934 13.88544-7.918933 63.488-55.432533 67.437227-95.0272 10.922667-0.989867 28.757333-11.8784 33.716907-55.432533 2.976427-22.766933-7.932587-36.625067-14.875307-40.584533 17.851733-54.442667 25.78432-224.703147-99.170987-241.53088C587.093333 127.92832 554.366293 116.053333 511.72352 116.053333c-170.571093 2.9696-221.149867 130.6624-183.463253 276.176214-6.94272 3.959467-17.851733 16.827733-14.875307 40.584533 4.959573 43.554133 22.807893 54.442667 33.716907 55.432533 3.966293 40.584533 53.551787 88.098133 67.437226 95.0272 0 28.706133 0.989867 50.4832-1.98656 82.158934C378.835627 756.497067 157.696 708.983467 146.773333 907.946667h733.866667c-5.94944-201.936213-232.05888-151.453013-265.775787-243.510614z\"/>`,\n // 更多(三个点)\n more: `<circle cx=\"180\" cy=\"512\" r=\"90\"/><circle cx=\"512\" cy=\"512\" r=\"90\"/><circle cx=\"844\" cy=\"512\" r=\"90\"/>`,\n // 加号\n plus: `<path d=\"M480 224v256H224a32 32 0 1 0 0 64h256v256a32 32 0 1 0 64 0V544h256a32 32 0 1 0 0-64H544V224a32 32 0 1 0-64 0z\"/>`,\n // 减号\n minus: `<path d=\"M192 480h640a32 32 0 1 1 0 64H192a32 32 0 1 1 0-64z\"/>`,\n} as const\n\nexport type IconName = keyof typeof ICON_PATHS\n\nconst props = withDefaults(\n defineProps<{\n name: IconName\n size?: number | string\n color?: string\n title?: string\n }>(),\n {\n size: 16,\n color: 'currentColor',\n },\n)\n</script>\n\n<template>\n <svg\n :width=\"props.size\"\n :height=\"props.size\"\n viewBox=\"0 0 1024 1024\"\n xmlns=\"http://www.w3.org/2000/svg\"\n :fill=\"props.color\"\n :aria-label=\"props.title\"\n :aria-hidden=\"props.title ? undefined : 'true'\"\n v-html=\"ICON_PATHS[props.name]\"\n />\n</template>\n","<script setup lang=\"ts\">\n// 通用图标组件:SVG 路径内联在 ICON_PATHS 常量里,无运行时文件加载\n// 这样通过 npm 安装到其他项目后无需额外配置即可正常显示\n//\n// 用法:\n// <Icon name=\"mic-on\" :size=\"18\" />\n// <Icon name=\"mic-off\" :size=\"18\" color=\"#ef4444\" />\n//\n// 颜色继承 currentColor(父元素的 color CSS 属性),或显式传 color prop\n\n// 所有图标统一 viewBox=\"0 0 1024 1024\",方便对齐\n// fill 默认移除(继承 currentColor),少数图标有特殊 fill 在 path 上保留\nconst ICON_PATHS = {\n // 麦克风开启\n 'mic-on': `<path d=\"M514.986667 112c80.170667 0 145.109333 62.08 145.109333 138.666667v277.248c0 76.586667-64.938667 138.624-145.066667 138.624-80.213333 0-145.152-62.08-145.152-138.624V250.666667c0-76.586667 64.981333-138.666667 145.109334-138.666667z m236.970666 411.221333c0 118.485333-93.013333 215.893333-212.608 228.992v135.637334a24.149333 24.149333 0 0 1-48.341333 0v-135.04c-122.709333-10.24-219.008-109.013333-219.008-229.546667v-96.426667a24.704 24.704 0 0 1 49.408 0v109.184c0 98.56 83.712 169.088 190.592 169.088 106.837333 0 196.309333-92.8 196.309333-169.088V423.936a21.845333 21.845333 0 0 1 43.690667 0v99.285333z\"/>`,\n // 麦克风关闭\n 'mic-off': `<path d=\"M656.384 559.232L369.877333 272.725333v-22.058666c0-76.586667 64.981333-138.666667 145.109334-138.666667 80.170667 0 145.109333 62.08 145.109333 138.666667v277.248c0 10.794667-1.28 21.248-3.712 31.317333z m-84.693333 96.341333a150.528 150.528 0 0 1-56.704 10.965334c-80.128 0-145.066667-62.08-145.066667-138.624v-74.154667l201.770667 201.813333z m153.898666-27.136l-29.610666-29.568a165.973333 165.973333 0 0 0 12.373333-62.848V423.936a21.845333 21.845333 0 0 1 43.648 0v99.285333c0 37.930667-9.557333 73.685333-26.453333 105.216z m-89.173333 91.904a245.290667 245.290667 0 0 1-97.066667 31.872v135.637334a24.149333 24.149333 0 0 1-48.341333 0v-135.04c-122.709333-10.24-219.008-109.013333-219.008-229.546667v-96.426667a24.704 24.704 0 0 1 49.408 0v109.184c0 98.56 86.613333 178.474667 193.450667 178.474667 33.92 0 65.834667-8.064 93.568-22.186667l28.032 28.032zM145.28 183.893333l45.226667-45.226666 678.826666 678.826666-45.226666 45.226667z\"/>`,\n // 摄像头开启\n 'camera-on': `<path d=\"M873.770667 314.922667c19.797333-12.202667 37.632-2.048 37.632 18.304v335.232c0 24.362667-15.872 32.512-37.632 18.261333l-112.938667-67.029333c-19.797333-12.202667-37.632-26.453333-37.632-46.72V424.618667c0-18.261333 17.834667-30.464 37.632-42.666667l112.938667-67.029333zM207.232 288h391.936c43.562667 0 79.232 32 79.232 71.125333v305.749334c0 39.125333-35.669333 71.125333-79.232 71.125333H207.232C163.669333 736 128 704 128 664.874667V359.125333C128 320 163.669333 288 207.232 288z\"/>`,\n // 摄像头关闭\n 'camera-off': `<path d=\"M873.770667 314.922667c19.797333-12.202667 37.632-2.048 37.632 18.304v335.232c0 24.362667-15.872 32.512-37.632 18.261333l-112.938667-67.029333c-19.797333-12.202667-37.632-26.453333-37.632-46.72V424.618667c0-18.261333 17.834667-30.464 37.632-42.666667l112.938667-67.029333zM385.152 288h214.016c43.562667 0 79.232 32 79.232 71.125333v222.122667L385.152 288z m256.042667 437.077333a85.333333 85.333333 0 0 1-42.026667 10.922667H207.232C163.669333 736 128 704 128 664.874667V359.125333c0-38.186667 34.005333-69.632 76.16-71.082666l437.034667 437.034666zM145.28 183.893333l45.226667-45.226666 678.826666 678.826666-45.226666 45.226667z\"/>`,\n // 声音(扬声器开启)\n 'sound-on': `<path d=\"M767.317333 767.018667a33.237333 33.237333 0 0 1-20.565333 6.656 34.56 34.56 0 0 1-25.386667-10.709334 34.986667 34.986667 0 0 1-10.666666-25.557333 35.2 35.2 0 0 1 13.994666-28.501333 5.76 5.76 0 0 0 1.450667-1.109334l0.725333-0.725333c13.738667-9.386667 26.453333-20.48 38.272-33.28 11.733333-12.842667 22.058667-26.794667 30.848-41.813333 8.832-15.061333 15.701333-31.146667 20.608-48.128a190.293333 190.293333 0 0 0 7.338667-52.906667c0-37.034667-9.301333-70.826667-27.946667-101.418667a250.794667 250.794667 0 0 0-69.845333-75.52 34.730667 34.730667 0 0 1-15.445333-29.568 34.986667 34.986667 0 0 1 10.666666-25.557333 34.56 34.56 0 0 1 25.386667-10.709333c8.32 0 15.658667 2.688 22.058667 8.106666 18.133333 12.842667 35.029333 27.776 50.730666 44.8 15.658667 17.066667 29.141333 35.541333 40.448 55.509334 11.264 19.968 20.053333 41.344 26.453334 64 6.4 22.698667 9.557333 46.165333 9.557333 70.314666 0 24.704-3.2 48.384-9.557333 71.082667a285.653333 285.653333 0 0 1-26.837334 64.384 305.450667 305.450667 0 0 1-92.288 100.693333v-0.042666z m-41.173333-332.330667c10.282667 14.336 18.517333 28.757333 24.661333 43.306667 6.101333 14.549333 9.173333 32.213333 9.173334 52.906666 0 19.754667-3.2 37.546667-9.557334 53.333334-6.4 15.786667-14.464 30.08-24.277333 42.88-3.925333 5.461333-8.106667 10.24-12.501333 14.464a23.552 23.552 0 0 1-16.896 6.272 29.866667 29.866667 0 0 1-22.442667-9.6 32.085333 32.085333 0 0 1-9.173333-22.954667 33.706667 33.706667 0 0 1 2.218666-11.861333h-0.768c2.474667-5.418667 5.418667-10.581333 8.832-15.530667 3.413333-4.949333 6.741333-9.984 9.941334-15.146667a84.48 84.48 0 0 0 8.106666-17.792c2.176-6.656 3.285333-14.677333 3.285334-24.064 0-8.874667-1.109333-16.64-3.328-23.296a94.208 94.208 0 0 0-8.106667-18.133333 240.170667 240.170667 0 0 0-9.898667-15.530667 67.072 67.072 0 0 1-8.106666-15.573333 29.994667 29.994667 0 0 1-2.176-11.818667c0-8.874667 3.072-16.512 9.173333-22.954666a29.866667 29.866667 0 0 1 22.442667-9.6c6.826667 0 12.501333 2.090667 16.896 6.272 4.437333 4.181333 8.576 9.002667 12.501333 14.421333z m-237.482667 439.637333a9078.869333 9078.869333 0 0 0-157.354666-139.136 10554.453333 10554.453333 0 0 0-83.797334-70.314666 177.408 177.408 0 0 1-16.213333 0.725333 125.781333 125.781333 0 0 1-13.952 0.768h-12.501333c-13.226667 0-24.021333-4.096-32.341334-12.202667a39.424 39.424 0 0 1-12.501333-29.269333v-186.453333h0.725333l-0.725333-0.768c0-13.824 4.266667-25.173333 12.885333-34.048a42.666667 42.666667 0 0 1 32-13.312h41.856a13012.906667 13012.906667 0 0 1 122.794667-103.637334c15.701333-13.312 31.146667-26.282667 46.336-38.826666 15.189333-12.586667 28.928-24.064 41.173333-34.432 12.245333-10.368 22.570667-18.986667 30.890667-25.898667 8.32-6.912 13.482667-11.093333 15.445333-12.586667 14.677333-10.88 26.581333-15.786667 35.626667-14.805333 9.088 0.981333 16.213333 5.418667 21.333333 13.312s8.704 17.493333 10.666667 28.885333c1.962667 11.349333 2.944 21.674667 2.944 31.061334v599.466666c0 9.386667-1.706667 19.413333-5.12 29.994667-3.456 10.624-8.746667 19.029333-15.829333 25.173333-7.125333 6.186667-16.042667 8.746667-26.88 7.765334-10.752-0.981333-23.253333-8.106667-37.461334-21.461334z\"/>`,\n // 静音(扬声器关闭)\n 'sound-off': `<path d=\"M767.872 670.72c10.624-11.946667 20.010667-24.917333 28.16-38.784 8.789333-15.018667 15.658667-31.061333 20.565333-48.085333a190.293333 190.293333 0 0 0 7.338667-52.906667c0-37.034667-9.301333-70.826667-27.946667-101.418667a250.794667 250.794667 0 0 0-69.845333-75.52 34.730667 34.730667 0 0 1-15.445333-29.568 34.986667 34.986667 0 0 1 10.666666-25.557333 34.56 34.56 0 0 1 25.386667-10.709333c8.32 0 15.658667 2.688 22.058667 8.106666 18.133333 12.842667 35.029333 27.776 50.730666 44.8 15.658667 17.066667 29.141333 35.541333 40.448 55.509334 11.264 19.968 20.053333 41.344 26.453334 64 6.4 22.698667 9.557333 46.165333 9.557333 70.314666 0 24.704-3.2 48.384-9.557333 71.082667a285.653333 285.653333 0 0 1-26.837334 64.384 304.64 304.64 0 0 1-40.533333 55.552l-51.2-51.2zM725.333333 628.224l-46.08-46.08c2.090667-3.072 4.096-6.229333 6.058667-9.386667a84.48 84.48 0 0 0 8.106667-17.792c2.176-6.656 3.285333-14.677333 3.285333-24.064 0-8.874667-1.109333-16.64-3.328-23.296a94.208 94.208 0 0 0-8.106667-18.133333 240.170667 240.170667 0 0 0-9.898666-15.530667 67.072 67.072 0 0 1-8.106667-15.573333 29.994667 29.994667 0 0 1-2.176-11.818667c0-8.874667 3.072-16.512 9.173333-22.954666a29.866667 29.866667 0 0 1 22.442667-9.6c6.826667 0 12.501333 2.090667 16.896 6.272 4.437333 4.181333 8.576 9.002667 12.501333 14.421333 10.282667 14.336 18.517333 28.757333 24.661334 43.306667 6.101333 14.549333 9.173333 32.213333 9.173333 52.906666 0 19.754667-3.2 37.546667-9.557333 53.333334A183.168 183.168 0 0 1 725.333333 628.181333zM278.997333 362.837333l294.954667 294.997334v175.061333c0 9.386667-1.706667 19.370667-5.12 29.952-3.456 10.624-8.746667 19.029333-15.829333 25.173333-7.125333 6.186667-16.042667 8.746667-26.88 7.765334-10.752-0.981333-23.253333-8.106667-37.461334-21.461334a9078.869333 9078.869333 0 0 0-157.354666-139.136 10554.453333 10554.453333 0 0 0-83.797334-70.314666 177.408 177.408 0 0 1-16.213333 0.725333 125.781333 125.781333 0 0 1-13.952 0.768h-12.501333c-13.226667 0-24.021333-4.096-32.341334-12.202667a39.424 39.424 0 0 1-12.501333-29.269333v-186.453333h0.725333l-0.725333-0.768c0-13.824 4.266667-25.173333 12.885333-34.048a42.666667 42.666667 0 0 1 32-13.312h41.856l32.256-27.434667z m98.261334-82.688c13.056-11.050667 25.898667-21.802667 38.613333-32.341333 15.189333-12.544 28.928-24.021333 41.173333-34.389333 12.245333-10.368 22.570667-18.986667 30.890667-25.898667 8.32-6.912 13.482667-11.093333 15.445333-12.586667 14.677333-10.88 26.581333-15.786667 35.626667-14.805333 9.088 0.981333 16.213333 5.418667 21.333333 13.312s8.704 17.493333 10.666667 28.885333c1.962667 11.349333 2.944 21.674667 2.944 31.061334v243.413333l-196.693333-196.693333zM145.28 183.893333l45.226667-45.226666 678.826666 678.826666-45.226666 45.226667z\"/>`,\n // 接听(电话听筒)\n answer: `<path d=\"M864.597333 647.338667l11.52 11.562666c0.341333 0.682667 0.597333 1.408 1.066667 2.005334 11.52 14.762667 14.848 32.213333 10.325333 52.266666-3.285333 14.421333-9.216 28.16-16.597333 41.386667-6.613333 11.861333-13.994667 23.381333-21.034667 34.986667l-9.045333 9.088c-5.845333 3.541333-11.477333 7.594667-17.493333 10.453333-15.018667 7.04-30.421333 8.533333-45.781334 8.490667-20.906667-0.042667-40.96-3.498667-60.970666-7.210667-32.810667-6.101333-64.896-13.952-94.933334-26.88-42.154667-18.133333-79.658667-42.922667-115.2-70.528a987.818667 987.818667 0 0 1-78.08-68.010667c-0.682667-0.682667-1.536-1.152-2.346666-1.706666l-44.373334-44.373334c-0.682667-0.938667-1.322667-1.962667-2.133333-2.816-19.072-22.058667-38.656-43.690667-57.216-66.218666a620.117333 620.117333 0 0 1-60.928-88.746667C236.245333 396.928 216.32 350.421333 212.608 296.362667c-1.450667-20.693333-4.224-41.088-2.389333-62.336 1.237333-14.677333 4.053333-29.184 11.52-43.008 2.048-3.797333 4.522667-7.381333 6.826666-11.050667l9.045334-9.045333c8.32-5.248 16.554667-10.538667 24.917333-15.616 13.909333-8.490667 27.733333-17.109333 42.965333-22.442667 15.957333-5.632 30.634667-4.778667 43.605334 3.2 4.906667 3.029333 9.429333 6.613333 14.122666 9.941333l8.789334 8.789334c0.768 1.066667 1.493333 2.133333 2.304 3.157333 4.266667 5.12 8.746667 10.026667 12.714666 15.36 23.637333 31.616 42.325333 66.56 58.581334 103.082667 8.448 19.029333 6.186667 38.741333-6.826667 58.154666-6.101333 9.045333-13.226667 17.621333-20.736 25.642667-14.506667 15.488-19.84 32.213333-15.616 50.56 1.536 6.570667 3.754667 12.928 6.570667 18.901333 9.258667 19.754667 22.826667 36.266667 37.76 51.626667 27.178667 27.861333 54.869333 55.338667 82.389333 82.901333 15.488 15.488 31.530667 30.378667 50.133333 42.24 11.52 7.381333 23.68 13.525333 37.973334 15.445334 14.08 1.962667 27.562667-1.365333 39.808-11.264 6.442667-5.205333 12.330667-11.136 18.645333-16.512 22.784-19.285333 46.506667-25.642667 71.850667-15.914667 37.546667 14.506667 71.253333 34.645333 102.826666 58.197333 4.778667 3.584 9.472 7.338667 14.208 10.965334z\"/>`,\n // 挂断\n hangup: `<path d=\"M68.820008 642.540165a22.883151 22.883151 0 0 0 13.903686 21.04864c2.800052 1.158641 5.793203 1.737961 8.689804 1.737961l216.37612-0.38621a22.786601 22.786601 0 0 0 22.786601-22.786601l0-87.670471c11.682955-13.807125 25.779752-25.296981 41.517949-33.986785a274.115036 274.115036 0 0 1 140.002394-34.276445l0.482771 0a270.832225 270.832225 0 0 1 139.616184 34.083335c15.641647 8.786365 29.641883 20.276209 41.228289 34.083346l0 87.1877a22.69004 22.69004 0 0 0 22.59349 22.786601l194.072289-0.28966 22.30383 0a22.59349 22.59349 0 0 0 22.690051-22.786601l0.09655-49.531883a22.207281 22.207281 0 0 0-0.67587-5.310432l-0.965541-3.958692a150.140499 150.140499 0 0 0-11.586405-46.828392c-43.352459-100.898276-222.748643-176.982342-417.303691-176.982342l-0.965541 0a868.01487 868.01487 0 0 0-229.024605 32.152275c-76.566836 21.72451-137.105804 52.138824-170.609819 85.64285-17.765829 17.283058-32.152275 37.752367-42.290379 60.442418-8.207045 18.441688-12.551947 38.428237-12.841607 58.607897l-0.09655 47.021491z\"/>`,\n // 举手\n handup: `<path d=\"M166.997333 665.6H150.613333c-0.725333-0.213333-1.450667-0.597333-2.133333-0.682667-18.602667-2.261333-33.28-12.245333-44.288-29.653333a155.818667 155.818667 0 0 1-17.536-41.002667c-3.712-13.056-6.613333-26.410667-9.898667-39.594666v-12.842667c1.621333-6.613333 2.730667-13.482667 4.992-19.754667 5.632-15.573333 15.488-27.52 26.368-38.4 14.805333-14.72 31.402667-26.453333 48.213334-37.973333 27.477333-18.901333 55.765333-36.010667 86.144-48.128 42.624-16.981333 86.613333-25.941333 131.285333-31.573333 34.346667-4.266667 68.778667-6.4 103.296-7.125334 0.981333 0 1.92-0.298667 2.901333-0.469333h62.72c1.152 0.170667 2.346667 0.426667 3.498667 0.512 29.098667 2.090667 58.24 3.541333 87.296 6.314667 35.712 3.413333 70.997333 10.112 105.856 19.712 48.981333 13.44 95.957333 32.256 136.832 67.84 15.616 13.653333 32 26.069333 45.781333 42.368 9.472 11.264 17.749333 23.466667 22.229334 38.570666 1.28 4.138667 2.005333 8.405333 2.986666 12.629334v12.8c-2.176 9.6-4.266667 19.2-6.570666 28.714666-3.84 15.786667-7.552 31.701333-14.506667 46.208-7.338667 15.274667-18.304 25.002667-33.109333 28.586667-5.632 1.28-11.349333 1.962667-17.066667 2.944h-12.373333c-1.28-0.213333-2.56-0.469333-3.84-0.597333-6.656-0.64-13.269333-0.938667-19.882667-1.877334-39.082667-5.632-77.013333-17.152-114.346667-31.488-19.413333-7.466667-31.701333-22.997333-36.266666-45.952a205.098667 205.098667 0 0 1-3.456-32.810666c-0.682667-21.162667-8.746667-36.778667-24.746667-46.762667a94.72 94.72 0 0 0-17.92-8.704c-20.565333-7.424-41.856-9.514667-63.232-9.813333-38.954667-0.469333-77.952-0.341333-116.906667-0.341334-21.888 0-43.776 0.810667-65.365333 5.546667-13.312 2.944-26.24 7.210667-37.717333 15.914667-11.349333 8.618667-18.517333 20.48-20.181334 36.181333-0.853333 8.192-0.853333 16.554667-1.493333 24.789333-2.474667 29.781333-14.805333 51.072-39.594667 62.08-36.736 16.298667-74.88 25.898667-113.834666 31.573334-5.973333 0.853333-11.861333 1.493333-17.792 2.261333z\"/>`,\n // 关闭(叉)\n close: `<path d=\"M649.179 512l212.839-212.84c37.881-37.881 37.881-99.298 0-137.179s-99.298-37.881-137.179 0L512 374.821l-212.839-212.84c-37.881-37.881-99.298-37.881-137.179 0s-37.881 99.298 0 137.179L374.821 512 161.982 724.84c-37.881 37.881-37.881 99.297 0 137.179 18.94 18.94 43.765 28.41 68.589 28.41 24.825 0 49.649-9.47 68.589-28.41L512 649.179l212.839 212.84c18.94 18.94 43.765 28.41 68.589 28.41s49.649-9.47 68.59-28.41c37.881-37.882 37.881-99.298 0-137.179L649.179 512z\"/>`,\n // 用户\n user: `<path d=\"M0 0h1024v1024H0V0z m614.8608 664.439467c-2.973013-30.685867-1.983147-53.4528-1.983147-82.158934 13.88544-7.918933 63.488-55.432533 67.437227-95.0272 10.922667-0.989867 28.757333-11.8784 33.716907-55.432533 2.976427-22.766933-7.932587-36.625067-14.875307-40.584533 17.851733-54.442667 25.78432-224.703147-99.170987-241.53088C587.093333 127.92832 554.366293 116.053333 511.72352 116.053333c-170.571093 2.9696-221.149867 130.6624-183.463253 276.176214-6.94272 3.959467-17.851733 16.827733-14.875307 40.584533 4.959573 43.554133 22.807893 54.442667 33.716907 55.432533 3.966293 40.584533 53.551787 88.098133 67.437226 95.0272 0 28.706133 0.989867 50.4832-1.98656 82.158934C378.835627 756.497067 157.696 708.983467 146.773333 907.946667h733.866667c-5.94944-201.936213-232.05888-151.453013-265.775787-243.510614z\"/>`,\n // 更多(三个点)\n more: `<circle cx=\"180\" cy=\"512\" r=\"90\"/><circle cx=\"512\" cy=\"512\" r=\"90\"/><circle cx=\"844\" cy=\"512\" r=\"90\"/>`,\n // 加号\n plus: `<path d=\"M480 224v256H224a32 32 0 1 0 0 64h256v256a32 32 0 1 0 64 0V544h256a32 32 0 1 0 0-64H544V224a32 32 0 1 0-64 0z\"/>`,\n // 减号\n minus: `<path d=\"M192 480h640a32 32 0 1 1 0 64H192a32 32 0 1 1 0-64z\"/>`,\n} as const\n\nexport type IconName = keyof typeof ICON_PATHS\n\nconst props = withDefaults(\n defineProps<{\n name: IconName\n size?: number | string\n color?: string\n title?: string\n }>(),\n {\n size: 16,\n color: 'currentColor',\n },\n)\n</script>\n\n<template>\n <svg\n :width=\"props.size\"\n :height=\"props.size\"\n viewBox=\"0 0 1024 1024\"\n xmlns=\"http://www.w3.org/2000/svg\"\n :fill=\"props.color\"\n :aria-label=\"props.title\"\n :aria-hidden=\"props.title ? undefined : 'true'\"\n v-html=\"ICON_PATHS[props.name]\"\n />\n</template>\n","<script setup lang=\"ts\">\nimport { ref, onMounted, onUnmounted, watch } from 'vue'\nimport type { CallType } from '@quitting/contract'\nimport vDraggable from '../directives/draggable'\nimport Icon from './Icon.vue'\n\n// 主叫/被叫统一组件——样式完全一致,行为也一致\n// 大窗默认显示远端(对方),小窗默认显示本地(自己)\n// 点击小窗切换大小窗的视频源(参考 im-web AtVideo 双击切换)\n// 控制栏悬浮于视频底部 30px 处,水平居中 3 个圆形按钮,hover 视频才显示\n\nconst props = defineProps<{\n mode: 'caller' | 'callee'\n calleeUid?: string\n callType: CallType\n onClose?: () => void\n}>()\n\nconst emit = defineEmits<{ (e: 'close'): void }>()\n\nconst status = ref<'connecting' | 'connected' | 'ended'>('connecting')\nconst endedReason = ref('')\nconst micEnabled = ref(true)\nconst cameraEnabled = ref(props.callType === 'video')\nconst hasCamera = ref(true)\nconst hasMic = ref(true)\n\n// 大窗 / 小窗 video 元素 ref\nconst mainVideo = ref<HTMLVideoElement | null>(null)\nconst pipVideo = ref<HTMLVideoElement | null>(null)\n// 视频源是否已交换:默认 main=远端、pip=本地;交换后 main=本地、pip=远端\n// 用户点击小窗 toggle\nconst swapped = ref(false)\n\nconst remotePlaying = ref(false)\nconst duration = ref(0)\nlet timer: ReturnType<typeof setInterval> | null = null\n\nfunction getClient() {\n return (window as any).__imClient\n}\n\nasync function syncDeviceCapability() {\n try {\n const devices = await navigator.mediaDevices.enumerateDevices()\n hasCamera.value = devices.some((d) => d.kind === 'videoinput')\n hasMic.value = devices.some((d) => d.kind === 'audioinput')\n if (!hasCamera.value) cameraEnabled.value = false\n if (!hasMic.value) micEnabled.value = false\n } catch (e) {\n console.warn('[im] enumerateDevices 失败,按默认能力继续', e)\n }\n}\n\n// 按 swapped 状态把本地/远端 track attach 到对应 video 元素\n// remoteTrackSubscribed / devices / mount fallback / 交换 都会调此方法\nfunction attachByRole() {\n const session = getClient()?.call?.currentSession\n if (!session) return\n // 先把两个元素上的所有 track detach,避免 LiveKit track 同时挂多元素导致画面重复\n if (mainVideo.value) session.detachAllVideoFromEl(mainVideo.value)\n if (pipVideo.value) session.detachAllVideoFromEl(pipVideo.value)\n if (swapped.value) {\n // 交换后:本地大窗、远端小窗\n if (mainVideo.value && hasCamera.value && props.callType === 'video') {\n session.attachLocalVideo(mainVideo.value)\n }\n if (pipVideo.value) {\n session.attachRemoteVideo(pipVideo.value)\n }\n } else {\n // 默认:远端大窗、本地小窗\n if (mainVideo.value) {\n session.attachRemoteVideo(mainVideo.value)\n }\n if (pipVideo.value && hasCamera.value && props.callType === 'video') {\n session.attachLocalVideo(pipVideo.value)\n }\n }\n}\n\nfunction swapVideo() {\n swapped.value = !swapped.value\n attachByRole()\n}\n\nonMounted(async () => {\n await syncDeviceCapability()\n const client = getClient()\n if (!client) {\n console.error('[im] client 未注入,无法发起通话')\n return\n }\n const session = client.call.currentSession\n if (!session) return\n\n session.onSession('connected', () => {\n status.value = 'connected'\n attachByRole()\n timer = setInterval(() => duration.value++, 1000)\n })\n session.onSession('remoteConnected', () => {\n remotePlaying.value = true\n attachByRole()\n })\n // 远端 track 异步流入触发——participantConnected 时 track 还没到\n session.onSession('remoteTrackSubscribed', (info: { kind: string }) => {\n if (info.kind === 'video') {\n remotePlaying.value = true\n attachByRole()\n }\n })\n session.onSession('remoteDisconnected', () => {\n remotePlaying.value = false\n })\n session.onSession('devices', (info: { mic: boolean; camera: boolean; errors: string[] }) => {\n // 设备开启失败不在页面弹提示(core 已 console.warn):按钮区状态已足够表达\n // enableDevices 完成后重新 attach 本地 video\n if (info.camera) attachByRole()\n })\n session.onSession('ended', () => {\n status.value = 'ended'\n if (timer) clearInterval(timer)\n setTimeout(() => emit('close'), 1500)\n })\n\n // race 修复:session 可能已 connected(mount 前)\n if (session.isConnected) {\n status.value = 'connected'\n attachByRole()\n timer = setInterval(() => duration.value++, 1000)\n }\n\n // 远端 track 异步流入兜底\n if (props.callType === 'video') {\n let tries = 0\n const remoteRetry = setInterval(() => {\n tries++\n if (tries > 10 || remotePlaying.value) {\n clearInterval(remoteRetry)\n return\n }\n attachByRole()\n }, 300)\n }\n})\n\nonUnmounted(() => {\n if (timer) clearInterval(timer)\n})\n\nwatch(status, (s) => {\n if (s === 'ended') endedReason.value = '通话已结束'\n})\n\nasync function toggleMic() {\n if (!hasMic.value) return\n const session = getClient()?.call?.currentSession\n if (!session) return\n micEnabled.value = !micEnabled.value\n await session.setMicEnabled(micEnabled.value)\n}\n\nasync function toggleCamera() {\n if (!hasCamera.value) return\n const session = getClient()?.call?.currentSession\n if (!session) return\n cameraEnabled.value = !cameraEnabled.value\n await session.setCameraEnabled(cameraEnabled.value)\n // 摄像头开关后必须重新 attach(pub.track 引用变化)\n attachByRole()\n}\n\nasync function hangup() {\n endedReason.value = '已挂断'\n const client = getClient()\n await client?.call?.end('user_left')\n emit('close')\n}\n\nfunction formatDuration(s: number) {\n const m = Math.floor(s / 60)\n .toString()\n .padStart(2, '0')\n const sec = (s % 60).toString().padStart(2, '0')\n return `${m}:${sec}`\n}\n</script>\n\n<template>\n <div v-draggable=\"'.call-status-bar'\" class=\"call-window\">\n <!-- 视频区域(flex 占满,小窗叠加其上) -->\n <div class=\"video-area\">\n <!-- 大窗:默认远端,swapped 后本地 -->\n <video\n ref=\"mainVideo\"\n autoplay\n playsinline\n :class=\"[\n 'video-main',\n { 'network-error': status === 'connected' && !remotePlaying && !swapped },\n ]\"\n ></video>\n <!-- 小窗:默认本地,swapped 后远端。点击交换大小窗 -->\n <video\n v-if=\"callType === 'video'\"\n ref=\"pipVideo\"\n autoplay\n muted\n playsinline\n class=\"video-pip\"\n :title=\"swapped ? '点击切回对方主画面' : '点击切换自己为主画面'\"\n @click=\"swapVideo\"\n ></video>\n <!-- 连接中蒙层 -->\n <div v-if=\"status === 'connecting'\" class=\"connecting-overlay\">连接中...</div>\n <!-- 通话结束蒙层 -->\n <div v-else-if=\"status === 'ended'\" class=\"ended-overlay\">\n {{ endedReason || '通话已结束' }}\n </div>\n <!-- 顶部状态栏(拖拽句柄) -->\n <div class=\"call-status-bar\">\n <span v-if=\"status === 'connecting'\">连接中</span>\n <span v-else-if=\"status === 'connected'\">{{ formatDuration(duration) }}</span>\n <span v-else>{{ endedReason || '通话已结束' }}</span>\n </div>\n </div>\n <!-- 底部控制栏:方形图标按钮,与会议窗口风格一致 -->\n <div class=\"controls-bar\">\n <button v-if=\"hasMic\" class=\"ctrl-btn\" :class=\"{ off: !micEnabled }\" @click=\"toggleMic\">\n <Icon :name=\"micEnabled ? 'mic-on' : 'mic-off'\" :size=\"22\" />\n </button>\n <button\n v-if=\"callType === 'video' && hasCamera\"\n class=\"ctrl-btn\"\n :class=\"{ off: !cameraEnabled }\"\n @click=\"toggleCamera\"\n >\n <Icon :name=\"cameraEnabled ? 'camera-on' : 'camera-off'\" :size=\"22\" />\n </button>\n <button class=\"ctrl-btn hangup\" @click=\"hangup\">\n <Icon name=\"hangup\" :size=\"22\" />\n </button>\n </div>\n </div>\n</template>\n\n<style scoped>\n/* 通话窗口:与会议窗口风格一致——无圆角、flex 布局、深色面板背景 */\n.call-window {\n position: fixed;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n width: min(640px, 90vw);\n aspect-ratio: 16 / 9;\n background: #0f172a;\n border-radius: 0;\n overflow: hidden;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);\n z-index: 9999;\n display: flex;\n flex-direction: column;\n}\n/* 视频区域:flex 占满,相对定位供小窗叠加 */\n.video-area {\n position: relative;\n flex: 1;\n overflow: hidden;\n}\n/* 大窗:铺满视频区域 */\n.video-main {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n object-fit: cover;\n background: #000;\n}\n.video-main.network-error {\n filter: grayscale(1) blur(2px);\n opacity: 0.6;\n}\n/* 小窗:右下角贴边,16:9 比例,无边框无圆角(与会议小视频一致) */\n.video-pip {\n position: absolute;\n right: 0;\n bottom: 0;\n width: 96px;\n aspect-ratio: 16 / 9;\n object-fit: cover;\n background: #000;\n border-radius: 0;\n cursor: pointer;\n z-index: 2;\n}\n.video-pip:hover {\n outline: 1px solid rgba(255, 255, 255, 0.3);\n}\n/* 顶部状态栏(拖拽句柄) */\n.call-status-bar {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n height: 32px;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 13px;\n color: rgba(255, 255, 255, 0.9);\n background: linear-gradient(to bottom, rgba(0, 0, 0, 0.4), transparent);\n cursor: move;\n z-index: 3;\n pointer-events: auto;\n}\n/* 底部控制栏:常驻显示,方形图标按钮(与会议窗口一致) */\n.controls-bar {\n flex: 0 0 auto;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 16px;\n padding: 6px 0 8px;\n background: #0f172a;\n z-index: 4;\n}\n.ctrl-btn {\n width: 32px;\n height: 32px;\n border: none;\n background: transparent;\n color: #fff;\n border-radius: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n opacity: 0.85;\n cursor: pointer;\n transition: opacity 0.15s;\n}\n.ctrl-btn:hover {\n opacity: 1;\n}\n.ctrl-btn:active {\n transform: scale(0.92);\n}\n.ctrl-btn.off {\n color: #ef4444;\n opacity: 1;\n}\n.ctrl-btn.hangup {\n color: #ef4444;\n}\n/* 连接中 / 通话结束蒙层 */\n.connecting-overlay,\n.ended-overlay {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n background: rgba(0, 0, 0, 0.7);\n color: #fff;\n font-size: 15px;\n z-index: 5;\n}\n/* 设备失败提示(顶部居中黄条) */\n.device-warn {\n position: absolute;\n top: 40px;\n left: 50%;\n transform: translateX(-50%);\n background: rgba(212, 160, 23, 0.92);\n color: #fff;\n font-size: 12px;\n padding: 4px 12px;\n border-radius: 4px;\n z-index: 6;\n pointer-events: none;\n max-width: 80%;\n text-align: center;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { ref, onMounted, onUnmounted, watch } from 'vue'\nimport type { CallType } from '@quitting/contract'\nimport vDraggable from '../directives/draggable'\nimport Icon from './Icon.vue'\n\n// 主叫/被叫统一组件——样式完全一致,行为也一致\n// 大窗默认显示远端(对方),小窗默认显示本地(自己)\n// 点击小窗切换大小窗的视频源(参考 im-web AtVideo 双击切换)\n// 控制栏悬浮于视频底部 30px 处,水平居中 3 个圆形按钮,hover 视频才显示\n\nconst props = defineProps<{\n mode: 'caller' | 'callee'\n calleeUid?: string\n callType: CallType\n onClose?: () => void\n}>()\n\nconst emit = defineEmits<{ (e: 'close'): void }>()\n\nconst status = ref<'connecting' | 'connected' | 'ended'>('connecting')\nconst endedReason = ref('')\nconst micEnabled = ref(true)\nconst cameraEnabled = ref(props.callType === 'video')\nconst hasCamera = ref(true)\nconst hasMic = ref(true)\n\n// 大窗 / 小窗 video 元素 ref\nconst mainVideo = ref<HTMLVideoElement | null>(null)\nconst pipVideo = ref<HTMLVideoElement | null>(null)\n// 视频源是否已交换:默认 main=远端、pip=本地;交换后 main=本地、pip=远端\n// 用户点击小窗 toggle\nconst swapped = ref(false)\n\nconst remotePlaying = ref(false)\nconst duration = ref(0)\nlet timer: ReturnType<typeof setInterval> | null = null\n\nfunction getClient() {\n return (window as any).__imClient\n}\n\nasync function syncDeviceCapability() {\n try {\n const devices = await navigator.mediaDevices.enumerateDevices()\n hasCamera.value = devices.some((d) => d.kind === 'videoinput')\n hasMic.value = devices.some((d) => d.kind === 'audioinput')\n if (!hasCamera.value) cameraEnabled.value = false\n if (!hasMic.value) micEnabled.value = false\n } catch (e) {\n console.warn('[im] enumerateDevices 失败,按默认能力继续', e)\n }\n}\n\n// 按 swapped 状态把本地/远端 track attach 到对应 video 元素\n// remoteTrackSubscribed / devices / mount fallback / 交换 都会调此方法\nfunction attachByRole() {\n const session = getClient()?.call?.currentSession\n if (!session) return\n // 先把两个元素上的所有 track detach,避免 LiveKit track 同时挂多元素导致画面重复\n if (mainVideo.value) session.detachAllVideoFromEl(mainVideo.value)\n if (pipVideo.value) session.detachAllVideoFromEl(pipVideo.value)\n if (swapped.value) {\n // 交换后:本地大窗、远端小窗\n if (mainVideo.value && hasCamera.value && props.callType === 'video') {\n session.attachLocalVideo(mainVideo.value)\n }\n if (pipVideo.value) {\n session.attachRemoteVideo(pipVideo.value)\n }\n } else {\n // 默认:远端大窗、本地小窗\n if (mainVideo.value) {\n session.attachRemoteVideo(mainVideo.value)\n }\n if (pipVideo.value && hasCamera.value && props.callType === 'video') {\n session.attachLocalVideo(pipVideo.value)\n }\n }\n}\n\nfunction swapVideo() {\n swapped.value = !swapped.value\n attachByRole()\n}\n\nonMounted(async () => {\n await syncDeviceCapability()\n const client = getClient()\n if (!client) {\n console.error('[im] client 未注入,无法发起通话')\n return\n }\n const session = client.call.currentSession\n if (!session) return\n\n session.onSession('connected', () => {\n status.value = 'connected'\n attachByRole()\n timer = setInterval(() => duration.value++, 1000)\n })\n session.onSession('remoteConnected', () => {\n remotePlaying.value = true\n attachByRole()\n })\n // 远端 track 异步流入触发——participantConnected 时 track 还没到\n session.onSession('remoteTrackSubscribed', (info: { kind: string }) => {\n if (info.kind === 'video') {\n remotePlaying.value = true\n attachByRole()\n }\n })\n session.onSession('remoteDisconnected', () => {\n remotePlaying.value = false\n })\n session.onSession('devices', (info: { mic: boolean; camera: boolean; errors: string[] }) => {\n // 设备开启失败不在页面弹提示(core 已 console.warn):按钮区状态已足够表达\n // enableDevices 完成后重新 attach 本地 video\n if (info.camera) attachByRole()\n })\n session.onSession('ended', () => {\n status.value = 'ended'\n if (timer) clearInterval(timer)\n setTimeout(() => emit('close'), 1500)\n })\n\n // race 修复:session 可能已 connected(mount 前)\n if (session.isConnected) {\n status.value = 'connected'\n attachByRole()\n timer = setInterval(() => duration.value++, 1000)\n }\n\n // 远端 track 异步流入兜底\n if (props.callType === 'video') {\n let tries = 0\n const remoteRetry = setInterval(() => {\n tries++\n if (tries > 10 || remotePlaying.value) {\n clearInterval(remoteRetry)\n return\n }\n attachByRole()\n }, 300)\n }\n})\n\nonUnmounted(() => {\n if (timer) clearInterval(timer)\n})\n\nwatch(status, (s) => {\n if (s === 'ended') endedReason.value = '通话已结束'\n})\n\nasync function toggleMic() {\n if (!hasMic.value) return\n const session = getClient()?.call?.currentSession\n if (!session) return\n micEnabled.value = !micEnabled.value\n await session.setMicEnabled(micEnabled.value)\n}\n\nasync function toggleCamera() {\n if (!hasCamera.value) return\n const session = getClient()?.call?.currentSession\n if (!session) return\n cameraEnabled.value = !cameraEnabled.value\n await session.setCameraEnabled(cameraEnabled.value)\n // 摄像头开关后必须重新 attach(pub.track 引用变化)\n attachByRole()\n}\n\nasync function hangup() {\n endedReason.value = '已挂断'\n const client = getClient()\n await client?.call?.end('user_left')\n emit('close')\n}\n\nfunction formatDuration(s: number) {\n const m = Math.floor(s / 60)\n .toString()\n .padStart(2, '0')\n const sec = (s % 60).toString().padStart(2, '0')\n return `${m}:${sec}`\n}\n</script>\n\n<template>\n <div v-draggable=\"'.call-status-bar'\" class=\"call-window\">\n <!-- 视频区域(flex 占满,小窗叠加其上) -->\n <div class=\"video-area\">\n <!-- 大窗:默认远端,swapped 后本地 -->\n <video\n ref=\"mainVideo\"\n autoplay\n playsinline\n :class=\"[\n 'video-main',\n { 'network-error': status === 'connected' && !remotePlaying && !swapped },\n ]\"\n ></video>\n <!-- 小窗:默认本地,swapped 后远端。点击交换大小窗 -->\n <video\n v-if=\"callType === 'video'\"\n ref=\"pipVideo\"\n autoplay\n muted\n playsinline\n class=\"video-pip\"\n :title=\"swapped ? '点击切回对方主画面' : '点击切换自己为主画面'\"\n @click=\"swapVideo\"\n ></video>\n <!-- 连接中蒙层 -->\n <div v-if=\"status === 'connecting'\" class=\"connecting-overlay\">连接中...</div>\n <!-- 通话结束蒙层 -->\n <div v-else-if=\"status === 'ended'\" class=\"ended-overlay\">\n {{ endedReason || '通话已结束' }}\n </div>\n <!-- 顶部状态栏(拖拽句柄) -->\n <div class=\"call-status-bar\">\n <span v-if=\"status === 'connecting'\">连接中</span>\n <span v-else-if=\"status === 'connected'\">{{ formatDuration(duration) }}</span>\n <span v-else>{{ endedReason || '通话已结束' }}</span>\n </div>\n </div>\n <!-- 底部控制栏:方形图标按钮,与会议窗口风格一致 -->\n <div class=\"controls-bar\">\n <button v-if=\"hasMic\" class=\"ctrl-btn\" :class=\"{ off: !micEnabled }\" @click=\"toggleMic\">\n <Icon :name=\"micEnabled ? 'mic-on' : 'mic-off'\" :size=\"22\" />\n </button>\n <button\n v-if=\"callType === 'video' && hasCamera\"\n class=\"ctrl-btn\"\n :class=\"{ off: !cameraEnabled }\"\n @click=\"toggleCamera\"\n >\n <Icon :name=\"cameraEnabled ? 'camera-on' : 'camera-off'\" :size=\"22\" />\n </button>\n <button class=\"ctrl-btn hangup\" @click=\"hangup\">\n <Icon name=\"hangup\" :size=\"22\" />\n </button>\n </div>\n </div>\n</template>\n\n<style scoped>\n/* 通话窗口:与会议窗口风格一致——无圆角、flex 布局、深色面板背景 */\n.call-window {\n position: fixed;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n width: min(640px, 90vw);\n aspect-ratio: 16 / 9;\n background: #0f172a;\n border-radius: 0;\n overflow: hidden;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);\n z-index: 9999;\n display: flex;\n flex-direction: column;\n}\n/* 视频区域:flex 占满,相对定位供小窗叠加 */\n.video-area {\n position: relative;\n flex: 1;\n overflow: hidden;\n}\n/* 大窗:铺满视频区域 */\n.video-main {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n object-fit: cover;\n background: #000;\n}\n.video-main.network-error {\n filter: grayscale(1) blur(2px);\n opacity: 0.6;\n}\n/* 小窗:右下角贴边,16:9 比例,无边框无圆角(与会议小视频一致) */\n.video-pip {\n position: absolute;\n right: 0;\n bottom: 0;\n width: 96px;\n aspect-ratio: 16 / 9;\n object-fit: cover;\n background: #000;\n border-radius: 0;\n cursor: pointer;\n z-index: 2;\n}\n.video-pip:hover {\n outline: 1px solid rgba(255, 255, 255, 0.3);\n}\n/* 顶部状态栏(拖拽句柄) */\n.call-status-bar {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n height: 32px;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 13px;\n color: rgba(255, 255, 255, 0.9);\n background: linear-gradient(to bottom, rgba(0, 0, 0, 0.4), transparent);\n cursor: move;\n z-index: 3;\n pointer-events: auto;\n}\n/* 底部控制栏:常驻显示,方形图标按钮(与会议窗口一致) */\n.controls-bar {\n flex: 0 0 auto;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 16px;\n padding: 6px 0 8px;\n background: #0f172a;\n z-index: 4;\n}\n.ctrl-btn {\n width: 32px;\n height: 32px;\n border: none;\n background: transparent;\n color: #fff;\n border-radius: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n opacity: 0.85;\n cursor: pointer;\n transition: opacity 0.15s;\n}\n.ctrl-btn:hover {\n opacity: 1;\n}\n.ctrl-btn:active {\n transform: scale(0.92);\n}\n.ctrl-btn.off {\n color: #ef4444;\n opacity: 1;\n}\n.ctrl-btn.hangup {\n color: #ef4444;\n}\n/* 连接中 / 通话结束蒙层 */\n.connecting-overlay,\n.ended-overlay {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n background: rgba(0, 0, 0, 0.7);\n color: #fff;\n font-size: 15px;\n z-index: 5;\n}\n/* 设备失败提示(顶部居中黄条) */\n.device-warn {\n position: absolute;\n top: 40px;\n left: 50%;\n transform: translateX(-50%);\n background: rgba(212, 160, 23, 0.92);\n color: #fff;\n font-size: 12px;\n padding: 4px 12px;\n border-radius: 4px;\n z-index: 6;\n pointer-events: none;\n max-width: 80%;\n text-align: center;\n}\n</style>\n","export default \"data:audio/mpeg;base64,//uQBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASW5mbwAAAA8AAAB0AAC/BAAEBggLDQ8TFhgaHB4jJScpLC4yNDc5Oz1CREZIS01PU1ZYWlxeY2VnaWxucnR3eXt9goSGiIuNj5OWmJqcnqOlp6msrrK0t7m7vcLExsjLzc/T1tja3N7j5efp7O7y9Pf5+/0AAAA8TEFNRTMuMTAwAQAAAAAAAAAAABSAJAL0ZgAAgAAAvwRQljMCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//uQBAAP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAETEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uSBECP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kgRAj/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVdLVEUVJMoAkQIREQYQgmAwGTJkyZMmEECBAgQIECBMmTsLTvwTTvxBAgQL/+5IEQI/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARkyad//xnvf/ZAgQQ/e7IECCER3sgQIECEREE73+z0///+0ECZMmTJk073///+Lvf3u7IECBAgQIIQTTvWiIz97s8mTJkCAAAAAAAAIELsmTJk07//u7PJkyBAghGf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VVVVVVVVVVUD8aij///xVixUUFsWFWf+rrFRbFhcV4t//4szigH41FH///irFiooLYsKs/9XWKi2LC4rxb//xZnFFUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5AEXo/w6QC4mAEQDB0gFxMAIgGAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5IEQI/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uSBECP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kgRAj/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5IEQI/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uSBECP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kgRAj/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5IEQI/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uSBECP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kgRAj/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5IEQI/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uSBECP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kgRAj/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5IEQI/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uSBECP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kgRAj/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5IEQI/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uSBECP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kgRAj/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5IEQI/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uSBECP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kgRAj/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5IEQI/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uSBECP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kgRAj/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5AEQQ/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5IEQI/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uSBECP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kgRAj/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5IEQI/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uSBECP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kgRAj/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5IEQI/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uSBECP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kgRAj/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5IEQI/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uSBECP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kgRAj/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5IEQI/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uSBECP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kgRAj/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5IEQI/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uSBECP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kgRAj/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5IEQI/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uSBECP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kgRAj/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5IEQI/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uSBECP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kARBD/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kgRAj/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5IEQI/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uSBECP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kgRAj/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5IEQI/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uSBECP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kgRAj/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5IEQI/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uSBECP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kgRAj/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5IEQI/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uSBECP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kgRAj/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5IEQI/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVTEFNRTMuMTAwVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//uSBECP8AAAaQAAAAgAAA0gAAABAAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7kgRAj/AAAGkAAAAIAAANIAAAAQAAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5IEQI/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV\"","export default \"data:audio/mpeg;base64,SUQzBAAAAAAiNlRDT04AAAAHAAADQmx1ZXMAUFJJVgAAIXQAAFhNUAA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/Pgo8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjYtYzE0MyA3OS4xNjEyMTAsIDIwMTcvMDgvMTEtMTA6Mjg6MzYgICAgICAgICI+CiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iCiAgICB4bWxuczp4bXBETT0iaHR0cDovL25zLmFkb2JlLmNvbS94bXAvMS4wL0R5bmFtaWNNZWRpYS8iCiAgICB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIKICAgIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiCiAgICB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIKICAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIKICAgeG1wOkNyZWF0b3JUb29sPSJGaWxlIGNyZWF0ZWQgYnkgR29sZFdhdmUuICBHb2xkV2F2ZSBjb3B5cmlnaHQgKEMpIENocmlzIENyYWlnIgogICB4bXA6TWV0YWRhdGFEYXRlPSIyMDE3LTExLTAxVDE5OjI1OjUyKzA4OjAwIgogICB4bXA6TW9kaWZ5RGF0ZT0iMjAxNy0xMS0wMVQxOToyNTo1MiswODowMCIKICAgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDplZWRmZWNkMi0xNTYzLWJmNDUtOTQxZS03OWJjMzhjZmU0OGEiCiAgIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6ZWVkZmVjZDItMTU2My1iZjQ1LTk0MWUtNzliYzM4Y2ZlNDhhIgogICB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6NmI2MmI5ZGEtOThmNC03NjRkLWE2MGItMmZlOGFkYTZkMjYxIgogICBkYzpmb3JtYXQ9ImF1ZGlvL21wZWciPgogICA8eG1wRE06VHJhY2tzPgogICAgPHJkZjpCYWc+CiAgICAgPHJkZjpsaQogICAgICB4bXBETTp0cmFja05hbWU9IkN1ZVBvaW50IE1hcmtlcnMiCiAgICAgIHhtcERNOnRyYWNrVHlwZT0iQ3VlIgogICAgICB4bXBETTpmcmFtZVJhdGU9ImY4MDAwIi8+CiAgICAgPHJkZjpsaQogICAgICB4bXBETTp0cmFja05hbWU9IkNEIFRyYWNrIE1hcmtlcnMiCiAgICAgIHhtcERNOnRyYWNrVHlwZT0iVHJhY2siCiAgICAgIHhtcERNOmZyYW1lUmF0ZT0iZjgwMDAiLz4KICAgICA8cmRmOmxpCiAgICAgIHhtcERNOnRyYWNrTmFtZT0iU3ViY2xpcCBNYXJrZXJzIgogICAgICB4bXBETTp0cmFja1R5cGU9IkluT3V0IgogICAgICB4bXBETTpmcmFtZVJhdGU9ImY4MDAwIi8+CiAgICA8L3JkZjpCYWc+CiAgIDwveG1wRE06VHJhY2tzPgogICA8eG1wTU06SGlzdG9yeT4KICAgIDxyZGY6U2VxPgogICAgIDxyZGY6bGkKICAgICAgc3RFdnQ6YWN0aW9uPSJzYXZlZCIKICAgICAgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDo2YjYyYjlkYS05OGY0LTc2NGQtYTYwYi0yZmU4YWRhNmQyNjEiCiAgICAgIHN0RXZ0OndoZW49IjIwMTctMTEtMDFUMTk6MjU6NTIrMDg6MDAiCiAgICAgIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIEF1ZGl0aW9uIENDIDIwMTguMCAoV2luZG93cykiCiAgICAgIHN0RXZ0OmNoYW5nZWQ9Ii9tZXRhZGF0YSIvPgogICAgIDxyZGY6bGkKICAgICAgc3RFdnQ6YWN0aW9uPSJzYXZlZCIKICAgICAgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDplZWRmZWNkMi0xNTYzLWJmNDUtOTQxZS03OWJjMzhjZmU0OGEiCiAgICAgIHN0RXZ0OndoZW49IjIwMTctMTEtMDFUMTk6MjU6NTIrMDg6MDAiCiAgICAgIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIEF1ZGl0aW9uIENDIDIwMTguMCAoV2luZG93cykiCiAgICAgIHN0RXZ0OmNoYW5nZWQ9Ii8iLz4KICAgIDwvcmRmOlNlcT4KICAgPC94bXBNTTpIaXN0b3J5PgogICA8eG1wTU06RGVyaXZlZEZyb20KICAgIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6NmI2MmI5ZGEtOThmNC03NjRkLWE2MGItMmZlOGFkYTZkMjYxIgogICAgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo2YjYyYjlkYS05OGY0LTc2NGQtYTYwYi0yZmU4YWRhNmQyNjEiCiAgICBzdFJlZjpvcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6NmI2MmI5ZGEtOThmNC03NjRkLWE2MGItMmZlOGFkYTZkMjYxIi8+CiAgPC9yZGY6RGVzY3JpcHRpb24+CiA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgCjw/eHBhY2tldCBlbmQ9InciPz4AVFNTRQAAABMAAANVUFlVTiBUcmFuc2NvZGluZwAAAAAAAAAAAAAA/+PIwAAAAAAAAAAAAEluZm8AAAAPAAAAKwAAZUAAEREWFhwcIiIiJyctLTMzMzg4Pj5ERERJSU9PVVVVW1tgYGZmZmxscXF3d3d9fYKCiIiIjo6T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AExBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq/+OIxAAAAANIAAAAAExBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq/+OIxAAAAANIAAAAAExBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq/+OIxAAAAANIAAAAAExBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq/+OIxAAAAANIAAAAAExBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq/+OIxAAAAANIAAAAAExBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq/+OIxAAAAANIAAAAAExBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq/+OIxAAAAANIAAAAAExBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq/+OIxAAAAANIAAAAAExBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq/+OIxAAAAANIAAAAAExBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq/+OIxAAAAANIAAAAAExBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq/+OIxAAAAANIAAAAAExBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq/+OIxAAAAANIAAAAAExBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq/+OIxAAAAANIAAAAAExBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq/+OIxAAAAANIAAAAAExBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq/+OIxAAAAANIAAAAAExBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq/+OIxAAAAANIAAAAAExBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq/+OIxAAAAANIAAAAAExBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq/+OIxAAAAANIAAAAAExBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq/+OIxAAAAANIAAAAAExBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq/+OIxAAAAANIAAAAAExBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq/+OIxAAAAANIAAAAAExBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq/+OIxAAAAANIAAAAAExBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqTEFNRTMuMTAwqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\"","<script setup lang=\"ts\">\nimport { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'\nimport type { CallType } from '@quitting/contract'\nimport vDraggable from '../directives/draggable'\nimport ringCallerUrl from '../sounds/ring-caller.mp3'\nimport ringCalleeUrl from '../sounds/ring-callee.mp3'\n\n// 统一通话邀请/等待弹窗\n// mode='outgoing':主叫呼叫等待(响 caller.mp3)\n// - 显示\"呼叫中...\"动画 + 红色\"取消\"按钮\n// - 收到 call:accepted → emit('connected')\n// - 收到 call:rejected/busy/no_answer → emit('close')\n// - 用户点取消 → 调 client.call.end('cancelled', callId) → emit('close')\n// mode='incoming':被叫响铃(响 callee.mp3)\n// - 显示\"邀请你通话\" + 绿色\"接听\" + 红色\"拒接\" 按钮\n// - 用户点接听 → await client.call.accept() → emit('accepted', session) → emit('close')\n// - 用户点拒接 → client.call.end('rejected', callId) → emit('close')\n// - 收到 call:cancelled → emit('close')\n\nconst props = defineProps<{\n mode: 'outgoing' | 'incoming'\n peerUid: string\n peerName?: string\n callType: CallType\n // incoming 必须传 callId(来自 call:invite 事件);outgoing 可省略(取 currentSession.callId)\n callId?: string\n // 主叫超时秒数(默认 30s)\n timeoutSec?: number\n}>()\n\nconst emit = defineEmits<{\n (e: 'close'): void\n // outgoing 收到 call:accepted 时触发——父组件切到 CallWindow\n (e: 'connected'): void\n // incoming 接听成功时触发,携带 session——父组件切到 CallWindow\n (e: 'accepted', session: unknown): void\n}>()\n\ntype DialogStatus = 'ringing' | 'connected' | 'rejected' | 'busy' | 'no_answer' | 'cancelled'\nconst status = ref<DialogStatus>('ringing')\nconst statusText = ref('')\nconst ringAudio = ref<HTMLAudioElement | null>(null)\n\nconst unsubscribers: Array<() => void> = []\nlet timeoutTimer: ReturnType<typeof setTimeout> | null = null\n\nfunction getClient() {\n return (window as any).__imClient\n}\n\nfunction startRing() {\n if (!ringAudio.value) return\n ringAudio.value.currentTime = 0\n ringAudio.value.play().catch(() => {})\n}\n\nfunction stopRing() {\n if (!ringAudio.value) return\n ringAudio.value.pause()\n ringAudio.value.currentTime = 0\n}\n\n// 对端展示名:外部传 peerName 优先;未传时查 SDK 用户目录(client.users),\n// 仍无则回退 peerUid 本身——业务方零配置也能显示姓名\nconst fallbackName = ref('')\nwatch(\n () => props.peerUid,\n async (uid) => {\n fallbackName.value = ''\n if (props.peerName || !uid) return\n const dir = getClient()?.users\n if (!dir?.resolve) return\n const u = await dir.resolve(uid).catch(() => undefined)\n if (u?.name) fallbackName.value = u.name\n },\n { immediate: true },\n)\nconst displayPeerName = computed(() => props.peerName || fallbackName.value || props.peerUid)\nconst avatarText = computed(() => displayPeerName.value[0].toUpperCase())\nconst statusLabel = computed(() => {\n if (status.value === 'ringing') {\n return props.mode === 'outgoing' ? '呼叫中' : '邀请你通话'\n }\n return statusText.value\n})\n\nfunction setStatusAndClose(s: DialogStatus, text: string, delay = 1500) {\n status.value = s\n statusText.value = text\n setTimeout(() => emit('close'), delay)\n}\n\n// outgoing 模式:主叫取消\nasync function cancelOutgoing() {\n stopRing()\n status.value = 'cancelled'\n statusText.value = '已取消'\n const client = getClient()\n const cid = props.callId || client?.call?.currentSession?.callId\n await client?.call?.end('cancelled', cid).catch(() => {})\n setTimeout(() => emit('close'), 800)\n}\n\n// incoming 模式:被叫接听\nasync function acceptIncoming() {\n stopRing()\n const client = getClient()\n if (!client || !props.callId) return\n status.value = 'connected'\n statusText.value = '已接听'\n try {\n const session = await client.call.accept(props.callId, props.callType)\n emit('accepted', session)\n setTimeout(() => emit('close'), 200)\n } catch (err: any) {\n // connect 失败、设备异常等——SDK 已清 currentSession + 调后端兜底\n // UI 显示失败提示后关闭,让用户重试\n console.error('[im] 接听失败', err)\n status.value = 'rejected'\n statusText.value = err?.message || '接听失败'\n setTimeout(() => emit('close'), 1500)\n }\n}\n\n// incoming 模式:被叫拒接\nasync function rejectIncoming() {\n stopRing()\n status.value = 'rejected'\n statusText.value = '已拒接'\n const client = getClient()\n await client?.call?.end('rejected', props.callId).catch(() => {})\n setTimeout(() => emit('close'), 600)\n}\n\nonMounted(() => {\n const client = getClient()\n if (!client) {\n emit('close')\n return\n }\n startRing()\n\n if (props.mode === 'outgoing') {\n // 主叫监听被叫响应\n unsubscribers.push(\n client.on('call:accepted', () => {\n stopRing()\n status.value = 'connected'\n statusText.value = '对方已接听'\n setTimeout(() => emit('connected'), 300)\n }),\n )\n unsubscribers.push(client.on('call:rejected', () => setStatusAndClose('rejected', '对方拒接')))\n unsubscribers.push(client.on('call:busy', () => setStatusAndClose('busy', '对方繁忙')))\n unsubscribers.push(\n client.on('call:no_answer', () => setStatusAndClose('no_answer', '无人接听')),\n )\n // 客户端超时兜底\n const sec = props.timeoutSec ?? 30\n timeoutTimer = setTimeout(() => {\n if (status.value === 'ringing') setStatusAndClose('no_answer', '无人接听')\n }, sec * 1000)\n } else {\n // incoming 监听主叫取消 + 无人接听超时\n // no_answer 必须也监听:后端超时会同时通知主叫和被叫,\n // 只等 cancelled 的话被叫端铃声在无人接听后会永不停止\n unsubscribers.push(\n client.on('call:cancelled', () => {\n stopRing()\n setStatusAndClose('cancelled', '对方已取消', 600)\n }),\n )\n unsubscribers.push(\n client.on('call:no_answer', () => {\n stopRing()\n setStatusAndClose('cancelled', '未接听,已超时', 600)\n }),\n )\n }\n})\n\nonBeforeUnmount(() => {\n stopRing()\n unsubscribers.forEach((fn) => fn())\n if (timeoutTimer) clearTimeout(timeoutTimer)\n})\n</script>\n\n<template>\n <div v-draggable=\"'.call-dialog-status'\" class=\"call-dialog\">\n <audio\n ref=\"ringAudio\"\n :src=\"mode === 'outgoing' ? ringCallerUrl : ringCalleeUrl\"\n loop\n preload=\"auto\"\n />\n <div class=\"call-dialog-header\">\n <span>{{ callType === 'video' ? '视频通话' : '语音通话' }}</span>\n </div>\n <div class=\"call-dialog-body\">\n <div :class=\"['avatar', status === 'ringing' ? 'ringing' : status]\">\n <span>{{ avatarText }}</span>\n </div>\n <div class=\"peer-name\">{{ displayPeerName }}</div>\n <div class=\"status-text\">\n <template v-if=\"status === 'ringing'\">\n {{ statusLabel }}<span class=\"dots\"><i>.</i><i>.</i><i>.</i></span>\n </template>\n <template v-else>\n <span :class=\"status\">{{ statusText }}</span>\n </template>\n </div>\n </div>\n <div class=\"call-dialog-actions\">\n <!-- outgoing 模式:只显示取消 -->\n <template v-if=\"mode === 'outgoing'\">\n <button v-if=\"status === 'ringing'\" class=\"btn cancel\" @click=\"cancelOutgoing\">取消</button>\n </template>\n <!-- incoming 模式:接听 + 拒接 -->\n <template v-else>\n <button v-if=\"status === 'ringing'\" class=\"btn reject\" @click=\"rejectIncoming\">拒接</button>\n <button v-if=\"status === 'ringing'\" class=\"btn accept\" @click=\"acceptIncoming\">接听</button>\n </template>\n </div>\n </div>\n</template>\n\n<style scoped>\n.call-dialog {\n position: fixed;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n width: 320px;\n background: #0f172a;\n border-radius: 0;\n padding: 20px 20px 28px;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);\n color: #fff;\n z-index: 9999;\n text-align: center;\n}\n.call-dialog-header {\n cursor: move;\n font-size: 12px;\n color: #999;\n padding-bottom: 10px;\n border-bottom: 1px solid #333;\n margin-bottom: 16px;\n}\n.avatar {\n width: 80px;\n height: 80px;\n border-radius: 50%;\n background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n color: #fff;\n font-size: 36px;\n font-weight: 600;\n line-height: 80px;\n margin: 0 auto 12px;\n position: relative;\n}\n.avatar.ringing::before {\n content: '';\n position: absolute;\n inset: -8px;\n border: 2px solid #667eea;\n border-radius: 50%;\n animation: pulse 1.5s ease-out infinite;\n}\n.avatar.rejected,\n.avatar.cancelled {\n background: #888;\n}\n.avatar.busy,\n.avatar.no_answer {\n background: #d4a017;\n}\n.avatar.connected {\n background: #52c41a;\n}\n@keyframes pulse {\n 0% {\n transform: scale(1);\n opacity: 0.8;\n }\n 100% {\n transform: scale(1.4);\n opacity: 0;\n }\n}\n.peer-name {\n font-size: 16px;\n font-weight: 600;\n margin-bottom: 6px;\n}\n.status-text {\n font-size: 13px;\n color: #bbb;\n min-height: 20px;\n}\n.status-text .rejected,\n.status-text .cancelled {\n color: #888;\n}\n.status-text .busy,\n.status-text .no_answer {\n color: #d4a017;\n}\n.status-text .connected {\n color: #52c41a;\n}\n.dots i {\n display: inline-block;\n font-style: normal;\n animation: dot-bounce 1.4s infinite;\n}\n.dots i:nth-child(2) {\n animation-delay: 0.2s;\n}\n.dots i:nth-child(3) {\n animation-delay: 0.4s;\n}\n@keyframes dot-bounce {\n 0%,\n 60%,\n 100% {\n opacity: 0.3;\n }\n 30% {\n opacity: 1;\n }\n}\n.call-dialog-actions {\n margin-top: 20px;\n display: flex;\n justify-content: center;\n gap: 20px;\n min-height: 44px;\n}\n.btn {\n padding: 8px 24px;\n border: none;\n border-radius: 0;\n cursor: pointer;\n font-size: 14px;\n font-weight: 500;\n color: #fff;\n transition:\n transform 0.1s,\n opacity 0.1s;\n}\n.btn:active {\n transform: scale(0.96);\n}\n.btn.accept {\n background: #52c41a;\n}\n.btn.reject,\n.btn.cancel {\n background: #ef4444;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'\nimport type { CallType } from '@quitting/contract'\nimport vDraggable from '../directives/draggable'\nimport ringCallerUrl from '../sounds/ring-caller.mp3'\nimport ringCalleeUrl from '../sounds/ring-callee.mp3'\n\n// 统一通话邀请/等待弹窗\n// mode='outgoing':主叫呼叫等待(响 caller.mp3)\n// - 显示\"呼叫中...\"动画 + 红色\"取消\"按钮\n// - 收到 call:accepted → emit('connected')\n// - 收到 call:rejected/busy/no_answer → emit('close')\n// - 用户点取消 → 调 client.call.end('cancelled', callId) → emit('close')\n// mode='incoming':被叫响铃(响 callee.mp3)\n// - 显示\"邀请你通话\" + 绿色\"接听\" + 红色\"拒接\" 按钮\n// - 用户点接听 → await client.call.accept() → emit('accepted', session) → emit('close')\n// - 用户点拒接 → client.call.end('rejected', callId) → emit('close')\n// - 收到 call:cancelled → emit('close')\n\nconst props = defineProps<{\n mode: 'outgoing' | 'incoming'\n peerUid: string\n peerName?: string\n callType: CallType\n // incoming 必须传 callId(来自 call:invite 事件);outgoing 可省略(取 currentSession.callId)\n callId?: string\n // 主叫超时秒数(默认 30s)\n timeoutSec?: number\n}>()\n\nconst emit = defineEmits<{\n (e: 'close'): void\n // outgoing 收到 call:accepted 时触发——父组件切到 CallWindow\n (e: 'connected'): void\n // incoming 接听成功时触发,携带 session——父组件切到 CallWindow\n (e: 'accepted', session: unknown): void\n}>()\n\ntype DialogStatus = 'ringing' | 'connected' | 'rejected' | 'busy' | 'no_answer' | 'cancelled'\nconst status = ref<DialogStatus>('ringing')\nconst statusText = ref('')\nconst ringAudio = ref<HTMLAudioElement | null>(null)\n\nconst unsubscribers: Array<() => void> = []\nlet timeoutTimer: ReturnType<typeof setTimeout> | null = null\n\nfunction getClient() {\n return (window as any).__imClient\n}\n\nfunction startRing() {\n if (!ringAudio.value) return\n ringAudio.value.currentTime = 0\n ringAudio.value.play().catch(() => {})\n}\n\nfunction stopRing() {\n if (!ringAudio.value) return\n ringAudio.value.pause()\n ringAudio.value.currentTime = 0\n}\n\n// 对端展示名:外部传 peerName 优先;未传时查 SDK 用户目录(client.users),\n// 仍无则回退 peerUid 本身——业务方零配置也能显示姓名\nconst fallbackName = ref('')\nwatch(\n () => props.peerUid,\n async (uid) => {\n fallbackName.value = ''\n if (props.peerName || !uid) return\n const dir = getClient()?.users\n if (!dir?.resolve) return\n const u = await dir.resolve(uid).catch(() => undefined)\n if (u?.name) fallbackName.value = u.name\n },\n { immediate: true },\n)\nconst displayPeerName = computed(() => props.peerName || fallbackName.value || props.peerUid)\nconst avatarText = computed(() => displayPeerName.value[0].toUpperCase())\nconst statusLabel = computed(() => {\n if (status.value === 'ringing') {\n return props.mode === 'outgoing' ? '呼叫中' : '邀请你通话'\n }\n return statusText.value\n})\n\nfunction setStatusAndClose(s: DialogStatus, text: string, delay = 1500) {\n status.value = s\n statusText.value = text\n setTimeout(() => emit('close'), delay)\n}\n\n// outgoing 模式:主叫取消\nasync function cancelOutgoing() {\n stopRing()\n status.value = 'cancelled'\n statusText.value = '已取消'\n const client = getClient()\n const cid = props.callId || client?.call?.currentSession?.callId\n await client?.call?.end('cancelled', cid).catch(() => {})\n setTimeout(() => emit('close'), 800)\n}\n\n// incoming 模式:被叫接听\nasync function acceptIncoming() {\n stopRing()\n const client = getClient()\n if (!client || !props.callId) return\n status.value = 'connected'\n statusText.value = '已接听'\n try {\n const session = await client.call.accept(props.callId, props.callType)\n emit('accepted', session)\n setTimeout(() => emit('close'), 200)\n } catch (err: any) {\n // connect 失败、设备异常等——SDK 已清 currentSession + 调后端兜底\n // UI 显示失败提示后关闭,让用户重试\n console.error('[im] 接听失败', err)\n status.value = 'rejected'\n statusText.value = err?.message || '接听失败'\n setTimeout(() => emit('close'), 1500)\n }\n}\n\n// incoming 模式:被叫拒接\nasync function rejectIncoming() {\n stopRing()\n status.value = 'rejected'\n statusText.value = '已拒接'\n const client = getClient()\n await client?.call?.end('rejected', props.callId).catch(() => {})\n setTimeout(() => emit('close'), 600)\n}\n\nonMounted(() => {\n const client = getClient()\n if (!client) {\n emit('close')\n return\n }\n startRing()\n\n if (props.mode === 'outgoing') {\n // 主叫监听被叫响应\n unsubscribers.push(\n client.on('call:accepted', () => {\n stopRing()\n status.value = 'connected'\n statusText.value = '对方已接听'\n setTimeout(() => emit('connected'), 300)\n }),\n )\n unsubscribers.push(client.on('call:rejected', () => setStatusAndClose('rejected', '对方拒接')))\n unsubscribers.push(client.on('call:busy', () => setStatusAndClose('busy', '对方繁忙')))\n unsubscribers.push(\n client.on('call:no_answer', () => setStatusAndClose('no_answer', '无人接听')),\n )\n // 客户端超时兜底\n const sec = props.timeoutSec ?? 30\n timeoutTimer = setTimeout(() => {\n if (status.value === 'ringing') setStatusAndClose('no_answer', '无人接听')\n }, sec * 1000)\n } else {\n // incoming 监听主叫取消 + 无人接听超时\n // no_answer 必须也监听:后端超时会同时通知主叫和被叫,\n // 只等 cancelled 的话被叫端铃声在无人接听后会永不停止\n unsubscribers.push(\n client.on('call:cancelled', () => {\n stopRing()\n setStatusAndClose('cancelled', '对方已取消', 600)\n }),\n )\n unsubscribers.push(\n client.on('call:no_answer', () => {\n stopRing()\n setStatusAndClose('cancelled', '未接听,已超时', 600)\n }),\n )\n }\n})\n\nonBeforeUnmount(() => {\n stopRing()\n unsubscribers.forEach((fn) => fn())\n if (timeoutTimer) clearTimeout(timeoutTimer)\n})\n</script>\n\n<template>\n <div v-draggable=\"'.call-dialog-status'\" class=\"call-dialog\">\n <audio\n ref=\"ringAudio\"\n :src=\"mode === 'outgoing' ? ringCallerUrl : ringCalleeUrl\"\n loop\n preload=\"auto\"\n />\n <div class=\"call-dialog-header\">\n <span>{{ callType === 'video' ? '视频通话' : '语音通话' }}</span>\n </div>\n <div class=\"call-dialog-body\">\n <div :class=\"['avatar', status === 'ringing' ? 'ringing' : status]\">\n <span>{{ avatarText }}</span>\n </div>\n <div class=\"peer-name\">{{ displayPeerName }}</div>\n <div class=\"status-text\">\n <template v-if=\"status === 'ringing'\">\n {{ statusLabel }}<span class=\"dots\"><i>.</i><i>.</i><i>.</i></span>\n </template>\n <template v-else>\n <span :class=\"status\">{{ statusText }}</span>\n </template>\n </div>\n </div>\n <div class=\"call-dialog-actions\">\n <!-- outgoing 模式:只显示取消 -->\n <template v-if=\"mode === 'outgoing'\">\n <button v-if=\"status === 'ringing'\" class=\"btn cancel\" @click=\"cancelOutgoing\">取消</button>\n </template>\n <!-- incoming 模式:接听 + 拒接 -->\n <template v-else>\n <button v-if=\"status === 'ringing'\" class=\"btn reject\" @click=\"rejectIncoming\">拒接</button>\n <button v-if=\"status === 'ringing'\" class=\"btn accept\" @click=\"acceptIncoming\">接听</button>\n </template>\n </div>\n </div>\n</template>\n\n<style scoped>\n.call-dialog {\n position: fixed;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n width: 320px;\n background: #0f172a;\n border-radius: 0;\n padding: 20px 20px 28px;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);\n color: #fff;\n z-index: 9999;\n text-align: center;\n}\n.call-dialog-header {\n cursor: move;\n font-size: 12px;\n color: #999;\n padding-bottom: 10px;\n border-bottom: 1px solid #333;\n margin-bottom: 16px;\n}\n.avatar {\n width: 80px;\n height: 80px;\n border-radius: 50%;\n background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n color: #fff;\n font-size: 36px;\n font-weight: 600;\n line-height: 80px;\n margin: 0 auto 12px;\n position: relative;\n}\n.avatar.ringing::before {\n content: '';\n position: absolute;\n inset: -8px;\n border: 2px solid #667eea;\n border-radius: 50%;\n animation: pulse 1.5s ease-out infinite;\n}\n.avatar.rejected,\n.avatar.cancelled {\n background: #888;\n}\n.avatar.busy,\n.avatar.no_answer {\n background: #d4a017;\n}\n.avatar.connected {\n background: #52c41a;\n}\n@keyframes pulse {\n 0% {\n transform: scale(1);\n opacity: 0.8;\n }\n 100% {\n transform: scale(1.4);\n opacity: 0;\n }\n}\n.peer-name {\n font-size: 16px;\n font-weight: 600;\n margin-bottom: 6px;\n}\n.status-text {\n font-size: 13px;\n color: #bbb;\n min-height: 20px;\n}\n.status-text .rejected,\n.status-text .cancelled {\n color: #888;\n}\n.status-text .busy,\n.status-text .no_answer {\n color: #d4a017;\n}\n.status-text .connected {\n color: #52c41a;\n}\n.dots i {\n display: inline-block;\n font-style: normal;\n animation: dot-bounce 1.4s infinite;\n}\n.dots i:nth-child(2) {\n animation-delay: 0.2s;\n}\n.dots i:nth-child(3) {\n animation-delay: 0.4s;\n}\n@keyframes dot-bounce {\n 0%,\n 60%,\n 100% {\n opacity: 0.3;\n }\n 30% {\n opacity: 1;\n }\n}\n.call-dialog-actions {\n margin-top: 20px;\n display: flex;\n justify-content: center;\n gap: 20px;\n min-height: 44px;\n}\n.btn {\n padding: 8px 24px;\n border: none;\n border-radius: 0;\n cursor: pointer;\n font-size: 14px;\n font-weight: 500;\n color: #fff;\n transition:\n transform 0.1s,\n opacity 0.1s;\n}\n.btn:active {\n transform: scale(0.96);\n}\n.btn.accept {\n background: #52c41a;\n}\n.btn.reject,\n.btn.cancel {\n background: #ef4444;\n}\n</style>\n","// 共享 SVG 渲染函数——所有图标从 design/video-conference-layout.html 原样搬\n// 函数式组件,避免在每个用到的地方重复粘贴 SVG 字符串\n\n/** 麦克风开启:白色描边 */\nexport const MicOnIcon = `<svg class=\"mic-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M12 2a3 3 0 0 1 3 3v6a3 3 0 0 1-6 0V5a3 3 0 0 1 3-3z\"></path>\n <path d=\"M19 10v2a7 7 0 0 1-14 0v-2\"></path>\n <line x1=\"12\" y1=\"19\" x2=\"12\" y2=\"23\"></line>\n <line x1=\"8\" y1=\"23\" x2=\"16\" y2=\"23\"></line>\n</svg>`\n\n/** 麦克风关闭:红色描边 + 斜杠 */\nexport const MicOffIcon = `<svg class=\"mic-icon muted\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\">\n <line x1=\"1\" y1=\"1\" x2=\"23\" y2=\"23\"></line>\n <path d=\"M9 9v5a3 3 0 0 0 5.1 2.1\"></path>\n <path d=\"M15 9.34V5a3 3 0 0 0-5.94-1.1\"></path>\n <path d=\"M17 16.95A7 7 0 0 1 5 12v-2\"></path>\n <path d=\"M19 5v4\"></path>\n <path d=\"M12 19v4\"></path>\n <path d=\"M8 23h8\"></path>\n</svg>`\n\n/** 正在发言:声波柱动效(CSS keyframes 控制) */\nexport const SpeakingIndicator = `<span class=\"speaking-indicator\"><span class=\"wave-bar\"></span><span class=\"wave-bar\"></span><span class=\"wave-bar\"></span><span>正在发言</span></span>`\n\n/** 主持人 badge:用 \"👑\" emoji 太重,这里用简洁的\"主\"字徽标 */\nexport const HostBadge = `<span class=\"host-badge\">主持人</span>`\n\n/** 最小化按钮 SVG */\nexport const MinimizeIcon = `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"5\" y1=\"12\" x2=\"19\" y2=\"12\"></line></svg>`\n\n/** 还原(最大化)按钮 SVG:四角向外箭头 */\nexport const MaximizeIcon = `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"15 3 21 3 21 9\"></polyline><polyline points=\"9 21 3 21 3 15\"></polyline><line x1=\"21\" y1=\"3\" x2=\"14\" y2=\"10\"></line><line x1=\"3\" y1=\"21\" x2=\"10\" y2=\"14\"></line></svg>`\n\n/** 关闭按钮 SVG */\nexport const CloseIcon = `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"></line><line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"></line></svg>`\n\n/** 更多操作(⋯)按钮 SVG */\nexport const MoreIcon = `<svg viewBox=\"0 0 24 24\" fill=\"currentColor\"><circle cx=\"5\" cy=\"12\" r=\"2\"></circle><circle cx=\"12\" cy=\"12\" r=\"2\"></circle><circle cx=\"19\" cy=\"12\" r=\"2\"></circle></svg>`\n\n/** 进入全屏宫格按钮 SVG:田字型(四个等大方块) */\nexport const GridEnterIcon = `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"3\" width=\"7.5\" height=\"7.5\"></rect><rect x=\"13.5\" y=\"3\" width=\"7.5\" height=\"7.5\"></rect><rect x=\"3\" y=\"13.5\" width=\"7.5\" height=\"7.5\"></rect><rect x=\"13.5\" y=\"13.5\" width=\"7.5\" height=\"7.5\"></rect></svg>`\n\n/** 退出全屏宫格按钮 SVG:大小正方形交错(大空框 + 右下角小实心块) */\nexport const GridExitIcon = `<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"3\" width=\"18\" height=\"18\"></rect><rect x=\"12.5\" y=\"12.5\" width=\"8\" height=\"8\" fill=\"currentColor\" stroke=\"none\"></rect></svg>`\n\n/** 渲染麦克风图标——根据 muted 选 MicOnIcon/MicOffIcon */\nexport function renderMicIcon(muted: boolean): string {\n return muted ? MicOffIcon : MicOnIcon\n}\n","<script setup lang=\"ts\">\nimport { MinimizeIcon, CloseIcon, GridEnterIcon, GridExitIcon } from './icons'\n\ndefineProps<{\n title: string\n duration?: string\n // 全屏宫格模式:图标变为「退出宫格」,且隐藏最小化按钮(与全屏互斥)\n gridMode?: boolean\n}>()\n\nconst emit = defineEmits<{\n (e: 'close'): void\n (e: 'minimize'): void\n (e: 'toggleGrid'): void\n}>()\n</script>\n\n<template>\n <header class=\"meeting-header\">\n <div class=\"title-area\">\n <h1 class=\"title\">{{ title }}</h1>\n <span v-if=\"duration\" class=\"duration\">{{ duration }}</span>\n </div>\n <!-- 中央插槽:内容在 header 内水平 + 垂直居中(全屏宫格放控制按钮组) -->\n <div class=\"header-center\">\n <slot name=\"center\"></slot>\n </div>\n <div class=\"window-controls\">\n <button\n class=\"win-btn grid-btn\"\n type=\"button\"\n :aria-label=\"gridMode ? '恢复默认布局' : '全屏宫格'\"\n :title=\"gridMode ? '恢复默认布局' : '全屏宫格'\"\n @click=\"emit('toggleGrid')\"\n v-html=\"gridMode ? GridExitIcon : GridEnterIcon\"\n ></button>\n <button\n v-if=\"!gridMode\"\n class=\"win-btn minimize-btn\"\n type=\"button\"\n aria-label=\"最小化\"\n title=\"最小化\"\n @click=\"emit('minimize')\"\n v-html=\"MinimizeIcon\"\n ></button>\n <button\n class=\"win-btn close-btn\"\n type=\"button\"\n aria-label=\"关闭会议\"\n title=\"关闭\"\n @click=\"emit('close')\"\n v-html=\"CloseIcon\"\n ></button>\n </div>\n </header>\n</template>\n\n<style scoped>\n.meeting-header {\n flex: 0 0 auto;\n position: relative; /* 中央插槽绝对定位的锚点 */\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 6px 0;\n border-bottom: 1px solid var(--im-meeting-border, rgba(255, 255, 255, 0.08));\n cursor: move;\n}\n/* 中央插槽:相对整个 header 水平居中 + 垂直居中(不受左右两列宽度影响) */\n.header-center {\n position: absolute;\n left: 50%;\n top: 50%;\n transform: translate(-50%, -50%);\n}\n.title-area {\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 0;\n}\n.title {\n font-size: 15px;\n font-weight: 600;\n color: var(--im-meeting-text-primary, #fff);\n margin: 0;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.duration {\n font-size: 12px;\n color: var(--im-meeting-text-tertiary, rgba(255, 255, 255, 0.55));\n font-variant-numeric: tabular-nums;\n}\n.window-controls {\n display: flex;\n align-items: center;\n gap: 6px;\n}\n.win-btn {\n width: 24px;\n height: 24px;\n border: none;\n background: transparent;\n color: var(--im-meeting-text-secondary, rgba(255, 255, 255, 0.85));\n cursor: pointer;\n border-radius: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n transition:\n background 0.2s,\n color 0.2s;\n}\n.win-btn:hover {\n background: rgba(255, 255, 255, 0.1);\n color: var(--im-meeting-text-primary, #fff);\n}\n.win-btn :deep(svg) {\n width: 18px;\n height: 18px;\n}\n.win-btn.close-btn:hover {\n background: #e53935;\n color: #fff;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { MinimizeIcon, CloseIcon, GridEnterIcon, GridExitIcon } from './icons'\n\ndefineProps<{\n title: string\n duration?: string\n // 全屏宫格模式:图标变为「退出宫格」,且隐藏最小化按钮(与全屏互斥)\n gridMode?: boolean\n}>()\n\nconst emit = defineEmits<{\n (e: 'close'): void\n (e: 'minimize'): void\n (e: 'toggleGrid'): void\n}>()\n</script>\n\n<template>\n <header class=\"meeting-header\">\n <div class=\"title-area\">\n <h1 class=\"title\">{{ title }}</h1>\n <span v-if=\"duration\" class=\"duration\">{{ duration }}</span>\n </div>\n <!-- 中央插槽:内容在 header 内水平 + 垂直居中(全屏宫格放控制按钮组) -->\n <div class=\"header-center\">\n <slot name=\"center\"></slot>\n </div>\n <div class=\"window-controls\">\n <button\n class=\"win-btn grid-btn\"\n type=\"button\"\n :aria-label=\"gridMode ? '恢复默认布局' : '全屏宫格'\"\n :title=\"gridMode ? '恢复默认布局' : '全屏宫格'\"\n @click=\"emit('toggleGrid')\"\n v-html=\"gridMode ? GridExitIcon : GridEnterIcon\"\n ></button>\n <button\n v-if=\"!gridMode\"\n class=\"win-btn minimize-btn\"\n type=\"button\"\n aria-label=\"最小化\"\n title=\"最小化\"\n @click=\"emit('minimize')\"\n v-html=\"MinimizeIcon\"\n ></button>\n <button\n class=\"win-btn close-btn\"\n type=\"button\"\n aria-label=\"关闭会议\"\n title=\"关闭\"\n @click=\"emit('close')\"\n v-html=\"CloseIcon\"\n ></button>\n </div>\n </header>\n</template>\n\n<style scoped>\n.meeting-header {\n flex: 0 0 auto;\n position: relative; /* 中央插槽绝对定位的锚点 */\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 6px 0;\n border-bottom: 1px solid var(--im-meeting-border, rgba(255, 255, 255, 0.08));\n cursor: move;\n}\n/* 中央插槽:相对整个 header 水平居中 + 垂直居中(不受左右两列宽度影响) */\n.header-center {\n position: absolute;\n left: 50%;\n top: 50%;\n transform: translate(-50%, -50%);\n}\n.title-area {\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 0;\n}\n.title {\n font-size: 15px;\n font-weight: 600;\n color: var(--im-meeting-text-primary, #fff);\n margin: 0;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.duration {\n font-size: 12px;\n color: var(--im-meeting-text-tertiary, rgba(255, 255, 255, 0.55));\n font-variant-numeric: tabular-nums;\n}\n.window-controls {\n display: flex;\n align-items: center;\n gap: 6px;\n}\n.win-btn {\n width: 24px;\n height: 24px;\n border: none;\n background: transparent;\n color: var(--im-meeting-text-secondary, rgba(255, 255, 255, 0.85));\n cursor: pointer;\n border-radius: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n transition:\n background 0.2s,\n color 0.2s;\n}\n.win-btn:hover {\n background: rgba(255, 255, 255, 0.1);\n color: var(--im-meeting-text-primary, #fff);\n}\n.win-btn :deep(svg) {\n width: 18px;\n height: 18px;\n}\n.win-btn.close-btn:hover {\n background: #e53935;\n color: #fff;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'\nimport Icon from '../Icon.vue'\nimport { SpeakingIndicator } from './icons'\n\nconst props = defineProps<{\n identity: string\n name: string\n muted: boolean\n // 摄像头状态:false 时显示遮罩;主动关闭与不可用区分展示(传入方负责排除未入会成员)\n cameraOn?: boolean\n // 该成员摄像头不可用(无设备/无权限/被占用):灰图标 +「摄像头不可用」遮罩\n cameraUnavailable?: boolean\n speaking: boolean\n isHost: boolean\n isSelf?: boolean\n // 该成员麦克风不可用:状态图标置灰(红色保留给主动闭麦,口径与成员列表一致)\n micUnavailable?: boolean\n onVideoElReady?: (identity: string, el: HTMLVideoElement | null) => void\n}>()\n\nconst videoEl = ref<HTMLVideoElement | null>(null)\n// 首帧是否已渲染:区分「视频加载中」与「真的没视频」(与 SmallVideoTile 同口径)\nconst videoReady = ref(false)\n\n// 初始 mount + identity 切换(主持人转让后)都要通知父组件重新 attach\nfunction notify() {\n props.onVideoElReady?.(props.identity, videoEl.value)\n}\n\nonMounted(notify)\nonBeforeUnmount(() => {\n props.onVideoElReady?.(props.identity, null)\n})\nwatch(\n () => props.identity,\n () => {\n nextTick(notify)\n },\n)\n</script>\n\n<template>\n <section class=\"main-video\" aria-label=\"主视频画面\">\n <video\n ref=\"videoEl\"\n autoplay\n muted\n playsinline\n class=\"tile-video\"\n @loadeddata=\"videoReady = true\"\n @emptied=\"videoReady = false\"\n ></video>\n <!-- 摄像头遮罩:不可用=灰「摄像头不可用」;主动关闭=红「摄像头已关」。\n 盖住冻结帧(LiveKit mute 后 video 停在最后一帧) -->\n <div v-if=\"cameraUnavailable || cameraOn === false\" class=\"camera-off-mask\">\n <Icon\n name=\"camera-off\"\n :size=\"28\"\n :color=\"cameraUnavailable ? 'rgba(255, 255, 255, 0.45)' : '#ef4444'\"\n />\n <span class=\"camera-off-text\" :class=\"cameraUnavailable ? 'unavailable' : 'off'\">\n {{ cameraUnavailable ? '摄像头不可用' : '摄像头已关' }}\n </span>\n </div>\n <!-- 视频加载中:摄像头已开但首帧未到,避免加载期被误判为关闭 -->\n <div v-else-if=\"!videoReady && name\" class=\"video-loading-mask\">\n <span class=\"camera-off-text\">视频加载中…</span>\n </div>\n <div v-if=\"!name\" class=\"video-placeholder\">主视频画面</div>\n <div class=\"video-overlay\">\n <div class=\"video-meta-grid\">\n <div class=\"corner-tl\">\n <span class=\"participant-name\">{{ isSelf ? '我' : name || '等待主持人加入' }}</span>\n </div>\n <div v-if=\"speaking\" class=\"corner-tr\" v-html=\"SpeakingIndicator\"></div>\n <div class=\"corner-br\">\n <Icon\n :name=\"muted || micUnavailable ? 'mic-off' : 'mic-on'\"\n :size=\"18\"\n :color=\"\n micUnavailable ? 'rgba(255, 255, 255, 0.45)' : muted ? '#ef4444' : '#fff'\n \"\n :title=\"micUnavailable ? '麦克风不可用' : muted ? '麦克风已关' : '麦克风已开'\"\n />\n </div>\n </div>\n </div>\n </section>\n</template>\n\n<style scoped>\n.main-video {\n position: relative;\n width: 100%;\n height: 100%;\n background: var(--im-meeting-bg-video, #000);\n border-radius: 0;\n overflow: hidden;\n}\n.tile-video {\n width: 100%;\n height: 100%;\n object-fit: cover;\n background: var(--im-meeting-bg-video, #000);\n}\n.camera-off-mask {\n position: absolute;\n inset: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 8px;\n background: var(--im-meeting-bg-video, #000);\n pointer-events: none;\n}\n.camera-off-text {\n font-size: 13px;\n color: rgba(255, 255, 255, 0.45);\n}\n/* 主动关闭摄像头:红;不可用保持灰(默认) */\n.camera-off-text.off {\n color: #ef4444;\n}\n/* 视频加载中:同呼吸动画提示等待首帧 */\n.video-loading-mask {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n background: var(--im-meeting-bg-video, #000);\n pointer-events: none;\n animation: waiting-pulse 2s ease-in-out infinite;\n}\n@keyframes waiting-pulse {\n 0%,\n 100% {\n opacity: 0.55;\n }\n 50% {\n opacity: 1;\n }\n}\n.video-placeholder {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--im-meeting-text-secondary, rgba(255, 255, 255, 0.85));\n font-size: 14px;\n}\n.video-overlay {\n position: absolute;\n inset: 0;\n pointer-events: none;\n padding: var(--im-meeting-overlay-gap, 5px);\n}\n.video-meta-grid {\n position: absolute;\n inset: var(--im-meeting-overlay-gap, 5px);\n display: grid;\n grid-template-columns: 1fr auto;\n grid-template-rows: auto 1fr auto;\n pointer-events: none;\n}\n.corner-tl {\n grid-area: 3 / 1 / 4 / 2;\n align-self: end;\n justify-self: start;\n}\n.corner-tr {\n grid-area: 1 / 2 / 2 / 3;\n align-self: start;\n justify-self: end;\n}\n.corner-br {\n grid-area: 3 / 2 / 4 / 3;\n align-self: end;\n justify-self: end;\n}\n.participant-name {\n font-size: 14px;\n font-weight: 500;\n color: var(--im-meeting-text-primary, #fff);\n text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n max-width: 60%;\n}\n.corner-br {\n filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5));\n}\n.main-video :deep(.speaking-indicator) {\n display: inline-flex;\n align-items: center;\n gap: 3px;\n font-size: 11px;\n font-weight: 500;\n color: var(--im-meeting-speaking, #22c55e);\n background: rgba(34, 197, 94, 0.15);\n border: 1px solid rgba(34, 197, 94, 0.5);\n border-radius: 0;\n padding: 2px 6px;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);\n backdrop-filter: blur(2px);\n}\n.main-video :deep(.wave-bar) {\n width: 2px;\n background: var(--im-meeting-speaking, #22c55e);\n border-radius: 0;\n animation: wave 1s ease-in-out infinite;\n}\n.main-video :deep(.wave-bar:nth-child(1)) {\n height: 6px;\n animation-delay: 0s;\n}\n.main-video :deep(.wave-bar:nth-child(2)) {\n height: 10px;\n animation-delay: 0.15s;\n}\n.main-video :deep(.wave-bar:nth-child(3)) {\n height: 6px;\n animation-delay: 0.3s;\n}\n@keyframes wave {\n 0%,\n 100% {\n transform: scaleY(1);\n opacity: 0.7;\n }\n 50% {\n transform: scaleY(1.6);\n opacity: 1;\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'\nimport Icon from '../Icon.vue'\nimport { SpeakingIndicator } from './icons'\n\nconst props = defineProps<{\n identity: string\n name: string\n muted: boolean\n // 摄像头状态:false 时显示遮罩;主动关闭与不可用区分展示(传入方负责排除未入会成员)\n cameraOn?: boolean\n // 该成员摄像头不可用(无设备/无权限/被占用):灰图标 +「摄像头不可用」遮罩\n cameraUnavailable?: boolean\n speaking: boolean\n isHost: boolean\n isSelf?: boolean\n // 该成员麦克风不可用:状态图标置灰(红色保留给主动闭麦,口径与成员列表一致)\n micUnavailable?: boolean\n onVideoElReady?: (identity: string, el: HTMLVideoElement | null) => void\n}>()\n\nconst videoEl = ref<HTMLVideoElement | null>(null)\n// 首帧是否已渲染:区分「视频加载中」与「真的没视频」(与 SmallVideoTile 同口径)\nconst videoReady = ref(false)\n\n// 初始 mount + identity 切换(主持人转让后)都要通知父组件重新 attach\nfunction notify() {\n props.onVideoElReady?.(props.identity, videoEl.value)\n}\n\nonMounted(notify)\nonBeforeUnmount(() => {\n props.onVideoElReady?.(props.identity, null)\n})\nwatch(\n () => props.identity,\n () => {\n nextTick(notify)\n },\n)\n</script>\n\n<template>\n <section class=\"main-video\" aria-label=\"主视频画面\">\n <video\n ref=\"videoEl\"\n autoplay\n muted\n playsinline\n class=\"tile-video\"\n @loadeddata=\"videoReady = true\"\n @emptied=\"videoReady = false\"\n ></video>\n <!-- 摄像头遮罩:不可用=灰「摄像头不可用」;主动关闭=红「摄像头已关」。\n 盖住冻结帧(LiveKit mute 后 video 停在最后一帧) -->\n <div v-if=\"cameraUnavailable || cameraOn === false\" class=\"camera-off-mask\">\n <Icon\n name=\"camera-off\"\n :size=\"28\"\n :color=\"cameraUnavailable ? 'rgba(255, 255, 255, 0.45)' : '#ef4444'\"\n />\n <span class=\"camera-off-text\" :class=\"cameraUnavailable ? 'unavailable' : 'off'\">\n {{ cameraUnavailable ? '摄像头不可用' : '摄像头已关' }}\n </span>\n </div>\n <!-- 视频加载中:摄像头已开但首帧未到,避免加载期被误判为关闭 -->\n <div v-else-if=\"!videoReady && name\" class=\"video-loading-mask\">\n <span class=\"camera-off-text\">视频加载中…</span>\n </div>\n <div v-if=\"!name\" class=\"video-placeholder\">主视频画面</div>\n <div class=\"video-overlay\">\n <div class=\"video-meta-grid\">\n <div class=\"corner-tl\">\n <span class=\"participant-name\">{{ isSelf ? '我' : name || '等待主持人加入' }}</span>\n </div>\n <div v-if=\"speaking\" class=\"corner-tr\" v-html=\"SpeakingIndicator\"></div>\n <div class=\"corner-br\">\n <Icon\n :name=\"muted || micUnavailable ? 'mic-off' : 'mic-on'\"\n :size=\"18\"\n :color=\"\n micUnavailable ? 'rgba(255, 255, 255, 0.45)' : muted ? '#ef4444' : '#fff'\n \"\n :title=\"micUnavailable ? '麦克风不可用' : muted ? '麦克风已关' : '麦克风已开'\"\n />\n </div>\n </div>\n </div>\n </section>\n</template>\n\n<style scoped>\n.main-video {\n position: relative;\n width: 100%;\n height: 100%;\n background: var(--im-meeting-bg-video, #000);\n border-radius: 0;\n overflow: hidden;\n}\n.tile-video {\n width: 100%;\n height: 100%;\n object-fit: cover;\n background: var(--im-meeting-bg-video, #000);\n}\n.camera-off-mask {\n position: absolute;\n inset: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 8px;\n background: var(--im-meeting-bg-video, #000);\n pointer-events: none;\n}\n.camera-off-text {\n font-size: 13px;\n color: rgba(255, 255, 255, 0.45);\n}\n/* 主动关闭摄像头:红;不可用保持灰(默认) */\n.camera-off-text.off {\n color: #ef4444;\n}\n/* 视频加载中:同呼吸动画提示等待首帧 */\n.video-loading-mask {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n background: var(--im-meeting-bg-video, #000);\n pointer-events: none;\n animation: waiting-pulse 2s ease-in-out infinite;\n}\n@keyframes waiting-pulse {\n 0%,\n 100% {\n opacity: 0.55;\n }\n 50% {\n opacity: 1;\n }\n}\n.video-placeholder {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--im-meeting-text-secondary, rgba(255, 255, 255, 0.85));\n font-size: 14px;\n}\n.video-overlay {\n position: absolute;\n inset: 0;\n pointer-events: none;\n padding: var(--im-meeting-overlay-gap, 5px);\n}\n.video-meta-grid {\n position: absolute;\n inset: var(--im-meeting-overlay-gap, 5px);\n display: grid;\n grid-template-columns: 1fr auto;\n grid-template-rows: auto 1fr auto;\n pointer-events: none;\n}\n.corner-tl {\n grid-area: 3 / 1 / 4 / 2;\n align-self: end;\n justify-self: start;\n}\n.corner-tr {\n grid-area: 1 / 2 / 2 / 3;\n align-self: start;\n justify-self: end;\n}\n.corner-br {\n grid-area: 3 / 2 / 4 / 3;\n align-self: end;\n justify-self: end;\n}\n.participant-name {\n font-size: 14px;\n font-weight: 500;\n color: var(--im-meeting-text-primary, #fff);\n text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n max-width: 60%;\n}\n.corner-br {\n filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5));\n}\n.main-video :deep(.speaking-indicator) {\n display: inline-flex;\n align-items: center;\n gap: 3px;\n font-size: 11px;\n font-weight: 500;\n color: var(--im-meeting-speaking, #22c55e);\n background: rgba(34, 197, 94, 0.15);\n border: 1px solid rgba(34, 197, 94, 0.5);\n border-radius: 0;\n padding: 2px 6px;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);\n backdrop-filter: blur(2px);\n}\n.main-video :deep(.wave-bar) {\n width: 2px;\n background: var(--im-meeting-speaking, #22c55e);\n border-radius: 0;\n animation: wave 1s ease-in-out infinite;\n}\n.main-video :deep(.wave-bar:nth-child(1)) {\n height: 6px;\n animation-delay: 0s;\n}\n.main-video :deep(.wave-bar:nth-child(2)) {\n height: 10px;\n animation-delay: 0.15s;\n}\n.main-video :deep(.wave-bar:nth-child(3)) {\n height: 6px;\n animation-delay: 0.3s;\n}\n@keyframes wave {\n 0%,\n 100% {\n transform: scaleY(1);\n opacity: 0.7;\n }\n 50% {\n transform: scaleY(1.6);\n opacity: 1;\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { ref, onMounted, onBeforeUnmount } from 'vue'\nimport Icon from '../Icon.vue'\nimport { SpeakingIndicator, HostBadge } from './icons'\n\nconst props = defineProps<{\n identity: string\n name: string\n muted: boolean\n // 是否已入会:false 时显示「等待加入」占位(呼吸动画),不显示摄像头遮罩/麦克风图标\n joined?: boolean\n // 摄像头状态:false 时显示遮罩;主动关闭与不可用区分展示(传入方负责排除未入会成员)\n cameraOn?: boolean\n // 该成员摄像头不可用(无设备/无权限/被占用):灰图标 +「摄像头不可用」遮罩\n cameraUnavailable?: boolean\n speaking: boolean\n isHost: boolean\n isSelf?: boolean\n // 主持人标签放视频框左上角(全屏宫格模式用);默认在左下角与昵称同行\n hostBadgeTop?: boolean\n // 该成员麦克风不可用:状态图标置灰(红色保留给主动闭麦,口径与成员列表一致)\n micUnavailable?: boolean\n // 挂载/卸载时回调,把 video 元素交回父组件——避开 v-for + 函数 ref 的不稳定问题\n onVideoElReady?: (identity: string, el: HTMLVideoElement | null) => void\n}>()\n\nconst emit = defineEmits<{\n (e: 'click', identity: string): void\n}>()\n\nconst videoEl = ref<HTMLVideoElement | null>(null)\n// 首帧是否已渲染:区分「视频加载中」与「真的没视频」——\n// cameraOn=true 但首帧未到(attach 慢/远端刚 publish)时显示加载中,而非误判已关\nconst videoReady = ref(false)\n\nonMounted(() => {\n if (videoEl.value) {\n props.onVideoElReady?.(props.identity, videoEl.value)\n }\n})\n\nonBeforeUnmount(() => {\n props.onVideoElReady?.(props.identity, null)\n})\n</script>\n\n<template>\n <div\n class=\"small-video\"\n role=\"img\"\n :aria-label=\"`${isSelf ? '我' : name} 的视频画面`\"\n @click=\"emit('click', identity)\"\n >\n <video\n ref=\"videoEl\"\n autoplay\n muted\n playsinline\n class=\"tile-video\"\n @loadeddata=\"videoReady = true\"\n @emptied=\"videoReady = false\"\n ></video>\n <!-- 等待加入占位:未入会成员(已邀请/离线重连),呼吸动画提示等待中 -->\n <div v-if=\"joined === false\" class=\"waiting-mask\">\n <Icon name=\"user\" :size=\"26\" color=\"rgba(255, 255, 255, 0.4)\" />\n <span class=\"waiting-text\">等待加入</span>\n </div>\n <!-- 摄像头遮罩:不可用=灰「摄像头不可用」;主动关闭=红「摄像头已关」。\n 盖住冻结帧(LiveKit mute 后 video 停在最后一帧) -->\n <div v-else-if=\"cameraUnavailable || cameraOn === false\" class=\"camera-off-mask\">\n <Icon\n name=\"camera-off\"\n :size=\"22\"\n :color=\"cameraUnavailable ? 'rgba(255, 255, 255, 0.45)' : '#ef4444'\"\n />\n <span class=\"camera-off-text\" :class=\"cameraUnavailable ? 'unavailable' : 'off'\">\n {{ cameraUnavailable ? '摄像头不可用' : '摄像头已关' }}\n </span>\n </div>\n <!-- 视频加载中:摄像头已开但首帧未到(attach/解码/远端刚 publish),\n 与「已关」遮罩区分,避免加载期被误判为关闭 -->\n <div v-else-if=\"!videoReady\" class=\"waiting-mask\">\n <span class=\"waiting-text\">视频加载中…</span>\n </div>\n <div class=\"video-overlay\">\n <div class=\"video-meta-grid\">\n <!-- 左上角主持人标签(全屏宫格):与左下角昵称同 inset(5px)对齐 -->\n <div v-if=\"isHost && hostBadgeTop\" class=\"corner-host-top\">\n <span class=\"host-badge-inline\" v-html=\"HostBadge\"></span>\n </div>\n <div class=\"corner-tl\">\n <span class=\"participant-name\">{{ isSelf ? '我' : name }}</span>\n <span v-if=\"isHost && !hostBadgeTop\" class=\"host-badge-inline\" v-html=\"HostBadge\"></span>\n </div>\n <div v-if=\"speaking\" class=\"corner-tr\" v-html=\"SpeakingIndicator\"></div>\n <div v-if=\"joined !== false\" class=\"corner-br\">\n <Icon\n :name=\"muted || micUnavailable ? 'mic-off' : 'mic-on'\"\n :size=\"18\"\n :color=\"\n micUnavailable ? 'rgba(255, 255, 255, 0.45)' : muted ? '#ef4444' : '#fff'\n \"\n :title=\"micUnavailable ? '麦克风不可用' : muted ? '麦克风已关' : '麦克风已开'\"\n />\n </div>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.small-video {\n position: relative;\n background: var(--im-meeting-bg-video, #000);\n border-radius: 0;\n overflow: hidden;\n flex: 0 0 auto;\n cursor: pointer;\n}\n.tile-video {\n width: 100%;\n height: 100%;\n object-fit: cover;\n background: var(--im-meeting-bg-video, #000);\n}\n.camera-off-mask {\n position: absolute;\n inset: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 6px;\n background: var(--im-meeting-bg-video, #000);\n pointer-events: none;\n}\n.camera-off-text {\n font-size: 11px;\n color: rgba(255, 255, 255, 0.45);\n}\n/* 主动关闭摄像头:红;不可用保持灰(默认) */\n.camera-off-text.off {\n color: #ef4444;\n}\n/* 等待加入占位:呼吸动画(透明度缓慢起伏),提示等待中 */\n.waiting-mask {\n position: absolute;\n inset: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 6px;\n background: var(--im-meeting-bg-video, #000);\n pointer-events: none;\n animation: waiting-pulse 2s ease-in-out infinite;\n}\n.waiting-text {\n font-size: 11px;\n color: rgba(255, 255, 255, 0.4);\n}\n@keyframes waiting-pulse {\n 0%,\n 100% {\n opacity: 0.55;\n }\n 50% {\n opacity: 1;\n }\n}\n.video-placeholder {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n color: rgba(255, 255, 255, 0.4);\n font-size: 12px;\n pointer-events: none;\n}\n.video-overlay {\n position: absolute;\n inset: 0;\n pointer-events: none;\n padding: var(--im-meeting-overlay-gap, 5px);\n}\n.video-meta-grid {\n position: absolute;\n inset: var(--im-meeting-overlay-gap, 5px);\n display: grid;\n grid-template-columns: 1fr auto;\n grid-template-rows: auto 1fr auto;\n pointer-events: none;\n}\n.corner-tl {\n grid-area: 3 / 1 / 4 / 2;\n align-self: end;\n justify-self: start;\n display: flex;\n align-items: center;\n gap: 4px;\n max-width: 100%;\n}\n.corner-tr {\n grid-area: 1 / 2 / 2 / 3;\n align-self: start;\n justify-self: end;\n}\n/* 左上角主持人标签:与左下角昵称(corner-tl,同 inset 5px)边距一致 */\n.corner-host-top {\n grid-area: 1 / 1 / 2 / 2;\n align-self: start;\n justify-self: start;\n}\n.corner-br {\n grid-area: 3 / 2 / 4 / 3;\n align-self: end;\n justify-self: end;\n}\n.participant-name {\n font-size: 12px;\n font-weight: 500;\n color: var(--im-meeting-text-primary, #fff);\n text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n max-width: 100%;\n}\n.corner-br {\n filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5));\n}\n.small-video :deep(.speaking-indicator) {\n display: inline-flex;\n align-items: center;\n gap: 3px;\n font-size: 10px;\n font-weight: 500;\n color: var(--im-meeting-speaking, #22c55e);\n background: rgba(34, 197, 94, 0.15);\n border: 1px solid rgba(34, 197, 94, 0.5);\n border-radius: 0;\n padding: 1px 4px;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);\n}\n.small-video :deep(.wave-bar) {\n width: 2px;\n background: var(--im-meeting-speaking, #22c55e);\n border-radius: 0;\n animation: wave-sm 1s ease-in-out infinite;\n}\n.small-video :deep(.wave-bar:nth-child(1)) {\n height: 4px;\n animation-delay: 0s;\n}\n.small-video :deep(.wave-bar:nth-child(2)) {\n height: 7px;\n animation-delay: 0.15s;\n}\n.small-video :deep(.wave-bar:nth-child(3)) {\n height: 4px;\n animation-delay: 0.3s;\n}\n.host-badge-inline :deep(.host-badge) {\n font-size: 9px;\n color: #fbbf24;\n background: rgba(251, 191, 36, 0.15);\n border: 1px solid rgba(251, 191, 36, 0.5);\n padding: 1px 4px;\n white-space: nowrap;\n}\n@keyframes wave-sm {\n 0%,\n 100% {\n transform: scaleY(1);\n opacity: 0.7;\n }\n 50% {\n transform: scaleY(1.6);\n opacity: 1;\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { ref, onMounted, onBeforeUnmount } from 'vue'\nimport Icon from '../Icon.vue'\nimport { SpeakingIndicator, HostBadge } from './icons'\n\nconst props = defineProps<{\n identity: string\n name: string\n muted: boolean\n // 是否已入会:false 时显示「等待加入」占位(呼吸动画),不显示摄像头遮罩/麦克风图标\n joined?: boolean\n // 摄像头状态:false 时显示遮罩;主动关闭与不可用区分展示(传入方负责排除未入会成员)\n cameraOn?: boolean\n // 该成员摄像头不可用(无设备/无权限/被占用):灰图标 +「摄像头不可用」遮罩\n cameraUnavailable?: boolean\n speaking: boolean\n isHost: boolean\n isSelf?: boolean\n // 主持人标签放视频框左上角(全屏宫格模式用);默认在左下角与昵称同行\n hostBadgeTop?: boolean\n // 该成员麦克风不可用:状态图标置灰(红色保留给主动闭麦,口径与成员列表一致)\n micUnavailable?: boolean\n // 挂载/卸载时回调,把 video 元素交回父组件——避开 v-for + 函数 ref 的不稳定问题\n onVideoElReady?: (identity: string, el: HTMLVideoElement | null) => void\n}>()\n\nconst emit = defineEmits<{\n (e: 'click', identity: string): void\n}>()\n\nconst videoEl = ref<HTMLVideoElement | null>(null)\n// 首帧是否已渲染:区分「视频加载中」与「真的没视频」——\n// cameraOn=true 但首帧未到(attach 慢/远端刚 publish)时显示加载中,而非误判已关\nconst videoReady = ref(false)\n\nonMounted(() => {\n if (videoEl.value) {\n props.onVideoElReady?.(props.identity, videoEl.value)\n }\n})\n\nonBeforeUnmount(() => {\n props.onVideoElReady?.(props.identity, null)\n})\n</script>\n\n<template>\n <div\n class=\"small-video\"\n role=\"img\"\n :aria-label=\"`${isSelf ? '我' : name} 的视频画面`\"\n @click=\"emit('click', identity)\"\n >\n <video\n ref=\"videoEl\"\n autoplay\n muted\n playsinline\n class=\"tile-video\"\n @loadeddata=\"videoReady = true\"\n @emptied=\"videoReady = false\"\n ></video>\n <!-- 等待加入占位:未入会成员(已邀请/离线重连),呼吸动画提示等待中 -->\n <div v-if=\"joined === false\" class=\"waiting-mask\">\n <Icon name=\"user\" :size=\"26\" color=\"rgba(255, 255, 255, 0.4)\" />\n <span class=\"waiting-text\">等待加入</span>\n </div>\n <!-- 摄像头遮罩:不可用=灰「摄像头不可用」;主动关闭=红「摄像头已关」。\n 盖住冻结帧(LiveKit mute 后 video 停在最后一帧) -->\n <div v-else-if=\"cameraUnavailable || cameraOn === false\" class=\"camera-off-mask\">\n <Icon\n name=\"camera-off\"\n :size=\"22\"\n :color=\"cameraUnavailable ? 'rgba(255, 255, 255, 0.45)' : '#ef4444'\"\n />\n <span class=\"camera-off-text\" :class=\"cameraUnavailable ? 'unavailable' : 'off'\">\n {{ cameraUnavailable ? '摄像头不可用' : '摄像头已关' }}\n </span>\n </div>\n <!-- 视频加载中:摄像头已开但首帧未到(attach/解码/远端刚 publish),\n 与「已关」遮罩区分,避免加载期被误判为关闭 -->\n <div v-else-if=\"!videoReady\" class=\"waiting-mask\">\n <span class=\"waiting-text\">视频加载中…</span>\n </div>\n <div class=\"video-overlay\">\n <div class=\"video-meta-grid\">\n <!-- 左上角主持人标签(全屏宫格):与左下角昵称同 inset(5px)对齐 -->\n <div v-if=\"isHost && hostBadgeTop\" class=\"corner-host-top\">\n <span class=\"host-badge-inline\" v-html=\"HostBadge\"></span>\n </div>\n <div class=\"corner-tl\">\n <span class=\"participant-name\">{{ isSelf ? '我' : name }}</span>\n <span v-if=\"isHost && !hostBadgeTop\" class=\"host-badge-inline\" v-html=\"HostBadge\"></span>\n </div>\n <div v-if=\"speaking\" class=\"corner-tr\" v-html=\"SpeakingIndicator\"></div>\n <div v-if=\"joined !== false\" class=\"corner-br\">\n <Icon\n :name=\"muted || micUnavailable ? 'mic-off' : 'mic-on'\"\n :size=\"18\"\n :color=\"\n micUnavailable ? 'rgba(255, 255, 255, 0.45)' : muted ? '#ef4444' : '#fff'\n \"\n :title=\"micUnavailable ? '麦克风不可用' : muted ? '麦克风已关' : '麦克风已开'\"\n />\n </div>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.small-video {\n position: relative;\n background: var(--im-meeting-bg-video, #000);\n border-radius: 0;\n overflow: hidden;\n flex: 0 0 auto;\n cursor: pointer;\n}\n.tile-video {\n width: 100%;\n height: 100%;\n object-fit: cover;\n background: var(--im-meeting-bg-video, #000);\n}\n.camera-off-mask {\n position: absolute;\n inset: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 6px;\n background: var(--im-meeting-bg-video, #000);\n pointer-events: none;\n}\n.camera-off-text {\n font-size: 11px;\n color: rgba(255, 255, 255, 0.45);\n}\n/* 主动关闭摄像头:红;不可用保持灰(默认) */\n.camera-off-text.off {\n color: #ef4444;\n}\n/* 等待加入占位:呼吸动画(透明度缓慢起伏),提示等待中 */\n.waiting-mask {\n position: absolute;\n inset: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 6px;\n background: var(--im-meeting-bg-video, #000);\n pointer-events: none;\n animation: waiting-pulse 2s ease-in-out infinite;\n}\n.waiting-text {\n font-size: 11px;\n color: rgba(255, 255, 255, 0.4);\n}\n@keyframes waiting-pulse {\n 0%,\n 100% {\n opacity: 0.55;\n }\n 50% {\n opacity: 1;\n }\n}\n.video-placeholder {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n color: rgba(255, 255, 255, 0.4);\n font-size: 12px;\n pointer-events: none;\n}\n.video-overlay {\n position: absolute;\n inset: 0;\n pointer-events: none;\n padding: var(--im-meeting-overlay-gap, 5px);\n}\n.video-meta-grid {\n position: absolute;\n inset: var(--im-meeting-overlay-gap, 5px);\n display: grid;\n grid-template-columns: 1fr auto;\n grid-template-rows: auto 1fr auto;\n pointer-events: none;\n}\n.corner-tl {\n grid-area: 3 / 1 / 4 / 2;\n align-self: end;\n justify-self: start;\n display: flex;\n align-items: center;\n gap: 4px;\n max-width: 100%;\n}\n.corner-tr {\n grid-area: 1 / 2 / 2 / 3;\n align-self: start;\n justify-self: end;\n}\n/* 左上角主持人标签:与左下角昵称(corner-tl,同 inset 5px)边距一致 */\n.corner-host-top {\n grid-area: 1 / 1 / 2 / 2;\n align-self: start;\n justify-self: start;\n}\n.corner-br {\n grid-area: 3 / 2 / 4 / 3;\n align-self: end;\n justify-self: end;\n}\n.participant-name {\n font-size: 12px;\n font-weight: 500;\n color: var(--im-meeting-text-primary, #fff);\n text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n max-width: 100%;\n}\n.corner-br {\n filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5));\n}\n.small-video :deep(.speaking-indicator) {\n display: inline-flex;\n align-items: center;\n gap: 3px;\n font-size: 10px;\n font-weight: 500;\n color: var(--im-meeting-speaking, #22c55e);\n background: rgba(34, 197, 94, 0.15);\n border: 1px solid rgba(34, 197, 94, 0.5);\n border-radius: 0;\n padding: 1px 4px;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);\n}\n.small-video :deep(.wave-bar) {\n width: 2px;\n background: var(--im-meeting-speaking, #22c55e);\n border-radius: 0;\n animation: wave-sm 1s ease-in-out infinite;\n}\n.small-video :deep(.wave-bar:nth-child(1)) {\n height: 4px;\n animation-delay: 0s;\n}\n.small-video :deep(.wave-bar:nth-child(2)) {\n height: 7px;\n animation-delay: 0.15s;\n}\n.small-video :deep(.wave-bar:nth-child(3)) {\n height: 4px;\n animation-delay: 0.3s;\n}\n.host-badge-inline :deep(.host-badge) {\n font-size: 9px;\n color: #fbbf24;\n background: rgba(251, 191, 36, 0.15);\n border: 1px solid rgba(251, 191, 36, 0.5);\n padding: 1px 4px;\n white-space: nowrap;\n}\n@keyframes wave-sm {\n 0%,\n 100% {\n transform: scaleY(1);\n opacity: 0.7;\n }\n 50% {\n transform: scaleY(1.6);\n opacity: 1;\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { ref } from 'vue'\nimport SmallVideoTile from './SmallVideoTile.vue'\nimport type { MeetingMember } from '@quitting/contract'\n\nconst props = defineProps<{\n members: MeetingMember[]\n // 自己的 identity(tile 据此把昵称显示为「我」)\n selfIdentity?: string\n // 自己的麦克风/摄像头不可用(自己用本地实时检测;他人用后端推断字段,口径与成员列表一致)\n selfMicUnavailable?: boolean\n selfCameraUnavailable?: boolean\n // 每个小视频 tile 挂载/卸载时回调(去重,identity 不变不会重复触发)\n onTileMount?: (identity: string, el: HTMLVideoElement | null) => void\n}>()\n\nconst emit = defineEmits<{\n (e: 'tileClick', identity: string): void\n}>()\n\n// 暴露 listEl 给父组件做布局计算(设置 16:9 tile 尺寸)\nconst listEl = ref<HTMLElement | null>(null)\ndefineExpose({ listEl })\n\n// 闭包稳定:不会因 parent re-render 反复触发\nfunction handleTileVideoReady(identity: string, el: HTMLVideoElement | null) {\n props.onTileMount?.(identity, el)\n}\n\n// 设备不可用:自己用本地检测(更及时),他人用后端推断字段(已入会但从未发布对应 track)\nfunction isSelf(m: MeetingMember) {\n return m.identity === (props.selfIdentity || '')\n}\n</script>\n\n<template>\n <div ref=\"listEl\" class=\"sidebar-list\">\n <SmallVideoTile\n v-for=\"m in members\"\n :key=\"m.identity\"\n :identity=\"m.identity\"\n :name=\"m.name\"\n :muted=\"m.muted\"\n :joined=\"m.joined\"\n :camera-on=\"m.joined && m.cameraOn\"\n :camera-unavailable=\"\n m.joined && (isSelf(m) ? !!selfCameraUnavailable : !!m.cameraUnavailable)\n \"\n :speaking=\"m.speaking\"\n :is-host=\"m.isHost\"\n :is-self=\"isSelf(m)\"\n :mic-unavailable=\"isSelf(m) ? !!selfMicUnavailable : !!m.micUnavailable\"\n :on-video-el-ready=\"handleTileVideoReady\"\n @click=\"(id: string) => emit('tileClick', id)\"\n />\n </div>\n</template>\n\n<style scoped>\n.sidebar-list {\n flex: 1 1 0;\n min-height: 0;\n overflow-y: auto;\n overflow-x: hidden;\n display: flex;\n flex-direction: column;\n gap: 8px;\n /* 隐藏滚动条但保留滚动功能 */\n scrollbar-width: none;\n}\n.sidebar-list::-webkit-scrollbar {\n display: none;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { ref } from 'vue'\nimport SmallVideoTile from './SmallVideoTile.vue'\nimport type { MeetingMember } from '@quitting/contract'\n\nconst props = defineProps<{\n members: MeetingMember[]\n // 自己的 identity(tile 据此把昵称显示为「我」)\n selfIdentity?: string\n // 自己的麦克风/摄像头不可用(自己用本地实时检测;他人用后端推断字段,口径与成员列表一致)\n selfMicUnavailable?: boolean\n selfCameraUnavailable?: boolean\n // 每个小视频 tile 挂载/卸载时回调(去重,identity 不变不会重复触发)\n onTileMount?: (identity: string, el: HTMLVideoElement | null) => void\n}>()\n\nconst emit = defineEmits<{\n (e: 'tileClick', identity: string): void\n}>()\n\n// 暴露 listEl 给父组件做布局计算(设置 16:9 tile 尺寸)\nconst listEl = ref<HTMLElement | null>(null)\ndefineExpose({ listEl })\n\n// 闭包稳定:不会因 parent re-render 反复触发\nfunction handleTileVideoReady(identity: string, el: HTMLVideoElement | null) {\n props.onTileMount?.(identity, el)\n}\n\n// 设备不可用:自己用本地检测(更及时),他人用后端推断字段(已入会但从未发布对应 track)\nfunction isSelf(m: MeetingMember) {\n return m.identity === (props.selfIdentity || '')\n}\n</script>\n\n<template>\n <div ref=\"listEl\" class=\"sidebar-list\">\n <SmallVideoTile\n v-for=\"m in members\"\n :key=\"m.identity\"\n :identity=\"m.identity\"\n :name=\"m.name\"\n :muted=\"m.muted\"\n :joined=\"m.joined\"\n :camera-on=\"m.joined && m.cameraOn\"\n :camera-unavailable=\"\n m.joined && (isSelf(m) ? !!selfCameraUnavailable : !!m.cameraUnavailable)\n \"\n :speaking=\"m.speaking\"\n :is-host=\"m.isHost\"\n :is-self=\"isSelf(m)\"\n :mic-unavailable=\"isSelf(m) ? !!selfMicUnavailable : !!m.micUnavailable\"\n :on-video-el-ready=\"handleTileVideoReady\"\n @click=\"(id: string) => emit('tileClick', id)\"\n />\n </div>\n</template>\n\n<style scoped>\n.sidebar-list {\n flex: 1 1 0;\n min-height: 0;\n overflow-y: auto;\n overflow-x: hidden;\n display: flex;\n flex-direction: column;\n gap: 8px;\n /* 隐藏滚动条但保留滚动功能 */\n scrollbar-width: none;\n}\n.sidebar-list::-webkit-scrollbar {\n display: none;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { ref, computed, onMounted, onBeforeUnmount } from 'vue'\nimport Icon from '../Icon.vue'\nimport type { MeetingMember } from '@quitting/contract'\n\nconst props = defineProps<{\n member: MeetingMember\n // 当前用户是否主持人(决定能否触发转主持人/踢人/再次邀请)\n canPromote: boolean\n canKick: boolean\n canReinvite?: boolean\n // 自己的 identity:成员列表中自己显示「我」而非昵称\n selfIdentity?: string\n // 自己的麦克风/摄像头不可用(无设备/无权限/被占用):对应状态图标置灰而非红/白\n selfMicUnavailable?: boolean\n selfCameraUnavailable?: boolean\n}>()\n\n// 是否是自己那一行\nconst isSelf = computed(() => props.member.identity === (props.selfIdentity || ''))\n\n// 设备不可用判定:自己用本地检测(更及时),他人用后端推断字段\n// (已入会但从未发布对应 track = 无设备/无权限/被占用;本端检测结果无法跨端传递)\nconst micUnavailable = computed(\n () =>\n (isSelf.value && !!props.selfMicUnavailable) ||\n (!isSelf.value && !!props.member.micUnavailable),\n)\nconst cameraUnavailable = computed(\n () =>\n (isSelf.value && !!props.selfCameraUnavailable) ||\n (!isSelf.value && !!props.member.cameraUnavailable),\n)\n\nconst emit = defineEmits<{\n (e: 'transferHost', uid: string): void\n (e: 'kick', uid: string): void\n (e: 'reinvite', uid: string): void\n}>()\n\n// 三点菜单展开状态\nconst menuOpen = ref(false)\n// 鼠标离开关闭定时器\nlet closeTimer: ReturnType<typeof setTimeout> | null = null\nconst CLOSE_DELAY = 500\n\n// 入会(online + joined)才允许设为主持人;离线/等待加入不显示此菜单项\nconst canShowPromote = () =>\n props.canPromote && props.member.online && props.member.joined && !props.member.isHost\n// 入会后(无论在线还是暂时离线)都允许踢出;已离会不需要再踢\nconst canShowKick = () => props.canKick && !props.member.isHost && props.member.status !== 'left'\n// 再次邀请:等待加入(status=invited)或已离会(status=left)允许\nconst canShowReinvite = () =>\n !!props.canReinvite &&\n !props.member.isHost &&\n (props.member.status === 'invited' || props.member.status === 'left')\n\nfunction toggleMenu() {\n if (menuOpen.value) {\n closeMenu()\n } else {\n menuOpen.value = true\n }\n}\nfunction closeMenu() {\n menuOpen.value = false\n if (closeTimer) {\n clearTimeout(closeTimer)\n closeTimer = null\n }\n}\n// 鼠标进入按钮或菜单:取消关闭定时器\nfunction onMouseEnter() {\n if (closeTimer) {\n clearTimeout(closeTimer)\n closeTimer = null\n }\n}\n// 鼠标离开按钮或菜单:500ms 后关闭\nfunction onMouseLeave() {\n if (!menuOpen.value) return\n if (closeTimer) clearTimeout(closeTimer)\n closeTimer = setTimeout(() => {\n menuOpen.value = false\n closeTimer = null\n }, CLOSE_DELAY)\n}\nfunction onPromote() {\n closeMenu()\n emit('transferHost', props.member.identity)\n}\nfunction onKick() {\n closeMenu()\n emit('kick', props.member.identity)\n}\nfunction onReinvite() {\n closeMenu()\n emit('reinvite', props.member.identity)\n}\n\n// 点击外部关闭菜单\nfunction onDocClick(e: MouseEvent) {\n if (!menuOpen.value) return\n const target = e.target as HTMLElement\n if (!target.closest('.member-item-row')) {\n closeMenu()\n }\n}\nonMounted(() => {\n document.addEventListener('click', onDocClick)\n})\nonBeforeUnmount(() => {\n document.removeEventListener('click', onDocClick)\n if (closeTimer) clearTimeout(closeTimer)\n})\n</script>\n\n<template>\n <div\n class=\"member-item\"\n :class=\"{\n offline: !member.online && member.status !== 'left',\n waiting: member.online && !member.joined,\n left: member.status === 'left',\n }\"\n >\n <div class=\"member-row\">\n <div class=\"member-main\">\n <div class=\"member-name\">\n {{ isSelf ? '我' : member.name }}\n <span v-if=\"member.isHost\" class=\"host-tag\">主持人</span>\n </div>\n </div>\n <div class=\"member-status\">\n <span v-if=\"member.status === 'left'\" class=\"status-badge\">已离会</span>\n <span v-else-if=\"!member.online\" class=\"status-badge\">离线</span>\n <span v-else-if=\"!member.joined\" class=\"status-badge\">等待加入</span>\n <span v-else-if=\"member.speaking\" class=\"status-badge speaking\">正在发言</span>\n </div>\n </div>\n <div class=\"member-row member-item-row\">\n <div class=\"member-main\">\n <div class=\"member-dept\">{{ member.dept || '—' }}</div>\n </div>\n <div class=\"member-status\">\n <!-- 三点菜单按钮(左侧) -->\n <button\n v-if=\"canShowPromote() || canShowKick() || canShowReinvite()\"\n class=\"member-menu\"\n type=\"button\"\n aria-label=\"更多操作\"\n @click.stop=\"toggleMenu\"\n @mouseenter=\"onMouseEnter\"\n @mouseleave=\"onMouseLeave\"\n >\n <Icon name=\"more\" :size=\"14\" />\n </button>\n <!-- 下拉菜单 -->\n <div\n v-if=\"menuOpen\"\n class=\"member-menu-popover\"\n @mouseenter=\"onMouseEnter\"\n @mouseleave=\"onMouseLeave\"\n >\n <button v-if=\"canShowReinvite()\" class=\"menu-item\" type=\"button\" @click.stop=\"onReinvite\">\n 再次邀请\n </button>\n <button v-if=\"canShowPromote()\" class=\"menu-item\" type=\"button\" @click.stop=\"onPromote\">\n 设为主持人\n </button>\n <button v-if=\"canShowKick()\" class=\"menu-item\" type=\"button\" @click.stop=\"onKick\">\n 离会\n </button>\n </div>\n <!-- 摄像头/麦克风状态图标(最右侧):仅入会成员显示。\n 图标统一:不可用或关闭 → 带斜杠 off 图标;不可用置灰(自己用本地检测,他人用后端推断字段),\n 主动闭麦红色,开启亮白 -->\n <Icon\n v-if=\"member.joined\"\n :name=\"cameraUnavailable || !member.cameraOn ? 'camera-off' : 'camera-on'\"\n :size=\"14\"\n :color=\"\n cameraUnavailable\n ? 'rgba(255, 255, 255, 0.45)'\n : member.cameraOn\n ? 'rgba(255, 255, 255, 0.85)'\n : '#ef4444'\n \"\n :title=\"\n cameraUnavailable ? '摄像头不可用' : member.cameraOn ? '摄像头已开' : '摄像头已关'\n \"\n />\n <Icon\n v-if=\"member.joined\"\n :name=\"micUnavailable || member.muted ? 'mic-off' : 'mic-on'\"\n :size=\"14\"\n :color=\"\n micUnavailable\n ? 'rgba(255, 255, 255, 0.45)'\n : member.muted\n ? '#ef4444'\n : 'rgba(255, 255, 255, 0.85)'\n \"\n :title=\"micUnavailable ? '麦克风不可用' : member.muted ? '麦克风已关' : '麦克风已开'\"\n />\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.member-item {\n position: relative;\n display: flex;\n flex-direction: column;\n justify-content: center;\n gap: 6px;\n height: 56px;\n padding: 0 10px;\n background: rgba(255, 255, 255, 0.03);\n border: 1px solid var(--im-meeting-border, rgba(255, 255, 255, 0.08));\n border-radius: 0;\n}\n/* 离线/等待加入:仅文字透明度降低,边框/按钮/菜单正常显示 */\n/* 整行不再 opacity,避免影响 ⋯ 按钮 + 悬浮菜单可读性 */\n.member-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n min-height: 0;\n}\n.member-main {\n flex: 1 1 0;\n min-width: 0;\n}\n.member-name {\n /* 入会成员昵称:从中亮白降到中等亮度的暖灰,避免与背景对比过强 */\n font-size: 13px;\n font-weight: 500;\n color: rgba(255, 255, 255, 0.78);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n display: flex;\n align-items: center;\n gap: 6px;\n}\n.member-item.offline .member-name,\n.member-item.waiting .member-name,\n.member-item.left .member-name {\n color: var(--im-meeting-text-tertiary, rgba(255, 255, 255, 0.55));\n}\n.host-tag {\n font-size: 10px;\n color: #fbbf24;\n background: rgba(251, 191, 36, 0.15);\n border: 1px solid rgba(251, 191, 36, 0.5);\n padding: 1px 4px;\n white-space: nowrap;\n}\n.member-dept {\n /* 部门颜色同样降低亮度 */\n font-size: 11px;\n color: rgba(255, 255, 255, 0.55);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.member-item.offline .member-dept,\n.member-item.waiting .member-dept,\n.member-item.left .member-dept {\n color: var(--im-meeting-text-tertiary, rgba(255, 255, 255, 0.55));\n}\n.member-status {\n display: flex;\n align-items: center;\n gap: 6px;\n flex: 0 0 auto;\n}\n/* 状态标签:无圆角、无边框(仅靠字色区分) */\n.status-badge {\n font-size: 10px;\n color: var(--im-meeting-text-tertiary, rgba(255, 255, 255, 0.55));\n padding: 1px 5px;\n white-space: nowrap;\n border-radius: 0;\n background: transparent;\n}\n.status-badge.speaking {\n color: var(--im-meeting-speaking, #22c55e);\n}\n.member-menu {\n width: 22px;\n height: 22px;\n border: none;\n background: transparent;\n color: var(--im-meeting-text-secondary, rgba(255, 255, 255, 0.85));\n cursor: pointer;\n border-radius: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n}\n.member-menu:hover {\n background: rgba(255, 255, 255, 0.08);\n color: var(--im-meeting-text-primary, #fff);\n}\n/* 下拉菜单:弹在三点菜单按钮左侧(麦克图标现在在最右) */\n.member-menu-popover {\n position: absolute;\n top: calc(50% - 12px);\n right: 56px;\n z-index: 5;\n min-width: 96px;\n background: #1e293b;\n border: 1px solid var(--im-meeting-border, rgba(255, 255, 255, 0.08));\n border-radius: 0;\n display: flex;\n flex-direction: column;\n padding: 2px 0;\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);\n}\n.menu-item {\n border: none;\n background: transparent;\n /* 离会与设为主持人颜色一致——不再用红色突出 danger */\n color: var(--im-meeting-text-secondary, rgba(255, 255, 255, 0.85));\n font-size: 12px;\n padding: 6px 12px;\n text-align: left;\n cursor: pointer;\n border-radius: 0;\n}\n.menu-item:hover {\n background: rgba(255, 255, 255, 0.08);\n color: var(--im-meeting-text-primary, #fff);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { ref, computed, onMounted, onBeforeUnmount } from 'vue'\nimport Icon from '../Icon.vue'\nimport type { MeetingMember } from '@quitting/contract'\n\nconst props = defineProps<{\n member: MeetingMember\n // 当前用户是否主持人(决定能否触发转主持人/踢人/再次邀请)\n canPromote: boolean\n canKick: boolean\n canReinvite?: boolean\n // 自己的 identity:成员列表中自己显示「我」而非昵称\n selfIdentity?: string\n // 自己的麦克风/摄像头不可用(无设备/无权限/被占用):对应状态图标置灰而非红/白\n selfMicUnavailable?: boolean\n selfCameraUnavailable?: boolean\n}>()\n\n// 是否是自己那一行\nconst isSelf = computed(() => props.member.identity === (props.selfIdentity || ''))\n\n// 设备不可用判定:自己用本地检测(更及时),他人用后端推断字段\n// (已入会但从未发布对应 track = 无设备/无权限/被占用;本端检测结果无法跨端传递)\nconst micUnavailable = computed(\n () =>\n (isSelf.value && !!props.selfMicUnavailable) ||\n (!isSelf.value && !!props.member.micUnavailable),\n)\nconst cameraUnavailable = computed(\n () =>\n (isSelf.value && !!props.selfCameraUnavailable) ||\n (!isSelf.value && !!props.member.cameraUnavailable),\n)\n\nconst emit = defineEmits<{\n (e: 'transferHost', uid: string): void\n (e: 'kick', uid: string): void\n (e: 'reinvite', uid: string): void\n}>()\n\n// 三点菜单展开状态\nconst menuOpen = ref(false)\n// 鼠标离开关闭定时器\nlet closeTimer: ReturnType<typeof setTimeout> | null = null\nconst CLOSE_DELAY = 500\n\n// 入会(online + joined)才允许设为主持人;离线/等待加入不显示此菜单项\nconst canShowPromote = () =>\n props.canPromote && props.member.online && props.member.joined && !props.member.isHost\n// 入会后(无论在线还是暂时离线)都允许踢出;已离会不需要再踢\nconst canShowKick = () => props.canKick && !props.member.isHost && props.member.status !== 'left'\n// 再次邀请:等待加入(status=invited)或已离会(status=left)允许\nconst canShowReinvite = () =>\n !!props.canReinvite &&\n !props.member.isHost &&\n (props.member.status === 'invited' || props.member.status === 'left')\n\nfunction toggleMenu() {\n if (menuOpen.value) {\n closeMenu()\n } else {\n menuOpen.value = true\n }\n}\nfunction closeMenu() {\n menuOpen.value = false\n if (closeTimer) {\n clearTimeout(closeTimer)\n closeTimer = null\n }\n}\n// 鼠标进入按钮或菜单:取消关闭定时器\nfunction onMouseEnter() {\n if (closeTimer) {\n clearTimeout(closeTimer)\n closeTimer = null\n }\n}\n// 鼠标离开按钮或菜单:500ms 后关闭\nfunction onMouseLeave() {\n if (!menuOpen.value) return\n if (closeTimer) clearTimeout(closeTimer)\n closeTimer = setTimeout(() => {\n menuOpen.value = false\n closeTimer = null\n }, CLOSE_DELAY)\n}\nfunction onPromote() {\n closeMenu()\n emit('transferHost', props.member.identity)\n}\nfunction onKick() {\n closeMenu()\n emit('kick', props.member.identity)\n}\nfunction onReinvite() {\n closeMenu()\n emit('reinvite', props.member.identity)\n}\n\n// 点击外部关闭菜单\nfunction onDocClick(e: MouseEvent) {\n if (!menuOpen.value) return\n const target = e.target as HTMLElement\n if (!target.closest('.member-item-row')) {\n closeMenu()\n }\n}\nonMounted(() => {\n document.addEventListener('click', onDocClick)\n})\nonBeforeUnmount(() => {\n document.removeEventListener('click', onDocClick)\n if (closeTimer) clearTimeout(closeTimer)\n})\n</script>\n\n<template>\n <div\n class=\"member-item\"\n :class=\"{\n offline: !member.online && member.status !== 'left',\n waiting: member.online && !member.joined,\n left: member.status === 'left',\n }\"\n >\n <div class=\"member-row\">\n <div class=\"member-main\">\n <div class=\"member-name\">\n {{ isSelf ? '我' : member.name }}\n <span v-if=\"member.isHost\" class=\"host-tag\">主持人</span>\n </div>\n </div>\n <div class=\"member-status\">\n <span v-if=\"member.status === 'left'\" class=\"status-badge\">已离会</span>\n <span v-else-if=\"!member.online\" class=\"status-badge\">离线</span>\n <span v-else-if=\"!member.joined\" class=\"status-badge\">等待加入</span>\n <span v-else-if=\"member.speaking\" class=\"status-badge speaking\">正在发言</span>\n </div>\n </div>\n <div class=\"member-row member-item-row\">\n <div class=\"member-main\">\n <div class=\"member-dept\">{{ member.dept || '—' }}</div>\n </div>\n <div class=\"member-status\">\n <!-- 三点菜单按钮(左侧) -->\n <button\n v-if=\"canShowPromote() || canShowKick() || canShowReinvite()\"\n class=\"member-menu\"\n type=\"button\"\n aria-label=\"更多操作\"\n @click.stop=\"toggleMenu\"\n @mouseenter=\"onMouseEnter\"\n @mouseleave=\"onMouseLeave\"\n >\n <Icon name=\"more\" :size=\"14\" />\n </button>\n <!-- 下拉菜单 -->\n <div\n v-if=\"menuOpen\"\n class=\"member-menu-popover\"\n @mouseenter=\"onMouseEnter\"\n @mouseleave=\"onMouseLeave\"\n >\n <button v-if=\"canShowReinvite()\" class=\"menu-item\" type=\"button\" @click.stop=\"onReinvite\">\n 再次邀请\n </button>\n <button v-if=\"canShowPromote()\" class=\"menu-item\" type=\"button\" @click.stop=\"onPromote\">\n 设为主持人\n </button>\n <button v-if=\"canShowKick()\" class=\"menu-item\" type=\"button\" @click.stop=\"onKick\">\n 离会\n </button>\n </div>\n <!-- 摄像头/麦克风状态图标(最右侧):仅入会成员显示。\n 图标统一:不可用或关闭 → 带斜杠 off 图标;不可用置灰(自己用本地检测,他人用后端推断字段),\n 主动闭麦红色,开启亮白 -->\n <Icon\n v-if=\"member.joined\"\n :name=\"cameraUnavailable || !member.cameraOn ? 'camera-off' : 'camera-on'\"\n :size=\"14\"\n :color=\"\n cameraUnavailable\n ? 'rgba(255, 255, 255, 0.45)'\n : member.cameraOn\n ? 'rgba(255, 255, 255, 0.85)'\n : '#ef4444'\n \"\n :title=\"\n cameraUnavailable ? '摄像头不可用' : member.cameraOn ? '摄像头已开' : '摄像头已关'\n \"\n />\n <Icon\n v-if=\"member.joined\"\n :name=\"micUnavailable || member.muted ? 'mic-off' : 'mic-on'\"\n :size=\"14\"\n :color=\"\n micUnavailable\n ? 'rgba(255, 255, 255, 0.45)'\n : member.muted\n ? '#ef4444'\n : 'rgba(255, 255, 255, 0.85)'\n \"\n :title=\"micUnavailable ? '麦克风不可用' : member.muted ? '麦克风已关' : '麦克风已开'\"\n />\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.member-item {\n position: relative;\n display: flex;\n flex-direction: column;\n justify-content: center;\n gap: 6px;\n height: 56px;\n padding: 0 10px;\n background: rgba(255, 255, 255, 0.03);\n border: 1px solid var(--im-meeting-border, rgba(255, 255, 255, 0.08));\n border-radius: 0;\n}\n/* 离线/等待加入:仅文字透明度降低,边框/按钮/菜单正常显示 */\n/* 整行不再 opacity,避免影响 ⋯ 按钮 + 悬浮菜单可读性 */\n.member-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n min-height: 0;\n}\n.member-main {\n flex: 1 1 0;\n min-width: 0;\n}\n.member-name {\n /* 入会成员昵称:从中亮白降到中等亮度的暖灰,避免与背景对比过强 */\n font-size: 13px;\n font-weight: 500;\n color: rgba(255, 255, 255, 0.78);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n display: flex;\n align-items: center;\n gap: 6px;\n}\n.member-item.offline .member-name,\n.member-item.waiting .member-name,\n.member-item.left .member-name {\n color: var(--im-meeting-text-tertiary, rgba(255, 255, 255, 0.55));\n}\n.host-tag {\n font-size: 10px;\n color: #fbbf24;\n background: rgba(251, 191, 36, 0.15);\n border: 1px solid rgba(251, 191, 36, 0.5);\n padding: 1px 4px;\n white-space: nowrap;\n}\n.member-dept {\n /* 部门颜色同样降低亮度 */\n font-size: 11px;\n color: rgba(255, 255, 255, 0.55);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.member-item.offline .member-dept,\n.member-item.waiting .member-dept,\n.member-item.left .member-dept {\n color: var(--im-meeting-text-tertiary, rgba(255, 255, 255, 0.55));\n}\n.member-status {\n display: flex;\n align-items: center;\n gap: 6px;\n flex: 0 0 auto;\n}\n/* 状态标签:无圆角、无边框(仅靠字色区分) */\n.status-badge {\n font-size: 10px;\n color: var(--im-meeting-text-tertiary, rgba(255, 255, 255, 0.55));\n padding: 1px 5px;\n white-space: nowrap;\n border-radius: 0;\n background: transparent;\n}\n.status-badge.speaking {\n color: var(--im-meeting-speaking, #22c55e);\n}\n.member-menu {\n width: 22px;\n height: 22px;\n border: none;\n background: transparent;\n color: var(--im-meeting-text-secondary, rgba(255, 255, 255, 0.85));\n cursor: pointer;\n border-radius: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n}\n.member-menu:hover {\n background: rgba(255, 255, 255, 0.08);\n color: var(--im-meeting-text-primary, #fff);\n}\n/* 下拉菜单:弹在三点菜单按钮左侧(麦克图标现在在最右) */\n.member-menu-popover {\n position: absolute;\n top: calc(50% - 12px);\n right: 56px;\n z-index: 5;\n min-width: 96px;\n background: #1e293b;\n border: 1px solid var(--im-meeting-border, rgba(255, 255, 255, 0.08));\n border-radius: 0;\n display: flex;\n flex-direction: column;\n padding: 2px 0;\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);\n}\n.menu-item {\n border: none;\n background: transparent;\n /* 离会与设为主持人颜色一致——不再用红色突出 danger */\n color: var(--im-meeting-text-secondary, rgba(255, 255, 255, 0.85));\n font-size: 12px;\n padding: 6px 12px;\n text-align: left;\n cursor: pointer;\n border-radius: 0;\n}\n.menu-item:hover {\n background: rgba(255, 255, 255, 0.08);\n color: var(--im-meeting-text-primary, #fff);\n}\n</style>\n","<script setup lang=\"ts\">\nimport MemberItem from './MemberItem.vue'\nimport type { MeetingMember } from '@quitting/contract'\n\ndefineProps<{\n members: MeetingMember[]\n canPromote: boolean\n canKick: boolean\n // 主持人权限:允许「再次邀请」status=invited/left 的成员\n canReinvite?: boolean\n // 自己的 identity 与设备不可用状态(透传给 MemberItem:自己显示「我」、图标置灰)\n selfIdentity?: string\n selfMicUnavailable?: boolean\n selfCameraUnavailable?: boolean\n}>()\n\nconst emit = defineEmits<{\n (e: 'transferHost', uid: string): void\n (e: 'kick', uid: string): void\n (e: 'reinvite', uid: string): void\n}>()\n</script>\n\n<template>\n <div class=\"member-list\">\n <MemberItem\n v-for=\"m in members\"\n :key=\"m.identity\"\n :member=\"m\"\n :can-promote=\"canPromote\"\n :can-kick=\"canKick\"\n :can-reinvite=\"canReinvite\"\n :self-identity=\"selfIdentity\"\n :self-mic-unavailable=\"selfMicUnavailable\"\n :self-camera-unavailable=\"selfCameraUnavailable\"\n @transfer-host=\"(uid: string) => emit('transferHost', uid)\"\n @kick=\"(uid: string) => emit('kick', uid)\"\n @reinvite=\"(uid: string) => emit('reinvite', uid)\"\n />\n </div>\n</template>\n\n<style scoped>\n.member-list {\n flex: 1 1 0;\n min-height: 0;\n overflow-y: auto;\n overflow-x: hidden;\n display: flex;\n flex-direction: column;\n gap: 8px;\n /* 隐藏滚动条但保留滚动功能 */\n scrollbar-width: none;\n}\n.member-list::-webkit-scrollbar {\n display: none;\n}\n</style>\n","<script setup lang=\"ts\">\nimport MemberItem from './MemberItem.vue'\nimport type { MeetingMember } from '@quitting/contract'\n\ndefineProps<{\n members: MeetingMember[]\n canPromote: boolean\n canKick: boolean\n // 主持人权限:允许「再次邀请」status=invited/left 的成员\n canReinvite?: boolean\n // 自己的 identity 与设备不可用状态(透传给 MemberItem:自己显示「我」、图标置灰)\n selfIdentity?: string\n selfMicUnavailable?: boolean\n selfCameraUnavailable?: boolean\n}>()\n\nconst emit = defineEmits<{\n (e: 'transferHost', uid: string): void\n (e: 'kick', uid: string): void\n (e: 'reinvite', uid: string): void\n}>()\n</script>\n\n<template>\n <div class=\"member-list\">\n <MemberItem\n v-for=\"m in members\"\n :key=\"m.identity\"\n :member=\"m\"\n :can-promote=\"canPromote\"\n :can-kick=\"canKick\"\n :can-reinvite=\"canReinvite\"\n :self-identity=\"selfIdentity\"\n :self-mic-unavailable=\"selfMicUnavailable\"\n :self-camera-unavailable=\"selfCameraUnavailable\"\n @transfer-host=\"(uid: string) => emit('transferHost', uid)\"\n @kick=\"(uid: string) => emit('kick', uid)\"\n @reinvite=\"(uid: string) => emit('reinvite', uid)\"\n />\n </div>\n</template>\n\n<style scoped>\n.member-list {\n flex: 1 1 0;\n min-height: 0;\n overflow-y: auto;\n overflow-x: hidden;\n display: flex;\n flex-direction: column;\n gap: 8px;\n /* 隐藏滚动条但保留滚动功能 */\n scrollbar-width: none;\n}\n.member-list::-webkit-scrollbar {\n display: none;\n}\n</style>\n","<script setup lang=\"ts\">\nimport Icon from '../Icon.vue'\n\ndefineProps<{\n activeTab: 'video' | 'member'\n // 是否显示「+」添加参会人员按钮——仅主持人可邀请,默认不显示\n isHost?: boolean\n}>()\n\nconst emit = defineEmits<{\n (e: 'update:activeTab', tab: 'video' | 'member'): void\n (e: 'addMember'): void\n}>()\n</script>\n\n<template>\n <div class=\"tab-bar\">\n <button\n class=\"tab-btn\"\n :class=\"{ active: activeTab === 'video' }\"\n type=\"button\"\n @click=\"emit('update:activeTab', 'video')\"\n >\n 视频列表\n </button>\n <button\n class=\"tab-btn\"\n :class=\"{ active: activeTab === 'member', 'before-add': true }\"\n type=\"button\"\n @click=\"emit('update:activeTab', 'member')\"\n >\n 成员列表\n </button>\n <template v-if=\"isHost\">\n <span class=\"tab-divider\" aria-hidden=\"true\"></span>\n <button class=\"add-member-btn\" type=\"button\" title=\"添加参会人员\" @click=\"emit('addMember')\">\n <Icon name=\"plus\" :size=\"14\" />\n </button>\n </template>\n </div>\n</template>\n\n<style scoped>\n.tab-bar {\n flex: 0 0 auto;\n display: flex;\n flex-direction: row;\n align-items: stretch;\n /* 整个 tab-bar 撑满父级宽度,与下方列表对齐 */\n width: 100%;\n gap: 0;\n}\n.tab-btn {\n flex: 1 1 0;\n padding: 0 8px;\n height: 28px;\n line-height: 28px;\n background: var(--im-meeting-tab-inactive, #111827);\n border: 1px solid var(--im-meeting-border, rgba(255, 255, 255, 0.08));\n border-right: none;\n color: var(--im-meeting-text-secondary, rgba(255, 255, 255, 0.85));\n font-size: 12px;\n cursor: pointer;\n transition:\n background 0.2s,\n color 0.2s;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n border-radius: 0;\n min-width: 0;\n}\n/* 选中 tab 与 + 按钮之间需要可见分割线:两个 #1e293b 背景连成一片时 1px 边框看不出来\n 用一个 2px 深色实体分割条嵌在中间,颜色对比足够明显 */\n.tab-btn.before-add {\n border-right: none;\n}\n.tab-divider {\n flex: 0 0 auto;\n width: 2px;\n background: #0b1220;\n border: none;\n align-self: stretch;\n}\n.tab-btn.active {\n background: var(--im-meeting-tab-active, #1e293b);\n color: var(--im-meeting-text-primary, #fff);\n}\n.tab-btn:hover:not(.active) {\n background: #1f2937;\n color: var(--im-meeting-text-primary, #fff);\n}\n/* 添加按钮:始终 28×28 正方形;Icon 由 flex 居中 */\n.add-member-btn {\n flex: 0 0 28px;\n width: 28px;\n height: 28px;\n border: 1px solid var(--im-meeting-border, rgba(255, 255, 255, 0.08));\n border-left: none;\n background: var(--im-meeting-tab-active, #1e293b);\n color: var(--im-meeting-text-primary, #fff);\n cursor: pointer;\n border-radius: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n margin-left: -1px;\n box-sizing: border-box;\n}\n.add-member-btn:hover {\n background: #334155;\n}\n</style>\n","<script setup lang=\"ts\">\nimport Icon from '../Icon.vue'\n\ndefineProps<{\n activeTab: 'video' | 'member'\n // 是否显示「+」添加参会人员按钮——仅主持人可邀请,默认不显示\n isHost?: boolean\n}>()\n\nconst emit = defineEmits<{\n (e: 'update:activeTab', tab: 'video' | 'member'): void\n (e: 'addMember'): void\n}>()\n</script>\n\n<template>\n <div class=\"tab-bar\">\n <button\n class=\"tab-btn\"\n :class=\"{ active: activeTab === 'video' }\"\n type=\"button\"\n @click=\"emit('update:activeTab', 'video')\"\n >\n 视频列表\n </button>\n <button\n class=\"tab-btn\"\n :class=\"{ active: activeTab === 'member', 'before-add': true }\"\n type=\"button\"\n @click=\"emit('update:activeTab', 'member')\"\n >\n 成员列表\n </button>\n <template v-if=\"isHost\">\n <span class=\"tab-divider\" aria-hidden=\"true\"></span>\n <button class=\"add-member-btn\" type=\"button\" title=\"添加参会人员\" @click=\"emit('addMember')\">\n <Icon name=\"plus\" :size=\"14\" />\n </button>\n </template>\n </div>\n</template>\n\n<style scoped>\n.tab-bar {\n flex: 0 0 auto;\n display: flex;\n flex-direction: row;\n align-items: stretch;\n /* 整个 tab-bar 撑满父级宽度,与下方列表对齐 */\n width: 100%;\n gap: 0;\n}\n.tab-btn {\n flex: 1 1 0;\n padding: 0 8px;\n height: 28px;\n line-height: 28px;\n background: var(--im-meeting-tab-inactive, #111827);\n border: 1px solid var(--im-meeting-border, rgba(255, 255, 255, 0.08));\n border-right: none;\n color: var(--im-meeting-text-secondary, rgba(255, 255, 255, 0.85));\n font-size: 12px;\n cursor: pointer;\n transition:\n background 0.2s,\n color 0.2s;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n border-radius: 0;\n min-width: 0;\n}\n/* 选中 tab 与 + 按钮之间需要可见分割线:两个 #1e293b 背景连成一片时 1px 边框看不出来\n 用一个 2px 深色实体分割条嵌在中间,颜色对比足够明显 */\n.tab-btn.before-add {\n border-right: none;\n}\n.tab-divider {\n flex: 0 0 auto;\n width: 2px;\n background: #0b1220;\n border: none;\n align-self: stretch;\n}\n.tab-btn.active {\n background: var(--im-meeting-tab-active, #1e293b);\n color: var(--im-meeting-text-primary, #fff);\n}\n.tab-btn:hover:not(.active) {\n background: #1f2937;\n color: var(--im-meeting-text-primary, #fff);\n}\n/* 添加按钮:始终 28×28 正方形;Icon 由 flex 居中 */\n.add-member-btn {\n flex: 0 0 28px;\n width: 28px;\n height: 28px;\n border: 1px solid var(--im-meeting-border, rgba(255, 255, 255, 0.08));\n border-left: none;\n background: var(--im-meeting-tab-active, #1e293b);\n color: var(--im-meeting-text-primary, #fff);\n cursor: pointer;\n border-radius: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n margin-left: -1px;\n box-sizing: border-box;\n}\n.add-member-btn:hover {\n background: #334155;\n}\n</style>\n","<script setup lang=\"ts\">\nimport Icon from '../Icon.vue'\n\n// 会议控制按钮组(麦克风/摄像头/扬声器/挂断)\n// 默认模式放在窗口底部、全屏宫格模式放在 meeting-header 中央——两处复用同一组件,\n// 保证样式与行为完全一致。状态与动作由父组件持有,本组件只做展示与事件转发\n\ndefineProps<{\n micEnabled: boolean\n cameraEnabled: boolean\n speakerMuted: boolean\n // 设备不可用(无设备/无权限/被占用):按钮置灰禁用——红色仅表示主动关闭\n micUnavailable?: boolean\n cameraUnavailable?: boolean\n}>()\n\nconst emit = defineEmits<{\n (e: 'toggleMic'): void\n (e: 'toggleCamera'): void\n (e: 'toggleSpeaker'): void\n (e: 'hangup'): void\n}>()\n</script>\n\n<template>\n <div class=\"meeting-controls\">\n <button\n class=\"ctrl-btn\"\n :class=\"{ off: !micEnabled && !micUnavailable, unavailable: micUnavailable }\"\n type=\"button\"\n :disabled=\"micUnavailable\"\n :title=\"micUnavailable ? '麦克风不可用' : micEnabled ? '麦克风已开' : '麦克风已关'\"\n @click=\"emit('toggleMic')\"\n >\n <Icon :name=\"micEnabled && !micUnavailable ? 'mic-on' : 'mic-off'\" :size=\"22\" />\n </button>\n <button\n class=\"ctrl-btn\"\n :class=\"{ off: !cameraEnabled && !cameraUnavailable, unavailable: cameraUnavailable }\"\n type=\"button\"\n :disabled=\"cameraUnavailable\"\n :title=\"cameraUnavailable ? '摄像头不可用' : cameraEnabled ? '摄像头已开' : '摄像头已关'\"\n @click=\"emit('toggleCamera')\"\n >\n <Icon :name=\"cameraEnabled && !cameraUnavailable ? 'camera-on' : 'camera-off'\" :size=\"22\" />\n </button>\n <button\n class=\"ctrl-btn\"\n :class=\"{ off: speakerMuted }\"\n type=\"button\"\n :title=\"speakerMuted ? '扬声器已关' : '扬声器已开'\"\n @click=\"emit('toggleSpeaker')\"\n >\n <Icon :name=\"speakerMuted ? 'sound-off' : 'sound-on'\" :size=\"22\" />\n </button>\n <button class=\"ctrl-btn hangup\" type=\"button\" title=\"离开会议\" @click=\"emit('hangup')\">\n <Icon name=\"hangup\" :size=\"22\" />\n </button>\n </div>\n</template>\n\n<style scoped>\n.meeting-controls {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 16px;\n}\n.ctrl-btn {\n width: 32px;\n height: 32px;\n /* 无边框无背景:仅图标 */\n border: none;\n background: transparent;\n color: var(--im-meeting-text-primary, #fff);\n cursor: pointer;\n border-radius: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n transition:\n transform 0.1s,\n opacity 0.15s;\n opacity: 0.85;\n}\n.ctrl-btn:hover {\n opacity: 1;\n}\n.ctrl-btn:active {\n transform: scale(0.92);\n}\n/* 开关关闭状态:图标变红(仅主动关闭;设备不可用走 .unavailable 置灰) */\n.ctrl-btn.off {\n color: #ef4444;\n opacity: 1;\n}\n/* 设备不可用:置灰 + 禁点 */\n.ctrl-btn.unavailable {\n color: rgba(255, 255, 255, 0.45);\n opacity: 1;\n cursor: not-allowed;\n}\n/* 挂断按钮:图标红色(无红色背景块) */\n.ctrl-btn.hangup {\n color: #ef4444;\n}\n.ctrl-btn.hangup:hover {\n color: #f45f5b;\n}\n</style>\n","<script setup lang=\"ts\">\nimport Icon from '../Icon.vue'\n\n// 会议控制按钮组(麦克风/摄像头/扬声器/挂断)\n// 默认模式放在窗口底部、全屏宫格模式放在 meeting-header 中央——两处复用同一组件,\n// 保证样式与行为完全一致。状态与动作由父组件持有,本组件只做展示与事件转发\n\ndefineProps<{\n micEnabled: boolean\n cameraEnabled: boolean\n speakerMuted: boolean\n // 设备不可用(无设备/无权限/被占用):按钮置灰禁用——红色仅表示主动关闭\n micUnavailable?: boolean\n cameraUnavailable?: boolean\n}>()\n\nconst emit = defineEmits<{\n (e: 'toggleMic'): void\n (e: 'toggleCamera'): void\n (e: 'toggleSpeaker'): void\n (e: 'hangup'): void\n}>()\n</script>\n\n<template>\n <div class=\"meeting-controls\">\n <button\n class=\"ctrl-btn\"\n :class=\"{ off: !micEnabled && !micUnavailable, unavailable: micUnavailable }\"\n type=\"button\"\n :disabled=\"micUnavailable\"\n :title=\"micUnavailable ? '麦克风不可用' : micEnabled ? '麦克风已开' : '麦克风已关'\"\n @click=\"emit('toggleMic')\"\n >\n <Icon :name=\"micEnabled && !micUnavailable ? 'mic-on' : 'mic-off'\" :size=\"22\" />\n </button>\n <button\n class=\"ctrl-btn\"\n :class=\"{ off: !cameraEnabled && !cameraUnavailable, unavailable: cameraUnavailable }\"\n type=\"button\"\n :disabled=\"cameraUnavailable\"\n :title=\"cameraUnavailable ? '摄像头不可用' : cameraEnabled ? '摄像头已开' : '摄像头已关'\"\n @click=\"emit('toggleCamera')\"\n >\n <Icon :name=\"cameraEnabled && !cameraUnavailable ? 'camera-on' : 'camera-off'\" :size=\"22\" />\n </button>\n <button\n class=\"ctrl-btn\"\n :class=\"{ off: speakerMuted }\"\n type=\"button\"\n :title=\"speakerMuted ? '扬声器已关' : '扬声器已开'\"\n @click=\"emit('toggleSpeaker')\"\n >\n <Icon :name=\"speakerMuted ? 'sound-off' : 'sound-on'\" :size=\"22\" />\n </button>\n <button class=\"ctrl-btn hangup\" type=\"button\" title=\"离开会议\" @click=\"emit('hangup')\">\n <Icon name=\"hangup\" :size=\"22\" />\n </button>\n </div>\n</template>\n\n<style scoped>\n.meeting-controls {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 16px;\n}\n.ctrl-btn {\n width: 32px;\n height: 32px;\n /* 无边框无背景:仅图标 */\n border: none;\n background: transparent;\n color: var(--im-meeting-text-primary, #fff);\n cursor: pointer;\n border-radius: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n transition:\n transform 0.1s,\n opacity 0.15s;\n opacity: 0.85;\n}\n.ctrl-btn:hover {\n opacity: 1;\n}\n.ctrl-btn:active {\n transform: scale(0.92);\n}\n/* 开关关闭状态:图标变红(仅主动关闭;设备不可用走 .unavailable 置灰) */\n.ctrl-btn.off {\n color: #ef4444;\n opacity: 1;\n}\n/* 设备不可用:置灰 + 禁点 */\n.ctrl-btn.unavailable {\n color: rgba(255, 255, 255, 0.45);\n opacity: 1;\n cursor: not-allowed;\n}\n/* 挂断按钮:图标红色(无红色背景块) */\n.ctrl-btn.hangup {\n color: #ef4444;\n}\n.ctrl-btn.hangup:hover {\n color: #f45f5b;\n}\n</style>\n","<script setup lang=\"ts\">\n// 通用确认弹窗:深色主题、零圆角、水平居中\ndefineProps<{\n title?: string\n message: string\n confirmText?: string\n cancelText?: string\n danger?: boolean\n}>()\n\nconst emit = defineEmits<{\n (e: 'confirm'): void\n (e: 'cancel'): void\n}>()\n</script>\n\n<template>\n <div class=\"confirm-mask\" @click.self=\"emit('cancel')\">\n <div class=\"confirm-dialog\">\n <div v-if=\"title\" class=\"confirm-title\">{{ title }}</div>\n <div class=\"confirm-message\">{{ message }}</div>\n <div class=\"confirm-actions\">\n <button class=\"confirm-btn cancel\" type=\"button\" @click=\"emit('cancel')\">\n {{ cancelText || '取消' }}\n </button>\n <button class=\"confirm-btn ok\" :class=\"{ danger }\" type=\"button\" @click=\"emit('confirm')\">\n {{ confirmText || '确认' }}\n </button>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.confirm-mask {\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.5);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 99999;\n}\n.confirm-dialog {\n min-width: 320px;\n max-width: 440px;\n background: #0f172a;\n border: 1px solid rgba(255, 255, 255, 0.08);\n border-radius: 0;\n padding: 20px;\n color: #fff;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6);\n}\n.confirm-title {\n font-size: 15px;\n font-weight: 600;\n margin-bottom: 8px;\n}\n.confirm-message {\n font-size: 13px;\n color: rgba(255, 255, 255, 0.85);\n line-height: 1.5;\n margin-bottom: 16px;\n}\n.confirm-actions {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n}\n.confirm-btn {\n padding: 6px 14px;\n font-size: 12px;\n border: 1px solid rgba(255, 255, 255, 0.15);\n background: rgba(255, 255, 255, 0.08);\n color: rgba(255, 255, 255, 0.85);\n cursor: pointer;\n border-radius: 0;\n}\n.confirm-btn:hover {\n background: rgba(255, 255, 255, 0.15);\n color: #fff;\n}\n.confirm-btn.ok {\n background: #1e293b;\n color: #fff;\n}\n.confirm-btn.ok.danger {\n background: #e53935;\n border-color: #e53935;\n}\n.confirm-btn.ok.danger:hover {\n background: #f45f5b;\n}\n</style>\n","<script setup lang=\"ts\">\n// 通用确认弹窗:深色主题、零圆角、水平居中\ndefineProps<{\n title?: string\n message: string\n confirmText?: string\n cancelText?: string\n danger?: boolean\n}>()\n\nconst emit = defineEmits<{\n (e: 'confirm'): void\n (e: 'cancel'): void\n}>()\n</script>\n\n<template>\n <div class=\"confirm-mask\" @click.self=\"emit('cancel')\">\n <div class=\"confirm-dialog\">\n <div v-if=\"title\" class=\"confirm-title\">{{ title }}</div>\n <div class=\"confirm-message\">{{ message }}</div>\n <div class=\"confirm-actions\">\n <button class=\"confirm-btn cancel\" type=\"button\" @click=\"emit('cancel')\">\n {{ cancelText || '取消' }}\n </button>\n <button class=\"confirm-btn ok\" :class=\"{ danger }\" type=\"button\" @click=\"emit('confirm')\">\n {{ confirmText || '确认' }}\n </button>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.confirm-mask {\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.5);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 99999;\n}\n.confirm-dialog {\n min-width: 320px;\n max-width: 440px;\n background: #0f172a;\n border: 1px solid rgba(255, 255, 255, 0.08);\n border-radius: 0;\n padding: 20px;\n color: #fff;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6);\n}\n.confirm-title {\n font-size: 15px;\n font-weight: 600;\n margin-bottom: 8px;\n}\n.confirm-message {\n font-size: 13px;\n color: rgba(255, 255, 255, 0.85);\n line-height: 1.5;\n margin-bottom: 16px;\n}\n.confirm-actions {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n}\n.confirm-btn {\n padding: 6px 14px;\n font-size: 12px;\n border: 1px solid rgba(255, 255, 255, 0.15);\n background: rgba(255, 255, 255, 0.08);\n color: rgba(255, 255, 255, 0.85);\n cursor: pointer;\n border-radius: 0;\n}\n.confirm-btn:hover {\n background: rgba(255, 255, 255, 0.15);\n color: #fff;\n}\n.confirm-btn.ok {\n background: #1e293b;\n color: #fff;\n}\n.confirm-btn.ok.danger {\n background: #e53935;\n border-color: #e53935;\n}\n.confirm-btn.ok.danger:hover {\n background: #f45f5b;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { ref, onMounted, onBeforeUnmount, computed } from 'vue'\nimport Icon from './Icon.vue'\n\n// 会议结束独立提示浮窗:固定定位在屏幕右下角,不依赖会议主窗口存在\n// 用于解散/被踢/结束/主动离开等场景,显示原因 + 倒计时 + 进度条\nconst props = withDefaults(\n defineProps<{\n reason?: string\n duration?: number // 倒计时秒数\n }>(),\n {\n reason: '会议已结束',\n duration: 5,\n },\n)\n\nconst emit = defineEmits<{ (e: 'close'): void }>()\n\nconst remaining = ref(props.duration)\nlet timer: ReturnType<typeof setInterval> | null = null\n\nconst displayText = computed(() => `${props.reason}(${remaining.value}s)`)\n\nonMounted(() => {\n timer = setInterval(() => {\n remaining.value--\n if (remaining.value <= 0) {\n if (timer) clearInterval(timer)\n timer = null\n emit('close')\n }\n }, 1000)\n})\n\nonBeforeUnmount(() => {\n if (timer) clearInterval(timer)\n})\n</script>\n\n<template>\n <div class=\"ended-toast\">\n <div class=\"ended-toast-card\">\n <span class=\"ended-toast-text\">{{ displayText }}</span>\n <button class=\"ended-toast-close\" type=\"button\" title=\"立即关闭\" @click=\"emit('close')\">\n <Icon name=\"close\" :size=\"14\" />\n </button>\n </div>\n <!-- 倒计时进度条 -->\n <div class=\"ended-toast-progress\">\n <div\n class=\"ended-toast-progress-bar\"\n :style=\"{ animationDuration: `${props.duration}s` }\"\n ></div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.ended-toast {\n position: fixed;\n right: 20px;\n bottom: 20px;\n z-index: 10001;\n width: 240px;\n background: var(--im-meeting-bg-panel, #0f172a);\n border: 1px solid var(--im-meeting-border, rgba(255, 255, 255, 0.12));\n border-radius: 0;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);\n color: #fff;\n overflow: hidden;\n}\n.ended-toast-card {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n padding: 10px 12px;\n}\n.ended-toast-text {\n font-size: 13px;\n font-weight: 500;\n color: #fff;\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.ended-toast-close {\n width: 22px;\n height: 22px;\n flex: 0 0 auto;\n border: 1px solid rgba(255, 255, 255, 0.2);\n background: rgba(255, 255, 255, 0.08);\n color: #fff;\n cursor: pointer;\n border-radius: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n}\n.ended-toast-close:hover {\n background: rgba(255, 255, 255, 0.18);\n}\n.ended-toast-progress {\n height: 2px;\n background: rgba(255, 255, 255, 0.1);\n overflow: hidden;\n}\n.ended-toast-progress-bar {\n height: 100%;\n background: #e53935;\n width: 100%;\n transform-origin: left;\n animation: ended-toast-shrink linear forwards;\n}\n@keyframes ended-toast-shrink {\n from {\n transform: scaleX(1);\n }\n to {\n transform: scaleX(0);\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { ref, onMounted, onBeforeUnmount, computed } from 'vue'\nimport Icon from './Icon.vue'\n\n// 会议结束独立提示浮窗:固定定位在屏幕右下角,不依赖会议主窗口存在\n// 用于解散/被踢/结束/主动离开等场景,显示原因 + 倒计时 + 进度条\nconst props = withDefaults(\n defineProps<{\n reason?: string\n duration?: number // 倒计时秒数\n }>(),\n {\n reason: '会议已结束',\n duration: 5,\n },\n)\n\nconst emit = defineEmits<{ (e: 'close'): void }>()\n\nconst remaining = ref(props.duration)\nlet timer: ReturnType<typeof setInterval> | null = null\n\nconst displayText = computed(() => `${props.reason}(${remaining.value}s)`)\n\nonMounted(() => {\n timer = setInterval(() => {\n remaining.value--\n if (remaining.value <= 0) {\n if (timer) clearInterval(timer)\n timer = null\n emit('close')\n }\n }, 1000)\n})\n\nonBeforeUnmount(() => {\n if (timer) clearInterval(timer)\n})\n</script>\n\n<template>\n <div class=\"ended-toast\">\n <div class=\"ended-toast-card\">\n <span class=\"ended-toast-text\">{{ displayText }}</span>\n <button class=\"ended-toast-close\" type=\"button\" title=\"立即关闭\" @click=\"emit('close')\">\n <Icon name=\"close\" :size=\"14\" />\n </button>\n </div>\n <!-- 倒计时进度条 -->\n <div class=\"ended-toast-progress\">\n <div\n class=\"ended-toast-progress-bar\"\n :style=\"{ animationDuration: `${props.duration}s` }\"\n ></div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.ended-toast {\n position: fixed;\n right: 20px;\n bottom: 20px;\n z-index: 10001;\n width: 240px;\n background: var(--im-meeting-bg-panel, #0f172a);\n border: 1px solid var(--im-meeting-border, rgba(255, 255, 255, 0.12));\n border-radius: 0;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);\n color: #fff;\n overflow: hidden;\n}\n.ended-toast-card {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n padding: 10px 12px;\n}\n.ended-toast-text {\n font-size: 13px;\n font-weight: 500;\n color: #fff;\n font-variant-numeric: tabular-nums;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.ended-toast-close {\n width: 22px;\n height: 22px;\n flex: 0 0 auto;\n border: 1px solid rgba(255, 255, 255, 0.2);\n background: rgba(255, 255, 255, 0.08);\n color: #fff;\n cursor: pointer;\n border-radius: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n}\n.ended-toast-close:hover {\n background: rgba(255, 255, 255, 0.18);\n}\n.ended-toast-progress {\n height: 2px;\n background: rgba(255, 255, 255, 0.1);\n overflow: hidden;\n}\n.ended-toast-progress-bar {\n height: 100%;\n background: #e53935;\n width: 100%;\n transform-origin: left;\n animation: ended-toast-shrink linear forwards;\n}\n@keyframes ended-toast-shrink {\n from {\n transform: scaleX(1);\n }\n to {\n transform: scaleX(0);\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport type { PickerUser } from '../types'\n\n// 选人列表(无 mask 的纯列表区):工具栏(计数/全选)+ 多选列表 + 空态\n// 被 UserPickerDialog(邀请参会)与 MeetingCreateDialog(发起会议)复用\n// 自定义行渲染:#user-item 插槽(scope: { user, selected, toggle })\nconst props = defineProps<{\n users: PickerUser[]\n loading?: boolean\n selected?: string[]\n emptyText?: string\n}>()\n\nconst emit = defineEmits<{ (e: 'update:selected', uids: string[]): void }>()\n\nconst slots = defineSlots<{\n 'user-item'?(scope: { user: PickerUser; selected: boolean; toggle: () => void }): unknown\n}>()\n\nconst selectedSet = computed(() => new Set(props.selected || []))\n\nfunction toggleOne(uid: string) {\n const next = new Set(selectedSet.value)\n if (next.has(uid)) next.delete(uid)\n else next.add(uid)\n emit('update:selected', [...next])\n}\n\nconst allSelected = computed(\n () => props.users.length > 0 && selectedSet.value.size === props.users.length,\n)\n\nfunction toggleAll() {\n emit('update:selected', allSelected.value ? [] : props.users.map((u) => u.uid))\n}\n</script>\n\n<template>\n <div class=\"im-picker-list\">\n <div class=\"im-picker-toolbar\">\n <span class=\"im-picker-label\">选择人员</span>\n <div class=\"im-picker-toolbar-right\">\n <span class=\"im-picker-counter\">已选 {{ selectedSet.size }} / {{ users.length }} 人</span>\n <button\n v-if=\"users.length > 0\"\n class=\"im-picker-select-all\"\n type=\"button\"\n @click=\"toggleAll\"\n >\n {{ allSelected ? '取消全选' : '全选' }}\n </button>\n </div>\n </div>\n\n <div v-if=\"loading\" class=\"im-picker-empty\">加载中...</div>\n <div v-else-if=\"users.length === 0\" class=\"im-picker-empty\">\n {{ emptyText || '暂无可选人员' }}\n </div>\n <ul v-else class=\"im-picker-items\">\n <li\n v-for=\"u in users\"\n :key=\"u.uid\"\n :class=\"['im-picker-item', { active: selectedSet.has(u.uid) }]\"\n >\n <slot\n name=\"user-item\"\n :user=\"u\"\n :selected=\"selectedSet.has(u.uid)\"\n :toggle=\"() => toggleOne(u.uid)\"\n >\n <button class=\"im-picker-item-btn\" type=\"button\" @click=\"toggleOne(u.uid)\">\n <input\n type=\"checkbox\"\n :checked=\"selectedSet.has(u.uid)\"\n tabindex=\"-1\"\n @click.stop=\"toggleOne(u.uid)\"\n />\n <div class=\"im-picker-avatar\">{{ u.name[0] }}</div>\n <div class=\"im-picker-info\">\n <div class=\"im-picker-name\">{{ u.name }}</div>\n <div v-if=\"u.dept\" class=\"im-picker-dept\">{{ u.dept }}</div>\n </div>\n </button>\n </slot>\n </li>\n </ul>\n </div>\n</template>\n\n<style scoped>\n.im-picker-list {\n /* 默认主题变量:业务方在 :root/祖先覆盖即可定制,无需 :deep() */\n --im-picker-primary: #667eea;\n --im-picker-primary-hover: #5568d3;\n --im-picker-primary-weak: #eef2ff;\n --im-picker-text: #1a1a1a;\n --im-picker-text-secondary: #999;\n --im-picker-border: #f0f0f0;\n --im-picker-bg-hover: #f5f7fa;\n --im-picker-radius: 6px;\n\n display: flex;\n flex-direction: column;\n min-height: 0;\n}\n.im-picker-toolbar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-bottom: 8px;\n}\n.im-picker-label {\n font-size: 12px;\n color: var(--im-picker-text-secondary);\n}\n.im-picker-toolbar-right {\n display: flex;\n align-items: center;\n gap: 12px;\n}\n.im-picker-counter {\n font-size: 12px;\n color: var(--im-picker-text-secondary);\n}\n.im-picker-select-all {\n padding: 4px 10px;\n font-size: 12px;\n border: 1px solid var(--im-picker-primary);\n background: transparent;\n color: var(--im-picker-primary);\n border-radius: 4px;\n cursor: pointer;\n transition: all 0.2s;\n}\n.im-picker-select-all:hover {\n background: var(--im-picker-primary);\n color: #fff;\n}\n.im-picker-empty {\n text-align: center;\n padding: 32px 0;\n color: var(--im-picker-text-secondary);\n font-size: 13px;\n border: 1px solid var(--im-picker-border);\n border-radius: var(--im-picker-radius);\n}\n.im-picker-items {\n list-style: none;\n padding: 0;\n margin: 0;\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n border: 1px solid var(--im-picker-border);\n border-radius: var(--im-picker-radius);\n}\n.im-picker-item:last-child .im-picker-item-btn {\n border-bottom: none;\n}\n.im-picker-item.active .im-picker-item-btn {\n background: var(--im-picker-primary-weak);\n}\n.im-picker-item-btn {\n display: flex;\n align-items: center;\n gap: 12px;\n width: 100%;\n padding: 10px 12px;\n border: none;\n border-bottom: 1px solid #f7f7f7;\n background: transparent;\n cursor: pointer;\n text-align: left;\n transition: background 0.15s;\n font: inherit;\n}\n.im-picker-item-btn:hover {\n background: var(--im-picker-bg-hover);\n}\n.im-picker-item input[type='checkbox'] {\n width: 16px;\n height: 16px;\n flex-shrink: 0;\n cursor: pointer;\n accent-color: var(--im-picker-primary);\n pointer-events: none;\n}\n.im-picker-avatar {\n width: 32px;\n height: 32px;\n border-radius: 50%;\n background: var(--im-picker-primary);\n color: #fff;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 13px;\n font-weight: 600;\n flex-shrink: 0;\n}\n.im-picker-info {\n flex: 1;\n min-width: 0;\n}\n.im-picker-name {\n font-size: 14px;\n color: var(--im-picker-text);\n font-weight: 500;\n}\n.im-picker-dept {\n font-size: 11px;\n color: var(--im-picker-text-secondary);\n margin-top: 2px;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport type { PickerUser } from '../types'\n\n// 选人列表(无 mask 的纯列表区):工具栏(计数/全选)+ 多选列表 + 空态\n// 被 UserPickerDialog(邀请参会)与 MeetingCreateDialog(发起会议)复用\n// 自定义行渲染:#user-item 插槽(scope: { user, selected, toggle })\nconst props = defineProps<{\n users: PickerUser[]\n loading?: boolean\n selected?: string[]\n emptyText?: string\n}>()\n\nconst emit = defineEmits<{ (e: 'update:selected', uids: string[]): void }>()\n\nconst slots = defineSlots<{\n 'user-item'?(scope: { user: PickerUser; selected: boolean; toggle: () => void }): unknown\n}>()\n\nconst selectedSet = computed(() => new Set(props.selected || []))\n\nfunction toggleOne(uid: string) {\n const next = new Set(selectedSet.value)\n if (next.has(uid)) next.delete(uid)\n else next.add(uid)\n emit('update:selected', [...next])\n}\n\nconst allSelected = computed(\n () => props.users.length > 0 && selectedSet.value.size === props.users.length,\n)\n\nfunction toggleAll() {\n emit('update:selected', allSelected.value ? [] : props.users.map((u) => u.uid))\n}\n</script>\n\n<template>\n <div class=\"im-picker-list\">\n <div class=\"im-picker-toolbar\">\n <span class=\"im-picker-label\">选择人员</span>\n <div class=\"im-picker-toolbar-right\">\n <span class=\"im-picker-counter\">已选 {{ selectedSet.size }} / {{ users.length }} 人</span>\n <button\n v-if=\"users.length > 0\"\n class=\"im-picker-select-all\"\n type=\"button\"\n @click=\"toggleAll\"\n >\n {{ allSelected ? '取消全选' : '全选' }}\n </button>\n </div>\n </div>\n\n <div v-if=\"loading\" class=\"im-picker-empty\">加载中...</div>\n <div v-else-if=\"users.length === 0\" class=\"im-picker-empty\">\n {{ emptyText || '暂无可选人员' }}\n </div>\n <ul v-else class=\"im-picker-items\">\n <li\n v-for=\"u in users\"\n :key=\"u.uid\"\n :class=\"['im-picker-item', { active: selectedSet.has(u.uid) }]\"\n >\n <slot\n name=\"user-item\"\n :user=\"u\"\n :selected=\"selectedSet.has(u.uid)\"\n :toggle=\"() => toggleOne(u.uid)\"\n >\n <button class=\"im-picker-item-btn\" type=\"button\" @click=\"toggleOne(u.uid)\">\n <input\n type=\"checkbox\"\n :checked=\"selectedSet.has(u.uid)\"\n tabindex=\"-1\"\n @click.stop=\"toggleOne(u.uid)\"\n />\n <div class=\"im-picker-avatar\">{{ u.name[0] }}</div>\n <div class=\"im-picker-info\">\n <div class=\"im-picker-name\">{{ u.name }}</div>\n <div v-if=\"u.dept\" class=\"im-picker-dept\">{{ u.dept }}</div>\n </div>\n </button>\n </slot>\n </li>\n </ul>\n </div>\n</template>\n\n<style scoped>\n.im-picker-list {\n /* 默认主题变量:业务方在 :root/祖先覆盖即可定制,无需 :deep() */\n --im-picker-primary: #667eea;\n --im-picker-primary-hover: #5568d3;\n --im-picker-primary-weak: #eef2ff;\n --im-picker-text: #1a1a1a;\n --im-picker-text-secondary: #999;\n --im-picker-border: #f0f0f0;\n --im-picker-bg-hover: #f5f7fa;\n --im-picker-radius: 6px;\n\n display: flex;\n flex-direction: column;\n min-height: 0;\n}\n.im-picker-toolbar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-bottom: 8px;\n}\n.im-picker-label {\n font-size: 12px;\n color: var(--im-picker-text-secondary);\n}\n.im-picker-toolbar-right {\n display: flex;\n align-items: center;\n gap: 12px;\n}\n.im-picker-counter {\n font-size: 12px;\n color: var(--im-picker-text-secondary);\n}\n.im-picker-select-all {\n padding: 4px 10px;\n font-size: 12px;\n border: 1px solid var(--im-picker-primary);\n background: transparent;\n color: var(--im-picker-primary);\n border-radius: 4px;\n cursor: pointer;\n transition: all 0.2s;\n}\n.im-picker-select-all:hover {\n background: var(--im-picker-primary);\n color: #fff;\n}\n.im-picker-empty {\n text-align: center;\n padding: 32px 0;\n color: var(--im-picker-text-secondary);\n font-size: 13px;\n border: 1px solid var(--im-picker-border);\n border-radius: var(--im-picker-radius);\n}\n.im-picker-items {\n list-style: none;\n padding: 0;\n margin: 0;\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n border: 1px solid var(--im-picker-border);\n border-radius: var(--im-picker-radius);\n}\n.im-picker-item:last-child .im-picker-item-btn {\n border-bottom: none;\n}\n.im-picker-item.active .im-picker-item-btn {\n background: var(--im-picker-primary-weak);\n}\n.im-picker-item-btn {\n display: flex;\n align-items: center;\n gap: 12px;\n width: 100%;\n padding: 10px 12px;\n border: none;\n border-bottom: 1px solid #f7f7f7;\n background: transparent;\n cursor: pointer;\n text-align: left;\n transition: background 0.15s;\n font: inherit;\n}\n.im-picker-item-btn:hover {\n background: var(--im-picker-bg-hover);\n}\n.im-picker-item input[type='checkbox'] {\n width: 16px;\n height: 16px;\n flex-shrink: 0;\n cursor: pointer;\n accent-color: var(--im-picker-primary);\n pointer-events: none;\n}\n.im-picker-avatar {\n width: 32px;\n height: 32px;\n border-radius: 50%;\n background: var(--im-picker-primary);\n color: #fff;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 13px;\n font-weight: 600;\n flex-shrink: 0;\n}\n.im-picker-info {\n flex: 1;\n min-width: 0;\n}\n.im-picker-name {\n font-size: 14px;\n color: var(--im-picker-text);\n font-weight: 500;\n}\n.im-picker-dept {\n font-size: 11px;\n color: var(--im-picker-text-secondary);\n margin-top: 2px;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { ref, onMounted } from 'vue'\nimport type { ImClient } from '@quitting/core'\nimport { getImClient } from '../client-access'\nimport UserPickerList from './UserPickerList.vue'\nimport type { PickerUser } from '../types'\n\n// 邀请参会选人弹窗:mask + 头部 + UserPickerList + 底部按钮\n// 数据源默认 client.users.list()(uid 为 externalId);业务方可传 loader 完全自定义\n// 自定义行渲染:#user-item 插槽(透传给 UserPickerList)\nconst props = defineProps<{\n title?: string\n // 排除自己(externalId)\n myUid?: string\n // 排除列表(如已在会议中的成员,externalId)\n excludeUids?: string[]\n // 自定义数据源;缺省走 SDK 用户目录 client.users.list()\n loader?: () => Promise<PickerUser[]>\n confirmText?: string\n emptyText?: string\n onClose?: () => void\n}>()\n\nconst emit = defineEmits<{\n (e: 'confirm', selectedUids: string[]): void\n (e: 'cancel'): void\n (e: 'close'): void\n}>()\n\nconst slots = defineSlots<{\n 'user-item'?(scope: { user: PickerUser; selected: boolean; toggle: () => void }): unknown\n}>()\n\nconst users = ref<PickerUser[]>([])\nconst selected = ref<string[]>([])\nconst loading = ref(true)\n\nfunction getClient(): ImClient | undefined {\n return getImClient()\n}\n\n// 默认数据源:SDK 用户目录(externalId/name/dept),排除自己 + excludeUids\nasync function loadDefault(): Promise<PickerUser[]> {\n const client = getClient()\n if (!client?.users) return []\n const list = await client.users.list()\n // 自己不在候选里:resolve 支持内部 uid 反查(client.uid 是 UUID)\n const me = await client.users.resolve(client.uid).catch(() => undefined)\n const exclude = new Set<string>(props.excludeUids || [])\n if (me) exclude.add(me.externalId)\n if (props.myUid) exclude.add(props.myUid)\n return list\n .filter((u) => !exclude.has(u.externalId))\n .map((u) => ({ uid: u.externalId, name: u.name, dept: u.deptName || undefined }))\n}\n\nonMounted(async () => {\n try {\n users.value = props.loader ? await props.loader() : await loadDefault()\n } catch (err) {\n console.warn('[im] 选人列表加载失败', err)\n users.value = []\n } finally {\n loading.value = false\n }\n})\n\nfunction confirm() {\n if (selected.value.length === 0) return\n emit('confirm', [...selected.value])\n emit('close')\n}\n\nfunction cancel() {\n emit('cancel')\n emit('close')\n}\n</script>\n\n<template>\n <div class=\"im-picker-mask\" @click.self=\"cancel\">\n <div class=\"im-picker-dialog\">\n <div class=\"im-picker-header\">\n <h2 class=\"im-picker-title\">{{ title || '邀请参会' }}</h2>\n <button class=\"im-picker-close\" type=\"button\" aria-label=\"关闭\" @click=\"cancel\">✕</button>\n </div>\n <div class=\"im-picker-body\">\n <UserPickerList\n v-model:selected=\"selected\"\n :users=\"users\"\n :loading=\"loading\"\n :empty-text=\"emptyText || '所有成员都已在会议中'\"\n >\n <template v-if=\"slots['user-item']\" #user-item=\"scope\">\n <slot name=\"user-item\" v-bind=\"scope\" />\n </template>\n </UserPickerList>\n </div>\n <div class=\"im-picker-footer\">\n <button class=\"im-picker-btn cancel\" type=\"button\" @click=\"cancel\">取消</button>\n <button\n class=\"im-picker-btn confirm\"\n type=\"button\"\n :disabled=\"selected.length === 0\"\n @click=\"confirm\"\n >\n {{ confirmText || '邀请' }}\n </button>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.im-picker-mask {\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.45);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 100001;\n}\n.im-picker-dialog {\n /* 主题变量(与 UserPickerList 一致;:root 覆盖一处即可全局生效) */\n --im-picker-primary: #667eea;\n --im-picker-primary-hover: #5568d3;\n --im-picker-primary-weak: #eef2ff;\n\n width: 480px;\n max-height: 80vh;\n background: #fff;\n border-radius: 12px;\n box-shadow: 0 16px 48px rgba(0, 0, 0, 0.2);\n display: flex;\n flex-direction: column;\n overflow: hidden;\n}\n.im-picker-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 16px 20px;\n border-bottom: 1px solid #f0f0f0;\n}\n.im-picker-title {\n margin: 0;\n font-size: 16px;\n font-weight: 600;\n color: #1a1a1a;\n}\n.im-picker-close {\n width: 28px;\n height: 28px;\n border: none;\n background: transparent;\n color: #999;\n font-size: 16px;\n cursor: pointer;\n border-radius: 4px;\n}\n.im-picker-close:hover {\n background: #f5f7fa;\n color: #1a1a1a;\n}\n.im-picker-body {\n flex: 1;\n min-height: 0;\n display: flex;\n flex-direction: column;\n padding: 14px 20px;\n}\n.im-picker-body .im-picker-list {\n flex: 1;\n min-height: 0;\n}\n.im-picker-body :deep(.im-picker-items) {\n max-height: 320px;\n}\n.im-picker-footer {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n padding: 14px 20px;\n border-top: 1px solid #f0f0f0;\n}\n.im-picker-btn {\n padding: 8px 18px;\n font-size: 13px;\n border-radius: 6px;\n cursor: pointer;\n transition: all 0.2s;\n border: none;\n}\n.im-picker-btn.cancel {\n background: #f5f7fa;\n color: #666;\n}\n.im-picker-btn.cancel:hover {\n background: #eef0f4;\n}\n.im-picker-btn.confirm {\n background: var(--im-picker-primary, #667eea);\n color: #fff;\n}\n.im-picker-btn.confirm:hover:not(:disabled) {\n background: var(--im-picker-primary-hover, #5568d3);\n}\n.im-picker-btn.confirm:disabled {\n background: #bbb;\n cursor: not-allowed;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { ref, onMounted } from 'vue'\nimport type { ImClient } from '@quitting/core'\nimport { getImClient } from '../client-access'\nimport UserPickerList from './UserPickerList.vue'\nimport type { PickerUser } from '../types'\n\n// 邀请参会选人弹窗:mask + 头部 + UserPickerList + 底部按钮\n// 数据源默认 client.users.list()(uid 为 externalId);业务方可传 loader 完全自定义\n// 自定义行渲染:#user-item 插槽(透传给 UserPickerList)\nconst props = defineProps<{\n title?: string\n // 排除自己(externalId)\n myUid?: string\n // 排除列表(如已在会议中的成员,externalId)\n excludeUids?: string[]\n // 自定义数据源;缺省走 SDK 用户目录 client.users.list()\n loader?: () => Promise<PickerUser[]>\n confirmText?: string\n emptyText?: string\n onClose?: () => void\n}>()\n\nconst emit = defineEmits<{\n (e: 'confirm', selectedUids: string[]): void\n (e: 'cancel'): void\n (e: 'close'): void\n}>()\n\nconst slots = defineSlots<{\n 'user-item'?(scope: { user: PickerUser; selected: boolean; toggle: () => void }): unknown\n}>()\n\nconst users = ref<PickerUser[]>([])\nconst selected = ref<string[]>([])\nconst loading = ref(true)\n\nfunction getClient(): ImClient | undefined {\n return getImClient()\n}\n\n// 默认数据源:SDK 用户目录(externalId/name/dept),排除自己 + excludeUids\nasync function loadDefault(): Promise<PickerUser[]> {\n const client = getClient()\n if (!client?.users) return []\n const list = await client.users.list()\n // 自己不在候选里:resolve 支持内部 uid 反查(client.uid 是 UUID)\n const me = await client.users.resolve(client.uid).catch(() => undefined)\n const exclude = new Set<string>(props.excludeUids || [])\n if (me) exclude.add(me.externalId)\n if (props.myUid) exclude.add(props.myUid)\n return list\n .filter((u) => !exclude.has(u.externalId))\n .map((u) => ({ uid: u.externalId, name: u.name, dept: u.deptName || undefined }))\n}\n\nonMounted(async () => {\n try {\n users.value = props.loader ? await props.loader() : await loadDefault()\n } catch (err) {\n console.warn('[im] 选人列表加载失败', err)\n users.value = []\n } finally {\n loading.value = false\n }\n})\n\nfunction confirm() {\n if (selected.value.length === 0) return\n emit('confirm', [...selected.value])\n emit('close')\n}\n\nfunction cancel() {\n emit('cancel')\n emit('close')\n}\n</script>\n\n<template>\n <div class=\"im-picker-mask\" @click.self=\"cancel\">\n <div class=\"im-picker-dialog\">\n <div class=\"im-picker-header\">\n <h2 class=\"im-picker-title\">{{ title || '邀请参会' }}</h2>\n <button class=\"im-picker-close\" type=\"button\" aria-label=\"关闭\" @click=\"cancel\">✕</button>\n </div>\n <div class=\"im-picker-body\">\n <UserPickerList\n v-model:selected=\"selected\"\n :users=\"users\"\n :loading=\"loading\"\n :empty-text=\"emptyText || '所有成员都已在会议中'\"\n >\n <template v-if=\"slots['user-item']\" #user-item=\"scope\">\n <slot name=\"user-item\" v-bind=\"scope\" />\n </template>\n </UserPickerList>\n </div>\n <div class=\"im-picker-footer\">\n <button class=\"im-picker-btn cancel\" type=\"button\" @click=\"cancel\">取消</button>\n <button\n class=\"im-picker-btn confirm\"\n type=\"button\"\n :disabled=\"selected.length === 0\"\n @click=\"confirm\"\n >\n {{ confirmText || '邀请' }}\n </button>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.im-picker-mask {\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.45);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 100001;\n}\n.im-picker-dialog {\n /* 主题变量(与 UserPickerList 一致;:root 覆盖一处即可全局生效) */\n --im-picker-primary: #667eea;\n --im-picker-primary-hover: #5568d3;\n --im-picker-primary-weak: #eef2ff;\n\n width: 480px;\n max-height: 80vh;\n background: #fff;\n border-radius: 12px;\n box-shadow: 0 16px 48px rgba(0, 0, 0, 0.2);\n display: flex;\n flex-direction: column;\n overflow: hidden;\n}\n.im-picker-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 16px 20px;\n border-bottom: 1px solid #f0f0f0;\n}\n.im-picker-title {\n margin: 0;\n font-size: 16px;\n font-weight: 600;\n color: #1a1a1a;\n}\n.im-picker-close {\n width: 28px;\n height: 28px;\n border: none;\n background: transparent;\n color: #999;\n font-size: 16px;\n cursor: pointer;\n border-radius: 4px;\n}\n.im-picker-close:hover {\n background: #f5f7fa;\n color: #1a1a1a;\n}\n.im-picker-body {\n flex: 1;\n min-height: 0;\n display: flex;\n flex-direction: column;\n padding: 14px 20px;\n}\n.im-picker-body .im-picker-list {\n flex: 1;\n min-height: 0;\n}\n.im-picker-body :deep(.im-picker-items) {\n max-height: 320px;\n}\n.im-picker-footer {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n padding: 14px 20px;\n border-top: 1px solid #f0f0f0;\n}\n.im-picker-btn {\n padding: 8px 18px;\n font-size: 13px;\n border-radius: 6px;\n cursor: pointer;\n transition: all 0.2s;\n border: none;\n}\n.im-picker-btn.cancel {\n background: #f5f7fa;\n color: #666;\n}\n.im-picker-btn.cancel:hover {\n background: #eef0f4;\n}\n.im-picker-btn.confirm {\n background: var(--im-picker-primary, #667eea);\n color: #fff;\n}\n.im-picker-btn.confirm:hover:not(:disabled) {\n background: var(--im-picker-primary-hover, #5568d3);\n}\n.im-picker-btn.confirm:disabled {\n background: #bbb;\n cursor: not-allowed;\n}\n</style>\n","<script setup lang=\"ts\">\nimport {\n ref,\n onMounted,\n onUnmounted,\n computed,\n nextTick,\n watch,\n useSlots,\n getCurrentInstance,\n} from 'vue'\nimport vDraggable from '../directives/draggable'\nimport { mountComponent } from '../mount'\nimport { ImToastApi as toast } from '../toast'\nimport MeetingHeader from './meeting/MeetingHeader.vue'\nimport MainVideoTile from './meeting/MainVideoTile.vue'\nimport VideoListPanel from './meeting/VideoListPanel.vue'\nimport MemberListPanel from './meeting/MemberListPanel.vue'\nimport SidebarTabs from './meeting/SidebarTabs.vue'\nimport SmallVideoTile from './meeting/SmallVideoTile.vue'\nimport MeetingControlBar from './meeting/MeetingControlBar.vue'\nimport ConfirmDialog from './ConfirmDialog.vue'\nimport MeetingEndedToast from './MeetingEndedToast.vue'\nimport UserPickerDialog from './UserPickerDialog.vue'\nimport { MaximizeIcon, CloseIcon } from './meeting/icons'\nimport type {\n MeetingMember,\n MeetingHostChangedEvent,\n MeetingDissolvedEvent,\n} from '@quitting/contract'\n\n// 会议窗口:左主视频(默认主持人) + 右侧栏 Tab(视频列表/成员列表) + 底部成员状态条\n// 布局严格 16:9,所有组件无圆角;样式对齐 design/video-conference-layout.html\n// 视频挂载沿用 CallSession/MeetingSession 的 attach 模式:通过 onVideoElReady 把元素交回 SDK\n\nconst props = defineProps<{\n mode: 'host' | 'participant'\n title: string\n // 窗口高度(CSS 值),由调用方决定。支持 \"80vh\" / \"600px\" / \"100%\" 等\n // position: fixed 下百分比相对 viewport;宽度由 layout() 按 16:9 推算\n height?: string\n onClose?: () => void\n // 用户信息解析器:uid → { name, dept? }\n // 后端 ListMembers 在 user 表无对应记录时会回退为 uid(如 demo mock 用户),\n // 调用方可传此函数让 SDK UI 显示业务侧维护的 name/dept;返回 undefined 则保留后端原值\n resolveUser?: (uid: string) => { name: string; dept?: string } | undefined\n // 禁用内置邀请弹窗(点击「+」只抛 addMember 事件,不弹内置选人窗)\n disableDefaultInvite?: boolean\n // 自定义选人:业务方弹自己的 UI 并 resolve 所选 uid 列表(可返回 [] 表示取消),\n // SDK 拿到后内部完成批量 reinvite + 成员列表刷新;优先级介于插槽与 @add-member 之间\n invitePicker?: () => Promise<string[]>\n}>()\n\nconst emit = defineEmits<{\n (e: 'close'): void\n // 添加成员按钮被点击。默认行为按优先级退让:\n // 传了 #invite-panel 插槽 → 渲染插槽(完全自定义)\n // 传了 invitePicker prop → 业务方弹自己的选人 UI,resolve uid 列表后 SDK 统一邀请\n // 传了 @add-member 监听 → 仅抛事件(不弹内置选人窗,由外部接管)\n // 都没有 → 抛事件 + 弹内置 UserPickerDialog(批量 reinvite)\n (e: 'addMember'): void\n}>()\n\nconst slots = useSlots()\n// 自定义邀请面板插槽签名(运行时按名取用;类型仅作文档)\n// scope: members=当前成员列表 / excludeUids=已在会成员 / invite(uids)=批量邀请 / close()=收起面板\n\n// 自定义邀请面板开关(#invite-panel 插槽渲染层)\nconst invitePanelOpen = ref(false)\n// 当前会议成员 externalId 列表(插槽 scope 用:选人时排除已在会成员)\nconst memberIdentities = computed(() => members.value.map((m) => m.identity))\n\nconst micEnabled = ref(true)\nconst cameraEnabled = ref(true)\nconst speakerMuted = ref(false)\nconst hasCamera = ref(true)\nconst hasMic = ref(true)\n// 设备不可用(无设备/无权限/被占用):按钮与自己的状态图标置灰,与「主动关闭」(红色)区分\nconst micUnavailable = ref(false)\nconst cameraUnavailable = ref(false)\n// 通用操作提示(reinvite 失败等),复用 device-warn 样式,3 秒自动消失\nconst actionError = ref('')\nlet actionErrorTimer: ReturnType<typeof setTimeout> | null = null\nfunction pushToast(msg: string) {\n actionError.value = msg\n if (actionErrorTimer) clearTimeout(actionErrorTimer)\n actionErrorTimer = setTimeout(() => (actionError.value = ''), 3000)\n}\nconst duration = ref(0)\nconst endedReason = ref('')\nconst activeTab = ref<'video' | 'member'>('video')\nconst members = ref<MeetingMember[]>([])\nconst hostIdentity = ref<string>('')\n// 最小化状态:true 时隐藏主窗口内容,显示可拖拽的小浮条(LiveKit 会话不中断)\nconst minimized = ref(false)\n// 全屏宫格模式:true 时覆盖整个视口的 4 列宫格布局(LiveKit 会话不中断)\nconst gridMode = ref(false)\n// 宫格模式的成员列表抽屉(右侧滑入,可收起;复用 MemberListPanel,功能与默认布局一致)\nconst gridDrawerOpen = ref(false)\n\n// 元素引用\nconst contentEl = ref<HTMLElement | null>(null)\nconst mainVideoWrap = ref<HTMLElement | null>(null)\nconst sidebarEl = ref<HTMLElement | null>(null)\nconst tabBarWrap = ref<HTMLElement | null>(null)\nconst videoListPanelRef = ref<InstanceType<typeof VideoListPanel> | null>(null)\nconst mainVideoRef = ref<InstanceType<typeof MainVideoTile> | null>(null)\n\n// 视频 attach:identity → 元素;保留 self/remote 区分\nlet selfVideoEl: HTMLVideoElement | null = null\nlet mainVideoIdentity: string | null = null\nlet mainVideoEl: HTMLVideoElement | null = null\nconst participantVideoEls = new Map<string, HTMLVideoElement>()\n// 全屏宫格的视频元素池:identity → 元素(track 后到时补挂载用)\nconst gridVideoEls = new Map<string, HTMLVideoElement>()\n\nlet timer: ReturnType<typeof setInterval> | null = null\nlet resizeObserver: ResizeObserver | null = null\n\nfunction getClient() {\n return (window as any).__imClient\n}\n\nfunction getSession() {\n return getClient()?.meeting?.currentSession\n}\n\n// 成员设备不可用判定:自己用本地实时检测(更及时),他人用后端推断字段\n// (已入会但从未发布对应 track = 无设备/无权限/被占用)——与成员列表(MemberItem)口径一致\nfunction memberMicUnavailable(m: MeetingMember) {\n return m.identity === (getSession()?.identity || '') ? micUnavailable.value : !!m.micUnavailable\n}\nfunction memberCameraUnavailable(m: MeetingMember) {\n return (\n m.joined &&\n (m.identity === (getSession()?.identity || '') ? cameraUnavailable.value : !!m.cameraUnavailable)\n )\n}\n\n// 主视频参与者:当前 hostIdentity 对应的 member(可能未入会)\nconst mainVideoMember = computed<MeetingMember | null>(() => {\n const m = members.value.find((x) => x.identity === hostIdentity.value)\n return m || null\n})\n\n// 当前用户是否是「当前主持人」——动态判断,主持人转让后自动更新\n// mode prop 在创建时固化(host/participant),转让后原主持人 mode 仍是 host 但已无权限,\n// 新主持人 mode 是 participant 却应有权限。改用 identity === hostIdentity 解决菜单错乱。\nconst isCurrentHost = computed(() => {\n const sess = getSession()\n return !!sess && sess.identity === hostIdentity.value\n})\n\n// 正在发言的成员(最小化浮条显示用)——与成员列表 speaking 判定一致\nconst speakingMembers = computed(() =>\n members.value.filter((m) => m.speaking && m.joined && m.online),\n)\n// 发言人展示文本:≤3 个全显示(顿号分隔);>3 个显示第一个 + \"...等N人\"\nconst speakerDisplayText = computed(() => {\n const list = speakingMembers.value\n if (list.length === 0) return ''\n if (list.length <= 3) return list.map((m) => m.name).join('、')\n return `${list[0].name}...等${list.length}人`\n})\n\n// 小视频列表:除了主视频那位,其他在线成员(排序规则与所有布局统一)\nconst smallVideoMembers = computed<MeetingMember[]>(() => {\n return sortMembers(\n members.value.filter((m) => m.identity !== hostIdentity.value && (m.online || m.joined)),\n )\n})\n\n// 全屏宫格成员:所有在线/已入会成员(含主持人和自己),排序规则与所有布局统一\nconst gridMembers = computed<MeetingMember[]>(() => {\n return sortMembers(members.value.filter((m) => m.online || m.joined))\n})\n\n// 全屏宫格视频元素就绪:与 onSmallVideoReady 同模式(self 走本地 attach,其余走远端)\nfunction onGridVideoReady(identity: string, el: HTMLVideoElement | null) {\n const session = getSession()\n if (!session) return\n if (el) {\n gridVideoEls.set(identity, el)\n if (identity === session.identity) {\n session.attachLocalVideo(el)\n } else {\n session.attachParticipantVideo(identity, el)\n }\n } else {\n gridVideoEls.delete(identity)\n }\n}\n\n// 小视频列表变化后必须重新 layout:\n// mount 时 members 还是空 → small-video 没渲染 → layout() 跳过尺寸应用\n// 之后 refreshMembers 完成、small-video 渲染,需要重新 layout 把 inline style 设上\nwatch(smallVideoMembers, () => {\n nextTick(layout)\n})\n\n// 排序后的成员列表(成员列表 tab / 全屏宫格 / 小视频列表三处布局统一使用)\n// 排序规则:主持人最前 → 其余按加入时间升序(joinedAt)→ 未加入的按邀请序 → 已离会最后。\n// 不再按发言状态排序——speaking 仅用于 tile 角标与高亮,不影响位置\nfunction getStatusPriority(m: MeetingMember, hostUid: string): number {\n if (m.identity === hostUid) return 0\n if (m.status === 'left') return 3\n if (m.joinedAt > 0) return 1\n return 2\n}\nfunction sortMembers(list: MeetingMember[]): MeetingMember[] {\n const hostUid = hostIdentity.value\n return list.slice().sort((a, b) => {\n const pa = getStatusPriority(a, hostUid)\n const pb = getStatusPriority(b, hostUid)\n if (pa !== pb) return pa - pb\n // 同组内按加入时间;均为 0(未加入)时退化为邀请序\n if (a.joinedAt !== b.joinedAt) return a.joinedAt - b.joinedAt\n return (a.inviteOrder || 0) - (b.inviteOrder || 0)\n })\n}\nconst sortedMembers = computed(() => sortMembers(members.value))\n\nasync function refreshMembers(force = false) {\n const client = getClient()\n if (!client?.meeting) return\n try {\n const list = await client.meeting.getMembers(force)\n // 若调用方提供 resolveUser,用业务侧 name/dept 覆盖后端返回(demo 模式下后端回退 uid)\n if (props.resolveUser) {\n members.value = list.map((m: MeetingMember) => {\n const u = props.resolveUser?.(m.identity)\n if (!u) return m\n return { ...m, name: u.name, dept: u.dept ?? m.dept }\n })\n } else {\n members.value = list\n }\n } catch (err) {\n console.warn('[im] refreshMembers 失败', err)\n }\n}\n\nasync function reattachAllVideos() {\n const session = getSession()\n if (!session) return\n // 主视频:self 时走 attachLocalVideo,否则 attachParticipantVideo\n if (mainVideoIdentity && mainVideoEl) {\n if (mainVideoIdentity === session.identity) {\n session.attachLocalVideo(mainVideoEl)\n } else {\n session.attachParticipantVideo(mainVideoIdentity, mainVideoEl)\n }\n }\n}\n\n// 主视频元素就绪\nasync function onMainVideoReady(identity: string, el: HTMLVideoElement | null) {\n const session = getSession()\n if (!session) return\n if (el) {\n mainVideoIdentity = identity\n mainVideoEl = el\n if (identity === session.identity) {\n session.attachLocalVideo(el)\n } else {\n session.attachParticipantVideo(identity, el)\n }\n } else {\n mainVideoIdentity = null\n mainVideoEl = null\n }\n}\n\n// 小视频元素就绪\nfunction onSmallVideoReady(identity: string, el: HTMLVideoElement | null) {\n const session = getSession()\n if (!session) return\n if (el) {\n participantVideoEls.set(identity, el)\n if (identity === session.identity) {\n // 自己的 tile:attach 本地视频(非主持人在小视频列表里看不到自己就是因为这里跳过了)\n session.attachLocalVideo(el)\n } else {\n session.attachParticipantVideo(identity, el)\n }\n } else {\n participantVideoEls.delete(identity)\n }\n}\n\nasync function syncDeviceCapability() {\n try {\n const devices = await navigator.mediaDevices.enumerateDevices()\n hasCamera.value = devices.some((d) => d.kind === 'videoinput')\n hasMic.value = devices.some((d) => d.kind === 'audioinput')\n if (!hasCamera.value) cameraEnabled.value = false\n if (!hasMic.value) micEnabled.value = false\n // 无设备 = 不可用(按钮置灰而非隐藏)\n micUnavailable.value = !hasMic.value\n cameraUnavailable.value = !hasCamera.value\n // 无权限 = 不可用:permissions.query best-effort(Safari 等不支持会抛错,忽略——\n // 开启失败路径由 devices 事件的 errors 兜底)\n try {\n const p = await navigator.permissions.query({ name: 'microphone' })\n if (p.state === 'denied') micUnavailable.value = true\n } catch {\n /* 忽略 */\n }\n try {\n const p = await navigator.permissions.query({ name: 'camera' })\n if (p.state === 'denied') cameraUnavailable.value = true\n } catch {\n /* 忽略 */\n }\n } catch (e) {\n console.warn('[im] enumerateDevices 失败', e)\n }\n}\n\nfunction triggerLayout() {\n nextTick(layout)\n}\n\n// 布局计算:从 design/video-conference-layout.html layout() 函数搬\n// 所有尺寸(mwW/sidebarW/mainW/smallW)无条件计算,避免小视频列表为空时窗口宽度塌陷\nfunction layout() {\n const content = contentEl.value\n const mainVideo = mainVideoWrap.value\n const sidebar = sidebarEl.value\n const videoListEl = videoListPanelRef.value?.listEl as HTMLElement | undefined\n const items = videoListEl?.querySelectorAll<HTMLElement>('.small-video') ?? []\n if (!content || !mainVideo || !sidebar) return\n\n // 1. 主视频 16:9,高度 = content 高度\n const contentH = content.clientHeight\n if (contentH <= 0) return\n const mainH = contentH\n const mainW = (mainH * 16) / 9\n\n // 2. 小视频 16:9,3 个 + 3 个 gap 填满 sidebar 可用高度\n const smallGap = 8\n const tabBarH = tabBarWrap.value?.offsetHeight ?? 28\n const smallH = (mainH - tabBarH - smallGap * 3) / 3\n const smallW = (smallH * 16) / 9\n\n // 3. sidebar 宽度 = 小视频宽(不再保留自定义滚动条空间,列表与 tab 等宽)\n const sidebarW = smallW\n\n // 4. 窗口总宽度(padding + main + gap + sidebar + padding)—— 左右 padding 对称\n const padding = 10\n const gap = 10\n const mwW = padding + mainW + gap + sidebarW + padding\n\n // 5. 应用到 DOM(items 为空也照常设,保持窗口宽度稳定)\n const mw = document.getElementById('meetingWindow') as HTMLElement | null\n if (mw) {\n mw.style.width = mwW + 'px'\n }\n\n mainVideo.style.width = mainW + 'px'\n mainVideo.style.height = mainH + 'px'\n\n // sidebar 宽度 = smallW,与 list 完全对齐\n sidebar.style.width = sidebarW + 'px'\n sidebar.style.height = contentH + 'px'\n\n if (videoListEl) {\n // list 撑满 sidebar,与 tab-bar 等宽\n videoListEl.style.width = '100%'\n videoListEl.style.height = contentH - tabBarH - smallGap + 'px'\n }\n\n items.forEach((item) => {\n // 小视频保持 16:9\n item.style.width = smallW + 'px'\n // 不取整:Math.floor 会让 3 个 small 累计损失 ~2px,导致最后 small 底部与主视频底部错位\n item.style.height = smallH + 'px'\n })\n}\n\n// ============ 设备/会议控制 ============\nasync function toggleMic() {\n if (micUnavailable.value) return\n const session = getSession()\n if (!session) return\n const next = !micEnabled.value\n try {\n await session.setMicEnabled(next)\n micEnabled.value = next\n // 本端自己不走远端 trackMuted 事件,手动更新 members 里自己那行\n // (members[].muted 语义 = 已闭麦,与 micEnabled 相反),与远端事件路径合流\n const self = members.value.find((m) => m.identity === session.identity)\n if (self && self.muted !== !next) {\n self.muted = !next\n members.value = [...members.value]\n }\n } catch (err) {\n console.warn('[im] setMicEnabled 失败,UI 不切换', err)\n }\n}\n\nasync function toggleCamera() {\n if (cameraUnavailable.value) return\n const session = getSession()\n if (!session) return\n const next = !cameraEnabled.value\n try {\n await session.setCameraEnabled(next)\n cameraEnabled.value = next\n // 本端自己不走远端 trackMuted 事件,手动更新 members 里自己那行\n // (与远端事件路径合流,其他端/本端列表与遮罩立即反映)\n const self = members.value.find((m) => m.identity === session.identity)\n if (self && self.cameraOn !== next) {\n self.cameraOn = next\n members.value = [...members.value]\n }\n nextTick(reattachAllVideos)\n } catch (err) {\n console.warn('[im] setCameraEnabled 失败,UI 不切换', err)\n }\n}\n\nasync function toggleSpeaker() {\n const session = getSession()\n if (!session) return\n speakerMuted.value = !speakerMuted.value\n await session.setSpeakerMuted(speakerMuted.value)\n}\n\n// 是否显示「解散会议」确认弹窗\nconst showDissolveConfirm = ref(false)\nconst dissolvePending = ref(false)\n\nasync function leaveMeeting() {\n const client = getClient()\n if (!client) return\n // 当前主持人才能解散;非主持人(含已转让的前主持人)直接走普通 leave\n if (!isCurrentHost.value) {\n await client.meeting?.leave()\n // 主窗口立即隐藏,独立倒计时 toast 提示后由 toast 触发 close\n endedReason.value = '已离开'\n return\n }\n // host:查房内参与者数量\n // - count > 1:房内还有其他人,弹\"解散会议\"确认(让 host 明确选择 Leave 还是 Dissolve)\n // - count <= 1:只剩 host 自己——直接 dissolve,避免遗留僵尸会议让离线 invitee 上线后收到 stale 邀请\n // 之前的 bug:count=1 时走 leave,只改 host 自己 invite.status=left,meeting.status 仍 active,\n // 其他 invitee 的 status 仍 invited → pending-invites 返回 stale 邀请\n let count = 1\n try {\n count = await client.meeting.getParticipantCount()\n } catch (err) {\n console.warn('[im] 查参与者数量失败,按 1 处理(自动解散)', err)\n }\n if (count > 1) {\n showDissolveConfirm.value = true\n return\n }\n dissolvePending.value = true\n try {\n await client.meeting.dissolve()\n endedReason.value = '会议已解散'\n } catch (err) {\n // dissolve 失败兜底:后端 Leave 内部也会做同样的自动 dissolve 兜底,但前端先失败说明网络层有问题\n console.error('[im] host 单独时解散失败,回退到 leave', err)\n endedReason.value = '已离开'\n await client.meeting?.leave().catch(() => {})\n } finally {\n dissolvePending.value = false\n }\n}\n\n// 确认解散\nasync function confirmDissolve() {\n const client = getClient()\n if (!client) return\n showDissolveConfirm.value = false\n dissolvePending.value = true\n try {\n await client.meeting.dissolve()\n // 解散成功:endedReason 让 ended-overlay 显示;broadcast 到达后 SDK 也会触发 ended 事件\n endedReason.value = '会议已解散'\n } catch (err) {\n console.error('[im] 解散失败', err)\n endedReason.value = '解散失败'\n } finally {\n dissolvePending.value = false\n }\n}\n\nfunction cancelDissolve() {\n showDissolveConfirm.value = false\n}\n\nasync function kickParticipant(uid: string) {\n const session = getSession()\n if (!session) return\n try {\n await session.kick(getClient().transport, uid)\n await refreshMembers(true)\n } catch (err) {\n console.warn('[im] kickParticipant 失败', err)\n }\n}\n\nasync function transferHost(uid: string) {\n const session = getSession()\n if (!session) return\n try {\n await session.transferHost(getClient().transport, uid)\n hostIdentity.value = uid\n await refreshMembers(true)\n } catch (err) {\n console.warn('[im] transferHost 失败', err)\n }\n}\n\n// 主持人再次邀请某成员(等待加入/已离会状态)\nasync function reinviteMember(uid: string) {\n const session = getSession()\n if (!session) return\n try {\n await getClient().meeting.reinvite(session.meetingId, uid)\n await refreshMembers(true)\n } catch (err: unknown) {\n // 后端错误码映射成中文提示,避免静默失败:\n // target_busy=对方正在通话/会议或有待处理邀请;not_host=主持人已转让\n const msg = err instanceof Error ? err.message : ''\n if (msg.includes('对方正忙') || msg.includes('target_busy')) {\n pushToast('对方正忙,稍后再试')\n } else if (msg.includes('无权') || msg.includes('not_host')) {\n pushToast('仅主持人可邀请成员')\n } else if (msg.includes('meeting_ended')) {\n pushToast('会议已结束')\n } else {\n pushToast('再次邀请失败,请重试')\n }\n console.warn('[im] reinvite 失败', err)\n }\n}\n\n// uid → 展示名(内置邀请流程 toast 用;优先 SDK 用户目录,回退 uid 本身)\nasync function nameOf(uid: string): Promise<string> {\n const dir = getClient()?.users\n if (dir?.resolve) {\n const u = await dir.resolve(uid).catch(() => undefined)\n if (u?.name) return u.name\n }\n return uid\n}\n\n// 批量邀请入会:内置默认流程与 #invite-panel 插槽的 invite() 共用\n// 单条失败不阻塞其他;busy/其他失败分类提示\nasync function inviteUids(uids: string[]) {\n const client = getClient()\n const session = getSession()\n if (!client || !session || uids.length === 0) return\n const busyUids: string[] = []\n const failedUids: string[] = []\n await Promise.all(\n uids.map((uid) =>\n client.meeting.reinvite(session.meetingId, uid).catch((e: unknown) => {\n const msg = e instanceof Error ? e.message : ''\n if (msg.includes('对方正忙') || msg.includes('target_busy')) busyUids.push(uid)\n else failedUids.push(uid)\n }),\n ),\n )\n if (busyUids.length > 0) {\n const names = (await Promise.all(busyUids.map(nameOf))).join('、')\n toast.error(`${names} 正忙,未能邀请加入会议`)\n }\n if (failedUids.length > 0) {\n const names = (await Promise.all(failedUids.map(nameOf))).join('、')\n toast.error(`${names} 邀请失败,请重试`)\n }\n refreshMembers(true)\n}\n\n// 内置默认邀请流程:拉当前成员(含已邀请未加入)→ 选人弹窗 → 批量 reinvite\nasync function openDefaultInvite() {\n const client = getClient()\n const session = getSession()\n if (!client || !session) return\n let excludeUids: string[] = []\n try {\n const list = await client.meeting.getMembers(true)\n excludeUids = list.map((m: MeetingMember) => m.identity)\n } catch (err) {\n console.warn('[im] 拉取会议成员失败,邀请弹窗不排除已入会成员', err)\n }\n mountComponent(UserPickerDialog, {\n title: '邀请参会',\n excludeUids,\n emptyText: '所有成员都已在会议中',\n onConfirm: (uids: string[]) => {\n inviteUids(uids)\n },\n })\n}\n\n// 点击「+」添加成员:插槽 > invitePicker > 外部事件监听 > 内置默认弹窗(退让式)\nfunction onAddMemberClick() {\n emit('addMember')\n if (slots['invite-panel']) {\n invitePanelOpen.value = !invitePanelOpen.value\n return\n }\n // 业务方自定义选人:resolve 所选 uid 后走内部统一邀请+刷新流程\n if (props.invitePicker) {\n props\n .invitePicker()\n .then((uids) => {\n if (uids && uids.length > 0) inviteUids(uids)\n })\n .catch(() => {})\n return\n }\n // 外部传了 @add-member 监听 → 仅抛事件,不弹内置选人窗(保持旧边界行为)\n const instance = getCurrentInstance()\n if (instance?.vnode.props?.onAddMember) return\n if (props.disableDefaultInvite) return\n openDefaultInvite()\n}\n\n// 监听 hostChanged 广播:切换主视频\nfunction onHostChangedEvent(e: MeetingHostChangedEvent) {\n const session = getSession()\n if (!session || e.meetingId !== session.meetingId) return\n hostIdentity.value = e.hostUid\n // 主视频元素会被 v-if 重新挂载,nextTick 后 reattach\n nextTick(() => {\n refreshMembers(true)\n })\n}\n\n// 监听解散广播:主持人解散后所有参与人收到,立即隐藏视频窗口,只留独立倒计时提示。\n// 不依赖 session 'ended' 事件(它要等 sess.end() 的 async disconnect 完成才触发,有延迟)。\nfunction onDissolvedEvent(e: MeetingDissolvedEvent) {\n const session = getSession()\n if (!session || e.meetingId !== session.meetingId) return\n if (!endedReason.value) endedReason.value = '会议已解散'\n if (timer) clearInterval(timer)\n}\n\n// 监听被踢广播:立即隐藏视频窗口\nfunction onKickedEvent(e: { meetingId: string; by: string }) {\n const session = getSession()\n if (!session || e.meetingId !== session.meetingId) return\n if (!endedReason.value) endedReason.value = '已被移出会议'\n if (timer) clearInterval(timer)\n}\n\nfunction formatDuration(s: number) {\n const h = Math.floor(s / 3600)\n const m = Math.floor((s % 3600) / 60)\n .toString()\n .padStart(2, '0')\n const sec = (s % 60).toString().padStart(2, '0')\n return h > 0 ? `${h}:${m}:${sec}` : `${m}:${sec}`\n}\n\nonMounted(async () => {\n await syncDeviceCapability()\n const client = getClient()\n if (!client) return\n const session = client.meeting?.currentSession\n if (!session) return\n\n hostIdentity.value = session.hostIdentity || session.identity\n\n session.onSession('connected', () => {\n refreshMembers()\n nextTick(layout)\n timer = setInterval(() => {\n duration.value++\n // 每 10 秒刷一次成员列表(muted/online 实时态)\n // 后端 presence scanner 缓存也是 10s 刷新,更高频率纯浪费;muted 等变化由事件即时触发 force refresh\n if (duration.value % 10 === 0) refreshMembers()\n }, 1000)\n })\n session.onSession('participantJoined', () => {\n refreshMembers(true)\n })\n session.onSession('participantLeft', () => {\n refreshMembers(true)\n })\n session.onSession('participantTrackSubscribed', (info: { kind: string; identity: string }) => {\n if (info.kind !== 'video') return\n // 收到对方的 camera track = 对方摄像头开着:立即置 cameraOn=true,\n // 否则要等 10s 轮询,期间对方 tile 会被「摄像头已关」遮罩误盖\n const m = members.value.find((x) => x.identity === info.identity)\n if (m && !m.cameraOn) {\n m.cameraOn = true\n members.value = [...members.value]\n }\n // 优先用小视频池的元素\n const el = participantVideoEls.get(info.identity)\n if (el) {\n session.attachParticipantVideo(info.identity, el)\n }\n // 全屏宫格池的元素同样补挂载(track.attach 支持同一 track 挂多个元素)\n const gridEl = gridVideoEls.get(info.identity)\n if (gridEl) {\n session.attachParticipantVideo(info.identity, gridEl)\n }\n // 主视频位置的 track 流入:reattach 主视频(mainVideoEl 不在 participantVideoEls 里)\n if (mainVideoIdentity === info.identity) {\n nextTick(reattachAllVideos)\n }\n })\n // 发言态:LiveKit 给全量列表,覆盖式更新 members[].speaking\n // refreshMembers 每 10 秒会用后端 speaking=false 覆盖,下个 activeSpeakersChanged 事件会再次纠正——可接受\n // 就地修改成员字段,仅在状态变化时替换数组引用触发响应式\n session.onSession('activeSpeakersChanged', (info: { identities: string[] }) => {\n const set = new Set(info.identities)\n let changed = false\n for (const m of members.value) {\n const speaking = set.has(m.identity)\n if (m.speaking !== speaking) {\n m.speaking = speaking\n changed = true\n }\n }\n if (changed) members.value = [...members.value]\n })\n // 远端麦克风/摄像头开/关:毫秒级事件就地更新 members[].muted / cameraOn\n // (此前仅靠 10s 轮询后端快照;轮询保留作断线/丢事件后的兜底校正)\n session.onSession(\n 'participantMuteChanged',\n (info: { identity: string; muted: boolean; kind: 'audio' | 'video' }) => {\n const m = members.value.find((x) => x.identity === info.identity)\n if (!m) return\n let changed = false\n if (info.kind === 'audio') {\n if (m.muted !== info.muted) {\n m.muted = info.muted\n changed = true\n }\n } else {\n // video 事件 muted=true 表示摄像头关闭;cameraOn 语义相反\n if (m.cameraOn !== !info.muted) {\n m.cameraOn = !info.muted\n changed = true\n }\n }\n if (changed) members.value = [...members.value]\n },\n )\n session.onSession('devices', (info: { mic: boolean; camera: boolean; errors: string[] }) => {\n // 设备开启失败不在页面弹提示(core 已 console.warn):按钮区置灰 + 状态图标已足够表达\n // 同步设备实际开启状态(enableDevices 可能 mic/camera 开启失败,UI 需据此更新按钮)\n if (!info.mic) micEnabled.value = false\n if (!info.camera) cameraEnabled.value = false\n if (info.camera) nextTick(reattachAllVideos)\n // 立即更新 members 里自己那行:enableDevices 完成 ≠ 轮询周期到,\n // 不主动写会导致自己的 tile「摄像头已关」遮罩误显示最长一个轮询周期(10s)\n const selfRow = members.value.find((m) => m.identity === session.identity)\n if (selfRow) {\n if (selfRow.cameraOn !== info.camera) {\n selfRow.cameraOn = info.camera\n members.value = [...members.value]\n }\n }\n // 开启失败(无权限/被占用)= 不可用置灰;本次开启成功则恢复(如权限后来被授予)\n // errors 条目带 \"mic:\"/\"camera:\" 前缀(core enableDevices 约定),可精确区分设备\n if (info.errors.some((e) => e.startsWith('mic:'))) micUnavailable.value = true\n else if (info.mic) micUnavailable.value = false\n if (info.errors.some((e) => e.startsWith('camera:'))) cameraUnavailable.value = true\n else if (info.camera) cameraUnavailable.value = false\n })\n session.onSession('ended', () => {\n // 解散/被踢/对端断开都会触发——独立倒计时浮窗提示,5s 后自动关或用户手动关\n if (!endedReason.value) endedReason.value = '会议已结束'\n if (timer) clearInterval(timer)\n })\n\n // race 修复:mount 之前可能已 connected\n if (session.isConnected) {\n refreshMembers()\n nextTick(layout)\n timer = setInterval(() => {\n duration.value++\n if (duration.value % 10 === 0) refreshMembers()\n }, 1000)\n }\n\n // 监听 host 转让广播(所有客户端)\n client.on('meeting:hostChanged', onHostChangedEvent)\n // 监听解散/被踢广播:参与人端立即隐藏视频窗口(不等 sess.end 的 async 完成)\n client.on('meeting:dissolved', onDissolvedEvent)\n client.on('meeting:kicked', onKickedEvent)\n\n // 布局双保险:ResizeObserver + window resize\n if (contentEl.value) {\n resizeObserver = new ResizeObserver(() => layout())\n resizeObserver.observe(contentEl.value)\n }\n window.addEventListener('resize', layout)\n nextTick(layout)\n})\n\nonUnmounted(() => {\n if (timer) clearInterval(timer)\n if (actionErrorTimer) clearTimeout(actionErrorTimer)\n if (resizeObserver) {\n resizeObserver.disconnect()\n resizeObserver = null\n }\n window.removeEventListener('resize', layout)\n const client = getClient()\n client?.off?.('meeting:hostChanged', onHostChangedEvent)\n client?.off?.('meeting:dissolved', onDissolvedEvent)\n client?.off?.('meeting:kicked', onKickedEvent)\n})\n</script>\n\n<template>\n <div\n id=\"meetingWindow\"\n v-show=\"!minimized && !endedReason\"\n v-draggable=\"'.meeting-header'\"\n class=\"meeting-window\"\n :style=\"{ height: height || '80vh' }\"\n >\n <MeetingHeader\n :title=\"title\"\n :duration=\"formatDuration(duration)\"\n :grid-mode=\"gridMode\"\n @toggle-grid=\"gridMode = !gridMode\"\n @close=\"leaveMeeting\"\n @minimize=\"minimized = true\"\n />\n\n <div v-if=\"actionError\" class=\"device-warn\">\n {{ actionError }}\n </div>\n\n <div ref=\"contentEl\" class=\"content\">\n <!-- 主视频:当前主持人 -->\n <div ref=\"mainVideoWrap\">\n <MainVideoTile\n v-if=\"mainVideoMember\"\n ref=\"mainVideoRef\"\n :identity=\"mainVideoMember.identity\"\n :name=\"mainVideoMember.name\"\n :muted=\"mainVideoMember.muted\"\n :camera-on=\"mainVideoMember.cameraOn\"\n :camera-unavailable=\"memberCameraUnavailable(mainVideoMember)\"\n :speaking=\"mainVideoMember.speaking\"\n :is-host=\"mainVideoMember.isHost\"\n :is-self=\"mainVideoMember.identity === (getSession()?.identity || '')\"\n :mic-unavailable=\"memberMicUnavailable(mainVideoMember)\"\n :on-video-el-ready=\"onMainVideoReady\"\n />\n </div>\n\n <aside ref=\"sidebarEl\" class=\"sidebar\">\n <div ref=\"tabBarWrap\">\n <SidebarTabs\n v-model:active-tab=\"activeTab\"\n :is-host=\"isCurrentHost\"\n @add-member=\"onAddMemberClick\"\n />\n </div>\n <!-- 视频列表 tab -->\n <VideoListPanel\n v-show=\"activeTab === 'video'\"\n ref=\"videoListPanelRef\"\n :members=\"smallVideoMembers\"\n :self-identity=\"getSession()?.identity || ''\"\n :self-mic-unavailable=\"micUnavailable\"\n :self-camera-unavailable=\"cameraUnavailable\"\n :on-tile-mount=\"onSmallVideoReady\"\n />\n <!-- 成员列表 tab -->\n <MemberListPanel\n v-show=\"activeTab === 'member'\"\n :members=\"sortedMembers\"\n :can-promote=\"isCurrentHost\"\n :can-kick=\"isCurrentHost\"\n :can-reinvite=\"isCurrentHost\"\n :self-identity=\"getSession()?.identity || ''\"\n :self-mic-unavailable=\"micUnavailable\"\n :self-camera-unavailable=\"cameraUnavailable\"\n @transfer-host=\"(uid: string) => transferHost(uid)\"\n @kick=\"(uid: string) => kickParticipant(uid)\"\n @reinvite=\"(uid: string) => reinviteMember(uid)\"\n />\n </aside>\n </div>\n\n <!-- 底部控制按钮区:按钮组抽为 MeetingControlBar(全屏宫格的 header 中央复用同一组件) -->\n <div class=\"bottom-controls\">\n <MeetingControlBar\n :mic-enabled=\"micEnabled\"\n :camera-enabled=\"cameraEnabled\"\n :speaker-muted=\"speakerMuted\"\n :mic-unavailable=\"micUnavailable\"\n :camera-unavailable=\"cameraUnavailable\"\n @toggle-mic=\"toggleMic\"\n @toggle-camera=\"toggleCamera\"\n @toggle-speaker=\"toggleSpeaker\"\n @hangup=\"leaveMeeting\"\n />\n </div>\n </div>\n\n <!-- 全屏宫格模式:覆盖整个视口的 4 列 × N 行宫格。\n tile 固定 16:9,高度按可视高度 1/4 推算(减 3 条 5px 间隔)——\n 4 行时正好垂直铺满,超出 4 行出现纵向滚动条;宫格整体水平居中,左右留白。\n 复用 MeetingHeader(图标自动变为「退出宫格」+ 隐藏最小化)与 SmallVideoTile。\n 会议结束/被踢/解散时随 endedReason 自动退出 -->\n <div v-if=\"gridMode && !endedReason\" class=\"fs-grid-overlay\">\n <MeetingHeader\n :title=\"title\"\n :duration=\"formatDuration(duration)\"\n :grid-mode=\"true\"\n @toggle-grid=\"gridMode = false\"\n @close=\"leaveMeeting\"\n >\n <!-- 控制按钮组:与默认模式底部同一组件,放在 header 水平/垂直居中位 -->\n <template #center>\n <MeetingControlBar\n :mic-enabled=\"micEnabled\"\n :camera-enabled=\"cameraEnabled\"\n :speaker-muted=\"speakerMuted\"\n :mic-unavailable=\"micUnavailable\"\n :camera-unavailable=\"cameraUnavailable\"\n @toggle-mic=\"toggleMic\"\n @toggle-camera=\"toggleCamera\"\n @toggle-speaker=\"toggleSpeaker\"\n @hangup=\"leaveMeeting\"\n />\n </template>\n </MeetingHeader>\n <div class=\"fs-grid-body\">\n <div class=\"fs-grid-scroll\">\n <div class=\"fs-grid\">\n <SmallVideoTile\n v-for=\"m in gridMembers\"\n :key=\"m.identity\"\n :identity=\"m.identity\"\n :name=\"m.name\"\n :muted=\"m.muted\"\n :joined=\"m.joined\"\n :camera-on=\"m.joined && m.cameraOn\"\n :camera-unavailable=\"memberCameraUnavailable(m)\"\n :speaking=\"m.speaking\"\n :is-host=\"m.isHost\"\n :is-self=\"m.identity === (getSession()?.identity || '')\"\n :host-badge-top=\"true\"\n :mic-unavailable=\"memberMicUnavailable(m)\"\n :on-video-el-ready=\"onGridVideoReady\"\n />\n </div>\n </div>\n <!-- 成员列表抽屉:右侧滑入/收起,复用 MemberListPanel(功能/滚动条与默认布局一致);\n 高度 = 窗口高度 - header(撑满 body) -->\n <transition name=\"fs-drawer\">\n <aside v-if=\"gridDrawerOpen\" class=\"fs-drawer\">\n <MemberListPanel\n :members=\"sortedMembers\"\n :can-promote=\"isCurrentHost\"\n :can-kick=\"isCurrentHost\"\n :can-reinvite=\"isCurrentHost\"\n :self-identity=\"getSession()?.identity || ''\"\n :self-mic-unavailable=\"micUnavailable\"\n :self-camera-unavailable=\"cameraUnavailable\"\n @transfer-host=\"(uid: string) => transferHost(uid)\"\n @kick=\"(uid: string) => kickParticipant(uid)\"\n @reinvite=\"(uid: string) => reinviteMember(uid)\"\n />\n </aside>\n </transition>\n <!-- 抽屉开合把手:贴抽屉左缘垂直居中(抽屉收起时贴右缘) -->\n <button\n class=\"fs-drawer-toggle\"\n type=\"button\"\n :aria-label=\"gridDrawerOpen ? '收起成员列表' : '展开成员列表'\"\n :title=\"gridDrawerOpen ? '收起成员列表' : '展开成员列表'\"\n :style=\"{ right: gridDrawerOpen ? '200px' : '0px' }\"\n @click=\"gridDrawerOpen = !gridDrawerOpen\"\n >\n {{ gridDrawerOpen ? '›' : '‹' }}\n </button>\n </div>\n </div>\n\n <!-- 解散会议确认弹窗:独立于主窗口,最小化态下也能显示 -->\n <ConfirmDialog\n v-if=\"showDissolveConfirm\"\n title=\"解散会议\"\n message=\"会议中还有其他成员,确认要解散会议吗?所有成员将被强制离会。\"\n confirm-text=\"解散会议\"\n cancel-text=\"取消\"\n :danger=\"true\"\n @confirm=\"confirmDissolve\"\n @cancel=\"cancelDissolve\"\n />\n\n <!-- 自定义邀请面板:业务方传 #invite-panel 时替代内置选人弹窗。\n 插槽内容渲染在根级(无容器包裹),定位/遮罩完全由插槽内容自定 -->\n <slot\n v-if=\"invitePanelOpen\"\n name=\"invite-panel\"\n :members=\"members\"\n :exclude-uids=\"memberIdentities\"\n :invite=\"inviteUids\"\n :close=\"() => (invitePanelOpen = false)\"\n />\n\n <!-- 会议结束/离开倒计时提示:独立浮窗,不依赖主窗口 -->\n <MeetingEndedToast\n v-if=\"endedReason\"\n :reason=\"endedReason\"\n :duration=\"5\"\n @close=\"emit('close')\"\n />\n\n <!-- 最小化浮条:可拖拽,单行显示发言人/会议名称 + 时长 + 还原/关闭按钮 -->\n <div v-if=\"minimized && !endedReason\" class=\"mini-bar\" v-draggable=\"'.mini-bar-body'\">\n <div class=\"mini-bar-body\">\n <!-- 有人发言时显示声波动效 + 发言人;无人发言显示会议名称(同一位置切换) -->\n <span v-if=\"speakerDisplayText\" class=\"speaking-indicator\">\n <span class=\"wave-bar\"></span>\n <span class=\"wave-bar\"></span>\n <span class=\"wave-bar\"></span>\n </span>\n <span class=\"mini-main-text\" :class=\"{ speaking: !!speakerDisplayText }\">\n {{ speakerDisplayText || title }}\n </span>\n <span class=\"mini-duration\">{{ formatDuration(duration) }}</span>\n </div>\n <div class=\"mini-bar-actions\">\n <button\n class=\"mini-btn\"\n type=\"button\"\n aria-label=\"还原窗口\"\n title=\"还原窗口\"\n v-html=\"MaximizeIcon\"\n @click=\"minimized = false\"\n ></button>\n <button\n class=\"mini-btn mini-close\"\n type=\"button\"\n aria-label=\"离开会议\"\n title=\"离开会议\"\n v-html=\"CloseIcon\"\n @click=\"leaveMeeting\"\n ></button>\n </div>\n </div>\n</template>\n\n<style scoped>\n.meeting-window {\n position: fixed;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n /* height 由 prop 控制(inline style 注入),未传时 fallback 80vh */\n width: var(--mw-w, min(1200px, 95vw));\n background: var(--im-meeting-bg-panel, #0f172a);\n display: flex;\n flex-direction: column;\n overflow: hidden;\n margin: 0 auto;\n padding: 10px;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);\n color: var(--im-meeting-text-primary, #fff);\n z-index: 9999;\n border-radius: 0;\n /* 与 design/video-conference-layout.html :root 变量一一对应 */\n --im-meeting-bg-body: #0b1220;\n --im-meeting-bg-panel: #0f172a;\n --im-meeting-bg-video: #000000;\n --im-meeting-text-primary: #ffffff;\n --im-meeting-text-secondary: rgba(255, 255, 255, 0.85);\n --im-meeting-text-tertiary: rgba(255, 255, 255, 0.55);\n --im-meeting-border: rgba(255, 255, 255, 0.08);\n --im-meeting-scrollbar: rgba(255, 255, 255, 0.15);\n --im-meeting-scrollbar-hover: rgba(255, 255, 255, 0.35);\n --im-meeting-tab-active: #1e293b;\n --im-meeting-tab-inactive: #111827;\n --im-meeting-speaking: #22c55e;\n --im-meeting-online: #22c55e;\n --im-meeting-offline: #6b7280;\n --im-meeting-overlay-gap: 5px;\n}\n.content {\n flex: 1 1 0;\n min-height: 0;\n display: flex;\n flex-direction: row;\n gap: 10px;\n padding: 0;\n overflow: hidden;\n margin-top: 5px;\n}\n.content > div:first-child {\n flex: 0 0 auto;\n}\n.sidebar {\n flex: 0 0 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n /* 比会议窗口背景 #0f172a 稍深一点的暗色,让空列表区域不显得空洞 */\n background: #0b1220;\n /* tab-bar 与 list 之间的 8px 间距由 sidebar flex gap 直接管理,\n 不依赖子组件内部 margin(避免 BFC 包含 margin 导致 offsetHeight 多算 8px) */\n gap: 8px;\n overflow: hidden;\n}\n/* 所有子节点撑满 sidebar 宽度:tab-bar / video list / member list 三者左右对齐 */\n.sidebar > * {\n width: 100%;\n}\n.sidebar > div:first-child {\n flex: 0 0 auto;\n}\n/* 设备失败提示 */\n.device-warn {\n position: absolute;\n top: 44px;\n left: 50%;\n transform: translateX(-50%);\n background: rgba(212, 160, 23, 0.92);\n color: var(--im-meeting-text-primary, #fff);\n font-size: 12px;\n padding: 4px 12px;\n border-radius: 0;\n z-index: 6;\n pointer-events: none;\n max-width: 80%;\n text-align: center;\n}\n/* 底部控制按钮区:水平居中、常驻显示、紧凑高度 */\n.bottom-controls {\n flex: 0 0 auto;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 16px;\n padding: 6px 0 2px;\n z-index: 4;\n}\n\n/* ============ 全屏宫格模式 ============ */\n.fs-grid-overlay {\n position: fixed;\n inset: 0;\n z-index: 10000; /* 盖过会议窗口(9999) */\n background: var(--im-meeting-bg-body, #0b1220);\n display: flex;\n flex-direction: column;\n padding: 10px;\n color: var(--im-meeting-text-primary, #fff);\n}\n/* 全屏态的 header 不可拖拽(拖拽指令只绑在主窗口上) */\n.fs-grid-overlay :deep(.meeting-header) {\n cursor: default;\n}\n/* 宫格主体:横向 flex,左侧宫格滚动区 + 右侧成员列表抽屉 */\n.fs-grid-body {\n flex: 1 1 0;\n min-height: 0;\n margin-top: 5px;\n display: flex;\n position: relative;\n}\n.fs-grid-scroll {\n flex: 1 1 0;\n min-width: 0;\n overflow-y: auto;\n overflow-x: hidden;\n /* 容器查询单位:tile 高度按可视高度的 1/4 推算(减 3 条 5px 间隔);\n 窄屏(竖屏)时按容器宽度反推收缩,避免水平溢出 */\n container-type: size;\n scrollbar-width: thin;\n scrollbar-color: var(--im-meeting-scrollbar, rgba(255, 255, 255, 0.15)) transparent;\n}\n.fs-grid-scroll::-webkit-scrollbar {\n width: 8px;\n}\n.fs-grid-scroll::-webkit-scrollbar-thumb {\n background: var(--im-meeting-scrollbar, rgba(255, 255, 255, 0.15));\n}\n.fs-grid-scroll::-webkit-scrollbar-thumb:hover {\n background: var(--im-meeting-scrollbar-hover, rgba(255, 255, 255, 0.35));\n}\n/* 宫格:4 列,tile 16:9,间隔 5px;整体宽度 fit-content + margin auto 实现水平居中留白 */\n.fs-grid {\n --fs-tile-h: min(calc((100cqh - 15px) / 4), calc((100cqw - 15px) * 9 / 64));\n display: grid;\n grid-template-columns: repeat(4, calc(var(--fs-tile-h) * 16 / 9));\n grid-auto-rows: var(--fs-tile-h);\n gap: 5px;\n width: fit-content;\n margin: 0 auto;\n}\n.fs-grid :deep(.small-video) {\n width: 100%;\n height: 100%;\n}\n/* 成员列表抽屉:上层覆盖宫格(absolute 贴右侧,不挤占宫格宽度,tile 尺寸不变);\n 高度撑满 body(= 窗口高 - header),内部滚动由 MemberListPanel 自带(样式一致) */\n.fs-drawer {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n width: 200px;\n display: flex;\n flex-direction: column;\n background: var(--im-meeting-bg-panel, #0f172a);\n border-left: 1px solid var(--im-meeting-border, rgba(255, 255, 255, 0.12));\n padding: 8px;\n overflow: hidden;\n z-index: 4;\n}\n/* 抽屉滑入/滑出动画 */\n.fs-drawer-enter-active,\n.fs-drawer-leave-active {\n transition:\n transform 0.2s ease,\n opacity 0.2s ease;\n}\n.fs-drawer-enter-from,\n.fs-drawer-leave-to {\n transform: translateX(30px);\n opacity: 0;\n}\n/* 抽屉开合把手:贴右缘垂直居中,随抽屉开合平移;箭头指示方向 */\n.fs-drawer-toggle {\n position: absolute;\n top: 50%;\n transform: translateY(-50%);\n width: 18px;\n height: 56px;\n border: 1px solid var(--im-meeting-border, rgba(255, 255, 255, 0.12));\n border-right: none;\n border-radius: 6px 0 0 6px;\n background: var(--im-meeting-bg-panel, #0f172a);\n color: var(--im-meeting-text-secondary, rgba(255, 255, 255, 0.85));\n font-size: 14px;\n line-height: 1;\n cursor: pointer;\n z-index: 5;\n transition: right 0.2s ease;\n}\n.fs-drawer-toggle:hover {\n color: var(--im-meeting-text-primary, #fff);\n background: rgba(255, 255, 255, 0.08);\n}\n\n/* ============ 最小化浮条 ============ */\n.mini-bar {\n position: fixed;\n right: 20px;\n bottom: 20px;\n z-index: 9999;\n display: flex;\n align-items: stretch;\n max-width: 320px;\n min-width: 220px;\n background: var(--im-meeting-bg-panel, #0f172a);\n border: 1px solid var(--im-meeting-border, rgba(255, 255, 255, 0.12));\n border-radius: 0;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);\n color: var(--im-meeting-text-primary, #fff);\n overflow: hidden;\n}\n/* 拖拽区:单行——发言人/会议名称 + 时长 */\n.mini-bar-body {\n flex: 1 1 auto;\n min-width: 0;\n padding: 8px 10px;\n cursor: move;\n display: flex;\n align-items: center;\n gap: 6px;\n}\n/* 主体文本:有人发言时显示发言人(绿色),无人发言显示会议名称(白色) */\n.mini-main-text {\n font-size: 13px;\n font-weight: 500;\n color: var(--im-meeting-text-primary, #fff);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n flex: 1 1 auto;\n min-width: 0;\n}\n.mini-main-text.speaking {\n color: var(--im-meeting-speaking, #22c55e);\n font-weight: 600;\n}\n.mini-duration {\n font-size: 11px;\n color: var(--im-meeting-text-tertiary, rgba(255, 255, 255, 0.55));\n font-variant-numeric: tabular-nums;\n flex: 0 0 auto;\n}\n/* 声波动效:复用 MainVideoTile 的 wave-bar 三柱动效 + 颜色变量 */\n.mini-bar-body .speaking-indicator {\n display: inline-flex;\n align-items: flex-end;\n gap: 2px;\n flex: 0 0 auto;\n}\n.mini-bar-body .wave-bar {\n width: 2px;\n background: var(--im-meeting-speaking, #22c55e);\n animation: mini-wave 1s ease-in-out infinite;\n}\n.mini-bar-body .wave-bar:nth-child(1) {\n height: 6px;\n animation-delay: 0s;\n}\n.mini-bar-body .wave-bar:nth-child(2) {\n height: 10px;\n animation-delay: 0.15s;\n}\n.mini-bar-body .wave-bar:nth-child(3) {\n height: 6px;\n animation-delay: 0.3s;\n}\n@keyframes mini-wave {\n 0%,\n 100% {\n transform: scaleY(1);\n opacity: 0.7;\n }\n 50% {\n transform: scaleY(1.6);\n opacity: 1;\n }\n}\n/* 操作按钮区 */\n.mini-bar-actions {\n flex: 0 0 auto;\n display: flex;\n align-items: center;\n gap: 2px;\n padding: 0 4px;\n border-left: 1px solid var(--im-meeting-border, rgba(255, 255, 255, 0.08));\n}\n.mini-btn {\n width: 26px;\n height: 100%;\n min-height: 36px;\n border: none;\n background: transparent;\n color: var(--im-meeting-text-secondary, rgba(255, 255, 255, 0.85));\n cursor: pointer;\n border-radius: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n transition:\n background 0.2s,\n color 0.2s;\n}\n.mini-btn:hover {\n background: rgba(255, 255, 255, 0.1);\n color: var(--im-meeting-text-primary, #fff);\n}\n.mini-btn :deep(svg) {\n width: 16px;\n height: 16px;\n}\n.mini-btn.mini-close:hover {\n background: #e53935;\n color: #fff;\n}\n</style>\n","<script setup lang=\"ts\">\nimport {\n ref,\n onMounted,\n onUnmounted,\n computed,\n nextTick,\n watch,\n useSlots,\n getCurrentInstance,\n} from 'vue'\nimport vDraggable from '../directives/draggable'\nimport { mountComponent } from '../mount'\nimport { ImToastApi as toast } from '../toast'\nimport MeetingHeader from './meeting/MeetingHeader.vue'\nimport MainVideoTile from './meeting/MainVideoTile.vue'\nimport VideoListPanel from './meeting/VideoListPanel.vue'\nimport MemberListPanel from './meeting/MemberListPanel.vue'\nimport SidebarTabs from './meeting/SidebarTabs.vue'\nimport SmallVideoTile from './meeting/SmallVideoTile.vue'\nimport MeetingControlBar from './meeting/MeetingControlBar.vue'\nimport ConfirmDialog from './ConfirmDialog.vue'\nimport MeetingEndedToast from './MeetingEndedToast.vue'\nimport UserPickerDialog from './UserPickerDialog.vue'\nimport { MaximizeIcon, CloseIcon } from './meeting/icons'\nimport type {\n MeetingMember,\n MeetingHostChangedEvent,\n MeetingDissolvedEvent,\n} from '@quitting/contract'\n\n// 会议窗口:左主视频(默认主持人) + 右侧栏 Tab(视频列表/成员列表) + 底部成员状态条\n// 布局严格 16:9,所有组件无圆角;样式对齐 design/video-conference-layout.html\n// 视频挂载沿用 CallSession/MeetingSession 的 attach 模式:通过 onVideoElReady 把元素交回 SDK\n\nconst props = defineProps<{\n mode: 'host' | 'participant'\n title: string\n // 窗口高度(CSS 值),由调用方决定。支持 \"80vh\" / \"600px\" / \"100%\" 等\n // position: fixed 下百分比相对 viewport;宽度由 layout() 按 16:9 推算\n height?: string\n onClose?: () => void\n // 用户信息解析器:uid → { name, dept? }\n // 后端 ListMembers 在 user 表无对应记录时会回退为 uid(如 demo mock 用户),\n // 调用方可传此函数让 SDK UI 显示业务侧维护的 name/dept;返回 undefined 则保留后端原值\n resolveUser?: (uid: string) => { name: string; dept?: string } | undefined\n // 禁用内置邀请弹窗(点击「+」只抛 addMember 事件,不弹内置选人窗)\n disableDefaultInvite?: boolean\n // 自定义选人:业务方弹自己的 UI 并 resolve 所选 uid 列表(可返回 [] 表示取消),\n // SDK 拿到后内部完成批量 reinvite + 成员列表刷新;优先级介于插槽与 @add-member 之间\n invitePicker?: () => Promise<string[]>\n}>()\n\nconst emit = defineEmits<{\n (e: 'close'): void\n // 添加成员按钮被点击。默认行为按优先级退让:\n // 传了 #invite-panel 插槽 → 渲染插槽(完全自定义)\n // 传了 invitePicker prop → 业务方弹自己的选人 UI,resolve uid 列表后 SDK 统一邀请\n // 传了 @add-member 监听 → 仅抛事件(不弹内置选人窗,由外部接管)\n // 都没有 → 抛事件 + 弹内置 UserPickerDialog(批量 reinvite)\n (e: 'addMember'): void\n}>()\n\nconst slots = useSlots()\n// 自定义邀请面板插槽签名(运行时按名取用;类型仅作文档)\n// scope: members=当前成员列表 / excludeUids=已在会成员 / invite(uids)=批量邀请 / close()=收起面板\n\n// 自定义邀请面板开关(#invite-panel 插槽渲染层)\nconst invitePanelOpen = ref(false)\n// 当前会议成员 externalId 列表(插槽 scope 用:选人时排除已在会成员)\nconst memberIdentities = computed(() => members.value.map((m) => m.identity))\n\nconst micEnabled = ref(true)\nconst cameraEnabled = ref(true)\nconst speakerMuted = ref(false)\nconst hasCamera = ref(true)\nconst hasMic = ref(true)\n// 设备不可用(无设备/无权限/被占用):按钮与自己的状态图标置灰,与「主动关闭」(红色)区分\nconst micUnavailable = ref(false)\nconst cameraUnavailable = ref(false)\n// 通用操作提示(reinvite 失败等),复用 device-warn 样式,3 秒自动消失\nconst actionError = ref('')\nlet actionErrorTimer: ReturnType<typeof setTimeout> | null = null\nfunction pushToast(msg: string) {\n actionError.value = msg\n if (actionErrorTimer) clearTimeout(actionErrorTimer)\n actionErrorTimer = setTimeout(() => (actionError.value = ''), 3000)\n}\nconst duration = ref(0)\nconst endedReason = ref('')\nconst activeTab = ref<'video' | 'member'>('video')\nconst members = ref<MeetingMember[]>([])\nconst hostIdentity = ref<string>('')\n// 最小化状态:true 时隐藏主窗口内容,显示可拖拽的小浮条(LiveKit 会话不中断)\nconst minimized = ref(false)\n// 全屏宫格模式:true 时覆盖整个视口的 4 列宫格布局(LiveKit 会话不中断)\nconst gridMode = ref(false)\n// 宫格模式的成员列表抽屉(右侧滑入,可收起;复用 MemberListPanel,功能与默认布局一致)\nconst gridDrawerOpen = ref(false)\n\n// 元素引用\nconst contentEl = ref<HTMLElement | null>(null)\nconst mainVideoWrap = ref<HTMLElement | null>(null)\nconst sidebarEl = ref<HTMLElement | null>(null)\nconst tabBarWrap = ref<HTMLElement | null>(null)\nconst videoListPanelRef = ref<InstanceType<typeof VideoListPanel> | null>(null)\nconst mainVideoRef = ref<InstanceType<typeof MainVideoTile> | null>(null)\n\n// 视频 attach:identity → 元素;保留 self/remote 区分\nlet selfVideoEl: HTMLVideoElement | null = null\nlet mainVideoIdentity: string | null = null\nlet mainVideoEl: HTMLVideoElement | null = null\nconst participantVideoEls = new Map<string, HTMLVideoElement>()\n// 全屏宫格的视频元素池:identity → 元素(track 后到时补挂载用)\nconst gridVideoEls = new Map<string, HTMLVideoElement>()\n\nlet timer: ReturnType<typeof setInterval> | null = null\nlet resizeObserver: ResizeObserver | null = null\n\nfunction getClient() {\n return (window as any).__imClient\n}\n\nfunction getSession() {\n return getClient()?.meeting?.currentSession\n}\n\n// 成员设备不可用判定:自己用本地实时检测(更及时),他人用后端推断字段\n// (已入会但从未发布对应 track = 无设备/无权限/被占用)——与成员列表(MemberItem)口径一致\nfunction memberMicUnavailable(m: MeetingMember) {\n return m.identity === (getSession()?.identity || '') ? micUnavailable.value : !!m.micUnavailable\n}\nfunction memberCameraUnavailable(m: MeetingMember) {\n return (\n m.joined &&\n (m.identity === (getSession()?.identity || '') ? cameraUnavailable.value : !!m.cameraUnavailable)\n )\n}\n\n// 主视频参与者:当前 hostIdentity 对应的 member(可能未入会)\nconst mainVideoMember = computed<MeetingMember | null>(() => {\n const m = members.value.find((x) => x.identity === hostIdentity.value)\n return m || null\n})\n\n// 当前用户是否是「当前主持人」——动态判断,主持人转让后自动更新\n// mode prop 在创建时固化(host/participant),转让后原主持人 mode 仍是 host 但已无权限,\n// 新主持人 mode 是 participant 却应有权限。改用 identity === hostIdentity 解决菜单错乱。\nconst isCurrentHost = computed(() => {\n const sess = getSession()\n return !!sess && sess.identity === hostIdentity.value\n})\n\n// 正在发言的成员(最小化浮条显示用)——与成员列表 speaking 判定一致\nconst speakingMembers = computed(() =>\n members.value.filter((m) => m.speaking && m.joined && m.online),\n)\n// 发言人展示文本:≤3 个全显示(顿号分隔);>3 个显示第一个 + \"...等N人\"\nconst speakerDisplayText = computed(() => {\n const list = speakingMembers.value\n if (list.length === 0) return ''\n if (list.length <= 3) return list.map((m) => m.name).join('、')\n return `${list[0].name}...等${list.length}人`\n})\n\n// 小视频列表:除了主视频那位,其他在线成员(排序规则与所有布局统一)\nconst smallVideoMembers = computed<MeetingMember[]>(() => {\n return sortMembers(\n members.value.filter((m) => m.identity !== hostIdentity.value && (m.online || m.joined)),\n )\n})\n\n// 全屏宫格成员:所有在线/已入会成员(含主持人和自己),排序规则与所有布局统一\nconst gridMembers = computed<MeetingMember[]>(() => {\n return sortMembers(members.value.filter((m) => m.online || m.joined))\n})\n\n// 全屏宫格视频元素就绪:与 onSmallVideoReady 同模式(self 走本地 attach,其余走远端)\nfunction onGridVideoReady(identity: string, el: HTMLVideoElement | null) {\n const session = getSession()\n if (!session) return\n if (el) {\n gridVideoEls.set(identity, el)\n if (identity === session.identity) {\n session.attachLocalVideo(el)\n } else {\n session.attachParticipantVideo(identity, el)\n }\n } else {\n gridVideoEls.delete(identity)\n }\n}\n\n// 小视频列表变化后必须重新 layout:\n// mount 时 members 还是空 → small-video 没渲染 → layout() 跳过尺寸应用\n// 之后 refreshMembers 完成、small-video 渲染,需要重新 layout 把 inline style 设上\nwatch(smallVideoMembers, () => {\n nextTick(layout)\n})\n\n// 排序后的成员列表(成员列表 tab / 全屏宫格 / 小视频列表三处布局统一使用)\n// 排序规则:主持人最前 → 其余按加入时间升序(joinedAt)→ 未加入的按邀请序 → 已离会最后。\n// 不再按发言状态排序——speaking 仅用于 tile 角标与高亮,不影响位置\nfunction getStatusPriority(m: MeetingMember, hostUid: string): number {\n if (m.identity === hostUid) return 0\n if (m.status === 'left') return 3\n if (m.joinedAt > 0) return 1\n return 2\n}\nfunction sortMembers(list: MeetingMember[]): MeetingMember[] {\n const hostUid = hostIdentity.value\n return list.slice().sort((a, b) => {\n const pa = getStatusPriority(a, hostUid)\n const pb = getStatusPriority(b, hostUid)\n if (pa !== pb) return pa - pb\n // 同组内按加入时间;均为 0(未加入)时退化为邀请序\n if (a.joinedAt !== b.joinedAt) return a.joinedAt - b.joinedAt\n return (a.inviteOrder || 0) - (b.inviteOrder || 0)\n })\n}\nconst sortedMembers = computed(() => sortMembers(members.value))\n\nasync function refreshMembers(force = false) {\n const client = getClient()\n if (!client?.meeting) return\n try {\n const list = await client.meeting.getMembers(force)\n // 若调用方提供 resolveUser,用业务侧 name/dept 覆盖后端返回(demo 模式下后端回退 uid)\n if (props.resolveUser) {\n members.value = list.map((m: MeetingMember) => {\n const u = props.resolveUser?.(m.identity)\n if (!u) return m\n return { ...m, name: u.name, dept: u.dept ?? m.dept }\n })\n } else {\n members.value = list\n }\n } catch (err) {\n console.warn('[im] refreshMembers 失败', err)\n }\n}\n\nasync function reattachAllVideos() {\n const session = getSession()\n if (!session) return\n // 主视频:self 时走 attachLocalVideo,否则 attachParticipantVideo\n if (mainVideoIdentity && mainVideoEl) {\n if (mainVideoIdentity === session.identity) {\n session.attachLocalVideo(mainVideoEl)\n } else {\n session.attachParticipantVideo(mainVideoIdentity, mainVideoEl)\n }\n }\n}\n\n// 主视频元素就绪\nasync function onMainVideoReady(identity: string, el: HTMLVideoElement | null) {\n const session = getSession()\n if (!session) return\n if (el) {\n mainVideoIdentity = identity\n mainVideoEl = el\n if (identity === session.identity) {\n session.attachLocalVideo(el)\n } else {\n session.attachParticipantVideo(identity, el)\n }\n } else {\n mainVideoIdentity = null\n mainVideoEl = null\n }\n}\n\n// 小视频元素就绪\nfunction onSmallVideoReady(identity: string, el: HTMLVideoElement | null) {\n const session = getSession()\n if (!session) return\n if (el) {\n participantVideoEls.set(identity, el)\n if (identity === session.identity) {\n // 自己的 tile:attach 本地视频(非主持人在小视频列表里看不到自己就是因为这里跳过了)\n session.attachLocalVideo(el)\n } else {\n session.attachParticipantVideo(identity, el)\n }\n } else {\n participantVideoEls.delete(identity)\n }\n}\n\nasync function syncDeviceCapability() {\n try {\n const devices = await navigator.mediaDevices.enumerateDevices()\n hasCamera.value = devices.some((d) => d.kind === 'videoinput')\n hasMic.value = devices.some((d) => d.kind === 'audioinput')\n if (!hasCamera.value) cameraEnabled.value = false\n if (!hasMic.value) micEnabled.value = false\n // 无设备 = 不可用(按钮置灰而非隐藏)\n micUnavailable.value = !hasMic.value\n cameraUnavailable.value = !hasCamera.value\n // 无权限 = 不可用:permissions.query best-effort(Safari 等不支持会抛错,忽略——\n // 开启失败路径由 devices 事件的 errors 兜底)\n try {\n const p = await navigator.permissions.query({ name: 'microphone' })\n if (p.state === 'denied') micUnavailable.value = true\n } catch {\n /* 忽略 */\n }\n try {\n const p = await navigator.permissions.query({ name: 'camera' })\n if (p.state === 'denied') cameraUnavailable.value = true\n } catch {\n /* 忽略 */\n }\n } catch (e) {\n console.warn('[im] enumerateDevices 失败', e)\n }\n}\n\nfunction triggerLayout() {\n nextTick(layout)\n}\n\n// 布局计算:从 design/video-conference-layout.html layout() 函数搬\n// 所有尺寸(mwW/sidebarW/mainW/smallW)无条件计算,避免小视频列表为空时窗口宽度塌陷\nfunction layout() {\n const content = contentEl.value\n const mainVideo = mainVideoWrap.value\n const sidebar = sidebarEl.value\n const videoListEl = videoListPanelRef.value?.listEl as HTMLElement | undefined\n const items = videoListEl?.querySelectorAll<HTMLElement>('.small-video') ?? []\n if (!content || !mainVideo || !sidebar) return\n\n // 1. 主视频 16:9,高度 = content 高度\n const contentH = content.clientHeight\n if (contentH <= 0) return\n const mainH = contentH\n const mainW = (mainH * 16) / 9\n\n // 2. 小视频 16:9,3 个 + 3 个 gap 填满 sidebar 可用高度\n const smallGap = 8\n const tabBarH = tabBarWrap.value?.offsetHeight ?? 28\n const smallH = (mainH - tabBarH - smallGap * 3) / 3\n const smallW = (smallH * 16) / 9\n\n // 3. sidebar 宽度 = 小视频宽(不再保留自定义滚动条空间,列表与 tab 等宽)\n const sidebarW = smallW\n\n // 4. 窗口总宽度(padding + main + gap + sidebar + padding)—— 左右 padding 对称\n const padding = 10\n const gap = 10\n const mwW = padding + mainW + gap + sidebarW + padding\n\n // 5. 应用到 DOM(items 为空也照常设,保持窗口宽度稳定)\n const mw = document.getElementById('meetingWindow') as HTMLElement | null\n if (mw) {\n mw.style.width = mwW + 'px'\n }\n\n mainVideo.style.width = mainW + 'px'\n mainVideo.style.height = mainH + 'px'\n\n // sidebar 宽度 = smallW,与 list 完全对齐\n sidebar.style.width = sidebarW + 'px'\n sidebar.style.height = contentH + 'px'\n\n if (videoListEl) {\n // list 撑满 sidebar,与 tab-bar 等宽\n videoListEl.style.width = '100%'\n videoListEl.style.height = contentH - tabBarH - smallGap + 'px'\n }\n\n items.forEach((item) => {\n // 小视频保持 16:9\n item.style.width = smallW + 'px'\n // 不取整:Math.floor 会让 3 个 small 累计损失 ~2px,导致最后 small 底部与主视频底部错位\n item.style.height = smallH + 'px'\n })\n}\n\n// ============ 设备/会议控制 ============\nasync function toggleMic() {\n if (micUnavailable.value) return\n const session = getSession()\n if (!session) return\n const next = !micEnabled.value\n try {\n await session.setMicEnabled(next)\n micEnabled.value = next\n // 本端自己不走远端 trackMuted 事件,手动更新 members 里自己那行\n // (members[].muted 语义 = 已闭麦,与 micEnabled 相反),与远端事件路径合流\n const self = members.value.find((m) => m.identity === session.identity)\n if (self && self.muted !== !next) {\n self.muted = !next\n members.value = [...members.value]\n }\n } catch (err) {\n console.warn('[im] setMicEnabled 失败,UI 不切换', err)\n }\n}\n\nasync function toggleCamera() {\n if (cameraUnavailable.value) return\n const session = getSession()\n if (!session) return\n const next = !cameraEnabled.value\n try {\n await session.setCameraEnabled(next)\n cameraEnabled.value = next\n // 本端自己不走远端 trackMuted 事件,手动更新 members 里自己那行\n // (与远端事件路径合流,其他端/本端列表与遮罩立即反映)\n const self = members.value.find((m) => m.identity === session.identity)\n if (self && self.cameraOn !== next) {\n self.cameraOn = next\n members.value = [...members.value]\n }\n nextTick(reattachAllVideos)\n } catch (err) {\n console.warn('[im] setCameraEnabled 失败,UI 不切换', err)\n }\n}\n\nasync function toggleSpeaker() {\n const session = getSession()\n if (!session) return\n speakerMuted.value = !speakerMuted.value\n await session.setSpeakerMuted(speakerMuted.value)\n}\n\n// 是否显示「解散会议」确认弹窗\nconst showDissolveConfirm = ref(false)\nconst dissolvePending = ref(false)\n\nasync function leaveMeeting() {\n const client = getClient()\n if (!client) return\n // 当前主持人才能解散;非主持人(含已转让的前主持人)直接走普通 leave\n if (!isCurrentHost.value) {\n await client.meeting?.leave()\n // 主窗口立即隐藏,独立倒计时 toast 提示后由 toast 触发 close\n endedReason.value = '已离开'\n return\n }\n // host:查房内参与者数量\n // - count > 1:房内还有其他人,弹\"解散会议\"确认(让 host 明确选择 Leave 还是 Dissolve)\n // - count <= 1:只剩 host 自己——直接 dissolve,避免遗留僵尸会议让离线 invitee 上线后收到 stale 邀请\n // 之前的 bug:count=1 时走 leave,只改 host 自己 invite.status=left,meeting.status 仍 active,\n // 其他 invitee 的 status 仍 invited → pending-invites 返回 stale 邀请\n let count = 1\n try {\n count = await client.meeting.getParticipantCount()\n } catch (err) {\n console.warn('[im] 查参与者数量失败,按 1 处理(自动解散)', err)\n }\n if (count > 1) {\n showDissolveConfirm.value = true\n return\n }\n dissolvePending.value = true\n try {\n await client.meeting.dissolve()\n endedReason.value = '会议已解散'\n } catch (err) {\n // dissolve 失败兜底:后端 Leave 内部也会做同样的自动 dissolve 兜底,但前端先失败说明网络层有问题\n console.error('[im] host 单独时解散失败,回退到 leave', err)\n endedReason.value = '已离开'\n await client.meeting?.leave().catch(() => {})\n } finally {\n dissolvePending.value = false\n }\n}\n\n// 确认解散\nasync function confirmDissolve() {\n const client = getClient()\n if (!client) return\n showDissolveConfirm.value = false\n dissolvePending.value = true\n try {\n await client.meeting.dissolve()\n // 解散成功:endedReason 让 ended-overlay 显示;broadcast 到达后 SDK 也会触发 ended 事件\n endedReason.value = '会议已解散'\n } catch (err) {\n console.error('[im] 解散失败', err)\n endedReason.value = '解散失败'\n } finally {\n dissolvePending.value = false\n }\n}\n\nfunction cancelDissolve() {\n showDissolveConfirm.value = false\n}\n\nasync function kickParticipant(uid: string) {\n const session = getSession()\n if (!session) return\n try {\n await session.kick(getClient().transport, uid)\n await refreshMembers(true)\n } catch (err) {\n console.warn('[im] kickParticipant 失败', err)\n }\n}\n\nasync function transferHost(uid: string) {\n const session = getSession()\n if (!session) return\n try {\n await session.transferHost(getClient().transport, uid)\n hostIdentity.value = uid\n await refreshMembers(true)\n } catch (err) {\n console.warn('[im] transferHost 失败', err)\n }\n}\n\n// 主持人再次邀请某成员(等待加入/已离会状态)\nasync function reinviteMember(uid: string) {\n const session = getSession()\n if (!session) return\n try {\n await getClient().meeting.reinvite(session.meetingId, uid)\n await refreshMembers(true)\n } catch (err: unknown) {\n // 后端错误码映射成中文提示,避免静默失败:\n // target_busy=对方正在通话/会议或有待处理邀请;not_host=主持人已转让\n const msg = err instanceof Error ? err.message : ''\n if (msg.includes('对方正忙') || msg.includes('target_busy')) {\n pushToast('对方正忙,稍后再试')\n } else if (msg.includes('无权') || msg.includes('not_host')) {\n pushToast('仅主持人可邀请成员')\n } else if (msg.includes('meeting_ended')) {\n pushToast('会议已结束')\n } else {\n pushToast('再次邀请失败,请重试')\n }\n console.warn('[im] reinvite 失败', err)\n }\n}\n\n// uid → 展示名(内置邀请流程 toast 用;优先 SDK 用户目录,回退 uid 本身)\nasync function nameOf(uid: string): Promise<string> {\n const dir = getClient()?.users\n if (dir?.resolve) {\n const u = await dir.resolve(uid).catch(() => undefined)\n if (u?.name) return u.name\n }\n return uid\n}\n\n// 批量邀请入会:内置默认流程与 #invite-panel 插槽的 invite() 共用\n// 单条失败不阻塞其他;busy/其他失败分类提示\nasync function inviteUids(uids: string[]) {\n const client = getClient()\n const session = getSession()\n if (!client || !session || uids.length === 0) return\n const busyUids: string[] = []\n const failedUids: string[] = []\n await Promise.all(\n uids.map((uid) =>\n client.meeting.reinvite(session.meetingId, uid).catch((e: unknown) => {\n const msg = e instanceof Error ? e.message : ''\n if (msg.includes('对方正忙') || msg.includes('target_busy')) busyUids.push(uid)\n else failedUids.push(uid)\n }),\n ),\n )\n if (busyUids.length > 0) {\n const names = (await Promise.all(busyUids.map(nameOf))).join('、')\n toast.error(`${names} 正忙,未能邀请加入会议`)\n }\n if (failedUids.length > 0) {\n const names = (await Promise.all(failedUids.map(nameOf))).join('、')\n toast.error(`${names} 邀请失败,请重试`)\n }\n refreshMembers(true)\n}\n\n// 内置默认邀请流程:拉当前成员(含已邀请未加入)→ 选人弹窗 → 批量 reinvite\nasync function openDefaultInvite() {\n const client = getClient()\n const session = getSession()\n if (!client || !session) return\n let excludeUids: string[] = []\n try {\n const list = await client.meeting.getMembers(true)\n excludeUids = list.map((m: MeetingMember) => m.identity)\n } catch (err) {\n console.warn('[im] 拉取会议成员失败,邀请弹窗不排除已入会成员', err)\n }\n mountComponent(UserPickerDialog, {\n title: '邀请参会',\n excludeUids,\n emptyText: '所有成员都已在会议中',\n onConfirm: (uids: string[]) => {\n inviteUids(uids)\n },\n })\n}\n\n// 点击「+」添加成员:插槽 > invitePicker > 外部事件监听 > 内置默认弹窗(退让式)\nfunction onAddMemberClick() {\n emit('addMember')\n if (slots['invite-panel']) {\n invitePanelOpen.value = !invitePanelOpen.value\n return\n }\n // 业务方自定义选人:resolve 所选 uid 后走内部统一邀请+刷新流程\n if (props.invitePicker) {\n props\n .invitePicker()\n .then((uids) => {\n if (uids && uids.length > 0) inviteUids(uids)\n })\n .catch(() => {})\n return\n }\n // 外部传了 @add-member 监听 → 仅抛事件,不弹内置选人窗(保持旧边界行为)\n const instance = getCurrentInstance()\n if (instance?.vnode.props?.onAddMember) return\n if (props.disableDefaultInvite) return\n openDefaultInvite()\n}\n\n// 监听 hostChanged 广播:切换主视频\nfunction onHostChangedEvent(e: MeetingHostChangedEvent) {\n const session = getSession()\n if (!session || e.meetingId !== session.meetingId) return\n hostIdentity.value = e.hostUid\n // 主视频元素会被 v-if 重新挂载,nextTick 后 reattach\n nextTick(() => {\n refreshMembers(true)\n })\n}\n\n// 监听解散广播:主持人解散后所有参与人收到,立即隐藏视频窗口,只留独立倒计时提示。\n// 不依赖 session 'ended' 事件(它要等 sess.end() 的 async disconnect 完成才触发,有延迟)。\nfunction onDissolvedEvent(e: MeetingDissolvedEvent) {\n const session = getSession()\n if (!session || e.meetingId !== session.meetingId) return\n if (!endedReason.value) endedReason.value = '会议已解散'\n if (timer) clearInterval(timer)\n}\n\n// 监听被踢广播:立即隐藏视频窗口\nfunction onKickedEvent(e: { meetingId: string; by: string }) {\n const session = getSession()\n if (!session || e.meetingId !== session.meetingId) return\n if (!endedReason.value) endedReason.value = '已被移出会议'\n if (timer) clearInterval(timer)\n}\n\nfunction formatDuration(s: number) {\n const h = Math.floor(s / 3600)\n const m = Math.floor((s % 3600) / 60)\n .toString()\n .padStart(2, '0')\n const sec = (s % 60).toString().padStart(2, '0')\n return h > 0 ? `${h}:${m}:${sec}` : `${m}:${sec}`\n}\n\nonMounted(async () => {\n await syncDeviceCapability()\n const client = getClient()\n if (!client) return\n const session = client.meeting?.currentSession\n if (!session) return\n\n hostIdentity.value = session.hostIdentity || session.identity\n\n session.onSession('connected', () => {\n refreshMembers()\n nextTick(layout)\n timer = setInterval(() => {\n duration.value++\n // 每 10 秒刷一次成员列表(muted/online 实时态)\n // 后端 presence scanner 缓存也是 10s 刷新,更高频率纯浪费;muted 等变化由事件即时触发 force refresh\n if (duration.value % 10 === 0) refreshMembers()\n }, 1000)\n })\n session.onSession('participantJoined', () => {\n refreshMembers(true)\n })\n session.onSession('participantLeft', () => {\n refreshMembers(true)\n })\n session.onSession('participantTrackSubscribed', (info: { kind: string; identity: string }) => {\n if (info.kind !== 'video') return\n // 收到对方的 camera track = 对方摄像头开着:立即置 cameraOn=true,\n // 否则要等 10s 轮询,期间对方 tile 会被「摄像头已关」遮罩误盖\n const m = members.value.find((x) => x.identity === info.identity)\n if (m && !m.cameraOn) {\n m.cameraOn = true\n members.value = [...members.value]\n }\n // 优先用小视频池的元素\n const el = participantVideoEls.get(info.identity)\n if (el) {\n session.attachParticipantVideo(info.identity, el)\n }\n // 全屏宫格池的元素同样补挂载(track.attach 支持同一 track 挂多个元素)\n const gridEl = gridVideoEls.get(info.identity)\n if (gridEl) {\n session.attachParticipantVideo(info.identity, gridEl)\n }\n // 主视频位置的 track 流入:reattach 主视频(mainVideoEl 不在 participantVideoEls 里)\n if (mainVideoIdentity === info.identity) {\n nextTick(reattachAllVideos)\n }\n })\n // 发言态:LiveKit 给全量列表,覆盖式更新 members[].speaking\n // refreshMembers 每 10 秒会用后端 speaking=false 覆盖,下个 activeSpeakersChanged 事件会再次纠正——可接受\n // 就地修改成员字段,仅在状态变化时替换数组引用触发响应式\n session.onSession('activeSpeakersChanged', (info: { identities: string[] }) => {\n const set = new Set(info.identities)\n let changed = false\n for (const m of members.value) {\n const speaking = set.has(m.identity)\n if (m.speaking !== speaking) {\n m.speaking = speaking\n changed = true\n }\n }\n if (changed) members.value = [...members.value]\n })\n // 远端麦克风/摄像头开/关:毫秒级事件就地更新 members[].muted / cameraOn\n // (此前仅靠 10s 轮询后端快照;轮询保留作断线/丢事件后的兜底校正)\n session.onSession(\n 'participantMuteChanged',\n (info: { identity: string; muted: boolean; kind: 'audio' | 'video' }) => {\n const m = members.value.find((x) => x.identity === info.identity)\n if (!m) return\n let changed = false\n if (info.kind === 'audio') {\n if (m.muted !== info.muted) {\n m.muted = info.muted\n changed = true\n }\n } else {\n // video 事件 muted=true 表示摄像头关闭;cameraOn 语义相反\n if (m.cameraOn !== !info.muted) {\n m.cameraOn = !info.muted\n changed = true\n }\n }\n if (changed) members.value = [...members.value]\n },\n )\n session.onSession('devices', (info: { mic: boolean; camera: boolean; errors: string[] }) => {\n // 设备开启失败不在页面弹提示(core 已 console.warn):按钮区置灰 + 状态图标已足够表达\n // 同步设备实际开启状态(enableDevices 可能 mic/camera 开启失败,UI 需据此更新按钮)\n if (!info.mic) micEnabled.value = false\n if (!info.camera) cameraEnabled.value = false\n if (info.camera) nextTick(reattachAllVideos)\n // 立即更新 members 里自己那行:enableDevices 完成 ≠ 轮询周期到,\n // 不主动写会导致自己的 tile「摄像头已关」遮罩误显示最长一个轮询周期(10s)\n const selfRow = members.value.find((m) => m.identity === session.identity)\n if (selfRow) {\n if (selfRow.cameraOn !== info.camera) {\n selfRow.cameraOn = info.camera\n members.value = [...members.value]\n }\n }\n // 开启失败(无权限/被占用)= 不可用置灰;本次开启成功则恢复(如权限后来被授予)\n // errors 条目带 \"mic:\"/\"camera:\" 前缀(core enableDevices 约定),可精确区分设备\n if (info.errors.some((e) => e.startsWith('mic:'))) micUnavailable.value = true\n else if (info.mic) micUnavailable.value = false\n if (info.errors.some((e) => e.startsWith('camera:'))) cameraUnavailable.value = true\n else if (info.camera) cameraUnavailable.value = false\n })\n session.onSession('ended', () => {\n // 解散/被踢/对端断开都会触发——独立倒计时浮窗提示,5s 后自动关或用户手动关\n if (!endedReason.value) endedReason.value = '会议已结束'\n if (timer) clearInterval(timer)\n })\n\n // race 修复:mount 之前可能已 connected\n if (session.isConnected) {\n refreshMembers()\n nextTick(layout)\n timer = setInterval(() => {\n duration.value++\n if (duration.value % 10 === 0) refreshMembers()\n }, 1000)\n }\n\n // 监听 host 转让广播(所有客户端)\n client.on('meeting:hostChanged', onHostChangedEvent)\n // 监听解散/被踢广播:参与人端立即隐藏视频窗口(不等 sess.end 的 async 完成)\n client.on('meeting:dissolved', onDissolvedEvent)\n client.on('meeting:kicked', onKickedEvent)\n\n // 布局双保险:ResizeObserver + window resize\n if (contentEl.value) {\n resizeObserver = new ResizeObserver(() => layout())\n resizeObserver.observe(contentEl.value)\n }\n window.addEventListener('resize', layout)\n nextTick(layout)\n})\n\nonUnmounted(() => {\n if (timer) clearInterval(timer)\n if (actionErrorTimer) clearTimeout(actionErrorTimer)\n if (resizeObserver) {\n resizeObserver.disconnect()\n resizeObserver = null\n }\n window.removeEventListener('resize', layout)\n const client = getClient()\n client?.off?.('meeting:hostChanged', onHostChangedEvent)\n client?.off?.('meeting:dissolved', onDissolvedEvent)\n client?.off?.('meeting:kicked', onKickedEvent)\n})\n</script>\n\n<template>\n <div\n id=\"meetingWindow\"\n v-show=\"!minimized && !endedReason\"\n v-draggable=\"'.meeting-header'\"\n class=\"meeting-window\"\n :style=\"{ height: height || '80vh' }\"\n >\n <MeetingHeader\n :title=\"title\"\n :duration=\"formatDuration(duration)\"\n :grid-mode=\"gridMode\"\n @toggle-grid=\"gridMode = !gridMode\"\n @close=\"leaveMeeting\"\n @minimize=\"minimized = true\"\n />\n\n <div v-if=\"actionError\" class=\"device-warn\">\n {{ actionError }}\n </div>\n\n <div ref=\"contentEl\" class=\"content\">\n <!-- 主视频:当前主持人 -->\n <div ref=\"mainVideoWrap\">\n <MainVideoTile\n v-if=\"mainVideoMember\"\n ref=\"mainVideoRef\"\n :identity=\"mainVideoMember.identity\"\n :name=\"mainVideoMember.name\"\n :muted=\"mainVideoMember.muted\"\n :camera-on=\"mainVideoMember.cameraOn\"\n :camera-unavailable=\"memberCameraUnavailable(mainVideoMember)\"\n :speaking=\"mainVideoMember.speaking\"\n :is-host=\"mainVideoMember.isHost\"\n :is-self=\"mainVideoMember.identity === (getSession()?.identity || '')\"\n :mic-unavailable=\"memberMicUnavailable(mainVideoMember)\"\n :on-video-el-ready=\"onMainVideoReady\"\n />\n </div>\n\n <aside ref=\"sidebarEl\" class=\"sidebar\">\n <div ref=\"tabBarWrap\">\n <SidebarTabs\n v-model:active-tab=\"activeTab\"\n :is-host=\"isCurrentHost\"\n @add-member=\"onAddMemberClick\"\n />\n </div>\n <!-- 视频列表 tab -->\n <VideoListPanel\n v-show=\"activeTab === 'video'\"\n ref=\"videoListPanelRef\"\n :members=\"smallVideoMembers\"\n :self-identity=\"getSession()?.identity || ''\"\n :self-mic-unavailable=\"micUnavailable\"\n :self-camera-unavailable=\"cameraUnavailable\"\n :on-tile-mount=\"onSmallVideoReady\"\n />\n <!-- 成员列表 tab -->\n <MemberListPanel\n v-show=\"activeTab === 'member'\"\n :members=\"sortedMembers\"\n :can-promote=\"isCurrentHost\"\n :can-kick=\"isCurrentHost\"\n :can-reinvite=\"isCurrentHost\"\n :self-identity=\"getSession()?.identity || ''\"\n :self-mic-unavailable=\"micUnavailable\"\n :self-camera-unavailable=\"cameraUnavailable\"\n @transfer-host=\"(uid: string) => transferHost(uid)\"\n @kick=\"(uid: string) => kickParticipant(uid)\"\n @reinvite=\"(uid: string) => reinviteMember(uid)\"\n />\n </aside>\n </div>\n\n <!-- 底部控制按钮区:按钮组抽为 MeetingControlBar(全屏宫格的 header 中央复用同一组件) -->\n <div class=\"bottom-controls\">\n <MeetingControlBar\n :mic-enabled=\"micEnabled\"\n :camera-enabled=\"cameraEnabled\"\n :speaker-muted=\"speakerMuted\"\n :mic-unavailable=\"micUnavailable\"\n :camera-unavailable=\"cameraUnavailable\"\n @toggle-mic=\"toggleMic\"\n @toggle-camera=\"toggleCamera\"\n @toggle-speaker=\"toggleSpeaker\"\n @hangup=\"leaveMeeting\"\n />\n </div>\n </div>\n\n <!-- 全屏宫格模式:覆盖整个视口的 4 列 × N 行宫格。\n tile 固定 16:9,高度按可视高度 1/4 推算(减 3 条 5px 间隔)——\n 4 行时正好垂直铺满,超出 4 行出现纵向滚动条;宫格整体水平居中,左右留白。\n 复用 MeetingHeader(图标自动变为「退出宫格」+ 隐藏最小化)与 SmallVideoTile。\n 会议结束/被踢/解散时随 endedReason 自动退出 -->\n <div v-if=\"gridMode && !endedReason\" class=\"fs-grid-overlay\">\n <MeetingHeader\n :title=\"title\"\n :duration=\"formatDuration(duration)\"\n :grid-mode=\"true\"\n @toggle-grid=\"gridMode = false\"\n @close=\"leaveMeeting\"\n >\n <!-- 控制按钮组:与默认模式底部同一组件,放在 header 水平/垂直居中位 -->\n <template #center>\n <MeetingControlBar\n :mic-enabled=\"micEnabled\"\n :camera-enabled=\"cameraEnabled\"\n :speaker-muted=\"speakerMuted\"\n :mic-unavailable=\"micUnavailable\"\n :camera-unavailable=\"cameraUnavailable\"\n @toggle-mic=\"toggleMic\"\n @toggle-camera=\"toggleCamera\"\n @toggle-speaker=\"toggleSpeaker\"\n @hangup=\"leaveMeeting\"\n />\n </template>\n </MeetingHeader>\n <div class=\"fs-grid-body\">\n <div class=\"fs-grid-scroll\">\n <div class=\"fs-grid\">\n <SmallVideoTile\n v-for=\"m in gridMembers\"\n :key=\"m.identity\"\n :identity=\"m.identity\"\n :name=\"m.name\"\n :muted=\"m.muted\"\n :joined=\"m.joined\"\n :camera-on=\"m.joined && m.cameraOn\"\n :camera-unavailable=\"memberCameraUnavailable(m)\"\n :speaking=\"m.speaking\"\n :is-host=\"m.isHost\"\n :is-self=\"m.identity === (getSession()?.identity || '')\"\n :host-badge-top=\"true\"\n :mic-unavailable=\"memberMicUnavailable(m)\"\n :on-video-el-ready=\"onGridVideoReady\"\n />\n </div>\n </div>\n <!-- 成员列表抽屉:右侧滑入/收起,复用 MemberListPanel(功能/滚动条与默认布局一致);\n 高度 = 窗口高度 - header(撑满 body) -->\n <transition name=\"fs-drawer\">\n <aside v-if=\"gridDrawerOpen\" class=\"fs-drawer\">\n <MemberListPanel\n :members=\"sortedMembers\"\n :can-promote=\"isCurrentHost\"\n :can-kick=\"isCurrentHost\"\n :can-reinvite=\"isCurrentHost\"\n :self-identity=\"getSession()?.identity || ''\"\n :self-mic-unavailable=\"micUnavailable\"\n :self-camera-unavailable=\"cameraUnavailable\"\n @transfer-host=\"(uid: string) => transferHost(uid)\"\n @kick=\"(uid: string) => kickParticipant(uid)\"\n @reinvite=\"(uid: string) => reinviteMember(uid)\"\n />\n </aside>\n </transition>\n <!-- 抽屉开合把手:贴抽屉左缘垂直居中(抽屉收起时贴右缘) -->\n <button\n class=\"fs-drawer-toggle\"\n type=\"button\"\n :aria-label=\"gridDrawerOpen ? '收起成员列表' : '展开成员列表'\"\n :title=\"gridDrawerOpen ? '收起成员列表' : '展开成员列表'\"\n :style=\"{ right: gridDrawerOpen ? '200px' : '0px' }\"\n @click=\"gridDrawerOpen = !gridDrawerOpen\"\n >\n {{ gridDrawerOpen ? '›' : '‹' }}\n </button>\n </div>\n </div>\n\n <!-- 解散会议确认弹窗:独立于主窗口,最小化态下也能显示 -->\n <ConfirmDialog\n v-if=\"showDissolveConfirm\"\n title=\"解散会议\"\n message=\"会议中还有其他成员,确认要解散会议吗?所有成员将被强制离会。\"\n confirm-text=\"解散会议\"\n cancel-text=\"取消\"\n :danger=\"true\"\n @confirm=\"confirmDissolve\"\n @cancel=\"cancelDissolve\"\n />\n\n <!-- 自定义邀请面板:业务方传 #invite-panel 时替代内置选人弹窗。\n 插槽内容渲染在根级(无容器包裹),定位/遮罩完全由插槽内容自定 -->\n <slot\n v-if=\"invitePanelOpen\"\n name=\"invite-panel\"\n :members=\"members\"\n :exclude-uids=\"memberIdentities\"\n :invite=\"inviteUids\"\n :close=\"() => (invitePanelOpen = false)\"\n />\n\n <!-- 会议结束/离开倒计时提示:独立浮窗,不依赖主窗口 -->\n <MeetingEndedToast\n v-if=\"endedReason\"\n :reason=\"endedReason\"\n :duration=\"5\"\n @close=\"emit('close')\"\n />\n\n <!-- 最小化浮条:可拖拽,单行显示发言人/会议名称 + 时长 + 还原/关闭按钮 -->\n <div v-if=\"minimized && !endedReason\" class=\"mini-bar\" v-draggable=\"'.mini-bar-body'\">\n <div class=\"mini-bar-body\">\n <!-- 有人发言时显示声波动效 + 发言人;无人发言显示会议名称(同一位置切换) -->\n <span v-if=\"speakerDisplayText\" class=\"speaking-indicator\">\n <span class=\"wave-bar\"></span>\n <span class=\"wave-bar\"></span>\n <span class=\"wave-bar\"></span>\n </span>\n <span class=\"mini-main-text\" :class=\"{ speaking: !!speakerDisplayText }\">\n {{ speakerDisplayText || title }}\n </span>\n <span class=\"mini-duration\">{{ formatDuration(duration) }}</span>\n </div>\n <div class=\"mini-bar-actions\">\n <button\n class=\"mini-btn\"\n type=\"button\"\n aria-label=\"还原窗口\"\n title=\"还原窗口\"\n v-html=\"MaximizeIcon\"\n @click=\"minimized = false\"\n ></button>\n <button\n class=\"mini-btn mini-close\"\n type=\"button\"\n aria-label=\"离开会议\"\n title=\"离开会议\"\n v-html=\"CloseIcon\"\n @click=\"leaveMeeting\"\n ></button>\n </div>\n </div>\n</template>\n\n<style scoped>\n.meeting-window {\n position: fixed;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n /* height 由 prop 控制(inline style 注入),未传时 fallback 80vh */\n width: var(--mw-w, min(1200px, 95vw));\n background: var(--im-meeting-bg-panel, #0f172a);\n display: flex;\n flex-direction: column;\n overflow: hidden;\n margin: 0 auto;\n padding: 10px;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);\n color: var(--im-meeting-text-primary, #fff);\n z-index: 9999;\n border-radius: 0;\n /* 与 design/video-conference-layout.html :root 变量一一对应 */\n --im-meeting-bg-body: #0b1220;\n --im-meeting-bg-panel: #0f172a;\n --im-meeting-bg-video: #000000;\n --im-meeting-text-primary: #ffffff;\n --im-meeting-text-secondary: rgba(255, 255, 255, 0.85);\n --im-meeting-text-tertiary: rgba(255, 255, 255, 0.55);\n --im-meeting-border: rgba(255, 255, 255, 0.08);\n --im-meeting-scrollbar: rgba(255, 255, 255, 0.15);\n --im-meeting-scrollbar-hover: rgba(255, 255, 255, 0.35);\n --im-meeting-tab-active: #1e293b;\n --im-meeting-tab-inactive: #111827;\n --im-meeting-speaking: #22c55e;\n --im-meeting-online: #22c55e;\n --im-meeting-offline: #6b7280;\n --im-meeting-overlay-gap: 5px;\n}\n.content {\n flex: 1 1 0;\n min-height: 0;\n display: flex;\n flex-direction: row;\n gap: 10px;\n padding: 0;\n overflow: hidden;\n margin-top: 5px;\n}\n.content > div:first-child {\n flex: 0 0 auto;\n}\n.sidebar {\n flex: 0 0 auto;\n min-height: 0;\n display: flex;\n flex-direction: column;\n /* 比会议窗口背景 #0f172a 稍深一点的暗色,让空列表区域不显得空洞 */\n background: #0b1220;\n /* tab-bar 与 list 之间的 8px 间距由 sidebar flex gap 直接管理,\n 不依赖子组件内部 margin(避免 BFC 包含 margin 导致 offsetHeight 多算 8px) */\n gap: 8px;\n overflow: hidden;\n}\n/* 所有子节点撑满 sidebar 宽度:tab-bar / video list / member list 三者左右对齐 */\n.sidebar > * {\n width: 100%;\n}\n.sidebar > div:first-child {\n flex: 0 0 auto;\n}\n/* 设备失败提示 */\n.device-warn {\n position: absolute;\n top: 44px;\n left: 50%;\n transform: translateX(-50%);\n background: rgba(212, 160, 23, 0.92);\n color: var(--im-meeting-text-primary, #fff);\n font-size: 12px;\n padding: 4px 12px;\n border-radius: 0;\n z-index: 6;\n pointer-events: none;\n max-width: 80%;\n text-align: center;\n}\n/* 底部控制按钮区:水平居中、常驻显示、紧凑高度 */\n.bottom-controls {\n flex: 0 0 auto;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 16px;\n padding: 6px 0 2px;\n z-index: 4;\n}\n\n/* ============ 全屏宫格模式 ============ */\n.fs-grid-overlay {\n position: fixed;\n inset: 0;\n z-index: 10000; /* 盖过会议窗口(9999) */\n background: var(--im-meeting-bg-body, #0b1220);\n display: flex;\n flex-direction: column;\n padding: 10px;\n color: var(--im-meeting-text-primary, #fff);\n}\n/* 全屏态的 header 不可拖拽(拖拽指令只绑在主窗口上) */\n.fs-grid-overlay :deep(.meeting-header) {\n cursor: default;\n}\n/* 宫格主体:横向 flex,左侧宫格滚动区 + 右侧成员列表抽屉 */\n.fs-grid-body {\n flex: 1 1 0;\n min-height: 0;\n margin-top: 5px;\n display: flex;\n position: relative;\n}\n.fs-grid-scroll {\n flex: 1 1 0;\n min-width: 0;\n overflow-y: auto;\n overflow-x: hidden;\n /* 容器查询单位:tile 高度按可视高度的 1/4 推算(减 3 条 5px 间隔);\n 窄屏(竖屏)时按容器宽度反推收缩,避免水平溢出 */\n container-type: size;\n scrollbar-width: thin;\n scrollbar-color: var(--im-meeting-scrollbar, rgba(255, 255, 255, 0.15)) transparent;\n}\n.fs-grid-scroll::-webkit-scrollbar {\n width: 8px;\n}\n.fs-grid-scroll::-webkit-scrollbar-thumb {\n background: var(--im-meeting-scrollbar, rgba(255, 255, 255, 0.15));\n}\n.fs-grid-scroll::-webkit-scrollbar-thumb:hover {\n background: var(--im-meeting-scrollbar-hover, rgba(255, 255, 255, 0.35));\n}\n/* 宫格:4 列,tile 16:9,间隔 5px;整体宽度 fit-content + margin auto 实现水平居中留白 */\n.fs-grid {\n --fs-tile-h: min(calc((100cqh - 15px) / 4), calc((100cqw - 15px) * 9 / 64));\n display: grid;\n grid-template-columns: repeat(4, calc(var(--fs-tile-h) * 16 / 9));\n grid-auto-rows: var(--fs-tile-h);\n gap: 5px;\n width: fit-content;\n margin: 0 auto;\n}\n.fs-grid :deep(.small-video) {\n width: 100%;\n height: 100%;\n}\n/* 成员列表抽屉:上层覆盖宫格(absolute 贴右侧,不挤占宫格宽度,tile 尺寸不变);\n 高度撑满 body(= 窗口高 - header),内部滚动由 MemberListPanel 自带(样式一致) */\n.fs-drawer {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n width: 200px;\n display: flex;\n flex-direction: column;\n background: var(--im-meeting-bg-panel, #0f172a);\n border-left: 1px solid var(--im-meeting-border, rgba(255, 255, 255, 0.12));\n padding: 8px;\n overflow: hidden;\n z-index: 4;\n}\n/* 抽屉滑入/滑出动画 */\n.fs-drawer-enter-active,\n.fs-drawer-leave-active {\n transition:\n transform 0.2s ease,\n opacity 0.2s ease;\n}\n.fs-drawer-enter-from,\n.fs-drawer-leave-to {\n transform: translateX(30px);\n opacity: 0;\n}\n/* 抽屉开合把手:贴右缘垂直居中,随抽屉开合平移;箭头指示方向 */\n.fs-drawer-toggle {\n position: absolute;\n top: 50%;\n transform: translateY(-50%);\n width: 18px;\n height: 56px;\n border: 1px solid var(--im-meeting-border, rgba(255, 255, 255, 0.12));\n border-right: none;\n border-radius: 6px 0 0 6px;\n background: var(--im-meeting-bg-panel, #0f172a);\n color: var(--im-meeting-text-secondary, rgba(255, 255, 255, 0.85));\n font-size: 14px;\n line-height: 1;\n cursor: pointer;\n z-index: 5;\n transition: right 0.2s ease;\n}\n.fs-drawer-toggle:hover {\n color: var(--im-meeting-text-primary, #fff);\n background: rgba(255, 255, 255, 0.08);\n}\n\n/* ============ 最小化浮条 ============ */\n.mini-bar {\n position: fixed;\n right: 20px;\n bottom: 20px;\n z-index: 9999;\n display: flex;\n align-items: stretch;\n max-width: 320px;\n min-width: 220px;\n background: var(--im-meeting-bg-panel, #0f172a);\n border: 1px solid var(--im-meeting-border, rgba(255, 255, 255, 0.12));\n border-radius: 0;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);\n color: var(--im-meeting-text-primary, #fff);\n overflow: hidden;\n}\n/* 拖拽区:单行——发言人/会议名称 + 时长 */\n.mini-bar-body {\n flex: 1 1 auto;\n min-width: 0;\n padding: 8px 10px;\n cursor: move;\n display: flex;\n align-items: center;\n gap: 6px;\n}\n/* 主体文本:有人发言时显示发言人(绿色),无人发言显示会议名称(白色) */\n.mini-main-text {\n font-size: 13px;\n font-weight: 500;\n color: var(--im-meeting-text-primary, #fff);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n flex: 1 1 auto;\n min-width: 0;\n}\n.mini-main-text.speaking {\n color: var(--im-meeting-speaking, #22c55e);\n font-weight: 600;\n}\n.mini-duration {\n font-size: 11px;\n color: var(--im-meeting-text-tertiary, rgba(255, 255, 255, 0.55));\n font-variant-numeric: tabular-nums;\n flex: 0 0 auto;\n}\n/* 声波动效:复用 MainVideoTile 的 wave-bar 三柱动效 + 颜色变量 */\n.mini-bar-body .speaking-indicator {\n display: inline-flex;\n align-items: flex-end;\n gap: 2px;\n flex: 0 0 auto;\n}\n.mini-bar-body .wave-bar {\n width: 2px;\n background: var(--im-meeting-speaking, #22c55e);\n animation: mini-wave 1s ease-in-out infinite;\n}\n.mini-bar-body .wave-bar:nth-child(1) {\n height: 6px;\n animation-delay: 0s;\n}\n.mini-bar-body .wave-bar:nth-child(2) {\n height: 10px;\n animation-delay: 0.15s;\n}\n.mini-bar-body .wave-bar:nth-child(3) {\n height: 6px;\n animation-delay: 0.3s;\n}\n@keyframes mini-wave {\n 0%,\n 100% {\n transform: scaleY(1);\n opacity: 0.7;\n }\n 50% {\n transform: scaleY(1.6);\n opacity: 1;\n }\n}\n/* 操作按钮区 */\n.mini-bar-actions {\n flex: 0 0 auto;\n display: flex;\n align-items: center;\n gap: 2px;\n padding: 0 4px;\n border-left: 1px solid var(--im-meeting-border, rgba(255, 255, 255, 0.08));\n}\n.mini-btn {\n width: 26px;\n height: 100%;\n min-height: 36px;\n border: none;\n background: transparent;\n color: var(--im-meeting-text-secondary, rgba(255, 255, 255, 0.85));\n cursor: pointer;\n border-radius: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n transition:\n background 0.2s,\n color 0.2s;\n}\n.mini-btn:hover {\n background: rgba(255, 255, 255, 0.1);\n color: var(--im-meeting-text-primary, #fff);\n}\n.mini-btn :deep(svg) {\n width: 16px;\n height: 16px;\n}\n.mini-btn.mini-close:hover {\n background: #e53935;\n color: #fff;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { onMounted, onBeforeUnmount, ref, computed, watch } from 'vue'\nimport type { MeetingInviteEvent } from '@quitting/contract'\n// 响铃音频必须走 ESM import,Vite 才会处理资源 pipeline,运行时拿到带 hash 的真实 URL\n// 之前用 new Audio(\"./ring-callee.mp3\") 是相对页面 URL,demo public 目录没这个文件 → 404 → NotSupportedError\nimport ringCalleeUrl from '../sounds/ring-callee.mp3'\n\n// 会议邀请弹框:结构 / 样式严格对齐 design/meeting-invite-dialog.html\n// 调用方传 fromName 把 uid 翻译为业务侧用户名;不传则回退显示 invite.from\n\nconst props = defineProps<{\n invite: MeetingInviteEvent\n conflict?: boolean\n // 邀请者展示名:SDK UI 不知道 uid→name 映射,由调用方(如 demo 的 DEMO_USERS)解析后传入\n fromName?: string\n onClose?: () => void\n}>()\n\nconst emit = defineEmits<{\n (e: 'close'): void\n (e: 'accepted', session: unknown): void\n}>()\n\n// 响铃控制:默认循环播放,可点击「关闭响铃」静音\nconst ringMuted = ref(false)\nlet audioEl: HTMLAudioElement | null = null\n\n// 接受/拒绝进行中:响应期间禁用全部按钮并显示「加入中…」,\n// 避免 respond 空窗期(无摄像头/麦克风时可达数秒)重复点击导致重复请求报错\nconst responding = ref(false)\n\n// 顶部标题:根据 variant 描述邀请者的动作\n// 邀请者展示名:外部传 fromName 优先;未传时查 SDK 用户目录(client.users),\n// 仍无则回退 invite.from——业务方零配置也能显示姓名\nconst fallbackName = ref('')\nwatch(\n () => props.invite.from,\n async (from) => {\n fallbackName.value = ''\n if (props.fromName || !from) return\n const dir = getClient()?.users\n if (!dir?.resolve) return\n const u = await dir.resolve(from).catch(() => undefined)\n if (u?.name) fallbackName.value = u.name\n },\n { immediate: true },\n)\nconst inviterName = computed(() => props.fromName || fallbackName.value || props.invite.from)\nconst titleText = computed(() => {\n const name = inviterName.value\n const variant = props.invite.variant\n if (variant === 'rejoin') return '重新加入会议'\n if (variant === 'reinvite') return `${name} 再次邀请您入会`\n return `${name}邀请您入会`\n})\n\nfunction startRing() {\n if (audioEl) {\n audioEl.muted = ringMuted.value\n audioEl.currentTime = 0\n audioEl.play().catch((err) => {\n console.warn('[im] 邀请响铃播放失败(可能是 autoplay 策略)', err)\n })\n }\n}\n\nfunction toggleMute() {\n ringMuted.value = !ringMuted.value\n if (audioEl) {\n audioEl.muted = ringMuted.value\n if (!ringMuted.value) {\n audioEl.play().catch(() => {})\n }\n }\n}\n\nfunction getClient() {\n return (window as any).__imClient\n}\n\n// 把后端错误 message 映射成中文提示;解析失败回退通用文案\n// 后端错误关键字参考 server/internal/signaling/meeting.go 各处 errors.New\nfunction friendlyError(err: any): string {\n const msg = String(err?.message || err || '')\n if (msg.includes('meeting_ended') || msg.includes('meeting_not_found')) {\n return '会议已结束'\n }\n if (msg.includes('invite_status_invalid')) {\n return '邀请已失效'\n }\n if (msg.includes('not_invited')) {\n return '您未被邀请到该会议'\n }\n if (msg.includes('kicked')) {\n return '您已被移出该会议'\n }\n // 规则1:多端并发进入,本端是后进端,已被服务端拒绝\n if (msg.includes('conflict') || msg.includes('entered elsewhere')) {\n return '已在其他设备进入,本端邀请已失效'\n }\n // 规则2:已有待处理邀请或已在会话中\n if (msg.includes('busy') || msg.includes('pending')) {\n return '对方正忙,请稍后再试'\n }\n return '网络异常,请稍后重试'\n}\n\nasync function accept() {\n if (responding.value) return\n const client = getClient()\n if (!client) return\n responding.value = true\n if (props.conflict) {\n if (client.call?.currentSession) {\n await client.call.end('user_left').catch(() => {})\n } else if (client.meeting?.currentSession) {\n await client.meeting.leave().catch(() => {})\n }\n }\n try {\n const session = await client.meeting.respond(\n props.invite.meetingId,\n 'accept',\n props.invite.title,\n )\n if (session) {\n emit('accepted', session)\n }\n emit('close')\n } catch (err: any) {\n console.error('[im] 会议接听失败', err)\n // 用户场景:邀请弹窗未及时收到 dissolved 事件(如断线期间),\n // 点击\"加入\"时后端拒绝——必须给明确提示,不能静默关闭\n alert(friendlyError(err))\n emit('close')\n }\n}\n\nasync function reject() {\n if (responding.value) return\n responding.value = true\n const client = getClient()\n // reject 在后端等价于主动离会(status=left),与离会行为一致\n // 失败仅记录日志,不阻塞关闭弹窗(reject 失败不影响他人)\n try {\n await client?.meeting?.respond(props.invite.meetingId, 'reject')\n } catch (err: any) {\n console.warn('[im] 会议拒绝失败', err)\n }\n emit('close')\n}\n\nonMounted(() => {\n // 响铃:复用 ring-callee.mp3(被叫响铃,语义匹配)\n audioEl = new Audio(ringCalleeUrl)\n audioEl.loop = true\n audioEl.preload = 'auto'\n startRing()\n})\n\nonBeforeUnmount(() => {\n if (audioEl) {\n audioEl.pause()\n audioEl = null\n }\n})\n</script>\n\n<template>\n <div class=\"invite-dialog\">\n <div class=\"dialog-title\">{{ titleText }}</div>\n <div class=\"meeting-info\">\n <span class=\"info-label\">会议名:</span>\n <span class=\"info-value\">{{ invite.title }}</span>\n </div>\n <div class=\"dialog-actions\">\n <button class=\"btn btn-ghost\" type=\"button\" :disabled=\"responding\" @click=\"toggleMute\">\n {{ ringMuted ? '恢复响铃' : '关闭响铃' }}\n </button>\n <button class=\"btn btn-secondary\" type=\"button\" :disabled=\"responding\" @click=\"reject\">\n 拒绝\n </button>\n <button class=\"btn btn-primary\" type=\"button\" :disabled=\"responding\" @click=\"accept\">\n {{ responding ? '加入中…' : conflict ? '切换' : '加入' }}\n </button>\n </div>\n </div>\n</template>\n\n<style scoped>\n/* 颜色变量:同时兼容深浅背景 */\n.invite-dialog {\n --dialog-bg: rgba(255, 255, 255, 0.92);\n --dialog-bg-dark: rgba(18, 22, 32, 0.92);\n --text-primary: rgba(0, 0, 0, 0.88);\n --text-secondary: rgba(0, 0, 0, 0.55);\n --text-primary-dark: rgba(255, 255, 255, 0.92);\n --text-secondary-dark: rgba(255, 255, 255, 0.55);\n --border: rgba(0, 0, 0, 0.08);\n --border-dark: rgba(255, 255, 255, 0.1);\n --accent: #10b981;\n --accent-hover: #059669;\n --danger: #ef4444;\n --danger-hover: #dc2626;\n\n width: 260px;\n padding: 14px;\n /* 玻璃拟态:背景模糊 + 半透明白色 */\n background: var(--dialog-bg);\n backdrop-filter: blur(20px) saturate(180%);\n -webkit-backdrop-filter: blur(20px) saturate(180%);\n border: 1px solid rgba(255, 255, 255, 0.5);\n border-radius: 16px;\n box-shadow:\n 0 1px 2px rgba(0, 0, 0, 0.02),\n 0 4px 8px rgba(0, 0, 0, 0.04),\n 0 8px 16px rgba(0, 0, 0, 0.06),\n 0 16px 32px rgba(0, 0, 0, 0.08);\n color: var(--text-primary);\n position: relative;\n overflow: hidden;\n}\n\n/* 顶部柔和光条 */\n.invite-dialog::before {\n content: '';\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n height: 1px;\n background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.6), transparent);\n pointer-events: none;\n}\n\n/* 深色背景适配:用 .dark 类切换 */\n.invite-dialog.dark {\n background: var(--dialog-bg-dark);\n border-color: rgba(255, 255, 255, 0.12);\n color: var(--text-primary-dark);\n}\n.invite-dialog.dark .info-label {\n color: var(--text-secondary-dark);\n}\n.invite-dialog.dark .info-value {\n color: var(--text-primary-dark);\n}\n.invite-dialog.dark .btn-secondary {\n border-color: var(--border-dark);\n color: rgba(255, 255, 255, 0.8);\n}\n.invite-dialog.dark .btn-ghost {\n color: rgba(255, 255, 255, 0.5);\n}\n.invite-dialog.dark .btn-secondary:hover {\n background: rgba(255, 255, 255, 0.08);\n border-color: rgba(255, 255, 255, 0.2);\n}\n.invite-dialog.dark .btn-secondary.danger {\n border-color: rgba(239, 68, 68, 0.5);\n color: rgba(255, 255, 255, 0.9);\n}\n.invite-dialog.dark .btn-secondary.danger:hover {\n background: rgba(239, 68, 68, 0.15);\n border-color: rgba(239, 68, 68, 0.7);\n}\n\n.dialog-title {\n font-size: 13px;\n font-weight: 600;\n line-height: 1.4;\n margin-bottom: 8px;\n}\n\n.meeting-info {\n display: flex;\n align-items: baseline;\n gap: 4px;\n margin-bottom: 12px;\n font-size: 12px;\n}\n\n.info-label {\n color: var(--text-secondary);\n white-space: nowrap;\n}\n\n.info-value {\n color: var(--text-primary);\n font-weight: 600;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.dialog-actions {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 6px;\n}\n\n.btn {\n height: 26px;\n padding: 0 10px;\n border: 1px solid transparent;\n background: transparent;\n color: var(--text-primary);\n font-size: 12px;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.15s ease;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 8px;\n}\n\n.btn:hover {\n color: var(--text-primary);\n}\n\n.btn-primary {\n background: var(--accent);\n color: #fff;\n box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3);\n}\n.btn-primary:hover {\n background: var(--accent-hover);\n box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);\n transform: translateY(-1px);\n}\n\n.btn-secondary {\n border-color: var(--border);\n color: var(--text-primary);\n}\n.btn-secondary:hover {\n background: rgba(0, 0, 0, 0.04);\n border-color: rgba(0, 0, 0, 0.12);\n}\n\n.btn-ghost {\n padding: 0 6px;\n color: var(--text-secondary);\n}\n.btn-ghost:hover {\n color: var(--text-primary);\n background: rgba(0, 0, 0, 0.04);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { onMounted, onBeforeUnmount, ref, computed, watch } from 'vue'\nimport type { MeetingInviteEvent } from '@quitting/contract'\n// 响铃音频必须走 ESM import,Vite 才会处理资源 pipeline,运行时拿到带 hash 的真实 URL\n// 之前用 new Audio(\"./ring-callee.mp3\") 是相对页面 URL,demo public 目录没这个文件 → 404 → NotSupportedError\nimport ringCalleeUrl from '../sounds/ring-callee.mp3'\n\n// 会议邀请弹框:结构 / 样式严格对齐 design/meeting-invite-dialog.html\n// 调用方传 fromName 把 uid 翻译为业务侧用户名;不传则回退显示 invite.from\n\nconst props = defineProps<{\n invite: MeetingInviteEvent\n conflict?: boolean\n // 邀请者展示名:SDK UI 不知道 uid→name 映射,由调用方(如 demo 的 DEMO_USERS)解析后传入\n fromName?: string\n onClose?: () => void\n}>()\n\nconst emit = defineEmits<{\n (e: 'close'): void\n (e: 'accepted', session: unknown): void\n}>()\n\n// 响铃控制:默认循环播放,可点击「关闭响铃」静音\nconst ringMuted = ref(false)\nlet audioEl: HTMLAudioElement | null = null\n\n// 接受/拒绝进行中:响应期间禁用全部按钮并显示「加入中…」,\n// 避免 respond 空窗期(无摄像头/麦克风时可达数秒)重复点击导致重复请求报错\nconst responding = ref(false)\n\n// 顶部标题:根据 variant 描述邀请者的动作\n// 邀请者展示名:外部传 fromName 优先;未传时查 SDK 用户目录(client.users),\n// 仍无则回退 invite.from——业务方零配置也能显示姓名\nconst fallbackName = ref('')\nwatch(\n () => props.invite.from,\n async (from) => {\n fallbackName.value = ''\n if (props.fromName || !from) return\n const dir = getClient()?.users\n if (!dir?.resolve) return\n const u = await dir.resolve(from).catch(() => undefined)\n if (u?.name) fallbackName.value = u.name\n },\n { immediate: true },\n)\nconst inviterName = computed(() => props.fromName || fallbackName.value || props.invite.from)\nconst titleText = computed(() => {\n const name = inviterName.value\n const variant = props.invite.variant\n if (variant === 'rejoin') return '重新加入会议'\n if (variant === 'reinvite') return `${name} 再次邀请您入会`\n return `${name}邀请您入会`\n})\n\nfunction startRing() {\n if (audioEl) {\n audioEl.muted = ringMuted.value\n audioEl.currentTime = 0\n audioEl.play().catch((err) => {\n console.warn('[im] 邀请响铃播放失败(可能是 autoplay 策略)', err)\n })\n }\n}\n\nfunction toggleMute() {\n ringMuted.value = !ringMuted.value\n if (audioEl) {\n audioEl.muted = ringMuted.value\n if (!ringMuted.value) {\n audioEl.play().catch(() => {})\n }\n }\n}\n\nfunction getClient() {\n return (window as any).__imClient\n}\n\n// 把后端错误 message 映射成中文提示;解析失败回退通用文案\n// 后端错误关键字参考 server/internal/signaling/meeting.go 各处 errors.New\nfunction friendlyError(err: any): string {\n const msg = String(err?.message || err || '')\n if (msg.includes('meeting_ended') || msg.includes('meeting_not_found')) {\n return '会议已结束'\n }\n if (msg.includes('invite_status_invalid')) {\n return '邀请已失效'\n }\n if (msg.includes('not_invited')) {\n return '您未被邀请到该会议'\n }\n if (msg.includes('kicked')) {\n return '您已被移出该会议'\n }\n // 规则1:多端并发进入,本端是后进端,已被服务端拒绝\n if (msg.includes('conflict') || msg.includes('entered elsewhere')) {\n return '已在其他设备进入,本端邀请已失效'\n }\n // 规则2:已有待处理邀请或已在会话中\n if (msg.includes('busy') || msg.includes('pending')) {\n return '对方正忙,请稍后再试'\n }\n return '网络异常,请稍后重试'\n}\n\nasync function accept() {\n if (responding.value) return\n const client = getClient()\n if (!client) return\n responding.value = true\n if (props.conflict) {\n if (client.call?.currentSession) {\n await client.call.end('user_left').catch(() => {})\n } else if (client.meeting?.currentSession) {\n await client.meeting.leave().catch(() => {})\n }\n }\n try {\n const session = await client.meeting.respond(\n props.invite.meetingId,\n 'accept',\n props.invite.title,\n )\n if (session) {\n emit('accepted', session)\n }\n emit('close')\n } catch (err: any) {\n console.error('[im] 会议接听失败', err)\n // 用户场景:邀请弹窗未及时收到 dissolved 事件(如断线期间),\n // 点击\"加入\"时后端拒绝——必须给明确提示,不能静默关闭\n alert(friendlyError(err))\n emit('close')\n }\n}\n\nasync function reject() {\n if (responding.value) return\n responding.value = true\n const client = getClient()\n // reject 在后端等价于主动离会(status=left),与离会行为一致\n // 失败仅记录日志,不阻塞关闭弹窗(reject 失败不影响他人)\n try {\n await client?.meeting?.respond(props.invite.meetingId, 'reject')\n } catch (err: any) {\n console.warn('[im] 会议拒绝失败', err)\n }\n emit('close')\n}\n\nonMounted(() => {\n // 响铃:复用 ring-callee.mp3(被叫响铃,语义匹配)\n audioEl = new Audio(ringCalleeUrl)\n audioEl.loop = true\n audioEl.preload = 'auto'\n startRing()\n})\n\nonBeforeUnmount(() => {\n if (audioEl) {\n audioEl.pause()\n audioEl = null\n }\n})\n</script>\n\n<template>\n <div class=\"invite-dialog\">\n <div class=\"dialog-title\">{{ titleText }}</div>\n <div class=\"meeting-info\">\n <span class=\"info-label\">会议名:</span>\n <span class=\"info-value\">{{ invite.title }}</span>\n </div>\n <div class=\"dialog-actions\">\n <button class=\"btn btn-ghost\" type=\"button\" :disabled=\"responding\" @click=\"toggleMute\">\n {{ ringMuted ? '恢复响铃' : '关闭响铃' }}\n </button>\n <button class=\"btn btn-secondary\" type=\"button\" :disabled=\"responding\" @click=\"reject\">\n 拒绝\n </button>\n <button class=\"btn btn-primary\" type=\"button\" :disabled=\"responding\" @click=\"accept\">\n {{ responding ? '加入中…' : conflict ? '切换' : '加入' }}\n </button>\n </div>\n </div>\n</template>\n\n<style scoped>\n/* 颜色变量:同时兼容深浅背景 */\n.invite-dialog {\n --dialog-bg: rgba(255, 255, 255, 0.92);\n --dialog-bg-dark: rgba(18, 22, 32, 0.92);\n --text-primary: rgba(0, 0, 0, 0.88);\n --text-secondary: rgba(0, 0, 0, 0.55);\n --text-primary-dark: rgba(255, 255, 255, 0.92);\n --text-secondary-dark: rgba(255, 255, 255, 0.55);\n --border: rgba(0, 0, 0, 0.08);\n --border-dark: rgba(255, 255, 255, 0.1);\n --accent: #10b981;\n --accent-hover: #059669;\n --danger: #ef4444;\n --danger-hover: #dc2626;\n\n width: 260px;\n padding: 14px;\n /* 玻璃拟态:背景模糊 + 半透明白色 */\n background: var(--dialog-bg);\n backdrop-filter: blur(20px) saturate(180%);\n -webkit-backdrop-filter: blur(20px) saturate(180%);\n border: 1px solid rgba(255, 255, 255, 0.5);\n border-radius: 16px;\n box-shadow:\n 0 1px 2px rgba(0, 0, 0, 0.02),\n 0 4px 8px rgba(0, 0, 0, 0.04),\n 0 8px 16px rgba(0, 0, 0, 0.06),\n 0 16px 32px rgba(0, 0, 0, 0.08);\n color: var(--text-primary);\n position: relative;\n overflow: hidden;\n}\n\n/* 顶部柔和光条 */\n.invite-dialog::before {\n content: '';\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n height: 1px;\n background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.6), transparent);\n pointer-events: none;\n}\n\n/* 深色背景适配:用 .dark 类切换 */\n.invite-dialog.dark {\n background: var(--dialog-bg-dark);\n border-color: rgba(255, 255, 255, 0.12);\n color: var(--text-primary-dark);\n}\n.invite-dialog.dark .info-label {\n color: var(--text-secondary-dark);\n}\n.invite-dialog.dark .info-value {\n color: var(--text-primary-dark);\n}\n.invite-dialog.dark .btn-secondary {\n border-color: var(--border-dark);\n color: rgba(255, 255, 255, 0.8);\n}\n.invite-dialog.dark .btn-ghost {\n color: rgba(255, 255, 255, 0.5);\n}\n.invite-dialog.dark .btn-secondary:hover {\n background: rgba(255, 255, 255, 0.08);\n border-color: rgba(255, 255, 255, 0.2);\n}\n.invite-dialog.dark .btn-secondary.danger {\n border-color: rgba(239, 68, 68, 0.5);\n color: rgba(255, 255, 255, 0.9);\n}\n.invite-dialog.dark .btn-secondary.danger:hover {\n background: rgba(239, 68, 68, 0.15);\n border-color: rgba(239, 68, 68, 0.7);\n}\n\n.dialog-title {\n font-size: 13px;\n font-weight: 600;\n line-height: 1.4;\n margin-bottom: 8px;\n}\n\n.meeting-info {\n display: flex;\n align-items: baseline;\n gap: 4px;\n margin-bottom: 12px;\n font-size: 12px;\n}\n\n.info-label {\n color: var(--text-secondary);\n white-space: nowrap;\n}\n\n.info-value {\n color: var(--text-primary);\n font-weight: 600;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.dialog-actions {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 6px;\n}\n\n.btn {\n height: 26px;\n padding: 0 10px;\n border: 1px solid transparent;\n background: transparent;\n color: var(--text-primary);\n font-size: 12px;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.15s ease;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 8px;\n}\n\n.btn:hover {\n color: var(--text-primary);\n}\n\n.btn-primary {\n background: var(--accent);\n color: #fff;\n box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3);\n}\n.btn-primary:hover {\n background: var(--accent-hover);\n box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);\n transform: translateY(-1px);\n}\n\n.btn-secondary {\n border-color: var(--border);\n color: var(--text-primary);\n}\n.btn-secondary:hover {\n background: rgba(0, 0, 0, 0.04);\n border-color: rgba(0, 0, 0, 0.12);\n}\n\n.btn-ghost {\n padding: 0 6px;\n color: var(--text-secondary);\n}\n.btn-ghost:hover {\n color: var(--text-primary);\n background: rgba(0, 0, 0, 0.04);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { ref, computed, onMounted } from 'vue'\nimport type { ImClient } from '@quitting/core'\nimport { getImClient } from '../client-access'\nimport UserPickerList from './UserPickerList.vue'\nimport type { PickerUser } from '../types'\n\n// 发起会议弹窗:会议名输入 + UserPickerList + 底部按钮\n// 纯 UI 组件——confirm 把 (uids, title) 抛给调用方(global-methods 的 ImUI.meeting 接管建会)\n// 数据源默认 client.users.list();业务方可传 loader 完全自定义\nconst props = defineProps<{\n myUid?: string\n loader?: () => Promise<PickerUser[]>\n defaultTitle?: string\n onClose?: () => void\n}>()\n\nconst emit = defineEmits<{\n (e: 'confirm', selectedUids: string[], title: string): void\n (e: 'cancel'): void\n (e: 'close'): void\n}>()\n\nconst slots = defineSlots<{\n 'user-item'?(scope: { user: PickerUser; selected: boolean; toggle: () => void }): unknown\n}>()\n\nconst title = ref(props.defaultTitle || '')\nconst users = ref<PickerUser[]>([])\nconst selected = ref<string[]>([])\nconst loading = ref(true)\n\nconst trimmedTitle = computed(() => title.value.trim())\nconst canConfirm = computed(() => trimmedTitle.value.length > 0 && selected.value.length > 0)\n\nfunction getClient(): ImClient | undefined {\n return getImClient()\n}\n\nasync function loadDefault(): Promise<PickerUser[]> {\n const client = getClient()\n if (!client?.users) return []\n const list = await client.users.list()\n // 自己不在候选里:resolve 支持内部 uid 反查(client.uid 是 UUID)\n const me = await client.users.resolve(client.uid).catch(() => undefined)\n const exclude = new Set<string>()\n if (me) exclude.add(me.externalId)\n if (props.myUid) exclude.add(props.myUid)\n return list\n .filter((u) => !exclude.has(u.externalId))\n .map((u) => ({ uid: u.externalId, name: u.name, dept: u.deptName || undefined }))\n}\n\nonMounted(async () => {\n try {\n users.value = props.loader ? await props.loader() : await loadDefault()\n } catch (err) {\n console.warn('[im] 选人列表加载失败', err)\n users.value = []\n } finally {\n loading.value = false\n }\n})\n\nfunction confirm() {\n if (!canConfirm.value) return\n // 标题不以「会议」结尾则补全(保持与 demo 旧行为一致)\n const t = trimmedTitle.value\n emit('confirm', [...selected.value], t.endsWith('会议') ? t : `${t}会议`)\n emit('close')\n}\n\nfunction cancel() {\n emit('cancel')\n emit('close')\n}\n</script>\n\n<template>\n <div class=\"im-create-mask\" @click.self=\"cancel\">\n <div class=\"im-create-dialog\">\n <div class=\"im-create-header\">\n <h2 class=\"im-create-title\">发起会议</h2>\n <button class=\"im-create-close\" type=\"button\" aria-label=\"关闭\" @click=\"cancel\">✕</button>\n </div>\n\n <div class=\"im-create-row\">\n <label class=\"im-create-label\">会议名称</label>\n <input\n v-model=\"title\"\n class=\"im-create-input\"\n type=\"text\"\n placeholder=\"请输入会议名称\"\n maxlength=\"40\"\n />\n </div>\n\n <div class=\"im-create-row im-create-row-list\">\n <UserPickerList\n v-model:selected=\"selected\"\n :users=\"users\"\n :loading=\"loading\"\n empty-text=\"暂无其他用户\"\n >\n <template v-if=\"slots['user-item']\" #user-item=\"scope\">\n <slot name=\"user-item\" v-bind=\"scope\" />\n </template>\n </UserPickerList>\n </div>\n\n <div class=\"im-create-footer\">\n <button class=\"im-create-btn cancel\" type=\"button\" @click=\"cancel\">取消</button>\n <button\n class=\"im-create-btn confirm\"\n type=\"button\"\n :disabled=\"!canConfirm\"\n @click=\"confirm\"\n >\n 发起会议\n </button>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.im-create-mask {\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.45);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 100001;\n}\n.im-create-dialog {\n /* 主题变量(与 UserPickerList 一致;:root 覆盖一处即可全局生效) */\n --im-picker-primary: #667eea;\n --im-picker-primary-hover: #5568d3;\n\n width: 480px;\n max-height: 80vh;\n background: #fff;\n border-radius: 12px;\n box-shadow: 0 16px 48px rgba(0, 0, 0, 0.2);\n display: flex;\n flex-direction: column;\n overflow: hidden;\n}\n.im-create-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 16px 20px;\n border-bottom: 1px solid #f0f0f0;\n}\n.im-create-title {\n margin: 0;\n font-size: 16px;\n font-weight: 600;\n color: #1a1a1a;\n}\n.im-create-close {\n width: 28px;\n height: 28px;\n border: none;\n background: transparent;\n color: #999;\n font-size: 16px;\n cursor: pointer;\n border-radius: 4px;\n}\n.im-create-close:hover {\n background: #f5f7fa;\n color: #1a1a1a;\n}\n.im-create-row {\n padding: 14px 20px 0;\n}\n.im-create-label {\n display: block;\n font-size: 12px;\n color: #666;\n margin-bottom: 8px;\n}\n.im-create-input {\n width: 100%;\n padding: 8px 12px;\n border: 1px solid #ddd;\n border-radius: 6px;\n font-size: 14px;\n outline: none;\n transition: border-color 0.2s;\n}\n.im-create-input:focus {\n border-color: var(--im-picker-primary);\n}\n.im-create-row-list {\n flex: 1;\n min-height: 0;\n display: flex;\n flex-direction: column;\n padding-bottom: 14px;\n}\n.im-create-row-list .im-picker-list {\n flex: 1;\n min-height: 0;\n}\n.im-create-row-list :deep(.im-picker-items) {\n max-height: 240px;\n}\n.im-create-footer {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n padding: 14px 20px;\n border-top: 1px solid #f0f0f0;\n}\n.im-create-btn {\n padding: 8px 18px;\n font-size: 13px;\n border-radius: 6px;\n cursor: pointer;\n transition: all 0.2s;\n border: none;\n}\n.im-create-btn.cancel {\n background: #f5f7fa;\n color: #666;\n}\n.im-create-btn.cancel:hover {\n background: #eef0f4;\n}\n.im-create-btn.confirm {\n background: var(--im-picker-primary);\n color: #fff;\n}\n.im-create-btn.confirm:hover:not(:disabled) {\n background: var(--im-picker-primary-hover);\n}\n.im-create-btn.confirm:disabled {\n background: #bbb;\n cursor: not-allowed;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { ref, computed, onMounted } from 'vue'\nimport type { ImClient } from '@quitting/core'\nimport { getImClient } from '../client-access'\nimport UserPickerList from './UserPickerList.vue'\nimport type { PickerUser } from '../types'\n\n// 发起会议弹窗:会议名输入 + UserPickerList + 底部按钮\n// 纯 UI 组件——confirm 把 (uids, title) 抛给调用方(global-methods 的 ImUI.meeting 接管建会)\n// 数据源默认 client.users.list();业务方可传 loader 完全自定义\nconst props = defineProps<{\n myUid?: string\n loader?: () => Promise<PickerUser[]>\n defaultTitle?: string\n onClose?: () => void\n}>()\n\nconst emit = defineEmits<{\n (e: 'confirm', selectedUids: string[], title: string): void\n (e: 'cancel'): void\n (e: 'close'): void\n}>()\n\nconst slots = defineSlots<{\n 'user-item'?(scope: { user: PickerUser; selected: boolean; toggle: () => void }): unknown\n}>()\n\nconst title = ref(props.defaultTitle || '')\nconst users = ref<PickerUser[]>([])\nconst selected = ref<string[]>([])\nconst loading = ref(true)\n\nconst trimmedTitle = computed(() => title.value.trim())\nconst canConfirm = computed(() => trimmedTitle.value.length > 0 && selected.value.length > 0)\n\nfunction getClient(): ImClient | undefined {\n return getImClient()\n}\n\nasync function loadDefault(): Promise<PickerUser[]> {\n const client = getClient()\n if (!client?.users) return []\n const list = await client.users.list()\n // 自己不在候选里:resolve 支持内部 uid 反查(client.uid 是 UUID)\n const me = await client.users.resolve(client.uid).catch(() => undefined)\n const exclude = new Set<string>()\n if (me) exclude.add(me.externalId)\n if (props.myUid) exclude.add(props.myUid)\n return list\n .filter((u) => !exclude.has(u.externalId))\n .map((u) => ({ uid: u.externalId, name: u.name, dept: u.deptName || undefined }))\n}\n\nonMounted(async () => {\n try {\n users.value = props.loader ? await props.loader() : await loadDefault()\n } catch (err) {\n console.warn('[im] 选人列表加载失败', err)\n users.value = []\n } finally {\n loading.value = false\n }\n})\n\nfunction confirm() {\n if (!canConfirm.value) return\n // 标题不以「会议」结尾则补全(保持与 demo 旧行为一致)\n const t = trimmedTitle.value\n emit('confirm', [...selected.value], t.endsWith('会议') ? t : `${t}会议`)\n emit('close')\n}\n\nfunction cancel() {\n emit('cancel')\n emit('close')\n}\n</script>\n\n<template>\n <div class=\"im-create-mask\" @click.self=\"cancel\">\n <div class=\"im-create-dialog\">\n <div class=\"im-create-header\">\n <h2 class=\"im-create-title\">发起会议</h2>\n <button class=\"im-create-close\" type=\"button\" aria-label=\"关闭\" @click=\"cancel\">✕</button>\n </div>\n\n <div class=\"im-create-row\">\n <label class=\"im-create-label\">会议名称</label>\n <input\n v-model=\"title\"\n class=\"im-create-input\"\n type=\"text\"\n placeholder=\"请输入会议名称\"\n maxlength=\"40\"\n />\n </div>\n\n <div class=\"im-create-row im-create-row-list\">\n <UserPickerList\n v-model:selected=\"selected\"\n :users=\"users\"\n :loading=\"loading\"\n empty-text=\"暂无其他用户\"\n >\n <template v-if=\"slots['user-item']\" #user-item=\"scope\">\n <slot name=\"user-item\" v-bind=\"scope\" />\n </template>\n </UserPickerList>\n </div>\n\n <div class=\"im-create-footer\">\n <button class=\"im-create-btn cancel\" type=\"button\" @click=\"cancel\">取消</button>\n <button\n class=\"im-create-btn confirm\"\n type=\"button\"\n :disabled=\"!canConfirm\"\n @click=\"confirm\"\n >\n 发起会议\n </button>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.im-create-mask {\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.45);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 100001;\n}\n.im-create-dialog {\n /* 主题变量(与 UserPickerList 一致;:root 覆盖一处即可全局生效) */\n --im-picker-primary: #667eea;\n --im-picker-primary-hover: #5568d3;\n\n width: 480px;\n max-height: 80vh;\n background: #fff;\n border-radius: 12px;\n box-shadow: 0 16px 48px rgba(0, 0, 0, 0.2);\n display: flex;\n flex-direction: column;\n overflow: hidden;\n}\n.im-create-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 16px 20px;\n border-bottom: 1px solid #f0f0f0;\n}\n.im-create-title {\n margin: 0;\n font-size: 16px;\n font-weight: 600;\n color: #1a1a1a;\n}\n.im-create-close {\n width: 28px;\n height: 28px;\n border: none;\n background: transparent;\n color: #999;\n font-size: 16px;\n cursor: pointer;\n border-radius: 4px;\n}\n.im-create-close:hover {\n background: #f5f7fa;\n color: #1a1a1a;\n}\n.im-create-row {\n padding: 14px 20px 0;\n}\n.im-create-label {\n display: block;\n font-size: 12px;\n color: #666;\n margin-bottom: 8px;\n}\n.im-create-input {\n width: 100%;\n padding: 8px 12px;\n border: 1px solid #ddd;\n border-radius: 6px;\n font-size: 14px;\n outline: none;\n transition: border-color 0.2s;\n}\n.im-create-input:focus {\n border-color: var(--im-picker-primary);\n}\n.im-create-row-list {\n flex: 1;\n min-height: 0;\n display: flex;\n flex-direction: column;\n padding-bottom: 14px;\n}\n.im-create-row-list .im-picker-list {\n flex: 1;\n min-height: 0;\n}\n.im-create-row-list :deep(.im-picker-items) {\n max-height: 240px;\n}\n.im-create-footer {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n padding: 14px 20px;\n border-top: 1px solid #f0f0f0;\n}\n.im-create-btn {\n padding: 8px 18px;\n font-size: 13px;\n border-radius: 6px;\n cursor: pointer;\n transition: all 0.2s;\n border: none;\n}\n.im-create-btn.cancel {\n background: #f5f7fa;\n color: #666;\n}\n.im-create-btn.cancel:hover {\n background: #eef0f4;\n}\n.im-create-btn.confirm {\n background: var(--im-picker-primary);\n color: #fff;\n}\n.im-create-btn.confirm:hover:not(:disabled) {\n background: var(--im-picker-primary-hover);\n}\n.im-create-btn.confirm:disabled {\n background: #bbb;\n cursor: not-allowed;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { ref, computed, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'\nimport type { ImClient } from '@quitting/core'\nimport type { Conversation, Message } from '@quitting/contract'\nimport { getImClient } from '../client-access'\n\n// 聊天面板:与单个 peer 的 1:1 对话。\n// 两种布局模式:\n// 浮窗(默认):fixed 右下角,ImUI.chat() 弹出用\n// embedded:随容器铺满(position:static + 100%),业务方嵌入自己的布局用\n// 能力:历史拉取 + dialog 通道订阅 + 乐观渲染 + ack + 撤回 + 已读上报 +\n// Enter 发送(IME 安全)/Shift+Enter 换行/输入自动增高/表情面板/时间分隔线\n// 自定义:#message 插槽(自定义气泡)/ #input 插槽(自定义输入区)\nconst props = defineProps<{\n to: string\n // 对端展示名;未传时查 SDK 用户目录,仍无则显示 to 本身\n peerName?: string\n // 我的 externalId(左右气泡归属判定用);未传时自动查用户目录解析\n myUid?: string\n embedded?: boolean\n onClose?: () => void\n}>()\n\nconst emit = defineEmits<{ (e: 'close'): void }>()\n\ndefineSlots<{\n // 自定义消息气泡渲染(scope: message 为完整 Message,含 status)\n message?(scope: { message: Message; mine: boolean }): unknown\n // 自定义输入区(scope: send 为发送函数,业务方自管输入状态时可忽略)\n input?(scope: { send: (text: string) => void }): unknown\n}>()\n\nconst messages = ref<Message[]>([])\nconst input = ref('')\nconst conversationId = ref('')\nconst lastReadSeq = ref(0)\nconst listEl = ref<HTMLDivElement | null>(null)\nconst textareaEl = ref<HTMLTextAreaElement | null>(null)\nconst unsubscribers: Array<() => void> = []\n// 表情面板开关\nconst emojiPickerOpen = ref(false)\n// 常用 emoji 子集;业务方要更全的可通过 #input 插槽接 emoji-mart 等\nconst emojis = [\n '😀',\n '😁',\n '😂',\n '🤣',\n '😊',\n '😍',\n '🥰',\n '😘',\n '😎',\n '🤔',\n '😴',\n '😭',\n '😱',\n '🤯',\n '🤗',\n '👍',\n '👎',\n '👏',\n '🙏',\n '💪',\n '🤝',\n '✌️',\n '🤞',\n '❤️',\n '💔',\n '💕',\n '💯',\n '🔥',\n '⭐',\n '🎉',\n '🎁',\n '🚀',\n]\n\nfunction getClient(): ImClient | undefined {\n return getImClient()\n}\n\n// 对端展示名:外部传 peerName 优先;未传查用户目录\nconst fallbackName = ref('')\nwatch(\n () => props.to,\n async (to) => {\n fallbackName.value = ''\n if (props.peerName || !to) return\n const dir = getClient()?.users\n if (!dir?.resolve) return\n const u = await dir.resolve(to).catch(() => undefined)\n if (u?.name) fallbackName.value = u.name\n },\n { immediate: true },\n)\nconst displayPeerName = computed(() => props.peerName || fallbackName.value || props.to)\n\n// 解析与 to(externalId)的会话 ID:反查 peer UUID 后在会话列表里匹配 dialog 两端\n// (后端 dialogUserA/B 存的是内部 UUID,见 server signaling/message.go GetOrCreateDialog)\n// 后端会话在首条消息发送时才创建,没发过消息时返回空串\nasync function resolveConversation(client: ImClient): Promise<string> {\n try {\n const resp = await client.transport.usersByExternal([props.to])\n const peerUUID = resp.users[props.to]?.uid\n if (!peerUUID) return ''\n const convs = (await client.message.listConversations()) || []\n const me = client.uid\n const conv = convs.find(\n (c: Conversation) =>\n (c.dialogUserA === me && c.dialogUserB === peerUUID) ||\n (c.dialogUserA === peerUUID && c.dialogUserB === me),\n )\n return conv?.id || ''\n } catch (err) {\n console.warn('[im] 解析会话 ID 失败', err)\n return ''\n }\n}\n\n// dialog 通道 message publication 的原始 payload(realtime 透传给 'message' 事件,\n// 字段与 contract Message 不同:msgId/from(externalId)/msgType/ts,见后端 signaling/message.go)\ninterface DialogPublication {\n msgId?: string\n conversationId?: string\n seq?: number\n from?: string\n msgType?: string\n body?: string\n ts?: number\n}\n\n// 我的 externalId:历史消息的 senderId 是 externalId(后端 History 已转换),\n// 左右归属判定必须用它而非内部 uid。prop 优先,缺省查用户目录反查\nconst myExternalId = ref(props.myUid || '')\n\nonMounted(async () => {\n const client = getClient()\n if (!client) return\n\n if (!props.myUid) {\n const me = await client.users?.resolve(client.uid).catch(() => undefined)\n if (me?.externalId) myExternalId.value = me.externalId\n }\n // 订阅与 peer 的 dialog 通道——SDK 不自动订阅,不订阅则对端回复永远收不到\n client.message\n .openDialog(props.to)\n .catch((err: unknown) => console.warn('[im] openDialog 失败', err))\n\n // 已有会话则直接拉历史;没有(首次聊天)时发送成功后再解析\n conversationId.value = await resolveConversation(client)\n if (conversationId.value) {\n await loadHistory()\n }\n\n unsubscribers.push(\n client.on('message', (raw) => {\n const p = raw as unknown as DialogPublication\n // 会话过滤:conversationId 已知时精确匹配;未知(首次聊天/解析中)用 from 兜底,\n // 否则首条对端消息(自己还没发过、会话还没建)会被丢弃\n const belongs = conversationId.value\n ? p.conversationId === conversationId.value\n : p.from === props.to\n if (!belongs) return\n // 去重:自己发的消息 send() 已乐观插入并被回填真实 msgId\n if (!p.msgId || messages.value.some((m) => m.id === p.msgId)) return\n messages.value.push({\n id: p.msgId,\n appId: '',\n conversationId: p.conversationId || conversationId.value,\n seq: Number(p.seq) || 0,\n // 注意:来自 publication 的 senderId 是 externalId;自己的乐观消息也统一用\n // externalId(myExternalId),与后端 History 口径一致,isMine 判定两侧统一\n senderId: p.from || '',\n clientMid: p.msgId,\n type: (p.msgType as Message['type']) || 'text',\n body: p.body,\n status: 'sent',\n createdAt: p.ts || Date.now(),\n })\n scrollToBottom()\n // 自动 ACK(消息必达三保险的客户端侧)\n client.message.ack(p.msgId).catch(() => {})\n }),\n )\n unsubscribers.push(\n client.on('message:revoked', (e: { messageId: string }) => {\n const m = messages.value.find((x) => x.id === e.messageId)\n if (m) m.status = 'revoked'\n }),\n )\n})\n\nonBeforeUnmount(() => {\n unsubscribers.forEach((fn) => fn())\n getClient()?.message?.closeDialog?.(props.to)\n})\n\nasync function send() {\n const body = input.value.trim()\n if (!body) return\n input.value = ''\n resetTextareaHeight()\n emojiPickerOpen.value = false\n await sendText(body)\n}\n\n// #input 插槽的 scope.send:自定义输入区可调它发送任意文本\nasync function sendText(body: string) {\n const client = getClient()\n if (!client || !body) return\n // 乐观渲染:先本地加一条 status=sending(senderId 用 externalId,与历史消息口径一致)\n const tempId = 'temp-' + Date.now()\n messages.value.push({\n id: tempId,\n appId: '',\n conversationId: conversationId.value,\n seq: 0,\n senderId: myExternalId.value || client.uid,\n clientMid: tempId,\n type: 'text',\n body,\n status: 'sending',\n createdAt: Date.now(),\n })\n scrollToBottom()\n try {\n const resp = await client.message.send({ to: props.to, type: 'text', body })\n // 更新本地消息为已发送\n const m = messages.value.find((x) => x.id === tempId)\n if (m) {\n m.id = resp.msgId\n m.seq = resp.seq\n m.status = 'sent'\n }\n lastReadSeq.value = resp.seq\n // 首条消息发送后服务端才建会话——此时解析 conversationId 供后续收消息过滤\n if (!conversationId.value) {\n conversationId.value = await resolveConversation(client)\n if (m) m.conversationId = conversationId.value\n }\n } catch (err) {\n const m = messages.value.find((x) => x.id === tempId)\n if (m) m.status = 'failed'\n }\n}\n\nasync function loadHistory() {\n const client = getClient()\n if (!client || !conversationId.value) return\n const msgs = await client.message.history(conversationId.value, 0, 50)\n // 按 seq 升序渲染(后端升序返回,这里兜底排序保证展示稳定)\n messages.value = [...msgs].sort((a, b) => a.seq - b.seq)\n if (msgs.length > 0) {\n lastReadSeq.value = msgs[msgs.length - 1].seq\n await client.message.markRead(conversationId.value, lastReadSeq.value)\n }\n scrollToBottom()\n}\n\nfunction scrollToBottom() {\n nextTick(() => {\n if (listEl.value) listEl.value.scrollTop = listEl.value.scrollHeight\n })\n}\n\n// ===== 输入区交互 =====\n\n// textarea 自动增高:最大高度 96px(约 4 行)\nfunction autoResize() {\n const el = textareaEl.value\n if (!el) return\n el.style.height = 'auto'\n el.style.height = Math.min(el.scrollHeight, 96) + 'px'\n}\n\nfunction resetTextareaHeight() {\n if (textareaEl.value) {\n textareaEl.value.style.height = 'auto'\n }\n}\n\n// Enter 发送 / Shift+Enter 换行;IME 输入法 composing 状态(中文中间态)回车不触发发送\nfunction onInputKeyDown(e: KeyboardEvent) {\n if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) {\n e.preventDefault()\n send()\n }\n}\n\n// 表情插入到光标位置(避免用户在中间编辑时被推到末尾)\nfunction insertEmoji(emoji: string) {\n const el = textareaEl.value\n if (!el) {\n input.value += emoji\n return\n }\n const start = el.selectionStart ?? input.value.length\n const end = el.selectionEnd ?? input.value.length\n input.value = input.value.slice(0, start) + emoji + input.value.slice(end)\n nextTick(() => {\n el.focus()\n const pos = start + emoji.length\n el.setSelectionRange(pos, pos)\n autoResize()\n })\n}\n\n// ===== 渲染辅助 =====\n\nconst sortedMessages = computed(() => [...messages.value].sort((a, b) => a.seq - b.seq))\n\n// 时间分隔线:相邻消息间隔 > 5 分钟插入分隔元素\ntype FlatItem =\n | { type: 'time'; ts: number; key: string }\n | { type: 'msg'; msg: Message; key: string }\n\nconst flatList = computed<FlatItem[]>(() => {\n const list: FlatItem[] = []\n const sorted = sortedMessages.value\n const GAP_MS = 5 * 60 * 1000\n let lastTs = 0\n for (const m of sorted) {\n if (m.createdAt - lastTs > GAP_MS || lastTs === 0) {\n list.push({ type: 'time', ts: m.createdAt, key: `t-${m.id}` })\n }\n list.push({ type: 'msg', msg: m, key: m.id })\n lastTs = m.createdAt\n }\n return list\n})\n\nfunction fmtTime(ts: number): string {\n const d = new Date(ts)\n const hh = d.getHours().toString().padStart(2, '0')\n const mm = d.getMinutes().toString().padStart(2, '0')\n return `${hh}:${mm}`\n}\n\n// 分隔线完整时间:今天 HH:mm;昨天 HH:mm;更早 M月D日 HH:mm\nfunction fmtTimeFull(ts: number): string {\n const d = new Date(ts)\n const now = new Date()\n const hm = fmtTime(ts)\n if (d.toDateString() === now.toDateString()) return hm\n const yest = new Date(now)\n yest.setDate(now.getDate() - 1)\n if (d.toDateString() === yest.toDateString()) return `昨天 ${hm}`\n return `${d.getMonth() + 1}月${d.getDate()}日 ${hm}`\n}\n\nfunction isMine(m: Message): boolean {\n // 统一按 externalId 判定(历史消息/回推消息的 senderId 均为 externalId)\n const me = myExternalId.value\n return !!me && m.senderId === me\n}\n</script>\n\n<template>\n <div :class=\"['im-chat-panel', { 'im-chat-panel--embedded': embedded }]\">\n <div class=\"im-chat-header\">\n <span class=\"im-chat-name\">{{ displayPeerName }}</span>\n <button v-if=\"!embedded\" class=\"im-chat-close\" @click=\"emit('close')\">×</button>\n </div>\n <div ref=\"listEl\" class=\"im-chat-messages\">\n <div v-if=\"sortedMessages.length === 0\" class=\"im-chat-empty\">还没有消息,发个招呼吧 👋</div>\n <template v-for=\"item in flatList\" :key=\"item.key\">\n <div v-if=\"item.type === 'time'\" class=\"im-chat-time-sep\">\n {{ fmtTimeFull(item.ts) }}\n </div>\n <div v-else :class=\"['im-chat-msg', isMine(item.msg) ? 'mine' : 'theirs']\">\n <!-- 自定义气泡插槽:业务方处理富文本/媒体等渲染 -->\n <slot name=\"message\" :message=\"item.msg\" :mine=\"isMine(item.msg)\">\n <div class=\"im-chat-bubble\">\n <span v-if=\"item.msg.status === 'revoked'\" class=\"im-chat-revoked\">[已撤回]</span>\n <span v-else>{{ item.msg.body }}</span>\n <span v-if=\"item.msg.status === 'failed'\" class=\"im-chat-fail\">!发送失败</span>\n </div>\n </slot>\n <div class=\"im-chat-meta\">{{ fmtTime(item.msg.createdAt) }}</div>\n </div>\n </template>\n </div>\n <!-- 自定义输入区插槽:不传则用默认(表情 + textarea + 发送按钮) -->\n <slot name=\"input\" :send=\"sendText\">\n <div class=\"im-chat-input\">\n <button class=\"im-chat-emoji-btn\" title=\"表情\" @click=\"emojiPickerOpen = !emojiPickerOpen\">\n 😊\n </button>\n <textarea\n ref=\"textareaEl\"\n v-model=\"input\"\n rows=\"1\"\n placeholder=\"输入消息,Enter 发送,Shift+Enter 换行\"\n @keydown=\"onInputKeyDown\"\n @input=\"autoResize\"\n ></textarea>\n <button :disabled=\"!input.trim()\" @click=\"send\">发送</button>\n <!-- 表情面板:定位相对 .im-chat-input -->\n <div v-if=\"emojiPickerOpen\" class=\"im-chat-emoji-picker\">\n <button v-for=\"e in emojis\" :key=\"e\" class=\"im-chat-emoji-item\" @click=\"insertEmoji(e)\">\n {{ e }}\n </button>\n </div>\n </div>\n </slot>\n </div>\n</template>\n\n<style scoped>\n.im-chat-panel {\n /* 主题变量:业务方在 :root/祖先覆盖即可定制配色 */\n --im-chat-primary: #667eea;\n --im-chat-bg: #f5f7fa;\n --im-chat-bubble-mine: #95ec69;\n --im-chat-bubble-peer: #fff;\n\n position: fixed;\n bottom: 16px;\n right: 16px;\n width: 360px;\n height: 480px;\n background: #fff;\n border-radius: 8px;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);\n display: flex;\n flex-direction: column;\n z-index: 9999;\n font-family: -apple-system, 'Segoe UI', Roboto, 'PingFang SC', 'Microsoft YaHei', sans-serif;\n}\n/* 内嵌模式:随容器铺满,阴影/圆角交给外层容器 */\n.im-chat-panel--embedded {\n position: static;\n flex: 1;\n min-height: 0;\n width: 100%;\n height: 100%;\n border-radius: 0;\n box-shadow: none;\n z-index: auto;\n}\n.im-chat-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 12px 16px;\n border-bottom: 1px solid #eee;\n font-weight: 600;\n}\n.im-chat-name {\n font-size: 15px;\n}\n.im-chat-close {\n background: none;\n border: none;\n cursor: pointer;\n font-size: 20px;\n color: #999;\n}\n.im-chat-messages {\n flex: 1;\n overflow-y: auto;\n padding: 12px 16px;\n background: var(--im-chat-bg);\n}\n.im-chat-empty {\n color: #aaa;\n text-align: center;\n padding-top: 40px;\n font-size: 13px;\n}\n.im-chat-time-sep {\n text-align: center;\n font-size: 11px;\n color: #999;\n background: rgba(0, 0, 0, 0.05);\n border-radius: 4px;\n padding: 2px 8px;\n margin: 12px auto;\n width: fit-content;\n}\n.im-chat-msg {\n margin-bottom: 12px;\n display: flex;\n flex-direction: column;\n}\n.im-chat-msg.mine {\n align-items: flex-end;\n}\n.im-chat-msg.theirs {\n align-items: flex-start;\n}\n.im-chat-bubble {\n max-width: 70%;\n padding: 8px 12px;\n border-radius: 12px;\n font-size: 14px;\n word-break: break-word;\n}\n.im-chat-msg.mine .im-chat-bubble {\n background: var(--im-chat-bubble-mine);\n color: #1a1a1a;\n}\n.im-chat-msg.theirs .im-chat-bubble {\n background: var(--im-chat-bubble-peer);\n color: #1a1a1a;\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);\n}\n.im-chat-revoked {\n color: #999;\n font-style: italic;\n font-size: 12px;\n}\n.im-chat-fail {\n color: #ff4d4f;\n font-size: 11px;\n margin-left: 4px;\n}\n.im-chat-meta {\n font-size: 11px;\n color: #bbb;\n margin-top: 2px;\n}\n.im-chat-input {\n position: relative;\n display: flex;\n align-items: flex-end;\n padding: 10px 12px;\n border-top: 1px solid #eee;\n background: #fff;\n}\n.im-chat-emoji-btn {\n width: 32px;\n height: 32px;\n margin-right: 8px;\n border: none;\n background: transparent;\n font-size: 20px;\n cursor: pointer;\n border-radius: 4px;\n}\n.im-chat-emoji-btn:hover {\n background: #f0f0f0;\n}\n.im-chat-input textarea {\n flex: 1;\n padding: 8px 12px;\n border: 1px solid #ddd;\n border-radius: 4px;\n outline: none;\n font-size: 14px;\n font-family: inherit;\n resize: none;\n line-height: 1.4;\n min-height: 36px;\n max-height: 96px;\n overflow-y: auto;\n}\n.im-chat-input textarea:focus {\n border-color: var(--im-chat-primary);\n}\n.im-chat-input button {\n margin-left: 8px;\n padding: 0 16px;\n height: 36px;\n background: var(--im-chat-primary);\n color: #fff;\n border: none;\n border-radius: 4px;\n cursor: pointer;\n font-size: 13px;\n}\n.im-chat-input button:disabled {\n background: #bbb;\n cursor: not-allowed;\n}\n/* 表情面板:定位在输入框上方,4 行 × 8 列网格 */\n.im-chat-emoji-picker {\n position: absolute;\n bottom: 60px;\n left: 12px;\n right: 12px;\n background: #fff;\n border: 1px solid #eee;\n border-radius: 8px;\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);\n padding: 8px;\n display: grid;\n grid-template-columns: repeat(8, 1fr);\n gap: 4px;\n z-index: 10;\n}\n.im-chat-emoji-item {\n font-size: 20px;\n border: none;\n background: transparent;\n cursor: pointer;\n padding: 6px;\n border-radius: 4px;\n}\n.im-chat-emoji-item:hover {\n background: #f0f0f0;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { ref, computed, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'\nimport type { ImClient } from '@quitting/core'\nimport type { Conversation, Message } from '@quitting/contract'\nimport { getImClient } from '../client-access'\n\n// 聊天面板:与单个 peer 的 1:1 对话。\n// 两种布局模式:\n// 浮窗(默认):fixed 右下角,ImUI.chat() 弹出用\n// embedded:随容器铺满(position:static + 100%),业务方嵌入自己的布局用\n// 能力:历史拉取 + dialog 通道订阅 + 乐观渲染 + ack + 撤回 + 已读上报 +\n// Enter 发送(IME 安全)/Shift+Enter 换行/输入自动增高/表情面板/时间分隔线\n// 自定义:#message 插槽(自定义气泡)/ #input 插槽(自定义输入区)\nconst props = defineProps<{\n to: string\n // 对端展示名;未传时查 SDK 用户目录,仍无则显示 to 本身\n peerName?: string\n // 我的 externalId(左右气泡归属判定用);未传时自动查用户目录解析\n myUid?: string\n embedded?: boolean\n onClose?: () => void\n}>()\n\nconst emit = defineEmits<{ (e: 'close'): void }>()\n\ndefineSlots<{\n // 自定义消息气泡渲染(scope: message 为完整 Message,含 status)\n message?(scope: { message: Message; mine: boolean }): unknown\n // 自定义输入区(scope: send 为发送函数,业务方自管输入状态时可忽略)\n input?(scope: { send: (text: string) => void }): unknown\n}>()\n\nconst messages = ref<Message[]>([])\nconst input = ref('')\nconst conversationId = ref('')\nconst lastReadSeq = ref(0)\nconst listEl = ref<HTMLDivElement | null>(null)\nconst textareaEl = ref<HTMLTextAreaElement | null>(null)\nconst unsubscribers: Array<() => void> = []\n// 表情面板开关\nconst emojiPickerOpen = ref(false)\n// 常用 emoji 子集;业务方要更全的可通过 #input 插槽接 emoji-mart 等\nconst emojis = [\n '😀',\n '😁',\n '😂',\n '🤣',\n '😊',\n '😍',\n '🥰',\n '😘',\n '😎',\n '🤔',\n '😴',\n '😭',\n '😱',\n '🤯',\n '🤗',\n '👍',\n '👎',\n '👏',\n '🙏',\n '💪',\n '🤝',\n '✌️',\n '🤞',\n '❤️',\n '💔',\n '💕',\n '💯',\n '🔥',\n '⭐',\n '🎉',\n '🎁',\n '🚀',\n]\n\nfunction getClient(): ImClient | undefined {\n return getImClient()\n}\n\n// 对端展示名:外部传 peerName 优先;未传查用户目录\nconst fallbackName = ref('')\nwatch(\n () => props.to,\n async (to) => {\n fallbackName.value = ''\n if (props.peerName || !to) return\n const dir = getClient()?.users\n if (!dir?.resolve) return\n const u = await dir.resolve(to).catch(() => undefined)\n if (u?.name) fallbackName.value = u.name\n },\n { immediate: true },\n)\nconst displayPeerName = computed(() => props.peerName || fallbackName.value || props.to)\n\n// 解析与 to(externalId)的会话 ID:反查 peer UUID 后在会话列表里匹配 dialog 两端\n// (后端 dialogUserA/B 存的是内部 UUID,见 server signaling/message.go GetOrCreateDialog)\n// 后端会话在首条消息发送时才创建,没发过消息时返回空串\nasync function resolveConversation(client: ImClient): Promise<string> {\n try {\n const resp = await client.transport.usersByExternal([props.to])\n const peerUUID = resp.users[props.to]?.uid\n if (!peerUUID) return ''\n const convs = (await client.message.listConversations()) || []\n const me = client.uid\n const conv = convs.find(\n (c: Conversation) =>\n (c.dialogUserA === me && c.dialogUserB === peerUUID) ||\n (c.dialogUserA === peerUUID && c.dialogUserB === me),\n )\n return conv?.id || ''\n } catch (err) {\n console.warn('[im] 解析会话 ID 失败', err)\n return ''\n }\n}\n\n// dialog 通道 message publication 的原始 payload(realtime 透传给 'message' 事件,\n// 字段与 contract Message 不同:msgId/from(externalId)/msgType/ts,见后端 signaling/message.go)\ninterface DialogPublication {\n msgId?: string\n conversationId?: string\n seq?: number\n from?: string\n msgType?: string\n body?: string\n ts?: number\n}\n\n// 我的 externalId:历史消息的 senderId 是 externalId(后端 History 已转换),\n// 左右归属判定必须用它而非内部 uid。prop 优先,缺省查用户目录反查\nconst myExternalId = ref(props.myUid || '')\n\nonMounted(async () => {\n const client = getClient()\n if (!client) return\n\n if (!props.myUid) {\n const me = await client.users?.resolve(client.uid).catch(() => undefined)\n if (me?.externalId) myExternalId.value = me.externalId\n }\n // 订阅与 peer 的 dialog 通道——SDK 不自动订阅,不订阅则对端回复永远收不到\n client.message\n .openDialog(props.to)\n .catch((err: unknown) => console.warn('[im] openDialog 失败', err))\n\n // 已有会话则直接拉历史;没有(首次聊天)时发送成功后再解析\n conversationId.value = await resolveConversation(client)\n if (conversationId.value) {\n await loadHistory()\n }\n\n unsubscribers.push(\n client.on('message', (raw) => {\n const p = raw as unknown as DialogPublication\n // 会话过滤:conversationId 已知时精确匹配;未知(首次聊天/解析中)用 from 兜底,\n // 否则首条对端消息(自己还没发过、会话还没建)会被丢弃\n const belongs = conversationId.value\n ? p.conversationId === conversationId.value\n : p.from === props.to\n if (!belongs) return\n // 去重:自己发的消息 send() 已乐观插入并被回填真实 msgId\n if (!p.msgId || messages.value.some((m) => m.id === p.msgId)) return\n messages.value.push({\n id: p.msgId,\n appId: '',\n conversationId: p.conversationId || conversationId.value,\n seq: Number(p.seq) || 0,\n // 注意:来自 publication 的 senderId 是 externalId;自己的乐观消息也统一用\n // externalId(myExternalId),与后端 History 口径一致,isMine 判定两侧统一\n senderId: p.from || '',\n clientMid: p.msgId,\n type: (p.msgType as Message['type']) || 'text',\n body: p.body,\n status: 'sent',\n createdAt: p.ts || Date.now(),\n })\n scrollToBottom()\n // 自动 ACK(消息必达三保险的客户端侧)\n client.message.ack(p.msgId).catch(() => {})\n }),\n )\n unsubscribers.push(\n client.on('message:revoked', (e: { messageId: string }) => {\n const m = messages.value.find((x) => x.id === e.messageId)\n if (m) m.status = 'revoked'\n }),\n )\n})\n\nonBeforeUnmount(() => {\n unsubscribers.forEach((fn) => fn())\n getClient()?.message?.closeDialog?.(props.to)\n})\n\nasync function send() {\n const body = input.value.trim()\n if (!body) return\n input.value = ''\n resetTextareaHeight()\n emojiPickerOpen.value = false\n await sendText(body)\n}\n\n// #input 插槽的 scope.send:自定义输入区可调它发送任意文本\nasync function sendText(body: string) {\n const client = getClient()\n if (!client || !body) return\n // 乐观渲染:先本地加一条 status=sending(senderId 用 externalId,与历史消息口径一致)\n const tempId = 'temp-' + Date.now()\n messages.value.push({\n id: tempId,\n appId: '',\n conversationId: conversationId.value,\n seq: 0,\n senderId: myExternalId.value || client.uid,\n clientMid: tempId,\n type: 'text',\n body,\n status: 'sending',\n createdAt: Date.now(),\n })\n scrollToBottom()\n try {\n const resp = await client.message.send({ to: props.to, type: 'text', body })\n // 更新本地消息为已发送\n const m = messages.value.find((x) => x.id === tempId)\n if (m) {\n m.id = resp.msgId\n m.seq = resp.seq\n m.status = 'sent'\n }\n lastReadSeq.value = resp.seq\n // 首条消息发送后服务端才建会话——此时解析 conversationId 供后续收消息过滤\n if (!conversationId.value) {\n conversationId.value = await resolveConversation(client)\n if (m) m.conversationId = conversationId.value\n }\n } catch (err) {\n const m = messages.value.find((x) => x.id === tempId)\n if (m) m.status = 'failed'\n }\n}\n\nasync function loadHistory() {\n const client = getClient()\n if (!client || !conversationId.value) return\n const msgs = await client.message.history(conversationId.value, 0, 50)\n // 按 seq 升序渲染(后端升序返回,这里兜底排序保证展示稳定)\n messages.value = [...msgs].sort((a, b) => a.seq - b.seq)\n if (msgs.length > 0) {\n lastReadSeq.value = msgs[msgs.length - 1].seq\n await client.message.markRead(conversationId.value, lastReadSeq.value)\n }\n scrollToBottom()\n}\n\nfunction scrollToBottom() {\n nextTick(() => {\n if (listEl.value) listEl.value.scrollTop = listEl.value.scrollHeight\n })\n}\n\n// ===== 输入区交互 =====\n\n// textarea 自动增高:最大高度 96px(约 4 行)\nfunction autoResize() {\n const el = textareaEl.value\n if (!el) return\n el.style.height = 'auto'\n el.style.height = Math.min(el.scrollHeight, 96) + 'px'\n}\n\nfunction resetTextareaHeight() {\n if (textareaEl.value) {\n textareaEl.value.style.height = 'auto'\n }\n}\n\n// Enter 发送 / Shift+Enter 换行;IME 输入法 composing 状态(中文中间态)回车不触发发送\nfunction onInputKeyDown(e: KeyboardEvent) {\n if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) {\n e.preventDefault()\n send()\n }\n}\n\n// 表情插入到光标位置(避免用户在中间编辑时被推到末尾)\nfunction insertEmoji(emoji: string) {\n const el = textareaEl.value\n if (!el) {\n input.value += emoji\n return\n }\n const start = el.selectionStart ?? input.value.length\n const end = el.selectionEnd ?? input.value.length\n input.value = input.value.slice(0, start) + emoji + input.value.slice(end)\n nextTick(() => {\n el.focus()\n const pos = start + emoji.length\n el.setSelectionRange(pos, pos)\n autoResize()\n })\n}\n\n// ===== 渲染辅助 =====\n\nconst sortedMessages = computed(() => [...messages.value].sort((a, b) => a.seq - b.seq))\n\n// 时间分隔线:相邻消息间隔 > 5 分钟插入分隔元素\ntype FlatItem =\n | { type: 'time'; ts: number; key: string }\n | { type: 'msg'; msg: Message; key: string }\n\nconst flatList = computed<FlatItem[]>(() => {\n const list: FlatItem[] = []\n const sorted = sortedMessages.value\n const GAP_MS = 5 * 60 * 1000\n let lastTs = 0\n for (const m of sorted) {\n if (m.createdAt - lastTs > GAP_MS || lastTs === 0) {\n list.push({ type: 'time', ts: m.createdAt, key: `t-${m.id}` })\n }\n list.push({ type: 'msg', msg: m, key: m.id })\n lastTs = m.createdAt\n }\n return list\n})\n\nfunction fmtTime(ts: number): string {\n const d = new Date(ts)\n const hh = d.getHours().toString().padStart(2, '0')\n const mm = d.getMinutes().toString().padStart(2, '0')\n return `${hh}:${mm}`\n}\n\n// 分隔线完整时间:今天 HH:mm;昨天 HH:mm;更早 M月D日 HH:mm\nfunction fmtTimeFull(ts: number): string {\n const d = new Date(ts)\n const now = new Date()\n const hm = fmtTime(ts)\n if (d.toDateString() === now.toDateString()) return hm\n const yest = new Date(now)\n yest.setDate(now.getDate() - 1)\n if (d.toDateString() === yest.toDateString()) return `昨天 ${hm}`\n return `${d.getMonth() + 1}月${d.getDate()}日 ${hm}`\n}\n\nfunction isMine(m: Message): boolean {\n // 统一按 externalId 判定(历史消息/回推消息的 senderId 均为 externalId)\n const me = myExternalId.value\n return !!me && m.senderId === me\n}\n</script>\n\n<template>\n <div :class=\"['im-chat-panel', { 'im-chat-panel--embedded': embedded }]\">\n <div class=\"im-chat-header\">\n <span class=\"im-chat-name\">{{ displayPeerName }}</span>\n <button v-if=\"!embedded\" class=\"im-chat-close\" @click=\"emit('close')\">×</button>\n </div>\n <div ref=\"listEl\" class=\"im-chat-messages\">\n <div v-if=\"sortedMessages.length === 0\" class=\"im-chat-empty\">还没有消息,发个招呼吧 👋</div>\n <template v-for=\"item in flatList\" :key=\"item.key\">\n <div v-if=\"item.type === 'time'\" class=\"im-chat-time-sep\">\n {{ fmtTimeFull(item.ts) }}\n </div>\n <div v-else :class=\"['im-chat-msg', isMine(item.msg) ? 'mine' : 'theirs']\">\n <!-- 自定义气泡插槽:业务方处理富文本/媒体等渲染 -->\n <slot name=\"message\" :message=\"item.msg\" :mine=\"isMine(item.msg)\">\n <div class=\"im-chat-bubble\">\n <span v-if=\"item.msg.status === 'revoked'\" class=\"im-chat-revoked\">[已撤回]</span>\n <span v-else>{{ item.msg.body }}</span>\n <span v-if=\"item.msg.status === 'failed'\" class=\"im-chat-fail\">!发送失败</span>\n </div>\n </slot>\n <div class=\"im-chat-meta\">{{ fmtTime(item.msg.createdAt) }}</div>\n </div>\n </template>\n </div>\n <!-- 自定义输入区插槽:不传则用默认(表情 + textarea + 发送按钮) -->\n <slot name=\"input\" :send=\"sendText\">\n <div class=\"im-chat-input\">\n <button class=\"im-chat-emoji-btn\" title=\"表情\" @click=\"emojiPickerOpen = !emojiPickerOpen\">\n 😊\n </button>\n <textarea\n ref=\"textareaEl\"\n v-model=\"input\"\n rows=\"1\"\n placeholder=\"输入消息,Enter 发送,Shift+Enter 换行\"\n @keydown=\"onInputKeyDown\"\n @input=\"autoResize\"\n ></textarea>\n <button :disabled=\"!input.trim()\" @click=\"send\">发送</button>\n <!-- 表情面板:定位相对 .im-chat-input -->\n <div v-if=\"emojiPickerOpen\" class=\"im-chat-emoji-picker\">\n <button v-for=\"e in emojis\" :key=\"e\" class=\"im-chat-emoji-item\" @click=\"insertEmoji(e)\">\n {{ e }}\n </button>\n </div>\n </div>\n </slot>\n </div>\n</template>\n\n<style scoped>\n.im-chat-panel {\n /* 主题变量:业务方在 :root/祖先覆盖即可定制配色 */\n --im-chat-primary: #667eea;\n --im-chat-bg: #f5f7fa;\n --im-chat-bubble-mine: #95ec69;\n --im-chat-bubble-peer: #fff;\n\n position: fixed;\n bottom: 16px;\n right: 16px;\n width: 360px;\n height: 480px;\n background: #fff;\n border-radius: 8px;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);\n display: flex;\n flex-direction: column;\n z-index: 9999;\n font-family: -apple-system, 'Segoe UI', Roboto, 'PingFang SC', 'Microsoft YaHei', sans-serif;\n}\n/* 内嵌模式:随容器铺满,阴影/圆角交给外层容器 */\n.im-chat-panel--embedded {\n position: static;\n flex: 1;\n min-height: 0;\n width: 100%;\n height: 100%;\n border-radius: 0;\n box-shadow: none;\n z-index: auto;\n}\n.im-chat-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 12px 16px;\n border-bottom: 1px solid #eee;\n font-weight: 600;\n}\n.im-chat-name {\n font-size: 15px;\n}\n.im-chat-close {\n background: none;\n border: none;\n cursor: pointer;\n font-size: 20px;\n color: #999;\n}\n.im-chat-messages {\n flex: 1;\n overflow-y: auto;\n padding: 12px 16px;\n background: var(--im-chat-bg);\n}\n.im-chat-empty {\n color: #aaa;\n text-align: center;\n padding-top: 40px;\n font-size: 13px;\n}\n.im-chat-time-sep {\n text-align: center;\n font-size: 11px;\n color: #999;\n background: rgba(0, 0, 0, 0.05);\n border-radius: 4px;\n padding: 2px 8px;\n margin: 12px auto;\n width: fit-content;\n}\n.im-chat-msg {\n margin-bottom: 12px;\n display: flex;\n flex-direction: column;\n}\n.im-chat-msg.mine {\n align-items: flex-end;\n}\n.im-chat-msg.theirs {\n align-items: flex-start;\n}\n.im-chat-bubble {\n max-width: 70%;\n padding: 8px 12px;\n border-radius: 12px;\n font-size: 14px;\n word-break: break-word;\n}\n.im-chat-msg.mine .im-chat-bubble {\n background: var(--im-chat-bubble-mine);\n color: #1a1a1a;\n}\n.im-chat-msg.theirs .im-chat-bubble {\n background: var(--im-chat-bubble-peer);\n color: #1a1a1a;\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);\n}\n.im-chat-revoked {\n color: #999;\n font-style: italic;\n font-size: 12px;\n}\n.im-chat-fail {\n color: #ff4d4f;\n font-size: 11px;\n margin-left: 4px;\n}\n.im-chat-meta {\n font-size: 11px;\n color: #bbb;\n margin-top: 2px;\n}\n.im-chat-input {\n position: relative;\n display: flex;\n align-items: flex-end;\n padding: 10px 12px;\n border-top: 1px solid #eee;\n background: #fff;\n}\n.im-chat-emoji-btn {\n width: 32px;\n height: 32px;\n margin-right: 8px;\n border: none;\n background: transparent;\n font-size: 20px;\n cursor: pointer;\n border-radius: 4px;\n}\n.im-chat-emoji-btn:hover {\n background: #f0f0f0;\n}\n.im-chat-input textarea {\n flex: 1;\n padding: 8px 12px;\n border: 1px solid #ddd;\n border-radius: 4px;\n outline: none;\n font-size: 14px;\n font-family: inherit;\n resize: none;\n line-height: 1.4;\n min-height: 36px;\n max-height: 96px;\n overflow-y: auto;\n}\n.im-chat-input textarea:focus {\n border-color: var(--im-chat-primary);\n}\n.im-chat-input button {\n margin-left: 8px;\n padding: 0 16px;\n height: 36px;\n background: var(--im-chat-primary);\n color: #fff;\n border: none;\n border-radius: 4px;\n cursor: pointer;\n font-size: 13px;\n}\n.im-chat-input button:disabled {\n background: #bbb;\n cursor: not-allowed;\n}\n/* 表情面板:定位在输入框上方,4 行 × 8 列网格 */\n.im-chat-emoji-picker {\n position: absolute;\n bottom: 60px;\n left: 12px;\n right: 12px;\n background: #fff;\n border: 1px solid #eee;\n border-radius: 8px;\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);\n padding: 8px;\n display: grid;\n grid-template-columns: repeat(8, 1fr);\n gap: 4px;\n z-index: 10;\n}\n.im-chat-emoji-item {\n font-size: 20px;\n border: none;\n background: transparent;\n cursor: pointer;\n padding: 6px;\n border-radius: 4px;\n}\n.im-chat-emoji-item:hover {\n background: #f0f0f0;\n}\n</style>\n","import type { App } from 'vue'\n\nimport type { CallSession, ImClient } from '@quitting/core'\nimport type { CallType } from '@quitting/contract'\n\nimport { setRootApp, mountComponent, unmountRef, type MountedRef } from './mount'\nimport { setImClient } from './client-access'\nimport { ImToastApi as toast } from './toast'\nimport { friendlyApiError } from './error-text'\nimport CallWindow from './components/CallWindow.vue'\nimport CallDialog from './components/CallDialog.vue'\nimport MeetWindow from './components/MeetWindow.vue'\nimport MeetingInvite from './components/MeetingInvite.vue'\nimport MeetingCreateDialog from './components/MeetingCreateDialog.vue'\nimport ChatPanel from './components/ChatPanel.vue'\n\n// 全局方法:业务方零编码接入,调 ImUI.chat/call/meeting 即弹窗\n// 对方收到邀请自动弹 CallDialog(incoming)/MeetingInvite(由本模块监听 client 事件触发)\n//\n// 接线口径与 apps/demo 的 App.vue 一致(该链路已端到端验证):\n// 主叫:init 成功 → CallDialog(outgoing) → call:accepted → sess.connect()+enableDevices → CallWindow\n// 被叫:call:invite → CallDialog(incoming) → accepted → CallWindow\n// 会议:create 成功 → MeetWindow(host);被邀 → MeetingInvite → accepted → MeetWindow(participant)\n//\n// autoPopups=false 时关闭自动弹窗监听(业务方完全自定义 UI),ImUI.* 主动方法仍可用\n\nlet client: ImClient\n// setupGlobalOptions 传入的定制项(ImUI.* 主动方法弹窗时复用,如 resolveUser)\nlet uiOptions: SetupUIOptions | undefined\n// 已注册监听的 client 实例(幂等守卫用)\nlet wiredClient: ImClient | null = null\n\n// 会议邀请弹窗引用:按 meetingId 索引,dissolved/kicked 事件来时按 meetingId 清弹窗\ninterface MountedInvite {\n subApp: App\n div: HTMLDivElement\n}\nconst mountedInvites = new Map<string, MountedInvite>()\n\n// 通话等待/响铃弹窗是否挂着(忙线判断用)。\n// 不用 client.sessionState——它是登录时的一次性快照,之后不更新(见 core/index.ts 注释)\nlet callRingingMounted = false\n\nfunction callUiBusy(): boolean {\n return callRingingMounted || !!client.call?.currentSession || !!client.meeting?.currentSession\n}\n\n// 会议邀请弹窗堆叠容器:fixed 右上角、多条纵向排列。\n// MeetingInvite 组件自身是 position:relative(260px 卡片),不带定位——\n// 旧 demo 靠外层 .meeting-invites-stack 容器定位;动态 mount 必须提供同款容器,\n// 否则弹窗渲染在 body 文档流末尾(视口外),表现为\"收不到邀请\"\nfunction ensureInviteStack(): HTMLElement {\n let el = document.querySelector<HTMLElement>('div.im-invite-stack')\n if (el) return el\n el = document.createElement('div')\n el.className = 'im-invite-stack'\n el.style.position = 'fixed'\n el.style.top = '24px'\n el.style.right = '24px'\n el.style.zIndex = '10000'\n el.style.display = 'flex'\n el.style.flexDirection = 'column'\n el.style.gap = '12px'\n el.style.pointerEvents = 'none'\n document.body.appendChild(el)\n return el\n}\n\n// uid → 展示名(业务方未维护映射时的兜底:SDK 用户目录)\nasync function nameOf(uid: string): Promise<string> {\n const dir = client.users\n if (dir?.resolve) {\n const u = await dir.resolve(uid).catch(() => undefined)\n if (u?.name) return u.name\n }\n return uid\n}\n\nexport interface SetupUIOptions {\n autoPopups?: boolean\n // uid → { name, dept? } 自定义解析器:透传给自动弹出的 MeetWindow\n // (后端成员表 name 不全/有业务侧别名时用;缺省走 SDK 用户目录)\n resolveUser?: (uid: string) => { name: string; dept?: string } | undefined\n // 会议中「+」添加成员的自定义选人:业务方弹自己的 UI 并 resolve 所选 uid 列表\n // (返回 [] 表示取消),透传给自动弹出的 MeetWindow——SDK 统一完成 reinvite + 刷新。\n // 不传用内置选人弹窗(数据源 SDK 用户目录)\n invitePicker?: () => Promise<string[]>\n}\n\nexport function setupGlobalMethods(imClient: ImClient, vueApp: App, opts?: SetupUIOptions) {\n client = imClient\n uiOptions = opts\n setRootApp(vueApp)\n\n // 注入到 window,供 Vue 组件内部 getClient() 拿到\n // 严格 provide/inject 模式可由业务方自行实现,这里用全局引用简化跨组件访问\n setImClient(imClient)\n\n // 幂等守卫:同一 client 重复调用(单例复用 + 每次登录都调 setupImUI 的集成方式)\n // 只刷新上面的 client/options/rootApp,不重复注册监听——重复注册会导致每条事件弹两个窗。\n // 换新 client(如 demo 每次登录 new 一个)则正常全量注册,旧实例随丢弃被回收。\n if (wiredClient === imClient) return\n wiredClient = imClient\n\n // ===== 监听邀请事件,自动弹窗(autoPopups=false 可整体关闭) =====\n if (opts?.autoPopups !== false) {\n client.on('call:invite', (e: { callId: string; from: string; callType: CallType }) => {\n if (callUiBusy()) {\n // 会话冲突:对新 callId 直接回 busy。注意不能走 client.call.end('busy')——\n // 它在 currentSession 存在(正在通话/呼叫中)时会挂断当前这路通话\n client.transport.callEnd(e.callId, 'busy').catch(() => {})\n return\n }\n callRingingMounted = true\n mountComponent(CallDialog, {\n mode: 'incoming',\n peerUid: e.from,\n callType: e.callType,\n callId: e.callId,\n onAccepted: (session: unknown) => {\n // 被叫接听成功:accept 内部已完成 connect,直接弹通话窗口\n const sess = session as CallSession\n mountComponent(CallWindow, { mode: 'callee', callType: sess.callType })\n },\n onClose: () => {\n callRingingMounted = false\n },\n })\n })\n\n client.on('meeting:invite', async (e) => {\n // 已在同一会议中:忽略(避免重复弹)\n if (client.meeting?.currentSession) return\n // 已登出(登出瞬间在途的 pending 邀请拉取回调才会出现此态):忽略,避免登出后弹过期邀请\n if (client.realtime.state !== 'connected') return\n // 异步校验后端 pending:Centrifugo recovery 重放 stale publication 时丢弃\n try {\n const resp = await client.transport.meetingPendingInvites()\n const valid = (resp.invites || []).some((p) => p.meetingId === e.meetingId)\n if (!valid) {\n console.warn('[im-ui] stale meeting_invite 丢弃(会议已结束)', e.meetingId)\n return\n }\n } catch (err) {\n console.warn('[im-ui] invite 校验失败,默认透传', err)\n }\n // 同 meetingId 已弹过:先清旧的(避免堆叠),再 mount 新的(覆盖 reinvite 场景)\n unmountRef(mountedInvites.get(e.meetingId) || null)\n mountedInvites.delete(e.meetingId)\n const ref = mountComponent(\n MeetingInvite,\n {\n invite: e,\n conflict: callUiBusy(),\n onAccepted: () => {\n // 接听成功:弹会议窗口(accept 内部已建 currentSession)\n const sess = client.meeting?.currentSession\n mountComponent(MeetWindow, {\n mode: 'participant',\n title: sess?.title || '会议',\n resolveUser: opts?.resolveUser,\n invitePicker: opts?.invitePicker,\n })\n },\n onClose: () => {\n mountedInvites.delete(e.meetingId)\n },\n },\n // 挂进 fixed 右上角堆叠容器(MeetingInvite 自身无定位)\n ensureInviteStack(),\n )\n mountedInvites.set(e.meetingId, ref)\n })\n\n // 会议解散:清对应邀请弹窗(MeetWindow 自身监听 dissolved 显示结束态,无需在此处理)\n client.on('meeting:dissolved', (e: { meetingId: string }) => {\n unmountRef(mountedInvites.get(e.meetingId) || null)\n mountedInvites.delete(e.meetingId)\n })\n\n // 被主持人踢出:清对应邀请弹窗(MeetWindow 自身监听 kicked 显示结束态)\n client.on('meeting:kicked', (e: { meetingId: string }) => {\n unmountRef(mountedInvites.get(e.meetingId) || null)\n mountedInvites.delete(e.meetingId)\n })\n }\n\n client.on('session:kicked', (e) => {\n alert(\n `您的账号在其他 ${e.clientType === 4 ? 'Web' : e.clientType === 1 ? 'PC' : '移动端'} 登录,已强制下线`,\n )\n window.location.reload()\n })\n\n // beforeunload 兜底:通话/会议中关闭或刷新页面时直发挂断/离会。\n // 必须用 sendBeacon(keepalive)——普通 fetch 在 unload 时被取消,会留僵尸记录\n window.addEventListener('beforeunload', () => {\n const callSess = client?.call?.currentSession\n if (callSess) {\n client.transport.sendBeacon('/call/end', { callId: callSess.callId, reason: 'user_left' })\n }\n const meetSess = client?.meeting?.currentSession\n if (meetSess) {\n client.transport.sendBeacon(`/meeting/${meetSess.meetingId}/leave`, {})\n }\n })\n}\n\n// 暴露给业务方的全局方法\nexport const ImUI = {\n // 弹出与某用户的聊天框\n chat(userId: string) {\n mountComponent(ChatPanel, { to: userId })\n },\n\n // 发起 1:1 通话(主叫)\n async call(userId: string, type: CallType = 'video') {\n let sess: CallSession | null = null\n try {\n // 先 init 成功再弹窗:init 失败(对方忙/不存在)时不该出现死窗口\n sess = await client.call.init(userId, type)\n } catch (err) {\n toast.error(friendlyApiError(err, '发起通话失败'))\n return\n }\n callRingingMounted = true\n let dialog: MountedRef | null = null\n dialog = mountComponent(CallDialog, {\n mode: 'outgoing',\n peerUid: userId,\n callType: type,\n onConnected: () => {\n // 被叫已接听:主叫 connect + 开设备 → 关等待窗、弹通话窗口\n unmountRef(dialog)\n dialog = null\n callRingingMounted = false\n sess!\n .connect()\n .then(() => {\n sess!.enableDevices().catch((err) => console.warn('[im] 主叫 enableDevices 异常', err))\n mountComponent(CallWindow, { mode: 'caller', callType: type })\n })\n .catch((err) => {\n console.error('[im] 主叫 connect 失败', err)\n toast.error('连接建立失败,通话已取消')\n client.call.end('user_left').catch(() => {})\n })\n },\n })\n },\n\n // 发起会议(主持人)。\n // 传 invitees:直接建会;不传:弹内置选人弹窗(MeetingCreateDialog),确认后建会\n // 选人弹窗默认排除自己(内部 resolve(client.uid),无需外部传 myUid)\n // 防重入:选人弹窗已开 / 建会请求进行中时直接忽略重复调用——\n // 建会到 MeetWindow 打开之间有空窗(无摄像头/麦克风时更长),连点主屏按钮会叠弹窗/重复建会报错\n async meeting(opts?: { invitees?: string[]; title?: string }) {\n if (!opts?.invitees) {\n if (createDialogRef) return\n createDialogRef = mountComponent(MeetingCreateDialog, {\n onConfirm: (uids: string[], title: string) => {\n ImUI.meeting({ invitees: uids, title })\n },\n onClose: () => {\n createDialogRef = null\n },\n })\n return\n }\n if (meetingCreating) return\n meetingCreating = true\n try {\n await createMeeting(opts.invitees, opts.title || '即时会议')\n } finally {\n meetingCreating = false\n }\n },\n}\n\n// 会议发起防重入状态(ImUI.meeting 使用)\nlet createDialogRef: MountedRef | null = null\nlet meetingCreating = false\n\n// 建会流程主体:create 成功 → 挂 MeetWindow;全员忙 → 主动 leave 清理\nasync function createMeeting(invitees: string[], title: string) {\n let result: { busyInvitees?: string[] }\n try {\n result = await client.meeting.create(invitees, title)\n } catch (err) {\n toast.error(friendlyApiError(err, '创建会议失败'))\n return\n }\n const busyList = result.busyInvitees || []\n if (invitees.length > 0 && busyList.length >= invitees.length) {\n // 全员繁忙:会议已在 server 创建,主持人主动 leave 清理,不开会议窗口\n client.meeting.currentSession?.leave(client.transport).catch(() => {})\n const names = (await Promise.all(busyList.map(nameOf))).join('、')\n toast.error(`${names} 均正忙,会议未能创建`)\n return\n }\n // create 成功后 currentSession 已建立,MeetWindow 挂载时才能拿到\n mountComponent(MeetWindow, {\n mode: 'host',\n title,\n resolveUser: uiOptions?.resolveUser,\n invitePicker: uiOptions?.invitePicker,\n })\n if (busyList.length > 0) {\n const names = (await Promise.all(busyList.map(nameOf))).join('、')\n toast.info(`${names} 正忙,未能邀请加入会议`)\n }\n}\n","<script setup lang=\"ts\">\nimport { ref, onMounted, onBeforeUnmount } from 'vue'\nimport type { CallType } from '@quitting/contract'\nimport vDraggable from '../directives/draggable'\n\n// 主叫呼叫等待弹窗(不显示视频)\n// 在 SDK CallController.init 调用后、被叫接听前显示\n// 主叫收到 call:accepted → emit('connected') → 父组件切换到 CallWindow\n// 主叫收到 call:rejected → 显示\"对方拒接\" 1.5s 后 emit('close')\n// 主叫收到 call:busy → 显示\"对方繁忙\" 1.5s 后 emit('close')\n// 主叫收到 call:no_answer → 显示\"无人接听\" 1.5s 后 emit('close')\n// 主叫点取消 → 调 client.call.end('cancelled') + emit('close')\n\nconst props = defineProps<{\n calleeUid: string\n calleeName?: string\n callType: CallType\n // 可选:超时秒数(默认 30s),到时显示\"无人接听\"\n timeoutSec?: number\n}>()\n\nconst emit = defineEmits<{\n (e: 'close'): void\n (e: 'connected'): void\n}>()\n\nconst status = ref<'ringing' | 'rejected' | 'busy' | 'no_answer' | 'cancelled'>('ringing')\nconst statusText = ref('')\nconst unsubscribers: Array<() => void> = []\nlet timeoutTimer: ReturnType<typeof setTimeout> | null = null\n\nfunction getClient() {\n return (window as any).__imClient\n}\n\nfunction setStatusAndClose(s: typeof status.value, text: string, delay = 1500) {\n status.value = s\n statusText.value = text\n setTimeout(() => emit('close'), delay)\n}\n\nasync function cancel() {\n // 主叫主动取消:currentSession 存在时 SDK 走 hangup,否则用 callId 兜底\n status.value = 'cancelled'\n statusText.value = '已取消'\n const client = getClient()\n const callId = client?.call?.currentSession?.callId\n await client?.call?.end('cancelled', callId).catch(() => {})\n setTimeout(() => emit('close'), 800)\n}\n\nonMounted(() => {\n const client = getClient()\n if (!client) {\n emit('close')\n return\n }\n\n // 监听被叫响应——全部登记 unsubscribers,卸载时统一清理\n // (只保存 accepted 一个的话,多次呼叫后残留的 stale handler 会驱动已卸载组件)\n unsubscribers.push(\n client.on('call:accepted', () => {\n statusText.value = '对方已接听'\n setTimeout(() => emit('connected'), 300)\n }),\n )\n unsubscribers.push(\n client.on('call:rejected', () => {\n setStatusAndClose('rejected', '对方拒接')\n }),\n )\n unsubscribers.push(\n client.on('call:busy', () => {\n setStatusAndClose('busy', '对方繁忙')\n }),\n )\n unsubscribers.push(\n client.on('call:no_answer', () => {\n setStatusAndClose('no_answer', '无人接听')\n }),\n )\n // 极少情况:主叫取消后后端回推 call_cancelled 给自己——直接关\n unsubscribers.push(\n client.on('call:cancelled', () => {\n setStatusAndClose('cancelled', '已取消', 500)\n }),\n )\n\n const sec = props.timeoutSec ?? 30\n timeoutTimer = setTimeout(() => {\n if (status.value === 'ringing') {\n setStatusAndClose('no_answer', '无人接听')\n }\n }, sec * 1000)\n})\n\nonBeforeUnmount(() => {\n unsubscribers.forEach((fn) => fn())\n if (timeoutTimer) clearTimeout(timeoutTimer)\n})\n</script>\n\n<template>\n <div v-draggable=\"'.call-out-header'\" class=\"call-out-window\">\n <div class=\"call-out-header\">\n <span>{{ callType === 'video' ? '视频通话' : '语音通话' }}</span>\n </div>\n <div class=\"call-out-body\">\n <div :class=\"['call-out-avatar', status === 'ringing' ? 'ringing' : status]\">\n {{ (calleeName || calleeUid)[0].toUpperCase() }}\n </div>\n <div class=\"call-out-name\">{{ calleeName || calleeUid }}</div>\n <div class=\"call-out-status\">\n <span v-if=\"status === 'ringing'\" class=\"ringing-dots\">\n 呼叫中<span class=\"dot\">·</span><span class=\"dot\">·</span><span class=\"dot\">·</span>\n </span>\n <span v-else :class=\"status\">{{ statusText }}</span>\n </div>\n </div>\n <div class=\"call-out-actions\">\n <button v-if=\"status === 'ringing'\" class=\"cancel-btn\" @click=\"cancel\">取消</button>\n </div>\n </div>\n</template>\n\n<style scoped>\n.call-out-window {\n position: fixed;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n width: 320px;\n background: #0f172a;\n border-radius: 0;\n padding: 24px;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);\n color: #fff;\n z-index: 9999;\n text-align: center;\n}\n.call-out-header {\n cursor: move;\n font-size: 13px;\n color: #999;\n padding-bottom: 12px;\n border-bottom: 1px solid #333;\n margin-bottom: 16px;\n}\n.call-out-avatar {\n width: 80px;\n height: 80px;\n border-radius: 50%;\n background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n color: #fff;\n font-size: 36px;\n font-weight: 600;\n line-height: 80px;\n margin: 0 auto 12px;\n position: relative;\n}\n.call-out-avatar.ringing::before {\n content: '';\n position: absolute;\n inset: -8px;\n border: 2px solid #667eea;\n border-radius: 50%;\n animation: pulse 1.5s ease-out infinite;\n}\n.call-out-avatar.rejected,\n.call-out-avatar.cancelled {\n background: #888;\n}\n.call-out-avatar.busy,\n.call-out-avatar.no_answer {\n background: #d4a017;\n}\n@keyframes pulse {\n 0% {\n transform: scale(1);\n opacity: 0.8;\n }\n 100% {\n transform: scale(1.4);\n opacity: 0;\n }\n}\n.call-out-name {\n font-size: 16px;\n font-weight: 600;\n margin-bottom: 8px;\n}\n.call-out-status {\n font-size: 13px;\n color: #999;\n min-height: 20px;\n}\n.call-out-status .rejected,\n.call-out-status .cancelled {\n color: #888;\n}\n.call-out-status .busy,\n.call-out-status .no_answer {\n color: #d4a017;\n}\n.ringing-dots .dot {\n display: inline-block;\n animation: dot-bounce 1.4s infinite;\n}\n.ringing-dots .dot:nth-child(2) {\n animation-delay: 0.2s;\n}\n.ringing-dots .dot:nth-child(3) {\n animation-delay: 0.4s;\n}\n@keyframes dot-bounce {\n 0%,\n 60%,\n 100% {\n opacity: 0.3;\n }\n 30% {\n opacity: 1;\n }\n}\n.call-out-actions {\n margin-top: 24px;\n min-height: 40px;\n}\n.cancel-btn {\n padding: 8px 32px;\n background: #ef4444;\n color: #fff;\n border: none;\n border-radius: 0;\n cursor: pointer;\n font-size: 13px;\n transition: opacity 0.1s;\n}\n.cancel-btn:active {\n opacity: 0.8;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { ref, onMounted, onBeforeUnmount } from 'vue'\nimport type { CallType } from '@quitting/contract'\nimport vDraggable from '../directives/draggable'\n\n// 主叫呼叫等待弹窗(不显示视频)\n// 在 SDK CallController.init 调用后、被叫接听前显示\n// 主叫收到 call:accepted → emit('connected') → 父组件切换到 CallWindow\n// 主叫收到 call:rejected → 显示\"对方拒接\" 1.5s 后 emit('close')\n// 主叫收到 call:busy → 显示\"对方繁忙\" 1.5s 后 emit('close')\n// 主叫收到 call:no_answer → 显示\"无人接听\" 1.5s 后 emit('close')\n// 主叫点取消 → 调 client.call.end('cancelled') + emit('close')\n\nconst props = defineProps<{\n calleeUid: string\n calleeName?: string\n callType: CallType\n // 可选:超时秒数(默认 30s),到时显示\"无人接听\"\n timeoutSec?: number\n}>()\n\nconst emit = defineEmits<{\n (e: 'close'): void\n (e: 'connected'): void\n}>()\n\nconst status = ref<'ringing' | 'rejected' | 'busy' | 'no_answer' | 'cancelled'>('ringing')\nconst statusText = ref('')\nconst unsubscribers: Array<() => void> = []\nlet timeoutTimer: ReturnType<typeof setTimeout> | null = null\n\nfunction getClient() {\n return (window as any).__imClient\n}\n\nfunction setStatusAndClose(s: typeof status.value, text: string, delay = 1500) {\n status.value = s\n statusText.value = text\n setTimeout(() => emit('close'), delay)\n}\n\nasync function cancel() {\n // 主叫主动取消:currentSession 存在时 SDK 走 hangup,否则用 callId 兜底\n status.value = 'cancelled'\n statusText.value = '已取消'\n const client = getClient()\n const callId = client?.call?.currentSession?.callId\n await client?.call?.end('cancelled', callId).catch(() => {})\n setTimeout(() => emit('close'), 800)\n}\n\nonMounted(() => {\n const client = getClient()\n if (!client) {\n emit('close')\n return\n }\n\n // 监听被叫响应——全部登记 unsubscribers,卸载时统一清理\n // (只保存 accepted 一个的话,多次呼叫后残留的 stale handler 会驱动已卸载组件)\n unsubscribers.push(\n client.on('call:accepted', () => {\n statusText.value = '对方已接听'\n setTimeout(() => emit('connected'), 300)\n }),\n )\n unsubscribers.push(\n client.on('call:rejected', () => {\n setStatusAndClose('rejected', '对方拒接')\n }),\n )\n unsubscribers.push(\n client.on('call:busy', () => {\n setStatusAndClose('busy', '对方繁忙')\n }),\n )\n unsubscribers.push(\n client.on('call:no_answer', () => {\n setStatusAndClose('no_answer', '无人接听')\n }),\n )\n // 极少情况:主叫取消后后端回推 call_cancelled 给自己——直接关\n unsubscribers.push(\n client.on('call:cancelled', () => {\n setStatusAndClose('cancelled', '已取消', 500)\n }),\n )\n\n const sec = props.timeoutSec ?? 30\n timeoutTimer = setTimeout(() => {\n if (status.value === 'ringing') {\n setStatusAndClose('no_answer', '无人接听')\n }\n }, sec * 1000)\n})\n\nonBeforeUnmount(() => {\n unsubscribers.forEach((fn) => fn())\n if (timeoutTimer) clearTimeout(timeoutTimer)\n})\n</script>\n\n<template>\n <div v-draggable=\"'.call-out-header'\" class=\"call-out-window\">\n <div class=\"call-out-header\">\n <span>{{ callType === 'video' ? '视频通话' : '语音通话' }}</span>\n </div>\n <div class=\"call-out-body\">\n <div :class=\"['call-out-avatar', status === 'ringing' ? 'ringing' : status]\">\n {{ (calleeName || calleeUid)[0].toUpperCase() }}\n </div>\n <div class=\"call-out-name\">{{ calleeName || calleeUid }}</div>\n <div class=\"call-out-status\">\n <span v-if=\"status === 'ringing'\" class=\"ringing-dots\">\n 呼叫中<span class=\"dot\">·</span><span class=\"dot\">·</span><span class=\"dot\">·</span>\n </span>\n <span v-else :class=\"status\">{{ statusText }}</span>\n </div>\n </div>\n <div class=\"call-out-actions\">\n <button v-if=\"status === 'ringing'\" class=\"cancel-btn\" @click=\"cancel\">取消</button>\n </div>\n </div>\n</template>\n\n<style scoped>\n.call-out-window {\n position: fixed;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n width: 320px;\n background: #0f172a;\n border-radius: 0;\n padding: 24px;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);\n color: #fff;\n z-index: 9999;\n text-align: center;\n}\n.call-out-header {\n cursor: move;\n font-size: 13px;\n color: #999;\n padding-bottom: 12px;\n border-bottom: 1px solid #333;\n margin-bottom: 16px;\n}\n.call-out-avatar {\n width: 80px;\n height: 80px;\n border-radius: 50%;\n background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n color: #fff;\n font-size: 36px;\n font-weight: 600;\n line-height: 80px;\n margin: 0 auto 12px;\n position: relative;\n}\n.call-out-avatar.ringing::before {\n content: '';\n position: absolute;\n inset: -8px;\n border: 2px solid #667eea;\n border-radius: 50%;\n animation: pulse 1.5s ease-out infinite;\n}\n.call-out-avatar.rejected,\n.call-out-avatar.cancelled {\n background: #888;\n}\n.call-out-avatar.busy,\n.call-out-avatar.no_answer {\n background: #d4a017;\n}\n@keyframes pulse {\n 0% {\n transform: scale(1);\n opacity: 0.8;\n }\n 100% {\n transform: scale(1.4);\n opacity: 0;\n }\n}\n.call-out-name {\n font-size: 16px;\n font-weight: 600;\n margin-bottom: 8px;\n}\n.call-out-status {\n font-size: 13px;\n color: #999;\n min-height: 20px;\n}\n.call-out-status .rejected,\n.call-out-status .cancelled {\n color: #888;\n}\n.call-out-status .busy,\n.call-out-status .no_answer {\n color: #d4a017;\n}\n.ringing-dots .dot {\n display: inline-block;\n animation: dot-bounce 1.4s infinite;\n}\n.ringing-dots .dot:nth-child(2) {\n animation-delay: 0.2s;\n}\n.ringing-dots .dot:nth-child(3) {\n animation-delay: 0.4s;\n}\n@keyframes dot-bounce {\n 0%,\n 60%,\n 100% {\n opacity: 0.3;\n }\n 30% {\n opacity: 1;\n }\n}\n.call-out-actions {\n margin-top: 24px;\n min-height: 40px;\n}\n.cancel-btn {\n padding: 8px 32px;\n background: #ef4444;\n color: #fff;\n border: none;\n border-radius: 0;\n cursor: pointer;\n font-size: 13px;\n transition: opacity 0.1s;\n}\n.cancel-btn:active {\n opacity: 0.8;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { ref, onMounted, onBeforeUnmount } from 'vue'\nimport type { CallInviteEvent } from '@quitting/contract'\n// 被叫响铃中循环播放 callee.mp3\nimport ringCalleeUrl from '../sounds/ring-callee.mp3'\nimport Icon from './Icon.vue'\n\nconst props = defineProps<{\n invite: CallInviteEvent\n onClose?: () => void\n}>()\n\nconst emit = defineEmits<{\n (e: 'close'): void\n // 接听成功后携带 session 通知父组件渲染 CallWindow\n // 不能依赖父组件 watch currentSession——ImClient 不是 reactive,watch 不会触发\n (e: 'accepted', session: unknown): void\n}>()\n\nconst ringAudio = ref<HTMLAudioElement | null>(null)\n\nfunction getClient() {\n return (window as any).__imClient\n}\n\nfunction startRing() {\n if (!ringAudio.value) return\n ringAudio.value.currentTime = 0\n ringAudio.value.play().catch(() => {\n // 自动播放策略可能拒绝,不报错\n })\n}\n\nfunction stopRing() {\n if (!ringAudio.value) return\n ringAudio.value.pause()\n ringAudio.value.currentTime = 0\n}\n\nconst unsubscribers: Array<() => void> = []\n\nonMounted(() => {\n startRing()\n // 主叫取消 / 无人接听超时,被叫端响铃都应自动关\n // no_answer 必须监听:后端超时会同时通知主叫和被叫,只等 cancelled 会永不停止响铃\n const client = getClient()\n if (client) {\n unsubscribers.push(\n client.on('call:cancelled', () => {\n stopRing()\n emit('close')\n }),\n )\n unsubscribers.push(\n client.on('call:no_answer', () => {\n stopRing()\n emit('close')\n }),\n )\n }\n})\n\nonBeforeUnmount(() => {\n stopRing()\n unsubscribers.forEach((fn) => fn())\n})\n\nasync function accept() {\n stopRing()\n const client = getClient()\n try {\n const session = await client?.call?.accept(props.invite.callId, props.invite.callType)\n emit('accepted', session)\n emit('close')\n } catch (err: any) {\n // 规则1:多端并发进入,本端是后进端被拒(另一端已接听)\n const msg = String(err?.message || err || '')\n if (msg.includes('conflict') || msg.includes('entered elsewhere')) {\n alert('已在其他设备接听,本端通话已关闭')\n } else {\n alert('网络异常,请稍后重试')\n }\n emit('close')\n }\n}\n\nasync function reject() {\n stopRing()\n const client = getClient()\n // 被叫拒接:currentSession 为 null(还没 accept),必须显式传 callId 让 SDK 调后端 /call/end\n await client?.call?.end('rejected', props.invite.callId)\n emit('close')\n}\n</script>\n\n<template>\n <div class=\"im-incoming-ring\">\n <audio ref=\"ringAudio\" :src=\"ringCalleeUrl\" loop preload=\"auto\" />\n <div class=\"im-ring-content\">\n <div class=\"im-ring-avatar\"><Icon name=\"answer\" :size=\"24\" color=\"#fff\" /></div>\n <div class=\"im-ring-info\">\n <div class=\"im-ring-name\">{{ invite.from }}</div>\n <div class=\"im-ring-type\">\n {{ invite.callType === 'video' ? '视频通话' : '语音通话' }}\n </div>\n </div>\n <div class=\"im-ring-actions\">\n <button class=\"im-reject\" @click=\"reject\">拒接</button>\n <button class=\"im-accept\" @click=\"accept\">接听</button>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.im-incoming-ring {\n position: fixed;\n top: 24px;\n right: 24px;\n background: #0f172a;\n border-radius: 0;\n padding: 16px;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);\n z-index: 10000;\n animation: slide-in 0.3s ease;\n}\n@keyframes slide-in {\n from {\n transform: translateX(120%);\n }\n to {\n transform: translateX(0);\n }\n}\n.im-ring-content {\n display: flex;\n align-items: center;\n gap: 12px;\n min-width: 280px;\n}\n.im-ring-avatar {\n width: 48px;\n height: 48px;\n border-radius: 50%;\n background: #52c41a;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 24px;\n}\n.im-ring-info {\n flex: 1;\n}\n.im-ring-name {\n font-weight: 600;\n font-size: 15px;\n color: rgba(255, 255, 255, 0.85);\n}\n.im-ring-type {\n font-size: 12px;\n color: rgba(255, 255, 255, 0.55);\n margin-top: 2px;\n}\n.im-ring-actions {\n display: flex;\n gap: 8px;\n}\n.im-ring-actions button {\n padding: 6px 14px;\n border: none;\n border-radius: 0;\n cursor: pointer;\n font-size: 13px;\n color: #fff;\n}\n.im-reject {\n background: #ef4444;\n}\n.im-accept {\n background: #52c41a;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { ref, onMounted, onBeforeUnmount } from 'vue'\nimport type { CallInviteEvent } from '@quitting/contract'\n// 被叫响铃中循环播放 callee.mp3\nimport ringCalleeUrl from '../sounds/ring-callee.mp3'\nimport Icon from './Icon.vue'\n\nconst props = defineProps<{\n invite: CallInviteEvent\n onClose?: () => void\n}>()\n\nconst emit = defineEmits<{\n (e: 'close'): void\n // 接听成功后携带 session 通知父组件渲染 CallWindow\n // 不能依赖父组件 watch currentSession——ImClient 不是 reactive,watch 不会触发\n (e: 'accepted', session: unknown): void\n}>()\n\nconst ringAudio = ref<HTMLAudioElement | null>(null)\n\nfunction getClient() {\n return (window as any).__imClient\n}\n\nfunction startRing() {\n if (!ringAudio.value) return\n ringAudio.value.currentTime = 0\n ringAudio.value.play().catch(() => {\n // 自动播放策略可能拒绝,不报错\n })\n}\n\nfunction stopRing() {\n if (!ringAudio.value) return\n ringAudio.value.pause()\n ringAudio.value.currentTime = 0\n}\n\nconst unsubscribers: Array<() => void> = []\n\nonMounted(() => {\n startRing()\n // 主叫取消 / 无人接听超时,被叫端响铃都应自动关\n // no_answer 必须监听:后端超时会同时通知主叫和被叫,只等 cancelled 会永不停止响铃\n const client = getClient()\n if (client) {\n unsubscribers.push(\n client.on('call:cancelled', () => {\n stopRing()\n emit('close')\n }),\n )\n unsubscribers.push(\n client.on('call:no_answer', () => {\n stopRing()\n emit('close')\n }),\n )\n }\n})\n\nonBeforeUnmount(() => {\n stopRing()\n unsubscribers.forEach((fn) => fn())\n})\n\nasync function accept() {\n stopRing()\n const client = getClient()\n try {\n const session = await client?.call?.accept(props.invite.callId, props.invite.callType)\n emit('accepted', session)\n emit('close')\n } catch (err: any) {\n // 规则1:多端并发进入,本端是后进端被拒(另一端已接听)\n const msg = String(err?.message || err || '')\n if (msg.includes('conflict') || msg.includes('entered elsewhere')) {\n alert('已在其他设备接听,本端通话已关闭')\n } else {\n alert('网络异常,请稍后重试')\n }\n emit('close')\n }\n}\n\nasync function reject() {\n stopRing()\n const client = getClient()\n // 被叫拒接:currentSession 为 null(还没 accept),必须显式传 callId 让 SDK 调后端 /call/end\n await client?.call?.end('rejected', props.invite.callId)\n emit('close')\n}\n</script>\n\n<template>\n <div class=\"im-incoming-ring\">\n <audio ref=\"ringAudio\" :src=\"ringCalleeUrl\" loop preload=\"auto\" />\n <div class=\"im-ring-content\">\n <div class=\"im-ring-avatar\"><Icon name=\"answer\" :size=\"24\" color=\"#fff\" /></div>\n <div class=\"im-ring-info\">\n <div class=\"im-ring-name\">{{ invite.from }}</div>\n <div class=\"im-ring-type\">\n {{ invite.callType === 'video' ? '视频通话' : '语音通话' }}\n </div>\n </div>\n <div class=\"im-ring-actions\">\n <button class=\"im-reject\" @click=\"reject\">拒接</button>\n <button class=\"im-accept\" @click=\"accept\">接听</button>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.im-incoming-ring {\n position: fixed;\n top: 24px;\n right: 24px;\n background: #0f172a;\n border-radius: 0;\n padding: 16px;\n box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);\n z-index: 10000;\n animation: slide-in 0.3s ease;\n}\n@keyframes slide-in {\n from {\n transform: translateX(120%);\n }\n to {\n transform: translateX(0);\n }\n}\n.im-ring-content {\n display: flex;\n align-items: center;\n gap: 12px;\n min-width: 280px;\n}\n.im-ring-avatar {\n width: 48px;\n height: 48px;\n border-radius: 50%;\n background: #52c41a;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 24px;\n}\n.im-ring-info {\n flex: 1;\n}\n.im-ring-name {\n font-weight: 600;\n font-size: 15px;\n color: rgba(255, 255, 255, 0.85);\n}\n.im-ring-type {\n font-size: 12px;\n color: rgba(255, 255, 255, 0.55);\n margin-top: 2px;\n}\n.im-ring-actions {\n display: flex;\n gap: 8px;\n}\n.im-ring-actions button {\n padding: 6px 14px;\n border: none;\n border-radius: 0;\n cursor: pointer;\n font-size: 13px;\n color: #fff;\n}\n.im-reject {\n background: #ef4444;\n}\n.im-accept {\n background: #52c41a;\n}\n</style>\n","import type { App, Plugin } from 'vue'\nimport type { ImClient } from '@quitting/core'\n\nimport { setupGlobalMethods } from './global-methods'\nimport Icon from './components/Icon.vue'\nimport CallWindow from './components/CallWindow.vue'\nimport CallDialog from './components/CallDialog.vue'\nimport CallOutgoing from './components/CallOutgoing.vue'\nimport MeetWindow from './components/MeetWindow.vue'\nimport IncomingRing from './components/IncomingRing.vue'\nimport MeetingInvite from './components/MeetingInvite.vue'\nimport ChatPanel from './components/ChatPanel.vue'\nimport UserPickerList from './components/UserPickerList.vue'\nimport UserPickerDialog from './components/UserPickerDialog.vue'\nimport MeetingCreateDialog from './components/MeetingCreateDialog.vue'\n\n// 半 UI SDK 插件\n// 用法(业务方 main.ts):\n// import ImUI from '@quitting/ui'\n// import '@quitting/ui/style.css'\n// app.use(ImUI, { client: imClient })\n//\n// 之后任何业务组件都可调:\n// ImUI.chat('user-1')\n// ImUI.call('user-1', 'video')\n// ImUI.meeting({ invitees: ['user-1','user-2'], title: '周会' })\n// ImUI.meeting() // 不传参会人 → 弹内置选人窗\n//\n// client 在登录后才创建时(无法 main.ts 装插件),登录后手动调:\n// import { setupImUI } from '@quitting/ui'\n// setupImUI(client, getCurrentInstance()!.appContext.app)\nexport interface ImUIOptions {\n client: ImClient\n // 关闭自动弹窗监听(call:invite / meeting:invite 等不再自动弹窗),\n // 业务方完全自定义 UI 时置 false;ImUI.* 主动方法不受影响。默认 true\n autoPopups?: boolean\n // uid → { name, dept? } 自定义解析器:透传给自动弹出的 MeetWindow\n // (后端成员表 name 不全/有业务侧别名时用;缺省走 SDK 用户目录)\n resolveUser?: (uid: string) => { name: string; dept?: string } | undefined\n}\n\nexport const ImUIPlugin: Plugin = {\n install(app: App, options: ImUIOptions) {\n if (!options?.client) {\n throw new Error('ImUI 安装时必须传入 client(已初始化的 ImClient)')\n }\n // 注册全局组件\n app.component('ImIcon', Icon)\n app.component('ImCallWindow', CallWindow)\n app.component('ImCallDialog', CallDialog)\n app.component('ImCallOutgoing', CallOutgoing)\n app.component('ImMeetWindow', MeetWindow)\n app.component('ImIncomingRing', IncomingRing)\n app.component('ImMeetingInvite', MeetingInvite)\n app.component('ImChatPanel', ChatPanel)\n app.component('ImUserPickerList', UserPickerList)\n app.component('ImUserPickerDialog', UserPickerDialog)\n app.component('ImMeetingCreateDialog', MeetingCreateDialog)\n\n // 注入全局方法 + 自动监听邀请事件\n setupGlobalMethods(options.client, app, {\n autoPopups: options.autoPopups,\n resolveUser: options.resolveUser,\n })\n },\n}\n\n// 手动初始化(client 延迟创建场景):等价于 app.use(ImUIPlugin, { client })\nexport function setupImUI(\n client: ImClient,\n app: App,\n opts?: {\n autoPopups?: boolean\n resolveUser?: (uid: string) => { name: string; dept?: string } | undefined\n },\n) {\n setupGlobalMethods(client, app, opts)\n}\n\nexport default ImUIPlugin\n\n// 重新导出组件供按需引入\nexport {\n Icon,\n CallWindow,\n CallDialog,\n CallOutgoing,\n MeetWindow,\n IncomingRing,\n MeetingInvite,\n ChatPanel,\n UserPickerList,\n UserPickerDialog,\n MeetingCreateDialog,\n}\nexport { ImUI } from './global-methods'\n// toast:ImUI 链路错误/提示的默认出口,业务方也可直接用(如 toast.error('...'))\nexport { ImToastApi as ImToast } from './toast'\n// ApiError → 用户可读文案的统一映射(自定义错误处理场景用)\nexport { friendlyApiError } from './error-text'\nexport type { PickerUser } from './types'\n"],"mappings":"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"}