@lsamien/yaovi 0.1.4 → 0.1.5
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.
- package/build-info.json +12 -4
- package/dist/assets/{AboutDialog-CDpS-zAu.js → AboutDialog-2Zq3b7ma.js} +2 -2
- package/dist/assets/{AboutDialog-CDpS-zAu.js.map → AboutDialog-2Zq3b7ma.js.map} +1 -1
- package/dist/assets/{BotAutomationsView-RXBQzdfc.js → BotAutomationsView-C4EwHtYy.js} +2 -2
- package/dist/assets/{BotAutomationsView-RXBQzdfc.js.map → BotAutomationsView-C4EwHtYy.js.map} +1 -1
- package/dist/assets/{BotControlSettingsPanel-GQEVHXbw.js → BotControlSettingsPanel-DJ71O3Kh.js} +2 -2
- package/dist/assets/{BotControlSettingsPanel-GQEVHXbw.js.map → BotControlSettingsPanel-DJ71O3Kh.js.map} +1 -1
- package/dist/assets/{BotPluginsDialog-HIhXqyGn.js → BotPluginsDialog-Cgqn4RlP.js} +2 -2
- package/dist/assets/{BotPluginsDialog-HIhXqyGn.js.map → BotPluginsDialog-Cgqn4RlP.js.map} +1 -1
- package/dist/assets/{BotSettingsPanel-Bkqal3lR.js → BotSettingsPanel-C_1zAGWJ.js} +3 -3
- package/dist/assets/{BotSettingsPanel-Bkqal3lR.js.map → BotSettingsPanel-C_1zAGWJ.js.map} +1 -1
- package/dist/assets/{ComputerPanel-Dt73pgTG.js → ComputerPanel-CuVl9oX7.js} +2 -2
- package/dist/assets/{ComputerPanel-Dt73pgTG.js.map → ComputerPanel-CuVl9oX7.js.map} +1 -1
- package/dist/assets/{ComputerViewerView-ChYgaS9q.js → ComputerViewerView-BoI_aqLr.js} +2 -2
- package/dist/assets/{ComputerViewerView-ChYgaS9q.js.map → ComputerViewerView-BoI_aqLr.js.map} +1 -1
- package/dist/assets/{ConversationsView-EdF5AM1T.css → ConversationsView-aPMgqa0O.css} +1 -1
- package/dist/assets/{ConversationsView-DzcDJZh3.js → ConversationsView-kztie5lF.js} +16 -16
- package/dist/assets/ConversationsView-kztie5lF.js.map +1 -0
- package/dist/assets/{LocalVmSettingsPanel-DLbDb5Tq.js → LocalVmSettingsPanel-BZ1Jeqqk.js} +2 -2
- package/dist/assets/{LocalVmSettingsPanel-DLbDb5Tq.js.map → LocalVmSettingsPanel-BZ1Jeqqk.js.map} +1 -1
- package/dist/assets/{ModelNetworkChoice-Cfyt4Unl.js → ModelNetworkChoice-BQ8m2N3V.js} +2 -2
- package/dist/assets/{ModelNetworkChoice-Cfyt4Unl.js.map → ModelNetworkChoice-BQ8m2N3V.js.map} +1 -1
- package/dist/assets/{StandaloneDialog-Cte829Ai.js → StandaloneDialog-hF3z7bfi.js} +2 -2
- package/dist/assets/{StandaloneDialog-Cte829Ai.js.map → StandaloneDialog-hF3z7bfi.js.map} +1 -1
- package/dist/assets/{UpdateCheckDialog-BaMo_M4n.js → UpdateCheckDialog-Bz8JhRh4.js} +2 -2
- package/dist/assets/{UpdateCheckDialog-BaMo_M4n.js.map → UpdateCheckDialog-Bz8JhRh4.js.map} +1 -1
- package/dist/assets/{UsageView-CklMPG4n.js → UsageView-ZsSbQzIY.js} +2 -2
- package/dist/assets/{UsageView-CklMPG4n.js.map → UsageView-ZsSbQzIY.js.map} +1 -1
- package/dist/assets/WorkspaceKnowledgePanel-E549Ct1E.js +3 -0
- package/dist/assets/{WorkspaceKnowledgePanel-BdbhD3rO.js.map → WorkspaceKnowledgePanel-E549Ct1E.js.map} +1 -1
- package/dist/assets/{WorkspaceRoutinesPanel-VpRoMe1x.js → WorkspaceRoutinesPanel-NFWzy1R-.js} +2 -2
- package/dist/assets/{WorkspaceRoutinesPanel-VpRoMe1x.js.map → WorkspaceRoutinesPanel-NFWzy1R-.js.map} +1 -1
- package/dist/assets/{WorkstreamsView-BwZpuuhs.js → WorkstreamsView-CxZ8YUvP.js} +2 -2
- package/dist/assets/{WorkstreamsView-BwZpuuhs.js.map → WorkstreamsView-CxZ8YUvP.js.map} +1 -1
- package/dist/assets/{index-Cx1SdzRY.js → index-P_kZCZ2-.js} +3 -3
- package/dist/assets/{index-Cx1SdzRY.js.map → index-P_kZCZ2-.js.map} +1 -1
- package/dist/assets/{pdf-CoNh_Rmj.js → pdf-CNS_Rszb.js} +2 -2
- package/dist/assets/{pdf-CoNh_Rmj.js.map → pdf-CNS_Rszb.js.map} +1 -1
- package/dist/index.html +1 -1
- package/dist-server/server/workspaceCollaboration.js +9 -12
- package/dist-server/server/workspaceStore.js +3 -1
- package/dist-server/shared/peerExchange.js +26 -0
- package/package.json +1 -1
- package/release.json +3 -3
- package/dist/assets/ConversationsView-DzcDJZh3.js.map +0 -1
- package/dist/assets/WorkspaceKnowledgePanel-BdbhD3rO.js +0 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StandaloneDialog-
|
|
1
|
+
{"version":3,"file":"StandaloneDialog-hF3z7bfi.js","names":[],"sources":["../../src/client/components/common/StandaloneDialog.vue","../../src/client/components/common/StandaloneDialog.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { nextTick, onBeforeUnmount, onMounted, ref } from 'vue'\nimport AppIcon from './AppIcon.vue'\nconst props = defineProps<{ title: string; compact?: boolean; wide?: boolean; settings?: boolean; beforeClose?: () => boolean }>()\nconst emit = defineEmits<{ close: [] }>()\nconst dialog = ref<HTMLDialogElement>()\nconst returnFocus = document.activeElement instanceof HTMLElement ? document.activeElement : undefined\nfunction close() { if (props.beforeClose?.() !== false) emit('close') }\nfunction backdrop(event: MouseEvent) {\n if (event.target !== dialog.value) return\n const rect = dialog.value.getBoundingClientRect()\n if (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom) close()\n}\nonMounted(async () => { await nextTick(); dialog.value?.showModal() })\nonBeforeUnmount(() => { dialog.value?.close(); void nextTick(() => { if (returnFocus?.isConnected) returnFocus.focus() }) })\n</script>\n<template>\n <Teleport to=\"body\"><dialog ref=\"dialog\" class=\"standalone-dialog\" :class=\"{ 'standalone-dialog--compact': compact, 'standalone-dialog--wide': wide, 'standalone-dialog--settings': settings }\" :aria-label=\"title\" @cancel.prevent=\"close\" @click=\"backdrop\">\n <header><h2>{{ title }}</h2><button type=\"button\" :aria-label=\"`关闭${title}`\" autofocus @click=\"close\"><AppIcon name=\"close\" :size=\"20\" /></button></header>\n <div class=\"standalone-dialog__content\"><slot /></div>\n </dialog></Teleport>\n</template>\n<style scoped>\n.standalone-dialog--compact.standalone-dialog--wide{width:min(480px,calc(100vw - 32px))}\n.standalone-dialog{width:min(1040px,calc(100vw - 48px));height:min(780px,calc(100dvh - 48px));max-width:none;max-height:none;margin:auto;padding:0;border:1px solid var(--line);border-radius:22px;background:var(--surface);color:var(--text-primary);box-shadow:0 24px 100px #0005;overflow:hidden}.standalone-dialog[open]{display:flex;flex-direction:column}.standalone-dialog::backdrop{background:#0007;backdrop-filter:blur(3px)}header{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:20px 24px 16px;border-bottom:1px solid var(--line);flex-shrink:0}h2{font-size:21px;font-weight:650;margin:0;letter-spacing:-.02em}header button{display:grid;place-items:center;width:44px;height:44px;padding:0;flex-shrink:0;border:0;border-radius:10px;color:inherit;background:transparent;cursor:pointer}header button:hover{background:var(--surface-hover)}header button:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.standalone-dialog__content{padding:24px;overflow:auto;min-height:0;flex:1}.standalone-dialog--compact{width:min(380px,calc(100vw - 32px));height:fit-content;max-height:calc(100dvh - 32px);border-radius:18px}.standalone-dialog--compact header{padding:10px 12px 10px 20px}.standalone-dialog--compact h2{font-size:15px}.standalone-dialog--compact .standalone-dialog__content{padding:20px 24px 28px}@media(max-width:600px){.standalone-dialog:not(.standalone-dialog--compact){width:100vw;height:100dvh;border:0;border-radius:0}header{padding:10px 12px 10px 20px}h2{font-size:18px}.standalone-dialog__content{padding:20px}}\n</style>\n<style scoped>\n.standalone-dialog--settings{width:min(900px,calc(100vw - 48px));height:min(650px,calc(100dvh - 64px));border-radius:14px;box-shadow:0 24px 72px #0003}.standalone-dialog--settings>header{height:60px;padding:8px 16px 8px 20px;box-sizing:border-box}.standalone-dialog--settings h2{font-size:18px;font-weight:650}.standalone-dialog--settings .standalone-dialog__content{padding:0;overflow:hidden}.standalone-dialog--settings::backdrop{background:var(--scrim);backdrop-filter:blur(3px)}@media(max-width:600px){.standalone-dialog--settings{width:100vw;height:100dvh;border-radius:0}.standalone-dialog--settings>header{padding-left:20px}}\n</style>\n","<script setup lang=\"ts\">\nimport { nextTick, onBeforeUnmount, onMounted, ref } from 'vue'\nimport AppIcon from './AppIcon.vue'\nconst props = defineProps<{ title: string; compact?: boolean; wide?: boolean; settings?: boolean; beforeClose?: () => boolean }>()\nconst emit = defineEmits<{ close: [] }>()\nconst dialog = ref<HTMLDialogElement>()\nconst returnFocus = document.activeElement instanceof HTMLElement ? document.activeElement : undefined\nfunction close() { if (props.beforeClose?.() !== false) emit('close') }\nfunction backdrop(event: MouseEvent) {\n if (event.target !== dialog.value) return\n const rect = dialog.value.getBoundingClientRect()\n if (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom) close()\n}\nonMounted(async () => { await nextTick(); dialog.value?.showModal() })\nonBeforeUnmount(() => { dialog.value?.close(); void nextTick(() => { if (returnFocus?.isConnected) returnFocus.focus() }) })\n</script>\n<template>\n <Teleport to=\"body\"><dialog ref=\"dialog\" class=\"standalone-dialog\" :class=\"{ 'standalone-dialog--compact': compact, 'standalone-dialog--wide': wide, 'standalone-dialog--settings': settings }\" :aria-label=\"title\" @cancel.prevent=\"close\" @click=\"backdrop\">\n <header><h2>{{ title }}</h2><button type=\"button\" :aria-label=\"`关闭${title}`\" autofocus @click=\"close\"><AppIcon name=\"close\" :size=\"20\" /></button></header>\n <div class=\"standalone-dialog__content\"><slot /></div>\n </dialog></Teleport>\n</template>\n<style scoped>\n.standalone-dialog--compact.standalone-dialog--wide{width:min(480px,calc(100vw - 32px))}\n.standalone-dialog{width:min(1040px,calc(100vw - 48px));height:min(780px,calc(100dvh - 48px));max-width:none;max-height:none;margin:auto;padding:0;border:1px solid var(--line);border-radius:22px;background:var(--surface);color:var(--text-primary);box-shadow:0 24px 100px #0005;overflow:hidden}.standalone-dialog[open]{display:flex;flex-direction:column}.standalone-dialog::backdrop{background:#0007;backdrop-filter:blur(3px)}header{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:20px 24px 16px;border-bottom:1px solid var(--line);flex-shrink:0}h2{font-size:21px;font-weight:650;margin:0;letter-spacing:-.02em}header button{display:grid;place-items:center;width:44px;height:44px;padding:0;flex-shrink:0;border:0;border-radius:10px;color:inherit;background:transparent;cursor:pointer}header button:hover{background:var(--surface-hover)}header button:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.standalone-dialog__content{padding:24px;overflow:auto;min-height:0;flex:1}.standalone-dialog--compact{width:min(380px,calc(100vw - 32px));height:fit-content;max-height:calc(100dvh - 32px);border-radius:18px}.standalone-dialog--compact header{padding:10px 12px 10px 20px}.standalone-dialog--compact h2{font-size:15px}.standalone-dialog--compact .standalone-dialog__content{padding:20px 24px 28px}@media(max-width:600px){.standalone-dialog:not(.standalone-dialog--compact){width:100vw;height:100dvh;border:0;border-radius:0}header{padding:10px 12px 10px 20px}h2{font-size:18px}.standalone-dialog__content{padding:20px}}\n</style>\n<style scoped>\n.standalone-dialog--settings{width:min(900px,calc(100vw - 48px));height:min(650px,calc(100dvh - 64px));border-radius:14px;box-shadow:0 24px 72px #0003}.standalone-dialog--settings>header{height:60px;padding:8px 16px 8px 20px;box-sizing:border-box}.standalone-dialog--settings h2{font-size:18px;font-weight:650}.standalone-dialog--settings .standalone-dialog__content{padding:0;overflow:hidden}.standalone-dialog--settings::backdrop{background:var(--scrim);backdrop-filter:blur(3px)}@media(max-width:600px){.standalone-dialog--settings{width:100vw;height:100dvh;border-radius:0}.standalone-dialog--settings>header{padding-left:20px}}\n</style>\n"],"mappings":"0cAGA,IAAM,EAAQ,EACR,EAAO,EACP,EAAS,EAAuB,EAChC,EAAc,SAAS,yBAAyB,YAAc,SAAS,cAAgB,IAAA,GAC7F,SAAS,GAAQ,CAAM,EAAM,cAAc,IAAM,IAAO,EAAK,OAAO,CAAE,CACtE,SAAS,EAAS,EAAmB,CACnC,GAAI,EAAM,SAAW,EAAO,MAAO,OACnC,IAAM,EAAO,EAAO,MAAM,sBAAsB,GAC5C,EAAM,QAAU,EAAK,MAAQ,EAAM,QAAU,EAAK,OAAS,EAAM,QAAU,EAAK,KAAO,EAAM,QAAU,EAAK,SAAQ,EAAM,CAChI,QACA,EAAU,SAAY,CAAE,MAAM,EAAS,EAAG,EAAO,OAAO,UAAU,CAAE,CAAC,EACrE,MAAsB,CAAE,EAAO,OAAO,MAAM,EAAG,MAAoB,CAAM,GAAa,aAAa,EAAY,MAAM,CAAE,CAAC,CAAE,CAAC,UAGzH,EAAA,EAAA,EAGoB,EAAA,CAHV,GAAG,MAAM,EAAA,CAAC,EAGX,SAAA,CAHuB,QAAA,SAAJ,IAAI,EAAS,MAAK,EAAA,CAAC,oBAAmB,CAAA,6BAAyC,EAAA,QAAO,0BAA6B,EAAA,KAAI,8BAAiC,EAAA,QAAQ,CAAA,CAAA,EAAK,aAAY,EAAA,MAAQ,SAAM,EAAU,EAAK,CAAA,SAAA,CAAA,EAAG,QAAO,CAClP,EAAA,CAAA,EAA2J,SAAA,KAAA,CAAnJ,EAAoB,KAAA,KAAA,EAAb,EAAA,KAAK,EAAA,CAAA,EAAQ,EAAsH,SAAA,CAA9G,KAAK,SAAU,aAAU,KAAO,EAAA,QAAS,UAAA,GAAW,QAAO,CAAO,EAAA,CAAA,EAAmC,EAAA,CAA1B,KAAK,QAAS,KAAM,EACnI,CAAA,CAAA,EAAA,EAAA,CAAA,CAAA,CAAA,EAAA,EAAsD,MAAtD,EAAsD,CAAd,EAAQ,EAAA,OAAA,UAAA,CAAA,EAAA,IAAA,GAAA,EAAA,CAAA,CAAA,CAAA,EAAA,GAAA,CAAA,CAAA,CAAA"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{A as e,Et as t,F as n,M as r,Ot as i,W as a,_ as o,b as s,d as c,et as l,f as u,m as d,p as f,t as p,u as m,v as h}from"./_plugin-vue_export-helper-CD0r-vVb.js";import{B as g}from"./index-
|
|
2
|
-
//# sourceMappingURL=UpdateCheckDialog-
|
|
1
|
+
import{A as e,Et as t,F as n,M as r,Ot as i,W as a,_ as o,b as s,d as c,et as l,f as u,m as d,p as f,t as p,u as m,v as h}from"./_plugin-vue_export-helper-CD0r-vVb.js";import{B as g}from"./index-P_kZCZ2-.js";import{t as _}from"./StandaloneDialog-hF3z7bfi.js";import{c as v,l as y}from"./ConversationsView-kztie5lF.js";var b=[`aria-busy`],x={class:`update-check__status`,role:`status`,"aria-live":`polite`},S={"aria-label":`Web 版本信息`},C={key:0,class:`update-check__error`,role:`alert`},w={key:1,class:`update-check__note`},T=[`href`],E={class:`update-check__actions`},D=[`disabled`],O=p(s({__name:`UpdateCheckDialog`,emits:[`close`,`manage`],setup(s,{emit:p}){let O=p,k=l(),A=l(!0),j=l(!1),M=l(``),N=!1,P=m(()=>A.value?`正在检测更新…`:M.value?`检测失败,请重试`:k.value?.updateAvailable?`发现新版本 ${k.value.latest?.webVersion}`:`当前已是最新版本`),F=m(()=>{try{let e=new URL(k.value?.releasePageUrl||``);return e.protocol===`https:`?e.href:void 0}catch{return}});async function I(){A.value=!0,j.value=!1,M.value=``;try{let e=await v();if(N)return;k.value=e,j.value=!0}catch(e){N||(M.value=e instanceof Error?e.message:`无法连接更新服务`)}finally{N||(A.value=!1)}}return r(async()=>{try{let e=await y();N||(k.value=e)}catch{}N||await I()}),e(()=>{N=!0}),(e,r)=>(n(),u(_,{title:`检测更新`,compact:``,onClose:r[1]||=e=>O(`close`)},{default:a(()=>[c(`section`,{class:`update-check`,"aria-busy":A.value},[c(`div`,{class:t([`update-check__icon`,{spinning:A.value}])},[h(g,{name:A.value?`refresh`:M.value?`alert`:k.value?.updateAvailable?`download`:`check`,size:28},null,8,[`name`])],2),c(`p`,x,i(P.value),1),c(`dl`,S,[c(`div`,null,[r[2]||=c(`dt`,null,`当前 Web 版本`,-1),c(`dd`,null,i(k.value?.current.webVersion||`—`),1)]),c(`div`,null,[r[3]||=c(`dt`,null,`最新版本`,-1),c(`dd`,null,i(A.value?`检测中…`:j.value?k.value?.latest?.webVersion||k.value?.current.webVersion:`未获取`),1)])]),M.value?(n(),d(`p`,C,i(M.value),1)):f(``,!0),j.value&&k.value?.updateAvailable&&!k.value.supported?(n(),d(`p`,w,i(k.value.unsupportedReason||`请通过部署环境更新 Web。`),1)):f(``,!0),j.value&&F.value?(n(),d(`a`,{key:2,href:F.value,target:`_blank`,rel:`noopener noreferrer`},[r[4]||=o(`查看发布说明`,-1),h(g,{name:`external`,size:14})],8,T)):f(``,!0),c(`div`,E,[c(`button`,{type:`button`,disabled:A.value,onClick:I},i(A.value?`正在检测…`:M.value?`重试`:`重新检测`),9,D),j.value&&k.value?.updateAvailable&&k.value.supported?(n(),d(`button`,{key:0,class:`primary`,type:`button`,onClick:r[0]||=e=>O(`manage`)},`前往更新`)):f(``,!0)])],8,b)]),_:1}))}}),[[`__scopeId`,`data-v-bf7e6d73`]]);export{O as default};
|
|
2
|
+
//# sourceMappingURL=UpdateCheckDialog-Bz8JhRh4.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UpdateCheckDialog-BaMo_M4n.js","names":[],"sources":["../../src/client/components/app/UpdateCheckDialog.vue","../../src/client/components/app/UpdateCheckDialog.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref } from 'vue'\nimport { checkSystemUpdate, systemUpdateStatus, type SystemUpdateStatus } from '@/api/systemUpdate'\nimport AppIcon from '@/components/common/AppIcon.vue'\nimport StandaloneDialog from '@/components/common/StandaloneDialog.vue'\n\nconst emit = defineEmits<{ close: []; manage: [] }>()\nconst status = ref<SystemUpdateStatus>()\nconst checking = ref(true)\nconst checked = ref(false)\nconst error = ref('')\nlet disposed = false\nconst message = computed(() => checking.value ? '正在检测更新…'\n : error.value ? '检测失败,请重试'\n : status.value?.updateAvailable ? `发现新版本 ${status.value.latest?.webVersion}` : '当前已是最新版本')\nconst releasePage = computed(() => {\n try {\n const url = new URL(status.value?.releasePageUrl || '')\n return url.protocol === 'https:' ? url.href : undefined\n } catch { return undefined }\n})\n\nasync function check() {\n checking.value = true\n checked.value = false\n error.value = ''\n try {\n const next = await checkSystemUpdate()\n if (disposed) return\n status.value = next\n checked.value = true\n } catch (cause) {\n if (!disposed) error.value = cause instanceof Error ? cause.message : '无法连接更新服务'\n } finally { if (!disposed) checking.value = false }\n}\nonMounted(async () => {\n // Keep the installed version visible even when the remote check fails.\n try { const current = await systemUpdateStatus(); if (!disposed) status.value = current } catch { /* The check below reports actionable errors. */ }\n if (!disposed) await check()\n})\nonBeforeUnmount(() => { disposed = true })\n</script>\n<template>\n <StandaloneDialog title=\"检测更新\" compact @close=\"emit('close')\">\n <section class=\"update-check\" :aria-busy=\"checking\">\n <div class=\"update-check__icon\" :class=\"{ spinning: checking }\"><AppIcon :name=\"checking ? 'refresh' : error ? 'alert' : status?.updateAvailable ? 'download' : 'check'\" :size=\"28\" /></div>\n <p class=\"update-check__status\" role=\"status\" aria-live=\"polite\">{{ message }}</p>\n <dl aria-label=\"Web 版本信息\"><div><dt>当前 Web 版本</dt><dd>{{ status?.current.webVersion || '—' }}</dd></div><div><dt>最新版本</dt><dd>{{ checking ? '检测中…' : !checked ? '未获取' : status?.latest?.webVersion || status?.current.webVersion }}</dd></div></dl>\n <p v-if=\"error\" class=\"update-check__error\" role=\"alert\">{{ error }}</p>\n <p v-if=\"checked && status?.updateAvailable && !status.supported\" class=\"update-check__note\">{{ status.unsupportedReason || '请通过部署环境更新 Web。' }}</p>\n <a v-if=\"checked && releasePage\" :href=\"releasePage\" target=\"_blank\" rel=\"noopener noreferrer\">查看发布说明<AppIcon name=\"external\" :size=\"14\" /></a>\n <div class=\"update-check__actions\">\n <button type=\"button\" :disabled=\"checking\" @click=\"check\">{{ checking ? '正在检测…' : error ? '重试' : '重新检测' }}</button>\n <button v-if=\"checked && status?.updateAvailable && status.supported\" class=\"primary\" type=\"button\" @click=\"emit('manage')\">前往更新</button>\n </div>\n </section>\n </StandaloneDialog>\n</template>\n<style scoped>\n.update-check{display:grid;gap:18px;text-align:center}.update-check__icon{display:grid;place-items:center;color:var(--accent)}.update-check__status{margin:0;font-size:16px;font-weight:600;line-height:1.5}dl{display:grid;gap:12px;margin:0;padding:16px 0;border-block:1px solid var(--line);font-size:13px}dl div{display:flex;justify-content:space-between;gap:12px}dt{color:var(--text-secondary)}dd{margin:0;font-variant-numeric:tabular-nums}.update-check__error,.update-check__note{margin:0;font-size:13px;line-height:1.6;overflow-wrap:anywhere}.update-check__error{color:var(--danger)}.update-check__note{color:var(--text-secondary)}a{display:inline-flex;align-items:center;justify-content:center;gap:6px;min-height:44px;color:var(--accent);font-size:13px;text-decoration:none}a:hover{text-decoration:underline}.update-check__actions{display:flex;gap:10px;flex-wrap:wrap}button{flex:1;min-height:44px;padding:8px 14px;border:1px solid var(--line);border-radius:9px;background:var(--surface-raised);color:var(--text-primary);font:500 13px var(--font-ui);cursor:pointer}button:hover{background:var(--surface-hover)}button.primary{background:var(--accent);border-color:var(--accent);color:var(--text-on-solid)}button:disabled{opacity:.6;cursor:wait}button:focus-visible,a:focus-visible{outline:2px solid var(--accent);outline-offset:3px}.spinning{animation:update-spin 1s linear infinite}@keyframes update-spin{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){.spinning{animation:none}}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref } from 'vue'\nimport { checkSystemUpdate, systemUpdateStatus, type SystemUpdateStatus } from '@/api/systemUpdate'\nimport AppIcon from '@/components/common/AppIcon.vue'\nimport StandaloneDialog from '@/components/common/StandaloneDialog.vue'\n\nconst emit = defineEmits<{ close: []; manage: [] }>()\nconst status = ref<SystemUpdateStatus>()\nconst checking = ref(true)\nconst checked = ref(false)\nconst error = ref('')\nlet disposed = false\nconst message = computed(() => checking.value ? '正在检测更新…'\n : error.value ? '检测失败,请重试'\n : status.value?.updateAvailable ? `发现新版本 ${status.value.latest?.webVersion}` : '当前已是最新版本')\nconst releasePage = computed(() => {\n try {\n const url = new URL(status.value?.releasePageUrl || '')\n return url.protocol === 'https:' ? url.href : undefined\n } catch { return undefined }\n})\n\nasync function check() {\n checking.value = true\n checked.value = false\n error.value = ''\n try {\n const next = await checkSystemUpdate()\n if (disposed) return\n status.value = next\n checked.value = true\n } catch (cause) {\n if (!disposed) error.value = cause instanceof Error ? cause.message : '无法连接更新服务'\n } finally { if (!disposed) checking.value = false }\n}\nonMounted(async () => {\n // Keep the installed version visible even when the remote check fails.\n try { const current = await systemUpdateStatus(); if (!disposed) status.value = current } catch { /* The check below reports actionable errors. */ }\n if (!disposed) await check()\n})\nonBeforeUnmount(() => { disposed = true })\n</script>\n<template>\n <StandaloneDialog title=\"检测更新\" compact @close=\"emit('close')\">\n <section class=\"update-check\" :aria-busy=\"checking\">\n <div class=\"update-check__icon\" :class=\"{ spinning: checking }\"><AppIcon :name=\"checking ? 'refresh' : error ? 'alert' : status?.updateAvailable ? 'download' : 'check'\" :size=\"28\" /></div>\n <p class=\"update-check__status\" role=\"status\" aria-live=\"polite\">{{ message }}</p>\n <dl aria-label=\"Web 版本信息\"><div><dt>当前 Web 版本</dt><dd>{{ status?.current.webVersion || '—' }}</dd></div><div><dt>最新版本</dt><dd>{{ checking ? '检测中…' : !checked ? '未获取' : status?.latest?.webVersion || status?.current.webVersion }}</dd></div></dl>\n <p v-if=\"error\" class=\"update-check__error\" role=\"alert\">{{ error }}</p>\n <p v-if=\"checked && status?.updateAvailable && !status.supported\" class=\"update-check__note\">{{ status.unsupportedReason || '请通过部署环境更新 Web。' }}</p>\n <a v-if=\"checked && releasePage\" :href=\"releasePage\" target=\"_blank\" rel=\"noopener noreferrer\">查看发布说明<AppIcon name=\"external\" :size=\"14\" /></a>\n <div class=\"update-check__actions\">\n <button type=\"button\" :disabled=\"checking\" @click=\"check\">{{ checking ? '正在检测…' : error ? '重试' : '重新检测' }}</button>\n <button v-if=\"checked && status?.updateAvailable && status.supported\" class=\"primary\" type=\"button\" @click=\"emit('manage')\">前往更新</button>\n </div>\n </section>\n </StandaloneDialog>\n</template>\n<style scoped>\n.update-check{display:grid;gap:18px;text-align:center}.update-check__icon{display:grid;place-items:center;color:var(--accent)}.update-check__status{margin:0;font-size:16px;font-weight:600;line-height:1.5}dl{display:grid;gap:12px;margin:0;padding:16px 0;border-block:1px solid var(--line);font-size:13px}dl div{display:flex;justify-content:space-between;gap:12px}dt{color:var(--text-secondary)}dd{margin:0;font-variant-numeric:tabular-nums}.update-check__error,.update-check__note{margin:0;font-size:13px;line-height:1.6;overflow-wrap:anywhere}.update-check__error{color:var(--danger)}.update-check__note{color:var(--text-secondary)}a{display:inline-flex;align-items:center;justify-content:center;gap:6px;min-height:44px;color:var(--accent);font-size:13px;text-decoration:none}a:hover{text-decoration:underline}.update-check__actions{display:flex;gap:10px;flex-wrap:wrap}button{flex:1;min-height:44px;padding:8px 14px;border:1px solid var(--line);border-radius:9px;background:var(--surface-raised);color:var(--text-primary);font:500 13px var(--font-ui);cursor:pointer}button:hover{background:var(--surface-hover)}button.primary{background:var(--accent);border-color:var(--accent);color:var(--text-on-solid)}button:disabled{opacity:.6;cursor:wait}button:focus-visible,a:focus-visible{outline:2px solid var(--accent);outline-offset:3px}.spinning{animation:update-spin 1s linear infinite}@keyframes update-spin{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){.spinning{animation:none}}\n</style>\n"],"mappings":"mpBAMA,IAAM,EAAO,EACP,EAAS,EAAwB,EACjC,EAAW,EAAI,EAAI,EACnB,EAAU,EAAI,EAAK,EACnB,EAAQ,EAAI,EAAE,EAChB,EAAW,GACT,EAAU,MAAe,EAAS,MAAQ,UAC5C,EAAM,MAAQ,WACZ,EAAO,OAAO,gBAAkB,SAAS,EAAO,MAAM,QAAQ,aAAe,UAAU,EACvF,EAAc,MAAe,CACjC,GAAI,CACF,IAAM,EAAM,IAAI,IAAI,EAAO,OAAO,gBAAkB,EAAE,EACtD,OAAO,EAAI,WAAa,SAAW,EAAI,KAAO,IAAA,EAChD,MAAQ,CAAE,MAAiB,CAC7B,CAAC,EAED,eAAe,GAAQ,CACrB,EAAS,MAAQ,GACjB,EAAQ,MAAQ,GAChB,EAAM,MAAQ,GACd,GAAI,CACF,IAAM,EAAO,MAAM,EAAkB,EACrC,GAAI,EAAU,OACd,EAAO,MAAQ,EACf,EAAQ,MAAQ,EAClB,OAAS,EAAO,CACT,IAAU,EAAM,MAAQ,aAAiB,MAAQ,EAAM,QAAU,WACxE,QAAU,CAAO,IAAU,EAAS,MAAQ,GAAM,CACpD,QACA,EAAU,SAAY,CAEpB,GAAI,CAAE,IAAM,EAAU,MAAM,EAAmB,EAAQ,IAAU,EAAO,MAAQ,EAAQ,MAAQ,CAAmD,CAC9I,GAAU,MAAM,EAAM,CAC7B,CAAC,EACD,MAAsB,CAAE,EAAW,EAAK,CAAC,UAGvC,EAAA,EAAA,EAamB,EAAA,CAbD,MAAM,OAAO,QAAA,GAAS,QAAK,AAAA,EAAA,KAAA,GAAE,EAAI,OAAA,IACjD,QAAA,MAWU,CAXV,EAWU,UAAA,CAXD,MAAM,eAAgB,YAAW,EAAA,QACxC,EAA4L,MAAA,CAAvL,MAAK,EAAA,CAAC,qBAAoB,CAAA,SAAqB,EAAA,KAAQ,CAAA,CAAA,CAAA,EAAA,CAAI,EAAsH,EAAA,CAA5G,KAAM,EAAA,MAAQ,UAAe,EAAA,MAAK,QAAa,EAAA,OAAQ,gBAAe,WAAA,QAA0B,KAAM,yBAChL,EAAkF,IAAlF,EAAkF,EAAd,EAAA,KAAO,EAAA,CAAA,EAC3E,EAAkP,KAAlP,EAAkP,CAAxN,EAA6E,MAAA,KAAA,CAAxE,AAAA,EAAA,KAAA,EAAkB,KAAA,KAAd,YAAS,EAAA,EAAK,EAAgD,KAAA,KAAA,EAAzC,EAAA,OAAQ,QAAQ,YAAU,GAAA,EAAA,CAAA,CAAA,CAAA,EAAqB,EAAsI,MAAA,KAAA,CAAjI,AAAA,EAAA,KAAA,EAAa,KAAA,KAAT,OAAI,EAAA,EAAK,EAA8G,KAAA,KAAA,EAAvG,EAAA,MAAQ,OAAa,EAAA,MAAkB,EAAA,OAAQ,QAAQ,YAAc,EAAA,OAAQ,QAAQ,WAAzD,KAAmE,EAAA,CAAA,CAAA,CAAA,CAAA,CAAA,EACtN,EAAA,OAAT,EAAA,EAAA,EAAwE,IAAxE,EAAwE,EAAZ,EAAA,KAAK,EAAA,CAAA,GAAA,EAAA,GAAA,EAAA,EACxD,EAAA,OAAW,EAAA,OAAQ,iBAAe,CAAK,EAAA,MAAO,WAAvD,EAAA,EAAA,EAAmJ,IAAnJ,EAAmJ,EAAnD,EAAA,MAAO,mBAAiB,gBAAA,EAAA,CAAA,GAAA,EAAA,GAAA,EAAA,EAC/G,EAAA,OAAW,EAAA,OAApB,EAAA,EAAA,EAA+I,IAAA,OAA7G,KAAM,EAAA,MAAa,OAAO,SAAS,IAAI,qBAAsB,EAAA,CAAA,AAAA,EAAA,KAAA,EAAA,SAAM,EAAA,EAAA,EAAsC,EAAA,CAA7B,KAAK,WAAY,KAAM,qBACrI,EAGM,MAHN,EAGM,CAFJ,EAAmH,SAAA,CAA3G,KAAK,SAAU,SAAU,EAAA,MAAW,QAAO,CAAU,EAAA,EAAA,EAAA,MAAQ,QAAa,EAAA,MAAK,KAAA,MAAA,EAAA,EAAA,CAAA,EACzE,EAAA,OAAW,EAAA,OAAQ,iBAAmB,EAAA,MAAO,WAA3D,EAAA,EAAA,EAAyI,SAAA,OAAnE,MAAM,UAAU,KAAK,SAAU,QAAK,AAAA,EAAA,KAAA,GAAE,EAAI,QAAA,CAAY,EAAA,MAAI,GAAA,EAAA,GAAA,EAAA,CAAA,CAAA"}
|
|
1
|
+
{"version":3,"file":"UpdateCheckDialog-Bz8JhRh4.js","names":[],"sources":["../../src/client/components/app/UpdateCheckDialog.vue","../../src/client/components/app/UpdateCheckDialog.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref } from 'vue'\nimport { checkSystemUpdate, systemUpdateStatus, type SystemUpdateStatus } from '@/api/systemUpdate'\nimport AppIcon from '@/components/common/AppIcon.vue'\nimport StandaloneDialog from '@/components/common/StandaloneDialog.vue'\n\nconst emit = defineEmits<{ close: []; manage: [] }>()\nconst status = ref<SystemUpdateStatus>()\nconst checking = ref(true)\nconst checked = ref(false)\nconst error = ref('')\nlet disposed = false\nconst message = computed(() => checking.value ? '正在检测更新…'\n : error.value ? '检测失败,请重试'\n : status.value?.updateAvailable ? `发现新版本 ${status.value.latest?.webVersion}` : '当前已是最新版本')\nconst releasePage = computed(() => {\n try {\n const url = new URL(status.value?.releasePageUrl || '')\n return url.protocol === 'https:' ? url.href : undefined\n } catch { return undefined }\n})\n\nasync function check() {\n checking.value = true\n checked.value = false\n error.value = ''\n try {\n const next = await checkSystemUpdate()\n if (disposed) return\n status.value = next\n checked.value = true\n } catch (cause) {\n if (!disposed) error.value = cause instanceof Error ? cause.message : '无法连接更新服务'\n } finally { if (!disposed) checking.value = false }\n}\nonMounted(async () => {\n // Keep the installed version visible even when the remote check fails.\n try { const current = await systemUpdateStatus(); if (!disposed) status.value = current } catch { /* The check below reports actionable errors. */ }\n if (!disposed) await check()\n})\nonBeforeUnmount(() => { disposed = true })\n</script>\n<template>\n <StandaloneDialog title=\"检测更新\" compact @close=\"emit('close')\">\n <section class=\"update-check\" :aria-busy=\"checking\">\n <div class=\"update-check__icon\" :class=\"{ spinning: checking }\"><AppIcon :name=\"checking ? 'refresh' : error ? 'alert' : status?.updateAvailable ? 'download' : 'check'\" :size=\"28\" /></div>\n <p class=\"update-check__status\" role=\"status\" aria-live=\"polite\">{{ message }}</p>\n <dl aria-label=\"Web 版本信息\"><div><dt>当前 Web 版本</dt><dd>{{ status?.current.webVersion || '—' }}</dd></div><div><dt>最新版本</dt><dd>{{ checking ? '检测中…' : !checked ? '未获取' : status?.latest?.webVersion || status?.current.webVersion }}</dd></div></dl>\n <p v-if=\"error\" class=\"update-check__error\" role=\"alert\">{{ error }}</p>\n <p v-if=\"checked && status?.updateAvailable && !status.supported\" class=\"update-check__note\">{{ status.unsupportedReason || '请通过部署环境更新 Web。' }}</p>\n <a v-if=\"checked && releasePage\" :href=\"releasePage\" target=\"_blank\" rel=\"noopener noreferrer\">查看发布说明<AppIcon name=\"external\" :size=\"14\" /></a>\n <div class=\"update-check__actions\">\n <button type=\"button\" :disabled=\"checking\" @click=\"check\">{{ checking ? '正在检测…' : error ? '重试' : '重新检测' }}</button>\n <button v-if=\"checked && status?.updateAvailable && status.supported\" class=\"primary\" type=\"button\" @click=\"emit('manage')\">前往更新</button>\n </div>\n </section>\n </StandaloneDialog>\n</template>\n<style scoped>\n.update-check{display:grid;gap:18px;text-align:center}.update-check__icon{display:grid;place-items:center;color:var(--accent)}.update-check__status{margin:0;font-size:16px;font-weight:600;line-height:1.5}dl{display:grid;gap:12px;margin:0;padding:16px 0;border-block:1px solid var(--line);font-size:13px}dl div{display:flex;justify-content:space-between;gap:12px}dt{color:var(--text-secondary)}dd{margin:0;font-variant-numeric:tabular-nums}.update-check__error,.update-check__note{margin:0;font-size:13px;line-height:1.6;overflow-wrap:anywhere}.update-check__error{color:var(--danger)}.update-check__note{color:var(--text-secondary)}a{display:inline-flex;align-items:center;justify-content:center;gap:6px;min-height:44px;color:var(--accent);font-size:13px;text-decoration:none}a:hover{text-decoration:underline}.update-check__actions{display:flex;gap:10px;flex-wrap:wrap}button{flex:1;min-height:44px;padding:8px 14px;border:1px solid var(--line);border-radius:9px;background:var(--surface-raised);color:var(--text-primary);font:500 13px var(--font-ui);cursor:pointer}button:hover{background:var(--surface-hover)}button.primary{background:var(--accent);border-color:var(--accent);color:var(--text-on-solid)}button:disabled{opacity:.6;cursor:wait}button:focus-visible,a:focus-visible{outline:2px solid var(--accent);outline-offset:3px}.spinning{animation:update-spin 1s linear infinite}@keyframes update-spin{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){.spinning{animation:none}}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref } from 'vue'\nimport { checkSystemUpdate, systemUpdateStatus, type SystemUpdateStatus } from '@/api/systemUpdate'\nimport AppIcon from '@/components/common/AppIcon.vue'\nimport StandaloneDialog from '@/components/common/StandaloneDialog.vue'\n\nconst emit = defineEmits<{ close: []; manage: [] }>()\nconst status = ref<SystemUpdateStatus>()\nconst checking = ref(true)\nconst checked = ref(false)\nconst error = ref('')\nlet disposed = false\nconst message = computed(() => checking.value ? '正在检测更新…'\n : error.value ? '检测失败,请重试'\n : status.value?.updateAvailable ? `发现新版本 ${status.value.latest?.webVersion}` : '当前已是最新版本')\nconst releasePage = computed(() => {\n try {\n const url = new URL(status.value?.releasePageUrl || '')\n return url.protocol === 'https:' ? url.href : undefined\n } catch { return undefined }\n})\n\nasync function check() {\n checking.value = true\n checked.value = false\n error.value = ''\n try {\n const next = await checkSystemUpdate()\n if (disposed) return\n status.value = next\n checked.value = true\n } catch (cause) {\n if (!disposed) error.value = cause instanceof Error ? cause.message : '无法连接更新服务'\n } finally { if (!disposed) checking.value = false }\n}\nonMounted(async () => {\n // Keep the installed version visible even when the remote check fails.\n try { const current = await systemUpdateStatus(); if (!disposed) status.value = current } catch { /* The check below reports actionable errors. */ }\n if (!disposed) await check()\n})\nonBeforeUnmount(() => { disposed = true })\n</script>\n<template>\n <StandaloneDialog title=\"检测更新\" compact @close=\"emit('close')\">\n <section class=\"update-check\" :aria-busy=\"checking\">\n <div class=\"update-check__icon\" :class=\"{ spinning: checking }\"><AppIcon :name=\"checking ? 'refresh' : error ? 'alert' : status?.updateAvailable ? 'download' : 'check'\" :size=\"28\" /></div>\n <p class=\"update-check__status\" role=\"status\" aria-live=\"polite\">{{ message }}</p>\n <dl aria-label=\"Web 版本信息\"><div><dt>当前 Web 版本</dt><dd>{{ status?.current.webVersion || '—' }}</dd></div><div><dt>最新版本</dt><dd>{{ checking ? '检测中…' : !checked ? '未获取' : status?.latest?.webVersion || status?.current.webVersion }}</dd></div></dl>\n <p v-if=\"error\" class=\"update-check__error\" role=\"alert\">{{ error }}</p>\n <p v-if=\"checked && status?.updateAvailable && !status.supported\" class=\"update-check__note\">{{ status.unsupportedReason || '请通过部署环境更新 Web。' }}</p>\n <a v-if=\"checked && releasePage\" :href=\"releasePage\" target=\"_blank\" rel=\"noopener noreferrer\">查看发布说明<AppIcon name=\"external\" :size=\"14\" /></a>\n <div class=\"update-check__actions\">\n <button type=\"button\" :disabled=\"checking\" @click=\"check\">{{ checking ? '正在检测…' : error ? '重试' : '重新检测' }}</button>\n <button v-if=\"checked && status?.updateAvailable && status.supported\" class=\"primary\" type=\"button\" @click=\"emit('manage')\">前往更新</button>\n </div>\n </section>\n </StandaloneDialog>\n</template>\n<style scoped>\n.update-check{display:grid;gap:18px;text-align:center}.update-check__icon{display:grid;place-items:center;color:var(--accent)}.update-check__status{margin:0;font-size:16px;font-weight:600;line-height:1.5}dl{display:grid;gap:12px;margin:0;padding:16px 0;border-block:1px solid var(--line);font-size:13px}dl div{display:flex;justify-content:space-between;gap:12px}dt{color:var(--text-secondary)}dd{margin:0;font-variant-numeric:tabular-nums}.update-check__error,.update-check__note{margin:0;font-size:13px;line-height:1.6;overflow-wrap:anywhere}.update-check__error{color:var(--danger)}.update-check__note{color:var(--text-secondary)}a{display:inline-flex;align-items:center;justify-content:center;gap:6px;min-height:44px;color:var(--accent);font-size:13px;text-decoration:none}a:hover{text-decoration:underline}.update-check__actions{display:flex;gap:10px;flex-wrap:wrap}button{flex:1;min-height:44px;padding:8px 14px;border:1px solid var(--line);border-radius:9px;background:var(--surface-raised);color:var(--text-primary);font:500 13px var(--font-ui);cursor:pointer}button:hover{background:var(--surface-hover)}button.primary{background:var(--accent);border-color:var(--accent);color:var(--text-on-solid)}button:disabled{opacity:.6;cursor:wait}button:focus-visible,a:focus-visible{outline:2px solid var(--accent);outline-offset:3px}.spinning{animation:update-spin 1s linear infinite}@keyframes update-spin{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){.spinning{animation:none}}\n</style>\n"],"mappings":"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"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{Dt as e,Et as t,F as n,H as r,L as i,M as a,Ot as o,_ as s,a as c,b as l,d as u,et as d,g as f,m as p,p as m,st as h,t as g,u as _,v}from"./_plugin-vue_export-helper-CD0r-vVb.js";import{B as y,Q as b,Z as x,j as S}from"./index-
|
|
1
|
+
import{Dt as e,Et as t,F as n,H as r,L as i,M as a,Ot as o,_ as s,a as c,b as l,d as u,et as d,g as f,m as p,p as m,st as h,t as g,u as _,v}from"./_plugin-vue_export-helper-CD0r-vVb.js";import{B as y,Q as b,Z as x,j as S}from"./index-P_kZCZ2-.js";var C=[{id:`daily`,label:`每天`},{id:`weekly`,label:`每周`},{id:`cumulative`,label:`累计总量`}],w=e=>`${e.getFullYear()}-${String(e.getMonth()+1).padStart(2,`0`)}-${String(e.getDate()).padStart(2,`0`)}`;function T(e){let[t,n,r]=e.split(`-`).map(Number);return new Date(t,n-1,r)}function E(e){let t=T(e);return`${t.getMonth()+1}月${t.getDate()}日`}function D(e){let t=e=>e.toFixed(1).replace(/\.0$/,``);return e>=1e8?`${t(e/1e8)}亿`:e>=1e4?`${t(e/1e4)}万`:Math.round(e).toLocaleString(`zh-CN`)}function O(e,t){return`${E(e)}\n${D(t)} 个 Token`}var k=e=>(e.getDay()+6)%7;function A(e,t=new Date){let n=new Map(e.map(e=>[e.date,e.total])),r=new Date(t.getFullYear(),t.getMonth(),t.getDate()),i=new Date(r);i.setDate(i.getDate()-364),i.setDate(i.getDate()-k(i));let a=[];for(let e=new Date(i);e<=r;e.setDate(e.getDate()+1)){let t=w(e);a.push({date:t,total:n.get(t)??0})}return a}function j(e,t){if(e<=0||t<=0)return 0;let n=e/t;return n<=.25?1:n<=.5?2:n<=.75?3:4}function M(e,t){let n=new Map,r=0,i=e.map((e,t)=>{let i=Math.floor(t/7);return n.set(i,(n.get(i)??0)+e.total),r+=e.total,{...e,week:i,weekday:t%7,cumulative:r,weekTotal:0}});for(let e of i)e.weekTotal=n.get(e.week)??0;let a=i.map(e=>t===`daily`?e.total:t===`weekly`?e.weekTotal:e.cumulative),o=Math.max(0,...a);return i.map((e,t)=>({date:e.date,dayTotal:e.total,weekTotal:e.weekTotal,cumulative:e.cumulative,value:a[t],level:j(a[t],o),week:e.week,weekday:e.weekday}))}function N(e){let t=[],n=-1;for(let r of e)r.date.endsWith(`-01`)&&r.week!==n&&(n=r.week,t.push({week:r.week,label:`${T(r.date).getMonth()+1}月`}));return t}function P(e,t){let n=t===`daily`?e.dayTotal:t===`weekly`?e.weekTotal:e.cumulative;return t===`weekly`?`${E(e.date)} 所在周\n${D(n)} 个 Token`:t===`cumulative`?`${E(e.date)}\n累计 ${D(n)} 个 Token`:O(e.date,n)}var F={class:`activity-heatmap`,"aria-label":`Token 活动`},I={class:`activity-header`},L={class:`activity-modes`,role:`tablist`,"aria-label":`活动视图`},R=[`aria-selected`,`onClick`],z={key:0,class:`activity-empty`},B={key:1,class:`activity-body`},V={class:`activity-scroll`},H=[`aria-label`],U=[`aria-label`,`onMouseenter`,`onMousemove`,`onFocus`],ee=g(l({__name:`UsageActivityHeatmap`,props:{days:{},now:{}},setup(r){let a=r,s=d(`daily`),l=d(null),g=_(()=>A(a.days,a.now?new Date(a.now):new Date)),v=_(()=>M(g.value,s.value)),y=_(()=>N(v.value)),b=_(()=>v.value.length?v.value[v.value.length-1].week+1:0),x=_(()=>g.value.some(e=>e.total>0));function S(e,t){let n=v.value[t];if(!n)return;let r=e.currentTarget.closest(`.activity-heatmap`)?.getBoundingClientRect(),i=e.currentTarget.getBoundingClientRect(),a=`clientX`in e?e.clientX-(r?.left??0):i.left+i.width/2-(r?.left??0),o=`clientY`in e?e.clientY-(r?.top??0):i.top-(r?.top??0);l.value={text:P(n,s.value),x:a,y:o}}function w(){l.value=null}return(r,a)=>(n(),p(`section`,F,[u(`header`,I,[a[0]||=u(`h2`,null,`Token 活动`,-1),u(`div`,L,[(n(!0),p(c,null,i(h(C),e=>(n(),p(`button`,{key:e.id,type:`button`,role:`tab`,"aria-selected":s.value===e.id,onClick:t=>s.value=e.id},o(e.label),9,R))),128))])]),x.value?(n(),p(`div`,B,[a[1]||=u(`div`,{class:`activity-weekdays`,"aria-hidden":`true`},[u(`span`,null,`一`),u(`span`,null,`三`),u(`span`,null,`五`)],-1),u(`div`,V,[u(`div`,{class:`activity-months`,style:e({gridTemplateColumns:`repeat(${b.value}, minmax(11px, 1fr))`})},[(n(!0),p(c,null,i(y.value,t=>(n(),p(`span`,{key:t.week+t.label,class:`activity-month`,style:e({gridColumn:t.week+1})},o(t.label),5))),128))],4),u(`div`,{class:`activity-grid`,role:`img`,"aria-label":`近 12 个月 Token 活动,${s.value===`daily`?`按天`:s.value===`weekly`?`按周`:`累计`}着色`,style:e({gridTemplateColumns:`repeat(${b.value}, minmax(11px, 1fr))`})},[(n(!0),p(c,null,i(v.value,(r,i)=>(n(),p(`button`,{key:r.date,type:`button`,class:t([`activity-cell`,`level-${r.level}`]),style:e({gridColumn:r.week+1,gridRow:r.weekday+1}),"aria-label":h(P)(r,s.value).replace(`
|
|
2
2
|
`,`,`),onMouseenter:e=>S(e,i),onMousemove:e=>S(e,i),onMouseleave:w,onFocus:e=>S(e,i),onBlur:w},null,46,U))),128))],12,H)]),a[2]||=f(`<div class="activity-legend" aria-hidden="true" data-v-80043765><span data-v-80043765>少</span><i class="level-0" data-v-80043765></i><i class="level-1" data-v-80043765></i><i class="level-2" data-v-80043765></i><i class="level-3" data-v-80043765></i><i class="level-4" data-v-80043765></i><span data-v-80043765>多</span></div>`,1),l.value?(n(),p(`div`,{key:0,class:`activity-tip`,role:`tooltip`,style:e({left:l.value.x+`px`,top:l.value.y+`px`})},o(l.value.text),5)):m(``,!0)])):(n(),p(`p`,z,`近 12 个月还没有 Token 记录。开始对话后,这里会显示每天的用量。`))]))}}),[[`__scopeId`,`data-v-80043765`]]),te={class:`usage-page`,"aria-label":`Token 用量总览`},ne={class:`usage-header`},re={class:`segmented`,role:`tablist`,"aria-label":`时间范围`},ie=[`aria-selected`,`onClick`],ae=[`disabled`],W={class:`usage-body`},G={key:0,class:`usage-error`,role:`alert`},K={key:1,class:`usage-note`,role:`status`},q={class:`usage-note`},J={class:`usage-cards`,"aria-label":`总量`},oe={class:`usage-card`},se={class:`usage-card`},ce={class:`usage-card`},le={class:`usage-card`},ue={key:0,class:`usage-note estimated-note`},de={class:`usage-grid`},fe={class:`usage-panel`,"aria-label":`按模型`},pe={key:0,class:`usage-empty`},me={key:1,class:`share-list`},he={class:`share-head`},ge=[`title`],_e={class:`share-bar`},ve={class:`usage-panel`,"aria-label":`按 Bot`},ye={key:0,class:`usage-empty`},be={class:`share-list`},xe={class:`share-head`},Se={class:`share-bar`},Ce={class:`deleted-title`},we={class:`share-list deleted`},Te={class:`share-head`},Ee={class:`share-bar`},De={class:`usage-panel`,"aria-label":`按天趋势`},Oe={key:0,class:`usage-empty`},ke=[`aria-label`],Ae=[`title`],je={key:1,class:`usage-panel`,"aria-label":`历史(未分模型)`},Me={class:`usage-note`},Ne={class:`usage-table`},Pe={key:0,class:`badge`},Fe={class:`usage-panel`,"aria-label":`Bot 与模型明细`},Ie={key:0,class:`usage-empty`},Le={key:1,class:`table-scroll`},Re={class:`usage-table`},Y={key:0,class:`badge`},ze=[`title`],Be={key:0,class:`est`},Ve={key:2,class:`pager`},He=[`disabled`],Ue=[`disabled`],X=20,Z=g(l({__name:`UsageView`,setup(l){let f=b(),g=x(),C=[{id:`today`,label:`今天`},{id:`7d`,label:`7 天`},{id:`30d`,label:`30 天`},{id:`all`,label:`全部`}],w=d(`7d`),T=d(null),E=d(null),D=d([]),O=d(!0),k=d(``),A=d(!1),j=d(0),M=_(()=>typeof g.query.from==`string`&&/^\/conversations(?:\/[a-f0-9-]{36})?(?:\?taskId=[a-f0-9-]{36})?$/i.test(g.query.from)?g.query.from:`/conversations`),N=e=>e.toLocaleString(`zh-CN`),P=e=>e>=1e9?`${(e/1e9).toFixed(2)}B`:e>=1e6?`${(e/1e6).toFixed(2)}M`:e>=1e4?`${(e/1e3).toFixed(1)}K`:N(e),F=(e,t)=>t>0?Math.round(e/t*1e3)/10:0,I=e=>e.estimatedCalls>0,L=e=>new Date(e).toLocaleString(`zh-CN`,{hour12:!1}),R=_(()=>T.value?.byAgent.filter(e=>!e.deleted)??[]),z=_(()=>T.value?.byAgent.filter(e=>e.deleted)??[]),B=_(()=>Math.max(1,...T.value?.byDay.map(e=>e.total)??[0])),V=_(()=>Math.max(1,Math.ceil((E.value?.total??0)/X))),H=_(()=>T.value?.firstRecordedAt?`统计自 ${L(T.value.firstRecordedAt)} 起`:`尚无记录`),U=0;async function Z(){let e=++U;O.value=!0;try{let t=`range=${w.value}`,[n,r,i]=await Promise.all([S(`/api/app/usage/overview?${t}`),S(`/api/app/usage/details?${t}&limit=${X}&offset=${j.value*X}`),S(`/api/app/usage/overview?range=all`)]);if(e!==U)return;T.value=n,E.value=r,D.value=i.byDay.map(e=>({date:e.date,total:e.total})),k.value=``}catch(t){e===U&&(k.value=t instanceof Error?t.message:`无法读取 Token 用量`)}finally{e===U&&(O.value=!1)}}r(w,()=>{j.value=0,Z()});function Q(e){j.value=Math.min(V.value-1,Math.max(0,j.value+e)),Z()}async function We(){A.value=!0;try{let e=await S(`/api/app/usage/details?range=${w.value}&format=csv`),t=URL.createObjectURL(new Blob([e],{type:`text/csv;charset=utf-8`})),n=document.createElement(`a`);n.href=t,n.download=`yaovi-token-usage-${w.value}.csv`,n.click(),window.setTimeout(()=>URL.revokeObjectURL(t),1e3)}catch(e){k.value=e instanceof Error?e.message:`导出失败`}finally{A.value=!1}}let $=e=>`输入 ${N(e.input)} · 输出 ${N(e.output)}`;return a(Z),(r,a)=>(n(),p(`main`,te,[u(`header`,ne,[u(`button`,{class:`icon-button`,type:`button`,"aria-label":`返回聊天`,onClick:a[0]||=e=>h(f).push(M.value)},[v(y,{name:`chevron-left`,size:20})]),v(y,{name:`history`,size:23}),a[3]||=u(`h1`,null,`Token 用量`,-1),u(`div`,re,[(n(),p(c,null,i(C,e=>u(`button`,{key:e.id,role:`tab`,"aria-selected":w.value===e.id,onClick:t=>w.value=e.id},o(e.label),9,ie)),64))]),u(`button`,{class:`usage-action`,type:`button`,disabled:A.value||!T.value?.totals.calls,onClick:We},o(A.value?`导出中…`:`导出 CSV`),9,ae),u(`button`,{class:`icon-button`,type:`button`,"aria-label":`刷新用量`,onClick:Z},[v(y,{name:`refresh`,size:17})])]),u(`div`,W,[k.value?(n(),p(`p`,G,o(k.value),1)):m(``,!0),O.value&&!T.value?(n(),p(`p`,K,`正在读取 Token 用量…`)):m(``,!0),T.value?(n(),p(c,{key:2},[u(`p`,q,o(H.value)+`。合计 = 输入 + 输出;缓存和推理 Token 单独列出,不重复计入。`,1),u(`section`,J,[u(`div`,oe,[a[4]||=u(`small`,null,`合计`,-1),u(`strong`,null,o(P(T.value.totals.total)),1),u(`span`,null,o(I(T.value.totals)?`≈ `:``)+o(N(T.value.totals.calls))+` 次调用`,1)]),u(`div`,se,[a[5]||=u(`small`,null,`输入`,-1),u(`strong`,null,o(P(T.value.totals.input)),1),u(`span`,null,`缓存读 `+o(N(T.value.totals.cacheRead))+` · 缓存写 `+o(N(T.value.totals.cacheWrite)),1)]),u(`div`,ce,[a[6]||=u(`small`,null,`输出`,-1),u(`strong`,null,o(P(T.value.totals.output)),1),u(`span`,null,`推理 `+o(N(T.value.totals.reasoning)),1)]),u(`div`,le,[a[7]||=u(`small`,null,`已删除 Bot`,-1),u(`strong`,null,o(P(T.value.deletedTotals.total)),1),u(`span`,null,o(F(T.value.deletedTotals.total,T.value.totals.total))+`% 占比`,1)])]),T.value.totals.estimatedCalls?(n(),p(`p`,ue,`其中 `+o(N(T.value.totals.estimatedCalls))+` 次为本机估算值(供应商未返回用量),标记 ≈。`,1)):m(``,!0),v(ee,{days:D.value},null,8,[`days`]),u(`div`,de,[u(`section`,fe,[a[8]||=u(`h2`,null,`按模型`,-1),T.value.byModel.length?(n(),p(`ul`,me,[(n(!0),p(c,null,i(T.value.byModel,t=>(n(),p(`li`,{key:t.provider+t.model},[u(`div`,he,[u(`strong`,{title:t.provider},o(t.model),9,ge),u(`span`,null,o(I(t)?`≈ `:``)+o(N(t.total))+` · `+o(F(t.total,T.value.totals.total))+`%`,1)]),u(`div`,_e,[u(`i`,{style:e({width:F(t.total,T.value.totals.total)+`%`})},null,4)]),u(`small`,null,o(t.provider)+` · 输入 `+o(N(t.input))+` · 输出 `+o(N(t.output))+` · `+o(N(t.calls))+` 次`,1)]))),128))])):(n(),p(`p`,pe,`这个时间范围内还没有记录。`))]),u(`section`,ve,[a[11]||=u(`h2`,null,`按 Bot`,-1),T.value.byAgent.length?(n(),p(c,{key:1},[u(`ul`,be,[(n(!0),p(c,null,i(R.value,t=>(n(),p(`li`,{key:t.agentId},[u(`div`,xe,[u(`strong`,null,o(t.name),1),u(`span`,null,o(I(t)?`≈ `:``)+o(N(t.total))+` · `+o(F(t.total,T.value.totals.total))+`%`,1)]),u(`div`,Se,[u(`i`,{style:e({width:F(t.total,T.value.totals.total)+`%`})},null,4)]),u(`small`,null,o($(t))+` · `+o(N(t.calls))+` 次`,1)]))),128))]),z.value.length?(n(),p(c,{key:0},[u(`h3`,Ce,[a[9]||=s(`已删除的 Bot `,-1),u(`span`,null,o(N(T.value.deletedTotals.total)),1)]),u(`ul`,we,[(n(!0),p(c,null,i(z.value,t=>(n(),p(`li`,{key:t.agentId},[u(`div`,Te,[u(`strong`,null,[s(o(t.name)+` `,1),a[10]||=u(`em`,{class:`badge`},`已删除`,-1)]),u(`span`,null,o(I(t)?`≈ `:``)+o(N(t.total))+` · `+o(F(t.total,T.value.totals.total))+`%`,1)]),u(`div`,Ee,[u(`i`,{style:e({width:F(t.total,T.value.totals.total)+`%`})},null,4)]),u(`small`,null,o($(t))+` · `+o(N(t.calls))+` 次`,1)]))),128))])],64)):m(``,!0)],64)):(n(),p(`p`,ye,`这个时间范围内还没有记录。`))])]),u(`section`,De,[a[12]||=u(`h2`,null,`按天趋势`,-1),T.value.byDay.length?(n(),p(`div`,{key:1,class:`trend`,role:`img`,"aria-label":`按天 Token 用量,共 ${T.value.byDay.length} 天`},[(n(!0),p(c,null,i(T.value.byDay,t=>(n(),p(`div`,{key:t.date,class:`trend-col`,title:`${t.date}:${N(t.total)}(输入 ${N(t.input)},输出 ${N(t.output)})`},[u(`i`,{style:e({height:Math.max(2,t.total/B.value*100)+`%`})},null,4),u(`small`,null,o(t.date.slice(5)),1)],8,Ae))),128))],8,ke)):(n(),p(`p`,Oe,`这个时间范围内还没有记录。`))]),T.value.historical?(n(),p(`section`,je,[a[14]||=u(`h2`,null,`历史(未分模型)`,-1),u(`p`,Me,o(T.value.historical.before)+` 之前的旧统计,只有每个 Bot 的输入和输出,没有模型信息,也没有计入上面的总量。合计 `+o(N(T.value.historical.totals.total))+`(输入 `+o(N(T.value.historical.totals.input))+` · 输出 `+o(N(T.value.historical.totals.output))+`)。`,1),u(`table`,Ne,[a[13]||=u(`thead`,null,[u(`tr`,null,[u(`th`,null,`Bot`),u(`th`,null,`输入`),u(`th`,null,`输出`),u(`th`,null,`合计`)])],-1),u(`tbody`,null,[(n(!0),p(c,null,i(T.value.historical.byAgent,e=>(n(),p(`tr`,{key:e.agentId},[u(`td`,null,[s(o(e.name)+` `,1),e.deleted?(n(),p(`em`,Pe,`已删除`)):m(``,!0)]),u(`td`,null,o(N(e.input)),1),u(`td`,null,o(N(e.output)),1),u(`td`,null,o(N(e.total)),1)]))),128))])])])):m(``,!0),u(`section`,Fe,[a[16]||=u(`h2`,null,`明细:Bot × 模型`,-1),E.value?.rows.length?(n(),p(`div`,Le,[u(`table`,Re,[a[15]||=u(`thead`,null,[u(`tr`,null,[u(`th`,null,`Bot`),u(`th`,null,`模型`),u(`th`,null,`输入`),u(`th`,null,`输出`),u(`th`,null,`缓存读`),u(`th`,null,`缓存写`),u(`th`,null,`推理`),u(`th`,null,`合计`),u(`th`,null,`次数`),u(`th`,null,`最近使用`)])],-1),u(`tbody`,null,[(n(!0),p(c,null,i(E.value.rows,e=>(n(),p(`tr`,{key:e.agentId+e.provider+e.model,class:t({deleted:e.deleted})},[u(`td`,null,[s(o(e.name)+` `,1),e.deleted?(n(),p(`em`,Y,`已删除`)):m(``,!0)]),u(`td`,{title:e.provider},o(e.model),9,ze),u(`td`,null,o(N(e.input)),1),u(`td`,null,o(N(e.output)),1),u(`td`,null,o(N(e.cacheRead)),1),u(`td`,null,o(N(e.cacheWrite)),1),u(`td`,null,o(N(e.reasoning)),1),u(`td`,null,o(I(e)?`≈ `:``)+o(N(e.total)),1),u(`td`,null,[s(o(N(e.calls)),1),e.estimatedCalls?(n(),p(`span`,Be,` (估算 `+o(N(e.estimatedCalls))+`)`,1)):m(``,!0)]),u(`td`,null,o(L(e.lastAt)),1)],2))),128))])])])):(n(),p(`p`,Ie,`这个时间范围内还没有记录。`)),V.value>1?(n(),p(`div`,Ve,[u(`button`,{disabled:j.value===0||O.value,onClick:a[1]||=e=>Q(-1)},`上一页`,8,He),u(`span`,null,o(j.value+1)+` / `+o(V.value),1),u(`button`,{disabled:j.value>=V.value-1||O.value,onClick:a[2]||=e=>Q(1)},`下一页`,8,Ue)])):m(``,!0)]),a[17]||=u(`p`,{class:`usage-footer`},`不含辅助模型调用(标题、压缩、复盘、记忆提炼等)。已删除 Bot 的用量会保留。日期按 Host 本机时区划分。`,-1)],64)):m(``,!0)])]))}}),[[`__scopeId`,`data-v-61e5b2cf`]]);export{Z as default};
|
|
3
|
-
//# sourceMappingURL=UsageView-
|
|
3
|
+
//# sourceMappingURL=UsageView-ZsSbQzIY.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UsageView-CklMPG4n.js","names":[],"sources":["../../src/client/utils/usageActivity.ts","../../src/client/components/workspace/UsageActivityHeatmap.vue","../../src/client/components/workspace/UsageActivityHeatmap.vue","../../src/client/views/UsageView.vue","../../src/client/views/UsageView.vue"],"sourcesContent":["/** Calendar heatmap helpers for Token activity (Monday-first, host-local date keys YYYY-MM-DD). */\n\nexport type UsageActivityMode = 'daily' | 'weekly' | 'cumulative'\nexport type UsageActivityDay = { date: string; total: number }\n\nexport const usageActivityModes: Array<{ id: UsageActivityMode; label: string }> = [\n { id: 'daily', label: '每天' },\n { id: 'weekly', label: '每周' },\n { id: 'cumulative', label: '累计总量' },\n]\n\nexport const usageDateKey = (at: Date): string =>\n `${at.getFullYear()}-${String(at.getMonth() + 1).padStart(2, '0')}-${String(at.getDate()).padStart(2, '0')}`\n\n/** Parse a YYYY-MM-DD key as local midnight. */\nexport function parseUsageDate(date: string): Date {\n const [y, m, d] = date.split('-').map(Number)\n return new Date(y!, m! - 1, d!)\n}\n\n/** Chinese day label like 7月7日. */\nexport function formatUsageActivityDate(date: string): string {\n const d = parseUsageDate(date)\n return `${d.getMonth() + 1}月${d.getDate()}日`\n}\n\n/** Compact Chinese count for tooltips: 14.6万 / 1.2亿 / 1,234. */\nexport function formatUsageActivityTokens(n: number): string {\n const trim = (value: number) => value.toFixed(1).replace(/\\.0$/, '')\n if (n >= 1e8) return `${trim(n / 1e8)}亿`\n if (n >= 1e4) return `${trim(n / 1e4)}万`\n return Math.round(n).toLocaleString('zh-CN')\n}\n\nexport function usageActivityTooltip(date: string, tokens: number): string {\n return `${formatUsageActivityDate(date)}\\n${formatUsageActivityTokens(tokens)} 个 Token`\n}\n\n/** Monday-first week index: Mon=0 … Sun=6. */\nexport const mondayIndex = (day: Date): number => (day.getDay() + 6) % 7\n\n/**\n * Build the last ~12 months as a Monday-aligned week grid ending on `now`.\n * Missing days are filled with total 0 so empty cells stay visible.\n */\nexport function buildUsageActivityWindow(byDay: readonly UsageActivityDay[], now = new Date()): UsageActivityDay[] {\n const totals = new Map(byDay.map(d => [d.date, d.total]))\n const end = new Date(now.getFullYear(), now.getMonth(), now.getDate())\n const start = new Date(end)\n start.setDate(start.getDate() - 364)\n start.setDate(start.getDate() - mondayIndex(start))\n const days: UsageActivityDay[] = []\n for (const cursor = new Date(start); cursor <= end; cursor.setDate(cursor.getDate() + 1)) {\n const key = usageDateKey(cursor)\n days.push({ date: key, total: totals.get(key) ?? 0 })\n }\n return days\n}\n\nexport interface UsageActivityCell {\n date: string\n /** Value used for colouring in the active mode. */\n value: number\n /** Day total (always the raw daily amount). */\n dayTotal: number\n /** Week total containing this day (for weekly tooltips). */\n weekTotal: number\n /** Running sum from the window start through this day. */\n cumulative: number\n level: 0 | 1 | 2 | 3 | 4\n week: number\n weekday: number\n}\n\nfunction levelFor(value: number, max: number): 0 | 1 | 2 | 3 | 4 {\n if (value <= 0 || max <= 0) return 0\n const ratio = value / max\n if (ratio <= 0.25) return 1\n if (ratio <= 0.5) return 2\n if (ratio <= 0.75) return 3\n return 4\n}\n\n/** Compute cell values and colour levels for the selected mode. */\nexport function buildUsageActivityCells(days: readonly UsageActivityDay[], mode: UsageActivityMode): UsageActivityCell[] {\n const weekTotals = new Map<number, number>()\n let running = 0\n const prepared = days.map((day, index) => {\n const week = Math.floor(index / 7)\n weekTotals.set(week, (weekTotals.get(week) ?? 0) + day.total)\n running += day.total\n return { ...day, week, weekday: index % 7, cumulative: running, weekTotal: 0 }\n })\n for (const cell of prepared) cell.weekTotal = weekTotals.get(cell.week) ?? 0\n const values = prepared.map(cell => mode === 'daily' ? cell.total : mode === 'weekly' ? cell.weekTotal : cell.cumulative)\n const max = Math.max(0, ...values)\n return prepared.map((cell, i) => ({\n date: cell.date,\n dayTotal: cell.total,\n weekTotal: cell.weekTotal,\n cumulative: cell.cumulative,\n value: values[i]!,\n level: levelFor(values[i]!, max),\n week: cell.week,\n weekday: cell.weekday,\n }))\n}\n\nexport interface UsageActivityMonthLabel { week: number; label: string }\n\n/** Place a Chinese month label on the first week that contains day 1 of that month. */\nexport function usageActivityMonthLabels(cells: readonly UsageActivityCell[]): UsageActivityMonthLabel[] {\n const labels: UsageActivityMonthLabel[] = []\n let last = -1\n for (const cell of cells) {\n if (!cell.date.endsWith('-01')) continue\n if (cell.week === last) continue\n last = cell.week\n labels.push({ week: cell.week, label: `${parseUsageDate(cell.date).getMonth() + 1}月` })\n }\n return labels\n}\n\nexport function usageActivityTooltipFor(cell: UsageActivityCell, mode: UsageActivityMode): string {\n const tokens = mode === 'daily' ? cell.dayTotal : mode === 'weekly' ? cell.weekTotal : cell.cumulative\n if (mode === 'weekly') {\n return `${formatUsageActivityDate(cell.date)} 所在周\\n${formatUsageActivityTokens(tokens)} 个 Token`\n }\n if (mode === 'cumulative') {\n return `${formatUsageActivityDate(cell.date)}\\n累计 ${formatUsageActivityTokens(tokens)} 个 Token`\n }\n return usageActivityTooltip(cell.date, tokens)\n}\n","<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport {\n buildUsageActivityCells, buildUsageActivityWindow, usageActivityModes, usageActivityMonthLabels,\n usageActivityTooltipFor, type UsageActivityDay, type UsageActivityMode,\n} from '@/utils/usageActivity'\n\nconst props = defineProps<{ days: UsageActivityDay[]; now?: number }>()\nconst mode = ref<UsageActivityMode>('daily')\nconst tip = ref<{ text: string; x: number; y: number } | null>(null)\nconst windowDays = computed(() => buildUsageActivityWindow(props.days, props.now ? new Date(props.now) : new Date()))\nconst cells = computed(() => buildUsageActivityCells(windowDays.value, mode.value))\nconst months = computed(() => usageActivityMonthLabels(cells.value))\nconst weekCount = computed(() => (cells.value.length ? cells.value[cells.value.length - 1]!.week + 1 : 0))\nconst hasAny = computed(() => windowDays.value.some(d => d.total > 0))\n\nfunction showTip(event: MouseEvent | FocusEvent, index: number) {\n const cell = cells.value[index]\n if (!cell) return\n const host = (event.currentTarget as HTMLElement).closest('.activity-heatmap') as HTMLElement | null\n const rect = host?.getBoundingClientRect()\n const target = (event.currentTarget as HTMLElement).getBoundingClientRect()\n const x = 'clientX' in event ? event.clientX - (rect?.left ?? 0) : target.left + target.width / 2 - (rect?.left ?? 0)\n const y = 'clientY' in event ? event.clientY - (rect?.top ?? 0) : target.top - (rect?.top ?? 0)\n tip.value = { text: usageActivityTooltipFor(cell, mode.value), x, y }\n}\nfunction hideTip() { tip.value = null }\n</script>\n\n<template>\n <section class=\"activity-heatmap\" aria-label=\"Token 活动\">\n <header class=\"activity-header\">\n <h2>Token 活动</h2>\n <div class=\"activity-modes\" role=\"tablist\" aria-label=\"活动视图\">\n <button v-for=\"item in usageActivityModes\" :key=\"item.id\" type=\"button\" role=\"tab\" :aria-selected=\"mode === item.id\" @click=\"mode = item.id\">{{ item.label }}</button>\n </div>\n </header>\n <p v-if=\"!hasAny\" class=\"activity-empty\">近 12 个月还没有 Token 记录。开始对话后,这里会显示每天的用量。</p>\n <div v-else class=\"activity-body\">\n <div class=\"activity-weekdays\" aria-hidden=\"true\"><span>一</span><span>三</span><span>五</span></div>\n <div class=\"activity-scroll\">\n <div class=\"activity-months\" :style=\"{ gridTemplateColumns: `repeat(${weekCount}, minmax(11px, 1fr))` }\">\n <span v-for=\"m in months\" :key=\"m.week + m.label\" class=\"activity-month\" :style=\"{ gridColumn: m.week + 1 }\">{{ m.label }}</span>\n </div>\n <div\n class=\"activity-grid\"\n role=\"img\"\n :aria-label=\"`近 12 个月 Token 活动,${mode === 'daily' ? '按天' : mode === 'weekly' ? '按周' : '累计'}着色`\"\n :style=\"{ gridTemplateColumns: `repeat(${weekCount}, minmax(11px, 1fr))` }\"\n >\n <button\n v-for=\"(cell, index) in cells\"\n :key=\"cell.date\"\n type=\"button\"\n class=\"activity-cell\"\n :class=\"`level-${cell.level}`\"\n :style=\"{ gridColumn: cell.week + 1, gridRow: cell.weekday + 1 }\"\n :aria-label=\"usageActivityTooltipFor(cell, mode).replace('\\n', ',')\"\n @mouseenter=\"showTip($event, index)\"\n @mousemove=\"showTip($event, index)\"\n @mouseleave=\"hideTip\"\n @focus=\"showTip($event, index)\"\n @blur=\"hideTip\"\n />\n </div>\n </div>\n <div class=\"activity-legend\" aria-hidden=\"true\"><span>少</span><i class=\"level-0\" /><i class=\"level-1\" /><i class=\"level-2\" /><i class=\"level-3\" /><i class=\"level-4\" /><span>多</span></div>\n <div v-if=\"tip\" class=\"activity-tip\" role=\"tooltip\" :style=\"{ left: tip.x + 'px', top: tip.y + 'px' }\">{{ tip.text }}</div>\n </div>\n </section>\n</template>\n\n<style scoped>\n.activity-heatmap{position:relative;background:var(--surface);border:1px solid var(--line);border-radius:14px;padding:16px;min-width:0}\n.activity-header{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-bottom:12px}\n.activity-header h2{margin:0;font-size:15px}\n.activity-modes{display:flex;gap:14px}\n.activity-modes button{border:0;background:transparent;color:var(--text-secondary);padding:0;min-height:auto;font:inherit;cursor:pointer}\n.activity-modes button[aria-selected=true]{color:var(--text-primary);font-weight:600}\n.activity-empty{margin:0;color:var(--text-secondary);font-size:13px}\n.activity-body{display:grid;grid-template-columns:18px minmax(0,1fr);grid-template-rows:auto auto;column-gap:8px;row-gap:8px;align-items:start}\n.activity-weekdays{display:grid;grid-template-rows:repeat(7,11px);gap:3px;padding-top:18px;font-size:10px;color:var(--text-muted);line-height:11px}\n.activity-weekdays span:nth-child(1){grid-row:1}.activity-weekdays span:nth-child(2){grid-row:3}.activity-weekdays span:nth-child(3){grid-row:5}\n.activity-scroll{grid-column:2;overflow-x:auto;min-width:0}\n.activity-months{display:grid;gap:3px;height:16px;margin-bottom:2px;font-size:11px;color:var(--text-secondary)}\n.activity-month{white-space:nowrap}\n.activity-grid{display:grid;grid-template-rows:repeat(7,11px);gap:3px;min-width:max-content}\n.activity-cell{width:100%;min-width:11px;height:11px;padding:0;border:0;border-radius:3px;cursor:default;background:var(--surface-soft);box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--line) 70%, transparent)}\n.activity-cell:focus-visible{outline:2px solid var(--workflow-accent);outline-offset:1px}\n.activity-cell.level-1,.activity-legend i.level-1{background:color-mix(in srgb, var(--workflow-accent) 22%, var(--surface-soft))}\n.activity-cell.level-2,.activity-legend i.level-2{background:color-mix(in srgb, var(--workflow-accent) 45%, var(--surface-soft))}\n.activity-cell.level-3,.activity-legend i.level-3{background:color-mix(in srgb, var(--workflow-accent) 70%, var(--surface-soft))}\n.activity-cell.level-4,.activity-legend i.level-4{background:var(--workflow-accent)}\n.activity-legend{grid-column:2;display:flex;align-items:center;justify-content:flex-end;gap:4px;font-size:11px;color:var(--text-muted)}\n.activity-legend i{display:block;width:11px;height:11px;border-radius:3px;background:var(--surface-soft);box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--line) 70%, transparent)}\n.activity-tip{position:absolute;z-index:5;transform:translate(-50%,calc(-100% - 10px));padding:8px 10px;border-radius:8px;background:var(--text-primary);color:var(--text-on-solid);font-size:12px;line-height:1.35;white-space:pre-line;pointer-events:none;box-shadow:var(--shadow-float);max-width:220px}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport {\n buildUsageActivityCells, buildUsageActivityWindow, usageActivityModes, usageActivityMonthLabels,\n usageActivityTooltipFor, type UsageActivityDay, type UsageActivityMode,\n} from '@/utils/usageActivity'\n\nconst props = defineProps<{ days: UsageActivityDay[]; now?: number }>()\nconst mode = ref<UsageActivityMode>('daily')\nconst tip = ref<{ text: string; x: number; y: number } | null>(null)\nconst windowDays = computed(() => buildUsageActivityWindow(props.days, props.now ? new Date(props.now) : new Date()))\nconst cells = computed(() => buildUsageActivityCells(windowDays.value, mode.value))\nconst months = computed(() => usageActivityMonthLabels(cells.value))\nconst weekCount = computed(() => (cells.value.length ? cells.value[cells.value.length - 1]!.week + 1 : 0))\nconst hasAny = computed(() => windowDays.value.some(d => d.total > 0))\n\nfunction showTip(event: MouseEvent | FocusEvent, index: number) {\n const cell = cells.value[index]\n if (!cell) return\n const host = (event.currentTarget as HTMLElement).closest('.activity-heatmap') as HTMLElement | null\n const rect = host?.getBoundingClientRect()\n const target = (event.currentTarget as HTMLElement).getBoundingClientRect()\n const x = 'clientX' in event ? event.clientX - (rect?.left ?? 0) : target.left + target.width / 2 - (rect?.left ?? 0)\n const y = 'clientY' in event ? event.clientY - (rect?.top ?? 0) : target.top - (rect?.top ?? 0)\n tip.value = { text: usageActivityTooltipFor(cell, mode.value), x, y }\n}\nfunction hideTip() { tip.value = null }\n</script>\n\n<template>\n <section class=\"activity-heatmap\" aria-label=\"Token 活动\">\n <header class=\"activity-header\">\n <h2>Token 活动</h2>\n <div class=\"activity-modes\" role=\"tablist\" aria-label=\"活动视图\">\n <button v-for=\"item in usageActivityModes\" :key=\"item.id\" type=\"button\" role=\"tab\" :aria-selected=\"mode === item.id\" @click=\"mode = item.id\">{{ item.label }}</button>\n </div>\n </header>\n <p v-if=\"!hasAny\" class=\"activity-empty\">近 12 个月还没有 Token 记录。开始对话后,这里会显示每天的用量。</p>\n <div v-else class=\"activity-body\">\n <div class=\"activity-weekdays\" aria-hidden=\"true\"><span>一</span><span>三</span><span>五</span></div>\n <div class=\"activity-scroll\">\n <div class=\"activity-months\" :style=\"{ gridTemplateColumns: `repeat(${weekCount}, minmax(11px, 1fr))` }\">\n <span v-for=\"m in months\" :key=\"m.week + m.label\" class=\"activity-month\" :style=\"{ gridColumn: m.week + 1 }\">{{ m.label }}</span>\n </div>\n <div\n class=\"activity-grid\"\n role=\"img\"\n :aria-label=\"`近 12 个月 Token 活动,${mode === 'daily' ? '按天' : mode === 'weekly' ? '按周' : '累计'}着色`\"\n :style=\"{ gridTemplateColumns: `repeat(${weekCount}, minmax(11px, 1fr))` }\"\n >\n <button\n v-for=\"(cell, index) in cells\"\n :key=\"cell.date\"\n type=\"button\"\n class=\"activity-cell\"\n :class=\"`level-${cell.level}`\"\n :style=\"{ gridColumn: cell.week + 1, gridRow: cell.weekday + 1 }\"\n :aria-label=\"usageActivityTooltipFor(cell, mode).replace('\\n', ',')\"\n @mouseenter=\"showTip($event, index)\"\n @mousemove=\"showTip($event, index)\"\n @mouseleave=\"hideTip\"\n @focus=\"showTip($event, index)\"\n @blur=\"hideTip\"\n />\n </div>\n </div>\n <div class=\"activity-legend\" aria-hidden=\"true\"><span>少</span><i class=\"level-0\" /><i class=\"level-1\" /><i class=\"level-2\" /><i class=\"level-3\" /><i class=\"level-4\" /><span>多</span></div>\n <div v-if=\"tip\" class=\"activity-tip\" role=\"tooltip\" :style=\"{ left: tip.x + 'px', top: tip.y + 'px' }\">{{ tip.text }}</div>\n </div>\n </section>\n</template>\n\n<style scoped>\n.activity-heatmap{position:relative;background:var(--surface);border:1px solid var(--line);border-radius:14px;padding:16px;min-width:0}\n.activity-header{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-bottom:12px}\n.activity-header h2{margin:0;font-size:15px}\n.activity-modes{display:flex;gap:14px}\n.activity-modes button{border:0;background:transparent;color:var(--text-secondary);padding:0;min-height:auto;font:inherit;cursor:pointer}\n.activity-modes button[aria-selected=true]{color:var(--text-primary);font-weight:600}\n.activity-empty{margin:0;color:var(--text-secondary);font-size:13px}\n.activity-body{display:grid;grid-template-columns:18px minmax(0,1fr);grid-template-rows:auto auto;column-gap:8px;row-gap:8px;align-items:start}\n.activity-weekdays{display:grid;grid-template-rows:repeat(7,11px);gap:3px;padding-top:18px;font-size:10px;color:var(--text-muted);line-height:11px}\n.activity-weekdays span:nth-child(1){grid-row:1}.activity-weekdays span:nth-child(2){grid-row:3}.activity-weekdays span:nth-child(3){grid-row:5}\n.activity-scroll{grid-column:2;overflow-x:auto;min-width:0}\n.activity-months{display:grid;gap:3px;height:16px;margin-bottom:2px;font-size:11px;color:var(--text-secondary)}\n.activity-month{white-space:nowrap}\n.activity-grid{display:grid;grid-template-rows:repeat(7,11px);gap:3px;min-width:max-content}\n.activity-cell{width:100%;min-width:11px;height:11px;padding:0;border:0;border-radius:3px;cursor:default;background:var(--surface-soft);box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--line) 70%, transparent)}\n.activity-cell:focus-visible{outline:2px solid var(--workflow-accent);outline-offset:1px}\n.activity-cell.level-1,.activity-legend i.level-1{background:color-mix(in srgb, var(--workflow-accent) 22%, var(--surface-soft))}\n.activity-cell.level-2,.activity-legend i.level-2{background:color-mix(in srgb, var(--workflow-accent) 45%, var(--surface-soft))}\n.activity-cell.level-3,.activity-legend i.level-3{background:color-mix(in srgb, var(--workflow-accent) 70%, var(--surface-soft))}\n.activity-cell.level-4,.activity-legend i.level-4{background:var(--workflow-accent)}\n.activity-legend{grid-column:2;display:flex;align-items:center;justify-content:flex-end;gap:4px;font-size:11px;color:var(--text-muted)}\n.activity-legend i{display:block;width:11px;height:11px;border-radius:3px;background:var(--surface-soft);box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--line) 70%, transparent)}\n.activity-tip{position:absolute;z-index:5;transform:translate(-50%,calc(-100% - 10px));padding:8px 10px;border-radius:8px;background:var(--text-primary);color:var(--text-on-solid);font-size:12px;line-height:1.35;white-space:pre-line;pointer-events:none;box-shadow:var(--shadow-float);max-width:220px}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, onMounted, ref, watch } from 'vue'\nimport { useRoute, useRouter } from 'vue-router'\nimport { apiRequest } from '@/api/client'\nimport AppIcon from '@/components/common/AppIcon.vue'\nimport UsageActivityHeatmap from '@/components/workspace/UsageActivityHeatmap.vue'\nimport type { WorkspaceUsageAgentRow, WorkspaceUsageDayRow, WorkspaceUsageDetails, WorkspaceUsageOverview, WorkspaceUsageRange, WorkspaceUsageTotals } from '@shared/workspace'\n\nconst router = useRouter(), route = useRoute()\nconst ranges: Array<{ id: WorkspaceUsageRange; label: string }> = [{ id: 'today', label: '今天' }, { id: '7d', label: '7 天' }, { id: '30d', label: '30 天' }, { id: 'all', label: '全部' }]\nconst range = ref<WorkspaceUsageRange>('7d')\nconst overview = ref<WorkspaceUsageOverview | null>(null), details = ref<WorkspaceUsageDetails | null>(null)\nconst calendarDays = ref<Array<Pick<WorkspaceUsageDayRow, 'date' | 'total'>>>([])\nconst loading = ref(true), error = ref(''), exporting = ref(false), page = ref(0)\nconst pageSize = 20\nconst backPath = computed(() => typeof route.query.from === 'string' && /^\\/conversations(?:\\/[a-f0-9-]{36})?(?:\\?taskId=[a-f0-9-]{36})?$/i.test(route.query.from) ? route.query.from : '/conversations')\nconst fmt = (n: number) => n.toLocaleString('zh-CN')\nconst compact = (n: number) => n >= 1e9 ? `${(n / 1e9).toFixed(2)}B` : n >= 1e6 ? `${(n / 1e6).toFixed(2)}M` : n >= 1e4 ? `${(n / 1e3).toFixed(1)}K` : fmt(n)\nconst share = (value: number, total: number) => total > 0 ? Math.round(value / total * 1000) / 10 : 0\nconst estimated = (t: WorkspaceUsageTotals) => t.estimatedCalls > 0\nconst when = (at: number) => new Date(at).toLocaleString('zh-CN', { hour12: false })\nconst liveAgents = computed(() => overview.value?.byAgent.filter(a => !a.deleted) ?? [])\nconst deletedAgents = computed(() => overview.value?.byAgent.filter(a => a.deleted) ?? [])\nconst maxDay = computed(() => Math.max(1, ...(overview.value?.byDay.map(d => d.total) ?? [0])))\nconst pages = computed(() => Math.max(1, Math.ceil((details.value?.total ?? 0) / pageSize)))\nconst sinceLabel = computed(() => overview.value?.firstRecordedAt ? `统计自 ${when(overview.value.firstRecordedAt)} 起` : '尚无记录')\n\nlet generation = 0\nasync function load() {\n const current = ++generation\n loading.value = true\n try {\n const query = `range=${range.value}`\n const [o, d, calendar] = await Promise.all([\n apiRequest<WorkspaceUsageOverview>(`/api/app/usage/overview?${query}`),\n apiRequest<WorkspaceUsageDetails>(`/api/app/usage/details?${query}&limit=${pageSize}&offset=${page.value * pageSize}`),\n // Heatmap always looks at the last ~12 months; ignore the page range filter.\n apiRequest<WorkspaceUsageOverview>('/api/app/usage/overview?range=all'),\n ])\n if (current !== generation) return\n overview.value = o; details.value = d\n calendarDays.value = calendar.byDay.map(day => ({ date: day.date, total: day.total }))\n error.value = ''\n } catch (e) { if (current === generation) error.value = e instanceof Error ? e.message : '无法读取 Token 用量' }\n finally { if (current === generation) loading.value = false }\n}\nwatch(range, () => { page.value = 0; void load() })\nfunction turn(delta: number) { page.value = Math.min(pages.value - 1, Math.max(0, page.value + delta)); void load() }\nasync function exportCsv() {\n exporting.value = true\n try {\n const text = await apiRequest<string>(`/api/app/usage/details?range=${range.value}&format=csv`)\n const url = URL.createObjectURL(new Blob([text], { type: 'text/csv;charset=utf-8' }))\n const link = document.createElement('a')\n link.href = url; link.download = `yaovi-token-usage-${range.value}.csv`; link.click()\n window.setTimeout(() => URL.revokeObjectURL(url), 1000)\n } catch (e) { error.value = e instanceof Error ? e.message : '导出失败' }\n finally { exporting.value = false }\n}\nconst agentLine = (a: WorkspaceUsageAgentRow) => `输入 ${fmt(a.input)} · 输出 ${fmt(a.output)}`\nonMounted(load)\n</script>\n\n<template>\n <main class=\"usage-page\" aria-label=\"Token 用量总览\">\n <header class=\"usage-header\">\n <button class=\"icon-button\" type=\"button\" aria-label=\"返回聊天\" @click=\"router.push(backPath)\"><AppIcon name=\"chevron-left\" :size=\"20\" /></button>\n <AppIcon name=\"history\" :size=\"23\" /><h1>Token 用量</h1>\n <div class=\"segmented\" role=\"tablist\" aria-label=\"时间范围\"><button v-for=\"item in ranges\" :key=\"item.id\" role=\"tab\" :aria-selected=\"range === item.id\" @click=\"range = item.id\">{{ item.label }}</button></div>\n <button class=\"usage-action\" type=\"button\" :disabled=\"exporting || !overview?.totals.calls\" @click=\"exportCsv\">{{ exporting ? '导出中…' : '导出 CSV' }}</button>\n <button class=\"icon-button\" type=\"button\" aria-label=\"刷新用量\" @click=\"load\"><AppIcon name=\"refresh\" :size=\"17\" /></button>\n </header>\n <div class=\"usage-body\">\n <p v-if=\"error\" class=\"usage-error\" role=\"alert\">{{ error }}</p>\n <p v-if=\"loading && !overview\" class=\"usage-note\" role=\"status\">正在读取 Token 用量…</p>\n <template v-if=\"overview\">\n <p class=\"usage-note\">{{ sinceLabel }}。合计 = 输入 + 输出;缓存和推理 Token 单独列出,不重复计入。</p>\n <section class=\"usage-cards\" aria-label=\"总量\">\n <div class=\"usage-card\"><small>合计</small><strong>{{ compact(overview.totals.total) }}</strong><span>{{ estimated(overview.totals) ? '≈ ' : '' }}{{ fmt(overview.totals.calls) }} 次调用</span></div>\n <div class=\"usage-card\"><small>输入</small><strong>{{ compact(overview.totals.input) }}</strong><span>缓存读 {{ fmt(overview.totals.cacheRead) }} · 缓存写 {{ fmt(overview.totals.cacheWrite) }}</span></div>\n <div class=\"usage-card\"><small>输出</small><strong>{{ compact(overview.totals.output) }}</strong><span>推理 {{ fmt(overview.totals.reasoning) }}</span></div>\n <div class=\"usage-card\"><small>已删除 Bot</small><strong>{{ compact(overview.deletedTotals.total) }}</strong><span>{{ share(overview.deletedTotals.total, overview.totals.total) }}% 占比</span></div>\n </section>\n <p v-if=\"overview.totals.estimatedCalls\" class=\"usage-note estimated-note\">其中 {{ fmt(overview.totals.estimatedCalls) }} 次为本机估算值(供应商未返回用量),标记 ≈。</p>\n\n <UsageActivityHeatmap :days=\"calendarDays\" />\n\n <div class=\"usage-grid\">\n <section class=\"usage-panel\" aria-label=\"按模型\">\n <h2>按模型</h2>\n <p v-if=\"!overview.byModel.length\" class=\"usage-empty\">这个时间范围内还没有记录。</p>\n <ul v-else class=\"share-list\">\n <li v-for=\"m in overview.byModel\" :key=\"m.provider + m.model\">\n <div class=\"share-head\"><strong :title=\"m.provider\">{{ m.model }}</strong><span>{{ estimated(m) ? '≈ ' : '' }}{{ fmt(m.total) }} · {{ share(m.total, overview.totals.total) }}%</span></div>\n <div class=\"share-bar\"><i :style=\"{ width: share(m.total, overview.totals.total) + '%' }\" /></div>\n <small>{{ m.provider }} · 输入 {{ fmt(m.input) }} · 输出 {{ fmt(m.output) }} · {{ fmt(m.calls) }} 次</small>\n </li>\n </ul>\n </section>\n\n <section class=\"usage-panel\" aria-label=\"按 Bot\">\n <h2>按 Bot</h2>\n <p v-if=\"!overview.byAgent.length\" class=\"usage-empty\">这个时间范围内还没有记录。</p>\n <template v-else>\n <ul class=\"share-list\">\n <li v-for=\"a in liveAgents\" :key=\"a.agentId\">\n <div class=\"share-head\"><strong>{{ a.name }}</strong><span>{{ estimated(a) ? '≈ ' : '' }}{{ fmt(a.total) }} · {{ share(a.total, overview.totals.total) }}%</span></div>\n <div class=\"share-bar\"><i :style=\"{ width: share(a.total, overview.totals.total) + '%' }\" /></div>\n <small>{{ agentLine(a) }} · {{ fmt(a.calls) }} 次</small>\n </li>\n </ul>\n <template v-if=\"deletedAgents.length\">\n <h3 class=\"deleted-title\">已删除的 Bot <span>{{ fmt(overview.deletedTotals.total) }}</span></h3>\n <ul class=\"share-list deleted\">\n <li v-for=\"a in deletedAgents\" :key=\"a.agentId\">\n <div class=\"share-head\"><strong>{{ a.name }} <em class=\"badge\">已删除</em></strong><span>{{ estimated(a) ? '≈ ' : '' }}{{ fmt(a.total) }} · {{ share(a.total, overview.totals.total) }}%</span></div>\n <div class=\"share-bar\"><i :style=\"{ width: share(a.total, overview.totals.total) + '%' }\" /></div>\n <small>{{ agentLine(a) }} · {{ fmt(a.calls) }} 次</small>\n </li>\n </ul>\n </template>\n </template>\n </section>\n </div>\n\n <section class=\"usage-panel\" aria-label=\"按天趋势\">\n <h2>按天趋势</h2>\n <p v-if=\"!overview.byDay.length\" class=\"usage-empty\">这个时间范围内还没有记录。</p>\n <div v-else class=\"trend\" role=\"img\" :aria-label=\"`按天 Token 用量,共 ${overview.byDay.length} 天`\">\n <div v-for=\"day in overview.byDay\" :key=\"day.date\" class=\"trend-col\" :title=\"`${day.date}:${fmt(day.total)}(输入 ${fmt(day.input)},输出 ${fmt(day.output)})`\">\n <i :style=\"{ height: Math.max(2, day.total / maxDay * 100) + '%' }\" /><small>{{ day.date.slice(5) }}</small>\n </div>\n </div>\n </section>\n\n <section v-if=\"overview.historical\" class=\"usage-panel\" aria-label=\"历史(未分模型)\">\n <h2>历史(未分模型)</h2>\n <p class=\"usage-note\">{{ overview.historical.before }} 之前的旧统计,只有每个 Bot 的输入和输出,没有模型信息,也没有计入上面的总量。合计 {{ fmt(overview.historical.totals.total) }}(输入 {{ fmt(overview.historical.totals.input) }} · 输出 {{ fmt(overview.historical.totals.output) }})。</p>\n <table class=\"usage-table\"><thead><tr><th>Bot</th><th>输入</th><th>输出</th><th>合计</th></tr></thead>\n <tbody><tr v-for=\"a in overview.historical.byAgent\" :key=\"a.agentId\"><td>{{ a.name }} <em v-if=\"a.deleted\" class=\"badge\">已删除</em></td><td>{{ fmt(a.input) }}</td><td>{{ fmt(a.output) }}</td><td>{{ fmt(a.total) }}</td></tr></tbody></table>\n </section>\n\n <section class=\"usage-panel\" aria-label=\"Bot 与模型明细\">\n <h2>明细:Bot × 模型</h2>\n <p v-if=\"!details?.rows.length\" class=\"usage-empty\">这个时间范围内还没有记录。</p>\n <div v-else class=\"table-scroll\"><table class=\"usage-table\">\n <thead><tr><th>Bot</th><th>模型</th><th>输入</th><th>输出</th><th>缓存读</th><th>缓存写</th><th>推理</th><th>合计</th><th>次数</th><th>最近使用</th></tr></thead>\n <tbody><tr v-for=\"r in details.rows\" :key=\"r.agentId + r.provider + r.model\" :class=\"{ deleted: r.deleted }\">\n <td>{{ r.name }} <em v-if=\"r.deleted\" class=\"badge\">已删除</em></td><td :title=\"r.provider\">{{ r.model }}</td>\n <td>{{ fmt(r.input) }}</td><td>{{ fmt(r.output) }}</td><td>{{ fmt(r.cacheRead) }}</td><td>{{ fmt(r.cacheWrite) }}</td><td>{{ fmt(r.reasoning) }}</td>\n <td>{{ estimated(r) ? '≈ ' : '' }}{{ fmt(r.total) }}</td><td>{{ fmt(r.calls) }}<span v-if=\"r.estimatedCalls\" class=\"est\"> (估算 {{ fmt(r.estimatedCalls) }})</span></td><td>{{ when(r.lastAt) }}</td>\n </tr></tbody></table></div>\n <div v-if=\"pages > 1\" class=\"pager\"><button :disabled=\"page === 0 || loading\" @click=\"turn(-1)\">上一页</button><span>{{ page + 1 }} / {{ pages }}</span><button :disabled=\"page >= pages - 1 || loading\" @click=\"turn(1)\">下一页</button></div>\n </section>\n <p class=\"usage-footer\">不含辅助模型调用(标题、压缩、复盘、记忆提炼等)。已删除 Bot 的用量会保留。日期按 Host 本机时区划分。</p>\n </template>\n </div>\n </main>\n</template>\n\n<style scoped>\n.usage-page{height:100dvh;display:flex;flex-direction:column;background:var(--canvas);color:var(--text-primary);min-width:0}\n.usage-header{display:flex;align-items:center;gap:12px;min-height:72px;box-sizing:border-box;padding:12px 24px;border-bottom:1px solid var(--line);background:var(--surface);flex-wrap:wrap}\nh1{font-size:20px;margin:0 20px 0 0;letter-spacing:-.02em}\nh2{font-size:15px;margin:0 0 12px}\n.usage-header button,.pager button{border:1px solid var(--line);background:var(--surface);color:var(--text-primary);border-radius:10px;min-height:36px;padding:0 14px;cursor:pointer}\n.usage-header button:disabled,.pager button:disabled{opacity:.5;cursor:default}\n.icon-button{padding:8px!important;width:40px;flex-shrink:0}\n.segmented{display:flex;gap:3px;border:1px solid var(--line);border-radius:10px;padding:3px;background:var(--surface);margin-left:auto}\n.segmented button{min-height:32px;border:0!important;background:transparent!important;white-space:nowrap}\n.segmented button[aria-selected=true]{background:var(--surface-soft)!important;font-weight:600}\n.usage-action{background:var(--accent)!important;color:var(--text-on-solid)!important;border-color:transparent!important}\n.usage-body{flex:1;overflow:auto;padding:20px 24px 32px;display:flex;flex-direction:column;gap:16px}\n.usage-note,.usage-footer,.usage-empty,small{color:var(--text-secondary);font-size:13px;margin:0}\n.estimated-note{color:var(--text-muted)}\n.usage-error{color:var(--danger);margin:0}\n.usage-cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px}\n.usage-card,.usage-panel{background:var(--surface);border:1px solid var(--line);border-radius:14px;padding:16px;min-width:0}\n.usage-card{display:flex;flex-direction:column;gap:4px}.usage-card strong{font-size:26px;letter-spacing:-.02em}.usage-card span{color:var(--text-secondary);font-size:12px}\n.usage-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(340px,1fr));gap:16px}\n.share-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:12px}\n.share-head{display:flex;justify-content:space-between;gap:12px;font-size:14px}.share-head strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.share-head span{color:var(--text-secondary);white-space:nowrap}\n.share-bar{height:6px;border-radius:3px;background:var(--surface-soft);margin:6px 0 4px;overflow:hidden}.share-bar i{display:block;height:100%;background:var(--accent);border-radius:3px}\n.deleted-title{font-size:13px;margin:16px 0 8px;color:var(--text-secondary);display:flex;justify-content:space-between}\n.deleted .share-bar i{background:var(--text-muted)}\n.badge{font-style:normal;font-size:11px;border:1px solid var(--line);border-radius:6px;padding:1px 6px;color:var(--text-secondary);margin-left:4px}\n.trend{display:flex;align-items:flex-end;gap:4px;height:140px;overflow-x:auto;padding-bottom:2px}\n.trend-col{flex:1 0 22px;max-width:48px;height:100%;display:flex;flex-direction:column;justify-content:flex-end;align-items:center;gap:4px}\n.trend-col i{display:block;width:100%;background:var(--accent);border-radius:4px 4px 0 0;min-height:2px}.trend-col small{font-size:10px}\n.table-scroll{overflow-x:auto}\n.usage-table{width:100%;border-collapse:collapse;font-size:13px}\n.usage-table th,.usage-table td{text-align:left;padding:8px 10px;border-bottom:1px solid var(--line);white-space:nowrap;font-variant-numeric:tabular-nums}\n.usage-table th{color:var(--text-secondary);font-weight:500}\n.usage-table tr.deleted td{color:var(--text-secondary)}\n.est{color:var(--text-muted)}\n.pager{display:flex;align-items:center;justify-content:flex-end;gap:12px;margin-top:12px;font-size:13px}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, onMounted, ref, watch } from 'vue'\nimport { useRoute, useRouter } from 'vue-router'\nimport { apiRequest } from '@/api/client'\nimport AppIcon from '@/components/common/AppIcon.vue'\nimport UsageActivityHeatmap from '@/components/workspace/UsageActivityHeatmap.vue'\nimport type { WorkspaceUsageAgentRow, WorkspaceUsageDayRow, WorkspaceUsageDetails, WorkspaceUsageOverview, WorkspaceUsageRange, WorkspaceUsageTotals } from '@shared/workspace'\n\nconst router = useRouter(), route = useRoute()\nconst ranges: Array<{ id: WorkspaceUsageRange; label: string }> = [{ id: 'today', label: '今天' }, { id: '7d', label: '7 天' }, { id: '30d', label: '30 天' }, { id: 'all', label: '全部' }]\nconst range = ref<WorkspaceUsageRange>('7d')\nconst overview = ref<WorkspaceUsageOverview | null>(null), details = ref<WorkspaceUsageDetails | null>(null)\nconst calendarDays = ref<Array<Pick<WorkspaceUsageDayRow, 'date' | 'total'>>>([])\nconst loading = ref(true), error = ref(''), exporting = ref(false), page = ref(0)\nconst pageSize = 20\nconst backPath = computed(() => typeof route.query.from === 'string' && /^\\/conversations(?:\\/[a-f0-9-]{36})?(?:\\?taskId=[a-f0-9-]{36})?$/i.test(route.query.from) ? route.query.from : '/conversations')\nconst fmt = (n: number) => n.toLocaleString('zh-CN')\nconst compact = (n: number) => n >= 1e9 ? `${(n / 1e9).toFixed(2)}B` : n >= 1e6 ? `${(n / 1e6).toFixed(2)}M` : n >= 1e4 ? `${(n / 1e3).toFixed(1)}K` : fmt(n)\nconst share = (value: number, total: number) => total > 0 ? Math.round(value / total * 1000) / 10 : 0\nconst estimated = (t: WorkspaceUsageTotals) => t.estimatedCalls > 0\nconst when = (at: number) => new Date(at).toLocaleString('zh-CN', { hour12: false })\nconst liveAgents = computed(() => overview.value?.byAgent.filter(a => !a.deleted) ?? [])\nconst deletedAgents = computed(() => overview.value?.byAgent.filter(a => a.deleted) ?? [])\nconst maxDay = computed(() => Math.max(1, ...(overview.value?.byDay.map(d => d.total) ?? [0])))\nconst pages = computed(() => Math.max(1, Math.ceil((details.value?.total ?? 0) / pageSize)))\nconst sinceLabel = computed(() => overview.value?.firstRecordedAt ? `统计自 ${when(overview.value.firstRecordedAt)} 起` : '尚无记录')\n\nlet generation = 0\nasync function load() {\n const current = ++generation\n loading.value = true\n try {\n const query = `range=${range.value}`\n const [o, d, calendar] = await Promise.all([\n apiRequest<WorkspaceUsageOverview>(`/api/app/usage/overview?${query}`),\n apiRequest<WorkspaceUsageDetails>(`/api/app/usage/details?${query}&limit=${pageSize}&offset=${page.value * pageSize}`),\n // Heatmap always looks at the last ~12 months; ignore the page range filter.\n apiRequest<WorkspaceUsageOverview>('/api/app/usage/overview?range=all'),\n ])\n if (current !== generation) return\n overview.value = o; details.value = d\n calendarDays.value = calendar.byDay.map(day => ({ date: day.date, total: day.total }))\n error.value = ''\n } catch (e) { if (current === generation) error.value = e instanceof Error ? e.message : '无法读取 Token 用量' }\n finally { if (current === generation) loading.value = false }\n}\nwatch(range, () => { page.value = 0; void load() })\nfunction turn(delta: number) { page.value = Math.min(pages.value - 1, Math.max(0, page.value + delta)); void load() }\nasync function exportCsv() {\n exporting.value = true\n try {\n const text = await apiRequest<string>(`/api/app/usage/details?range=${range.value}&format=csv`)\n const url = URL.createObjectURL(new Blob([text], { type: 'text/csv;charset=utf-8' }))\n const link = document.createElement('a')\n link.href = url; link.download = `yaovi-token-usage-${range.value}.csv`; link.click()\n window.setTimeout(() => URL.revokeObjectURL(url), 1000)\n } catch (e) { error.value = e instanceof Error ? e.message : '导出失败' }\n finally { exporting.value = false }\n}\nconst agentLine = (a: WorkspaceUsageAgentRow) => `输入 ${fmt(a.input)} · 输出 ${fmt(a.output)}`\nonMounted(load)\n</script>\n\n<template>\n <main class=\"usage-page\" aria-label=\"Token 用量总览\">\n <header class=\"usage-header\">\n <button class=\"icon-button\" type=\"button\" aria-label=\"返回聊天\" @click=\"router.push(backPath)\"><AppIcon name=\"chevron-left\" :size=\"20\" /></button>\n <AppIcon name=\"history\" :size=\"23\" /><h1>Token 用量</h1>\n <div class=\"segmented\" role=\"tablist\" aria-label=\"时间范围\"><button v-for=\"item in ranges\" :key=\"item.id\" role=\"tab\" :aria-selected=\"range === item.id\" @click=\"range = item.id\">{{ item.label }}</button></div>\n <button class=\"usage-action\" type=\"button\" :disabled=\"exporting || !overview?.totals.calls\" @click=\"exportCsv\">{{ exporting ? '导出中…' : '导出 CSV' }}</button>\n <button class=\"icon-button\" type=\"button\" aria-label=\"刷新用量\" @click=\"load\"><AppIcon name=\"refresh\" :size=\"17\" /></button>\n </header>\n <div class=\"usage-body\">\n <p v-if=\"error\" class=\"usage-error\" role=\"alert\">{{ error }}</p>\n <p v-if=\"loading && !overview\" class=\"usage-note\" role=\"status\">正在读取 Token 用量…</p>\n <template v-if=\"overview\">\n <p class=\"usage-note\">{{ sinceLabel }}。合计 = 输入 + 输出;缓存和推理 Token 单独列出,不重复计入。</p>\n <section class=\"usage-cards\" aria-label=\"总量\">\n <div class=\"usage-card\"><small>合计</small><strong>{{ compact(overview.totals.total) }}</strong><span>{{ estimated(overview.totals) ? '≈ ' : '' }}{{ fmt(overview.totals.calls) }} 次调用</span></div>\n <div class=\"usage-card\"><small>输入</small><strong>{{ compact(overview.totals.input) }}</strong><span>缓存读 {{ fmt(overview.totals.cacheRead) }} · 缓存写 {{ fmt(overview.totals.cacheWrite) }}</span></div>\n <div class=\"usage-card\"><small>输出</small><strong>{{ compact(overview.totals.output) }}</strong><span>推理 {{ fmt(overview.totals.reasoning) }}</span></div>\n <div class=\"usage-card\"><small>已删除 Bot</small><strong>{{ compact(overview.deletedTotals.total) }}</strong><span>{{ share(overview.deletedTotals.total, overview.totals.total) }}% 占比</span></div>\n </section>\n <p v-if=\"overview.totals.estimatedCalls\" class=\"usage-note estimated-note\">其中 {{ fmt(overview.totals.estimatedCalls) }} 次为本机估算值(供应商未返回用量),标记 ≈。</p>\n\n <UsageActivityHeatmap :days=\"calendarDays\" />\n\n <div class=\"usage-grid\">\n <section class=\"usage-panel\" aria-label=\"按模型\">\n <h2>按模型</h2>\n <p v-if=\"!overview.byModel.length\" class=\"usage-empty\">这个时间范围内还没有记录。</p>\n <ul v-else class=\"share-list\">\n <li v-for=\"m in overview.byModel\" :key=\"m.provider + m.model\">\n <div class=\"share-head\"><strong :title=\"m.provider\">{{ m.model }}</strong><span>{{ estimated(m) ? '≈ ' : '' }}{{ fmt(m.total) }} · {{ share(m.total, overview.totals.total) }}%</span></div>\n <div class=\"share-bar\"><i :style=\"{ width: share(m.total, overview.totals.total) + '%' }\" /></div>\n <small>{{ m.provider }} · 输入 {{ fmt(m.input) }} · 输出 {{ fmt(m.output) }} · {{ fmt(m.calls) }} 次</small>\n </li>\n </ul>\n </section>\n\n <section class=\"usage-panel\" aria-label=\"按 Bot\">\n <h2>按 Bot</h2>\n <p v-if=\"!overview.byAgent.length\" class=\"usage-empty\">这个时间范围内还没有记录。</p>\n <template v-else>\n <ul class=\"share-list\">\n <li v-for=\"a in liveAgents\" :key=\"a.agentId\">\n <div class=\"share-head\"><strong>{{ a.name }}</strong><span>{{ estimated(a) ? '≈ ' : '' }}{{ fmt(a.total) }} · {{ share(a.total, overview.totals.total) }}%</span></div>\n <div class=\"share-bar\"><i :style=\"{ width: share(a.total, overview.totals.total) + '%' }\" /></div>\n <small>{{ agentLine(a) }} · {{ fmt(a.calls) }} 次</small>\n </li>\n </ul>\n <template v-if=\"deletedAgents.length\">\n <h3 class=\"deleted-title\">已删除的 Bot <span>{{ fmt(overview.deletedTotals.total) }}</span></h3>\n <ul class=\"share-list deleted\">\n <li v-for=\"a in deletedAgents\" :key=\"a.agentId\">\n <div class=\"share-head\"><strong>{{ a.name }} <em class=\"badge\">已删除</em></strong><span>{{ estimated(a) ? '≈ ' : '' }}{{ fmt(a.total) }} · {{ share(a.total, overview.totals.total) }}%</span></div>\n <div class=\"share-bar\"><i :style=\"{ width: share(a.total, overview.totals.total) + '%' }\" /></div>\n <small>{{ agentLine(a) }} · {{ fmt(a.calls) }} 次</small>\n </li>\n </ul>\n </template>\n </template>\n </section>\n </div>\n\n <section class=\"usage-panel\" aria-label=\"按天趋势\">\n <h2>按天趋势</h2>\n <p v-if=\"!overview.byDay.length\" class=\"usage-empty\">这个时间范围内还没有记录。</p>\n <div v-else class=\"trend\" role=\"img\" :aria-label=\"`按天 Token 用量,共 ${overview.byDay.length} 天`\">\n <div v-for=\"day in overview.byDay\" :key=\"day.date\" class=\"trend-col\" :title=\"`${day.date}:${fmt(day.total)}(输入 ${fmt(day.input)},输出 ${fmt(day.output)})`\">\n <i :style=\"{ height: Math.max(2, day.total / maxDay * 100) + '%' }\" /><small>{{ day.date.slice(5) }}</small>\n </div>\n </div>\n </section>\n\n <section v-if=\"overview.historical\" class=\"usage-panel\" aria-label=\"历史(未分模型)\">\n <h2>历史(未分模型)</h2>\n <p class=\"usage-note\">{{ overview.historical.before }} 之前的旧统计,只有每个 Bot 的输入和输出,没有模型信息,也没有计入上面的总量。合计 {{ fmt(overview.historical.totals.total) }}(输入 {{ fmt(overview.historical.totals.input) }} · 输出 {{ fmt(overview.historical.totals.output) }})。</p>\n <table class=\"usage-table\"><thead><tr><th>Bot</th><th>输入</th><th>输出</th><th>合计</th></tr></thead>\n <tbody><tr v-for=\"a in overview.historical.byAgent\" :key=\"a.agentId\"><td>{{ a.name }} <em v-if=\"a.deleted\" class=\"badge\">已删除</em></td><td>{{ fmt(a.input) }}</td><td>{{ fmt(a.output) }}</td><td>{{ fmt(a.total) }}</td></tr></tbody></table>\n </section>\n\n <section class=\"usage-panel\" aria-label=\"Bot 与模型明细\">\n <h2>明细:Bot × 模型</h2>\n <p v-if=\"!details?.rows.length\" class=\"usage-empty\">这个时间范围内还没有记录。</p>\n <div v-else class=\"table-scroll\"><table class=\"usage-table\">\n <thead><tr><th>Bot</th><th>模型</th><th>输入</th><th>输出</th><th>缓存读</th><th>缓存写</th><th>推理</th><th>合计</th><th>次数</th><th>最近使用</th></tr></thead>\n <tbody><tr v-for=\"r in details.rows\" :key=\"r.agentId + r.provider + r.model\" :class=\"{ deleted: r.deleted }\">\n <td>{{ r.name }} <em v-if=\"r.deleted\" class=\"badge\">已删除</em></td><td :title=\"r.provider\">{{ r.model }}</td>\n <td>{{ fmt(r.input) }}</td><td>{{ fmt(r.output) }}</td><td>{{ fmt(r.cacheRead) }}</td><td>{{ fmt(r.cacheWrite) }}</td><td>{{ fmt(r.reasoning) }}</td>\n <td>{{ estimated(r) ? '≈ ' : '' }}{{ fmt(r.total) }}</td><td>{{ fmt(r.calls) }}<span v-if=\"r.estimatedCalls\" class=\"est\"> (估算 {{ fmt(r.estimatedCalls) }})</span></td><td>{{ when(r.lastAt) }}</td>\n </tr></tbody></table></div>\n <div v-if=\"pages > 1\" class=\"pager\"><button :disabled=\"page === 0 || loading\" @click=\"turn(-1)\">上一页</button><span>{{ page + 1 }} / {{ pages }}</span><button :disabled=\"page >= pages - 1 || loading\" @click=\"turn(1)\">下一页</button></div>\n </section>\n <p class=\"usage-footer\">不含辅助模型调用(标题、压缩、复盘、记忆提炼等)。已删除 Bot 的用量会保留。日期按 Host 本机时区划分。</p>\n </template>\n </div>\n </main>\n</template>\n\n<style scoped>\n.usage-page{height:100dvh;display:flex;flex-direction:column;background:var(--canvas);color:var(--text-primary);min-width:0}\n.usage-header{display:flex;align-items:center;gap:12px;min-height:72px;box-sizing:border-box;padding:12px 24px;border-bottom:1px solid var(--line);background:var(--surface);flex-wrap:wrap}\nh1{font-size:20px;margin:0 20px 0 0;letter-spacing:-.02em}\nh2{font-size:15px;margin:0 0 12px}\n.usage-header button,.pager button{border:1px solid var(--line);background:var(--surface);color:var(--text-primary);border-radius:10px;min-height:36px;padding:0 14px;cursor:pointer}\n.usage-header button:disabled,.pager button:disabled{opacity:.5;cursor:default}\n.icon-button{padding:8px!important;width:40px;flex-shrink:0}\n.segmented{display:flex;gap:3px;border:1px solid var(--line);border-radius:10px;padding:3px;background:var(--surface);margin-left:auto}\n.segmented button{min-height:32px;border:0!important;background:transparent!important;white-space:nowrap}\n.segmented button[aria-selected=true]{background:var(--surface-soft)!important;font-weight:600}\n.usage-action{background:var(--accent)!important;color:var(--text-on-solid)!important;border-color:transparent!important}\n.usage-body{flex:1;overflow:auto;padding:20px 24px 32px;display:flex;flex-direction:column;gap:16px}\n.usage-note,.usage-footer,.usage-empty,small{color:var(--text-secondary);font-size:13px;margin:0}\n.estimated-note{color:var(--text-muted)}\n.usage-error{color:var(--danger);margin:0}\n.usage-cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px}\n.usage-card,.usage-panel{background:var(--surface);border:1px solid var(--line);border-radius:14px;padding:16px;min-width:0}\n.usage-card{display:flex;flex-direction:column;gap:4px}.usage-card strong{font-size:26px;letter-spacing:-.02em}.usage-card span{color:var(--text-secondary);font-size:12px}\n.usage-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(340px,1fr));gap:16px}\n.share-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:12px}\n.share-head{display:flex;justify-content:space-between;gap:12px;font-size:14px}.share-head strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.share-head span{color:var(--text-secondary);white-space:nowrap}\n.share-bar{height:6px;border-radius:3px;background:var(--surface-soft);margin:6px 0 4px;overflow:hidden}.share-bar i{display:block;height:100%;background:var(--accent);border-radius:3px}\n.deleted-title{font-size:13px;margin:16px 0 8px;color:var(--text-secondary);display:flex;justify-content:space-between}\n.deleted .share-bar i{background:var(--text-muted)}\n.badge{font-style:normal;font-size:11px;border:1px solid var(--line);border-radius:6px;padding:1px 6px;color:var(--text-secondary);margin-left:4px}\n.trend{display:flex;align-items:flex-end;gap:4px;height:140px;overflow-x:auto;padding-bottom:2px}\n.trend-col{flex:1 0 22px;max-width:48px;height:100%;display:flex;flex-direction:column;justify-content:flex-end;align-items:center;gap:4px}\n.trend-col i{display:block;width:100%;background:var(--accent);border-radius:4px 4px 0 0;min-height:2px}.trend-col small{font-size:10px}\n.table-scroll{overflow-x:auto}\n.usage-table{width:100%;border-collapse:collapse;font-size:13px}\n.usage-table th,.usage-table td{text-align:left;padding:8px 10px;border-bottom:1px solid var(--line);white-space:nowrap;font-variant-numeric:tabular-nums}\n.usage-table th{color:var(--text-secondary);font-weight:500}\n.usage-table tr.deleted td{color:var(--text-secondary)}\n.est{color:var(--text-muted)}\n.pager{display:flex;align-items:center;justify-content:flex-end;gap:12px;margin-top:12px;font-size:13px}\n</style>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"UsageView-ZsSbQzIY.js","names":[],"sources":["../../src/client/utils/usageActivity.ts","../../src/client/components/workspace/UsageActivityHeatmap.vue","../../src/client/components/workspace/UsageActivityHeatmap.vue","../../src/client/views/UsageView.vue","../../src/client/views/UsageView.vue"],"sourcesContent":["/** Calendar heatmap helpers for Token activity (Monday-first, host-local date keys YYYY-MM-DD). */\n\nexport type UsageActivityMode = 'daily' | 'weekly' | 'cumulative'\nexport type UsageActivityDay = { date: string; total: number }\n\nexport const usageActivityModes: Array<{ id: UsageActivityMode; label: string }> = [\n { id: 'daily', label: '每天' },\n { id: 'weekly', label: '每周' },\n { id: 'cumulative', label: '累计总量' },\n]\n\nexport const usageDateKey = (at: Date): string =>\n `${at.getFullYear()}-${String(at.getMonth() + 1).padStart(2, '0')}-${String(at.getDate()).padStart(2, '0')}`\n\n/** Parse a YYYY-MM-DD key as local midnight. */\nexport function parseUsageDate(date: string): Date {\n const [y, m, d] = date.split('-').map(Number)\n return new Date(y!, m! - 1, d!)\n}\n\n/** Chinese day label like 7月7日. */\nexport function formatUsageActivityDate(date: string): string {\n const d = parseUsageDate(date)\n return `${d.getMonth() + 1}月${d.getDate()}日`\n}\n\n/** Compact Chinese count for tooltips: 14.6万 / 1.2亿 / 1,234. */\nexport function formatUsageActivityTokens(n: number): string {\n const trim = (value: number) => value.toFixed(1).replace(/\\.0$/, '')\n if (n >= 1e8) return `${trim(n / 1e8)}亿`\n if (n >= 1e4) return `${trim(n / 1e4)}万`\n return Math.round(n).toLocaleString('zh-CN')\n}\n\nexport function usageActivityTooltip(date: string, tokens: number): string {\n return `${formatUsageActivityDate(date)}\\n${formatUsageActivityTokens(tokens)} 个 Token`\n}\n\n/** Monday-first week index: Mon=0 … Sun=6. */\nexport const mondayIndex = (day: Date): number => (day.getDay() + 6) % 7\n\n/**\n * Build the last ~12 months as a Monday-aligned week grid ending on `now`.\n * Missing days are filled with total 0 so empty cells stay visible.\n */\nexport function buildUsageActivityWindow(byDay: readonly UsageActivityDay[], now = new Date()): UsageActivityDay[] {\n const totals = new Map(byDay.map(d => [d.date, d.total]))\n const end = new Date(now.getFullYear(), now.getMonth(), now.getDate())\n const start = new Date(end)\n start.setDate(start.getDate() - 364)\n start.setDate(start.getDate() - mondayIndex(start))\n const days: UsageActivityDay[] = []\n for (const cursor = new Date(start); cursor <= end; cursor.setDate(cursor.getDate() + 1)) {\n const key = usageDateKey(cursor)\n days.push({ date: key, total: totals.get(key) ?? 0 })\n }\n return days\n}\n\nexport interface UsageActivityCell {\n date: string\n /** Value used for colouring in the active mode. */\n value: number\n /** Day total (always the raw daily amount). */\n dayTotal: number\n /** Week total containing this day (for weekly tooltips). */\n weekTotal: number\n /** Running sum from the window start through this day. */\n cumulative: number\n level: 0 | 1 | 2 | 3 | 4\n week: number\n weekday: number\n}\n\nfunction levelFor(value: number, max: number): 0 | 1 | 2 | 3 | 4 {\n if (value <= 0 || max <= 0) return 0\n const ratio = value / max\n if (ratio <= 0.25) return 1\n if (ratio <= 0.5) return 2\n if (ratio <= 0.75) return 3\n return 4\n}\n\n/** Compute cell values and colour levels for the selected mode. */\nexport function buildUsageActivityCells(days: readonly UsageActivityDay[], mode: UsageActivityMode): UsageActivityCell[] {\n const weekTotals = new Map<number, number>()\n let running = 0\n const prepared = days.map((day, index) => {\n const week = Math.floor(index / 7)\n weekTotals.set(week, (weekTotals.get(week) ?? 0) + day.total)\n running += day.total\n return { ...day, week, weekday: index % 7, cumulative: running, weekTotal: 0 }\n })\n for (const cell of prepared) cell.weekTotal = weekTotals.get(cell.week) ?? 0\n const values = prepared.map(cell => mode === 'daily' ? cell.total : mode === 'weekly' ? cell.weekTotal : cell.cumulative)\n const max = Math.max(0, ...values)\n return prepared.map((cell, i) => ({\n date: cell.date,\n dayTotal: cell.total,\n weekTotal: cell.weekTotal,\n cumulative: cell.cumulative,\n value: values[i]!,\n level: levelFor(values[i]!, max),\n week: cell.week,\n weekday: cell.weekday,\n }))\n}\n\nexport interface UsageActivityMonthLabel { week: number; label: string }\n\n/** Place a Chinese month label on the first week that contains day 1 of that month. */\nexport function usageActivityMonthLabels(cells: readonly UsageActivityCell[]): UsageActivityMonthLabel[] {\n const labels: UsageActivityMonthLabel[] = []\n let last = -1\n for (const cell of cells) {\n if (!cell.date.endsWith('-01')) continue\n if (cell.week === last) continue\n last = cell.week\n labels.push({ week: cell.week, label: `${parseUsageDate(cell.date).getMonth() + 1}月` })\n }\n return labels\n}\n\nexport function usageActivityTooltipFor(cell: UsageActivityCell, mode: UsageActivityMode): string {\n const tokens = mode === 'daily' ? cell.dayTotal : mode === 'weekly' ? cell.weekTotal : cell.cumulative\n if (mode === 'weekly') {\n return `${formatUsageActivityDate(cell.date)} 所在周\\n${formatUsageActivityTokens(tokens)} 个 Token`\n }\n if (mode === 'cumulative') {\n return `${formatUsageActivityDate(cell.date)}\\n累计 ${formatUsageActivityTokens(tokens)} 个 Token`\n }\n return usageActivityTooltip(cell.date, tokens)\n}\n","<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport {\n buildUsageActivityCells, buildUsageActivityWindow, usageActivityModes, usageActivityMonthLabels,\n usageActivityTooltipFor, type UsageActivityDay, type UsageActivityMode,\n} from '@/utils/usageActivity'\n\nconst props = defineProps<{ days: UsageActivityDay[]; now?: number }>()\nconst mode = ref<UsageActivityMode>('daily')\nconst tip = ref<{ text: string; x: number; y: number } | null>(null)\nconst windowDays = computed(() => buildUsageActivityWindow(props.days, props.now ? new Date(props.now) : new Date()))\nconst cells = computed(() => buildUsageActivityCells(windowDays.value, mode.value))\nconst months = computed(() => usageActivityMonthLabels(cells.value))\nconst weekCount = computed(() => (cells.value.length ? cells.value[cells.value.length - 1]!.week + 1 : 0))\nconst hasAny = computed(() => windowDays.value.some(d => d.total > 0))\n\nfunction showTip(event: MouseEvent | FocusEvent, index: number) {\n const cell = cells.value[index]\n if (!cell) return\n const host = (event.currentTarget as HTMLElement).closest('.activity-heatmap') as HTMLElement | null\n const rect = host?.getBoundingClientRect()\n const target = (event.currentTarget as HTMLElement).getBoundingClientRect()\n const x = 'clientX' in event ? event.clientX - (rect?.left ?? 0) : target.left + target.width / 2 - (rect?.left ?? 0)\n const y = 'clientY' in event ? event.clientY - (rect?.top ?? 0) : target.top - (rect?.top ?? 0)\n tip.value = { text: usageActivityTooltipFor(cell, mode.value), x, y }\n}\nfunction hideTip() { tip.value = null }\n</script>\n\n<template>\n <section class=\"activity-heatmap\" aria-label=\"Token 活动\">\n <header class=\"activity-header\">\n <h2>Token 活动</h2>\n <div class=\"activity-modes\" role=\"tablist\" aria-label=\"活动视图\">\n <button v-for=\"item in usageActivityModes\" :key=\"item.id\" type=\"button\" role=\"tab\" :aria-selected=\"mode === item.id\" @click=\"mode = item.id\">{{ item.label }}</button>\n </div>\n </header>\n <p v-if=\"!hasAny\" class=\"activity-empty\">近 12 个月还没有 Token 记录。开始对话后,这里会显示每天的用量。</p>\n <div v-else class=\"activity-body\">\n <div class=\"activity-weekdays\" aria-hidden=\"true\"><span>一</span><span>三</span><span>五</span></div>\n <div class=\"activity-scroll\">\n <div class=\"activity-months\" :style=\"{ gridTemplateColumns: `repeat(${weekCount}, minmax(11px, 1fr))` }\">\n <span v-for=\"m in months\" :key=\"m.week + m.label\" class=\"activity-month\" :style=\"{ gridColumn: m.week + 1 }\">{{ m.label }}</span>\n </div>\n <div\n class=\"activity-grid\"\n role=\"img\"\n :aria-label=\"`近 12 个月 Token 活动,${mode === 'daily' ? '按天' : mode === 'weekly' ? '按周' : '累计'}着色`\"\n :style=\"{ gridTemplateColumns: `repeat(${weekCount}, minmax(11px, 1fr))` }\"\n >\n <button\n v-for=\"(cell, index) in cells\"\n :key=\"cell.date\"\n type=\"button\"\n class=\"activity-cell\"\n :class=\"`level-${cell.level}`\"\n :style=\"{ gridColumn: cell.week + 1, gridRow: cell.weekday + 1 }\"\n :aria-label=\"usageActivityTooltipFor(cell, mode).replace('\\n', ',')\"\n @mouseenter=\"showTip($event, index)\"\n @mousemove=\"showTip($event, index)\"\n @mouseleave=\"hideTip\"\n @focus=\"showTip($event, index)\"\n @blur=\"hideTip\"\n />\n </div>\n </div>\n <div class=\"activity-legend\" aria-hidden=\"true\"><span>少</span><i class=\"level-0\" /><i class=\"level-1\" /><i class=\"level-2\" /><i class=\"level-3\" /><i class=\"level-4\" /><span>多</span></div>\n <div v-if=\"tip\" class=\"activity-tip\" role=\"tooltip\" :style=\"{ left: tip.x + 'px', top: tip.y + 'px' }\">{{ tip.text }}</div>\n </div>\n </section>\n</template>\n\n<style scoped>\n.activity-heatmap{position:relative;background:var(--surface);border:1px solid var(--line);border-radius:14px;padding:16px;min-width:0}\n.activity-header{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-bottom:12px}\n.activity-header h2{margin:0;font-size:15px}\n.activity-modes{display:flex;gap:14px}\n.activity-modes button{border:0;background:transparent;color:var(--text-secondary);padding:0;min-height:auto;font:inherit;cursor:pointer}\n.activity-modes button[aria-selected=true]{color:var(--text-primary);font-weight:600}\n.activity-empty{margin:0;color:var(--text-secondary);font-size:13px}\n.activity-body{display:grid;grid-template-columns:18px minmax(0,1fr);grid-template-rows:auto auto;column-gap:8px;row-gap:8px;align-items:start}\n.activity-weekdays{display:grid;grid-template-rows:repeat(7,11px);gap:3px;padding-top:18px;font-size:10px;color:var(--text-muted);line-height:11px}\n.activity-weekdays span:nth-child(1){grid-row:1}.activity-weekdays span:nth-child(2){grid-row:3}.activity-weekdays span:nth-child(3){grid-row:5}\n.activity-scroll{grid-column:2;overflow-x:auto;min-width:0}\n.activity-months{display:grid;gap:3px;height:16px;margin-bottom:2px;font-size:11px;color:var(--text-secondary)}\n.activity-month{white-space:nowrap}\n.activity-grid{display:grid;grid-template-rows:repeat(7,11px);gap:3px;min-width:max-content}\n.activity-cell{width:100%;min-width:11px;height:11px;padding:0;border:0;border-radius:3px;cursor:default;background:var(--surface-soft);box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--line) 70%, transparent)}\n.activity-cell:focus-visible{outline:2px solid var(--workflow-accent);outline-offset:1px}\n.activity-cell.level-1,.activity-legend i.level-1{background:color-mix(in srgb, var(--workflow-accent) 22%, var(--surface-soft))}\n.activity-cell.level-2,.activity-legend i.level-2{background:color-mix(in srgb, var(--workflow-accent) 45%, var(--surface-soft))}\n.activity-cell.level-3,.activity-legend i.level-3{background:color-mix(in srgb, var(--workflow-accent) 70%, var(--surface-soft))}\n.activity-cell.level-4,.activity-legend i.level-4{background:var(--workflow-accent)}\n.activity-legend{grid-column:2;display:flex;align-items:center;justify-content:flex-end;gap:4px;font-size:11px;color:var(--text-muted)}\n.activity-legend i{display:block;width:11px;height:11px;border-radius:3px;background:var(--surface-soft);box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--line) 70%, transparent)}\n.activity-tip{position:absolute;z-index:5;transform:translate(-50%,calc(-100% - 10px));padding:8px 10px;border-radius:8px;background:var(--text-primary);color:var(--text-on-solid);font-size:12px;line-height:1.35;white-space:pre-line;pointer-events:none;box-shadow:var(--shadow-float);max-width:220px}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport {\n buildUsageActivityCells, buildUsageActivityWindow, usageActivityModes, usageActivityMonthLabels,\n usageActivityTooltipFor, type UsageActivityDay, type UsageActivityMode,\n} from '@/utils/usageActivity'\n\nconst props = defineProps<{ days: UsageActivityDay[]; now?: number }>()\nconst mode = ref<UsageActivityMode>('daily')\nconst tip = ref<{ text: string; x: number; y: number } | null>(null)\nconst windowDays = computed(() => buildUsageActivityWindow(props.days, props.now ? new Date(props.now) : new Date()))\nconst cells = computed(() => buildUsageActivityCells(windowDays.value, mode.value))\nconst months = computed(() => usageActivityMonthLabels(cells.value))\nconst weekCount = computed(() => (cells.value.length ? cells.value[cells.value.length - 1]!.week + 1 : 0))\nconst hasAny = computed(() => windowDays.value.some(d => d.total > 0))\n\nfunction showTip(event: MouseEvent | FocusEvent, index: number) {\n const cell = cells.value[index]\n if (!cell) return\n const host = (event.currentTarget as HTMLElement).closest('.activity-heatmap') as HTMLElement | null\n const rect = host?.getBoundingClientRect()\n const target = (event.currentTarget as HTMLElement).getBoundingClientRect()\n const x = 'clientX' in event ? event.clientX - (rect?.left ?? 0) : target.left + target.width / 2 - (rect?.left ?? 0)\n const y = 'clientY' in event ? event.clientY - (rect?.top ?? 0) : target.top - (rect?.top ?? 0)\n tip.value = { text: usageActivityTooltipFor(cell, mode.value), x, y }\n}\nfunction hideTip() { tip.value = null }\n</script>\n\n<template>\n <section class=\"activity-heatmap\" aria-label=\"Token 活动\">\n <header class=\"activity-header\">\n <h2>Token 活动</h2>\n <div class=\"activity-modes\" role=\"tablist\" aria-label=\"活动视图\">\n <button v-for=\"item in usageActivityModes\" :key=\"item.id\" type=\"button\" role=\"tab\" :aria-selected=\"mode === item.id\" @click=\"mode = item.id\">{{ item.label }}</button>\n </div>\n </header>\n <p v-if=\"!hasAny\" class=\"activity-empty\">近 12 个月还没有 Token 记录。开始对话后,这里会显示每天的用量。</p>\n <div v-else class=\"activity-body\">\n <div class=\"activity-weekdays\" aria-hidden=\"true\"><span>一</span><span>三</span><span>五</span></div>\n <div class=\"activity-scroll\">\n <div class=\"activity-months\" :style=\"{ gridTemplateColumns: `repeat(${weekCount}, minmax(11px, 1fr))` }\">\n <span v-for=\"m in months\" :key=\"m.week + m.label\" class=\"activity-month\" :style=\"{ gridColumn: m.week + 1 }\">{{ m.label }}</span>\n </div>\n <div\n class=\"activity-grid\"\n role=\"img\"\n :aria-label=\"`近 12 个月 Token 活动,${mode === 'daily' ? '按天' : mode === 'weekly' ? '按周' : '累计'}着色`\"\n :style=\"{ gridTemplateColumns: `repeat(${weekCount}, minmax(11px, 1fr))` }\"\n >\n <button\n v-for=\"(cell, index) in cells\"\n :key=\"cell.date\"\n type=\"button\"\n class=\"activity-cell\"\n :class=\"`level-${cell.level}`\"\n :style=\"{ gridColumn: cell.week + 1, gridRow: cell.weekday + 1 }\"\n :aria-label=\"usageActivityTooltipFor(cell, mode).replace('\\n', ',')\"\n @mouseenter=\"showTip($event, index)\"\n @mousemove=\"showTip($event, index)\"\n @mouseleave=\"hideTip\"\n @focus=\"showTip($event, index)\"\n @blur=\"hideTip\"\n />\n </div>\n </div>\n <div class=\"activity-legend\" aria-hidden=\"true\"><span>少</span><i class=\"level-0\" /><i class=\"level-1\" /><i class=\"level-2\" /><i class=\"level-3\" /><i class=\"level-4\" /><span>多</span></div>\n <div v-if=\"tip\" class=\"activity-tip\" role=\"tooltip\" :style=\"{ left: tip.x + 'px', top: tip.y + 'px' }\">{{ tip.text }}</div>\n </div>\n </section>\n</template>\n\n<style scoped>\n.activity-heatmap{position:relative;background:var(--surface);border:1px solid var(--line);border-radius:14px;padding:16px;min-width:0}\n.activity-header{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-bottom:12px}\n.activity-header h2{margin:0;font-size:15px}\n.activity-modes{display:flex;gap:14px}\n.activity-modes button{border:0;background:transparent;color:var(--text-secondary);padding:0;min-height:auto;font:inherit;cursor:pointer}\n.activity-modes button[aria-selected=true]{color:var(--text-primary);font-weight:600}\n.activity-empty{margin:0;color:var(--text-secondary);font-size:13px}\n.activity-body{display:grid;grid-template-columns:18px minmax(0,1fr);grid-template-rows:auto auto;column-gap:8px;row-gap:8px;align-items:start}\n.activity-weekdays{display:grid;grid-template-rows:repeat(7,11px);gap:3px;padding-top:18px;font-size:10px;color:var(--text-muted);line-height:11px}\n.activity-weekdays span:nth-child(1){grid-row:1}.activity-weekdays span:nth-child(2){grid-row:3}.activity-weekdays span:nth-child(3){grid-row:5}\n.activity-scroll{grid-column:2;overflow-x:auto;min-width:0}\n.activity-months{display:grid;gap:3px;height:16px;margin-bottom:2px;font-size:11px;color:var(--text-secondary)}\n.activity-month{white-space:nowrap}\n.activity-grid{display:grid;grid-template-rows:repeat(7,11px);gap:3px;min-width:max-content}\n.activity-cell{width:100%;min-width:11px;height:11px;padding:0;border:0;border-radius:3px;cursor:default;background:var(--surface-soft);box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--line) 70%, transparent)}\n.activity-cell:focus-visible{outline:2px solid var(--workflow-accent);outline-offset:1px}\n.activity-cell.level-1,.activity-legend i.level-1{background:color-mix(in srgb, var(--workflow-accent) 22%, var(--surface-soft))}\n.activity-cell.level-2,.activity-legend i.level-2{background:color-mix(in srgb, var(--workflow-accent) 45%, var(--surface-soft))}\n.activity-cell.level-3,.activity-legend i.level-3{background:color-mix(in srgb, var(--workflow-accent) 70%, var(--surface-soft))}\n.activity-cell.level-4,.activity-legend i.level-4{background:var(--workflow-accent)}\n.activity-legend{grid-column:2;display:flex;align-items:center;justify-content:flex-end;gap:4px;font-size:11px;color:var(--text-muted)}\n.activity-legend i{display:block;width:11px;height:11px;border-radius:3px;background:var(--surface-soft);box-shadow:inset 0 0 0 1px color-mix(in srgb, var(--line) 70%, transparent)}\n.activity-tip{position:absolute;z-index:5;transform:translate(-50%,calc(-100% - 10px));padding:8px 10px;border-radius:8px;background:var(--text-primary);color:var(--text-on-solid);font-size:12px;line-height:1.35;white-space:pre-line;pointer-events:none;box-shadow:var(--shadow-float);max-width:220px}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, onMounted, ref, watch } from 'vue'\nimport { useRoute, useRouter } from 'vue-router'\nimport { apiRequest } from '@/api/client'\nimport AppIcon from '@/components/common/AppIcon.vue'\nimport UsageActivityHeatmap from '@/components/workspace/UsageActivityHeatmap.vue'\nimport type { WorkspaceUsageAgentRow, WorkspaceUsageDayRow, WorkspaceUsageDetails, WorkspaceUsageOverview, WorkspaceUsageRange, WorkspaceUsageTotals } from '@shared/workspace'\n\nconst router = useRouter(), route = useRoute()\nconst ranges: Array<{ id: WorkspaceUsageRange; label: string }> = [{ id: 'today', label: '今天' }, { id: '7d', label: '7 天' }, { id: '30d', label: '30 天' }, { id: 'all', label: '全部' }]\nconst range = ref<WorkspaceUsageRange>('7d')\nconst overview = ref<WorkspaceUsageOverview | null>(null), details = ref<WorkspaceUsageDetails | null>(null)\nconst calendarDays = ref<Array<Pick<WorkspaceUsageDayRow, 'date' | 'total'>>>([])\nconst loading = ref(true), error = ref(''), exporting = ref(false), page = ref(0)\nconst pageSize = 20\nconst backPath = computed(() => typeof route.query.from === 'string' && /^\\/conversations(?:\\/[a-f0-9-]{36})?(?:\\?taskId=[a-f0-9-]{36})?$/i.test(route.query.from) ? route.query.from : '/conversations')\nconst fmt = (n: number) => n.toLocaleString('zh-CN')\nconst compact = (n: number) => n >= 1e9 ? `${(n / 1e9).toFixed(2)}B` : n >= 1e6 ? `${(n / 1e6).toFixed(2)}M` : n >= 1e4 ? `${(n / 1e3).toFixed(1)}K` : fmt(n)\nconst share = (value: number, total: number) => total > 0 ? Math.round(value / total * 1000) / 10 : 0\nconst estimated = (t: WorkspaceUsageTotals) => t.estimatedCalls > 0\nconst when = (at: number) => new Date(at).toLocaleString('zh-CN', { hour12: false })\nconst liveAgents = computed(() => overview.value?.byAgent.filter(a => !a.deleted) ?? [])\nconst deletedAgents = computed(() => overview.value?.byAgent.filter(a => a.deleted) ?? [])\nconst maxDay = computed(() => Math.max(1, ...(overview.value?.byDay.map(d => d.total) ?? [0])))\nconst pages = computed(() => Math.max(1, Math.ceil((details.value?.total ?? 0) / pageSize)))\nconst sinceLabel = computed(() => overview.value?.firstRecordedAt ? `统计自 ${when(overview.value.firstRecordedAt)} 起` : '尚无记录')\n\nlet generation = 0\nasync function load() {\n const current = ++generation\n loading.value = true\n try {\n const query = `range=${range.value}`\n const [o, d, calendar] = await Promise.all([\n apiRequest<WorkspaceUsageOverview>(`/api/app/usage/overview?${query}`),\n apiRequest<WorkspaceUsageDetails>(`/api/app/usage/details?${query}&limit=${pageSize}&offset=${page.value * pageSize}`),\n // Heatmap always looks at the last ~12 months; ignore the page range filter.\n apiRequest<WorkspaceUsageOverview>('/api/app/usage/overview?range=all'),\n ])\n if (current !== generation) return\n overview.value = o; details.value = d\n calendarDays.value = calendar.byDay.map(day => ({ date: day.date, total: day.total }))\n error.value = ''\n } catch (e) { if (current === generation) error.value = e instanceof Error ? e.message : '无法读取 Token 用量' }\n finally { if (current === generation) loading.value = false }\n}\nwatch(range, () => { page.value = 0; void load() })\nfunction turn(delta: number) { page.value = Math.min(pages.value - 1, Math.max(0, page.value + delta)); void load() }\nasync function exportCsv() {\n exporting.value = true\n try {\n const text = await apiRequest<string>(`/api/app/usage/details?range=${range.value}&format=csv`)\n const url = URL.createObjectURL(new Blob([text], { type: 'text/csv;charset=utf-8' }))\n const link = document.createElement('a')\n link.href = url; link.download = `yaovi-token-usage-${range.value}.csv`; link.click()\n window.setTimeout(() => URL.revokeObjectURL(url), 1000)\n } catch (e) { error.value = e instanceof Error ? e.message : '导出失败' }\n finally { exporting.value = false }\n}\nconst agentLine = (a: WorkspaceUsageAgentRow) => `输入 ${fmt(a.input)} · 输出 ${fmt(a.output)}`\nonMounted(load)\n</script>\n\n<template>\n <main class=\"usage-page\" aria-label=\"Token 用量总览\">\n <header class=\"usage-header\">\n <button class=\"icon-button\" type=\"button\" aria-label=\"返回聊天\" @click=\"router.push(backPath)\"><AppIcon name=\"chevron-left\" :size=\"20\" /></button>\n <AppIcon name=\"history\" :size=\"23\" /><h1>Token 用量</h1>\n <div class=\"segmented\" role=\"tablist\" aria-label=\"时间范围\"><button v-for=\"item in ranges\" :key=\"item.id\" role=\"tab\" :aria-selected=\"range === item.id\" @click=\"range = item.id\">{{ item.label }}</button></div>\n <button class=\"usage-action\" type=\"button\" :disabled=\"exporting || !overview?.totals.calls\" @click=\"exportCsv\">{{ exporting ? '导出中…' : '导出 CSV' }}</button>\n <button class=\"icon-button\" type=\"button\" aria-label=\"刷新用量\" @click=\"load\"><AppIcon name=\"refresh\" :size=\"17\" /></button>\n </header>\n <div class=\"usage-body\">\n <p v-if=\"error\" class=\"usage-error\" role=\"alert\">{{ error }}</p>\n <p v-if=\"loading && !overview\" class=\"usage-note\" role=\"status\">正在读取 Token 用量…</p>\n <template v-if=\"overview\">\n <p class=\"usage-note\">{{ sinceLabel }}。合计 = 输入 + 输出;缓存和推理 Token 单独列出,不重复计入。</p>\n <section class=\"usage-cards\" aria-label=\"总量\">\n <div class=\"usage-card\"><small>合计</small><strong>{{ compact(overview.totals.total) }}</strong><span>{{ estimated(overview.totals) ? '≈ ' : '' }}{{ fmt(overview.totals.calls) }} 次调用</span></div>\n <div class=\"usage-card\"><small>输入</small><strong>{{ compact(overview.totals.input) }}</strong><span>缓存读 {{ fmt(overview.totals.cacheRead) }} · 缓存写 {{ fmt(overview.totals.cacheWrite) }}</span></div>\n <div class=\"usage-card\"><small>输出</small><strong>{{ compact(overview.totals.output) }}</strong><span>推理 {{ fmt(overview.totals.reasoning) }}</span></div>\n <div class=\"usage-card\"><small>已删除 Bot</small><strong>{{ compact(overview.deletedTotals.total) }}</strong><span>{{ share(overview.deletedTotals.total, overview.totals.total) }}% 占比</span></div>\n </section>\n <p v-if=\"overview.totals.estimatedCalls\" class=\"usage-note estimated-note\">其中 {{ fmt(overview.totals.estimatedCalls) }} 次为本机估算值(供应商未返回用量),标记 ≈。</p>\n\n <UsageActivityHeatmap :days=\"calendarDays\" />\n\n <div class=\"usage-grid\">\n <section class=\"usage-panel\" aria-label=\"按模型\">\n <h2>按模型</h2>\n <p v-if=\"!overview.byModel.length\" class=\"usage-empty\">这个时间范围内还没有记录。</p>\n <ul v-else class=\"share-list\">\n <li v-for=\"m in overview.byModel\" :key=\"m.provider + m.model\">\n <div class=\"share-head\"><strong :title=\"m.provider\">{{ m.model }}</strong><span>{{ estimated(m) ? '≈ ' : '' }}{{ fmt(m.total) }} · {{ share(m.total, overview.totals.total) }}%</span></div>\n <div class=\"share-bar\"><i :style=\"{ width: share(m.total, overview.totals.total) + '%' }\" /></div>\n <small>{{ m.provider }} · 输入 {{ fmt(m.input) }} · 输出 {{ fmt(m.output) }} · {{ fmt(m.calls) }} 次</small>\n </li>\n </ul>\n </section>\n\n <section class=\"usage-panel\" aria-label=\"按 Bot\">\n <h2>按 Bot</h2>\n <p v-if=\"!overview.byAgent.length\" class=\"usage-empty\">这个时间范围内还没有记录。</p>\n <template v-else>\n <ul class=\"share-list\">\n <li v-for=\"a in liveAgents\" :key=\"a.agentId\">\n <div class=\"share-head\"><strong>{{ a.name }}</strong><span>{{ estimated(a) ? '≈ ' : '' }}{{ fmt(a.total) }} · {{ share(a.total, overview.totals.total) }}%</span></div>\n <div class=\"share-bar\"><i :style=\"{ width: share(a.total, overview.totals.total) + '%' }\" /></div>\n <small>{{ agentLine(a) }} · {{ fmt(a.calls) }} 次</small>\n </li>\n </ul>\n <template v-if=\"deletedAgents.length\">\n <h3 class=\"deleted-title\">已删除的 Bot <span>{{ fmt(overview.deletedTotals.total) }}</span></h3>\n <ul class=\"share-list deleted\">\n <li v-for=\"a in deletedAgents\" :key=\"a.agentId\">\n <div class=\"share-head\"><strong>{{ a.name }} <em class=\"badge\">已删除</em></strong><span>{{ estimated(a) ? '≈ ' : '' }}{{ fmt(a.total) }} · {{ share(a.total, overview.totals.total) }}%</span></div>\n <div class=\"share-bar\"><i :style=\"{ width: share(a.total, overview.totals.total) + '%' }\" /></div>\n <small>{{ agentLine(a) }} · {{ fmt(a.calls) }} 次</small>\n </li>\n </ul>\n </template>\n </template>\n </section>\n </div>\n\n <section class=\"usage-panel\" aria-label=\"按天趋势\">\n <h2>按天趋势</h2>\n <p v-if=\"!overview.byDay.length\" class=\"usage-empty\">这个时间范围内还没有记录。</p>\n <div v-else class=\"trend\" role=\"img\" :aria-label=\"`按天 Token 用量,共 ${overview.byDay.length} 天`\">\n <div v-for=\"day in overview.byDay\" :key=\"day.date\" class=\"trend-col\" :title=\"`${day.date}:${fmt(day.total)}(输入 ${fmt(day.input)},输出 ${fmt(day.output)})`\">\n <i :style=\"{ height: Math.max(2, day.total / maxDay * 100) + '%' }\" /><small>{{ day.date.slice(5) }}</small>\n </div>\n </div>\n </section>\n\n <section v-if=\"overview.historical\" class=\"usage-panel\" aria-label=\"历史(未分模型)\">\n <h2>历史(未分模型)</h2>\n <p class=\"usage-note\">{{ overview.historical.before }} 之前的旧统计,只有每个 Bot 的输入和输出,没有模型信息,也没有计入上面的总量。合计 {{ fmt(overview.historical.totals.total) }}(输入 {{ fmt(overview.historical.totals.input) }} · 输出 {{ fmt(overview.historical.totals.output) }})。</p>\n <table class=\"usage-table\"><thead><tr><th>Bot</th><th>输入</th><th>输出</th><th>合计</th></tr></thead>\n <tbody><tr v-for=\"a in overview.historical.byAgent\" :key=\"a.agentId\"><td>{{ a.name }} <em v-if=\"a.deleted\" class=\"badge\">已删除</em></td><td>{{ fmt(a.input) }}</td><td>{{ fmt(a.output) }}</td><td>{{ fmt(a.total) }}</td></tr></tbody></table>\n </section>\n\n <section class=\"usage-panel\" aria-label=\"Bot 与模型明细\">\n <h2>明细:Bot × 模型</h2>\n <p v-if=\"!details?.rows.length\" class=\"usage-empty\">这个时间范围内还没有记录。</p>\n <div v-else class=\"table-scroll\"><table class=\"usage-table\">\n <thead><tr><th>Bot</th><th>模型</th><th>输入</th><th>输出</th><th>缓存读</th><th>缓存写</th><th>推理</th><th>合计</th><th>次数</th><th>最近使用</th></tr></thead>\n <tbody><tr v-for=\"r in details.rows\" :key=\"r.agentId + r.provider + r.model\" :class=\"{ deleted: r.deleted }\">\n <td>{{ r.name }} <em v-if=\"r.deleted\" class=\"badge\">已删除</em></td><td :title=\"r.provider\">{{ r.model }}</td>\n <td>{{ fmt(r.input) }}</td><td>{{ fmt(r.output) }}</td><td>{{ fmt(r.cacheRead) }}</td><td>{{ fmt(r.cacheWrite) }}</td><td>{{ fmt(r.reasoning) }}</td>\n <td>{{ estimated(r) ? '≈ ' : '' }}{{ fmt(r.total) }}</td><td>{{ fmt(r.calls) }}<span v-if=\"r.estimatedCalls\" class=\"est\"> (估算 {{ fmt(r.estimatedCalls) }})</span></td><td>{{ when(r.lastAt) }}</td>\n </tr></tbody></table></div>\n <div v-if=\"pages > 1\" class=\"pager\"><button :disabled=\"page === 0 || loading\" @click=\"turn(-1)\">上一页</button><span>{{ page + 1 }} / {{ pages }}</span><button :disabled=\"page >= pages - 1 || loading\" @click=\"turn(1)\">下一页</button></div>\n </section>\n <p class=\"usage-footer\">不含辅助模型调用(标题、压缩、复盘、记忆提炼等)。已删除 Bot 的用量会保留。日期按 Host 本机时区划分。</p>\n </template>\n </div>\n </main>\n</template>\n\n<style scoped>\n.usage-page{height:100dvh;display:flex;flex-direction:column;background:var(--canvas);color:var(--text-primary);min-width:0}\n.usage-header{display:flex;align-items:center;gap:12px;min-height:72px;box-sizing:border-box;padding:12px 24px;border-bottom:1px solid var(--line);background:var(--surface);flex-wrap:wrap}\nh1{font-size:20px;margin:0 20px 0 0;letter-spacing:-.02em}\nh2{font-size:15px;margin:0 0 12px}\n.usage-header button,.pager button{border:1px solid var(--line);background:var(--surface);color:var(--text-primary);border-radius:10px;min-height:36px;padding:0 14px;cursor:pointer}\n.usage-header button:disabled,.pager button:disabled{opacity:.5;cursor:default}\n.icon-button{padding:8px!important;width:40px;flex-shrink:0}\n.segmented{display:flex;gap:3px;border:1px solid var(--line);border-radius:10px;padding:3px;background:var(--surface);margin-left:auto}\n.segmented button{min-height:32px;border:0!important;background:transparent!important;white-space:nowrap}\n.segmented button[aria-selected=true]{background:var(--surface-soft)!important;font-weight:600}\n.usage-action{background:var(--accent)!important;color:var(--text-on-solid)!important;border-color:transparent!important}\n.usage-body{flex:1;overflow:auto;padding:20px 24px 32px;display:flex;flex-direction:column;gap:16px}\n.usage-note,.usage-footer,.usage-empty,small{color:var(--text-secondary);font-size:13px;margin:0}\n.estimated-note{color:var(--text-muted)}\n.usage-error{color:var(--danger);margin:0}\n.usage-cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px}\n.usage-card,.usage-panel{background:var(--surface);border:1px solid var(--line);border-radius:14px;padding:16px;min-width:0}\n.usage-card{display:flex;flex-direction:column;gap:4px}.usage-card strong{font-size:26px;letter-spacing:-.02em}.usage-card span{color:var(--text-secondary);font-size:12px}\n.usage-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(340px,1fr));gap:16px}\n.share-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:12px}\n.share-head{display:flex;justify-content:space-between;gap:12px;font-size:14px}.share-head strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.share-head span{color:var(--text-secondary);white-space:nowrap}\n.share-bar{height:6px;border-radius:3px;background:var(--surface-soft);margin:6px 0 4px;overflow:hidden}.share-bar i{display:block;height:100%;background:var(--accent);border-radius:3px}\n.deleted-title{font-size:13px;margin:16px 0 8px;color:var(--text-secondary);display:flex;justify-content:space-between}\n.deleted .share-bar i{background:var(--text-muted)}\n.badge{font-style:normal;font-size:11px;border:1px solid var(--line);border-radius:6px;padding:1px 6px;color:var(--text-secondary);margin-left:4px}\n.trend{display:flex;align-items:flex-end;gap:4px;height:140px;overflow-x:auto;padding-bottom:2px}\n.trend-col{flex:1 0 22px;max-width:48px;height:100%;display:flex;flex-direction:column;justify-content:flex-end;align-items:center;gap:4px}\n.trend-col i{display:block;width:100%;background:var(--accent);border-radius:4px 4px 0 0;min-height:2px}.trend-col small{font-size:10px}\n.table-scroll{overflow-x:auto}\n.usage-table{width:100%;border-collapse:collapse;font-size:13px}\n.usage-table th,.usage-table td{text-align:left;padding:8px 10px;border-bottom:1px solid var(--line);white-space:nowrap;font-variant-numeric:tabular-nums}\n.usage-table th{color:var(--text-secondary);font-weight:500}\n.usage-table tr.deleted td{color:var(--text-secondary)}\n.est{color:var(--text-muted)}\n.pager{display:flex;align-items:center;justify-content:flex-end;gap:12px;margin-top:12px;font-size:13px}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, onMounted, ref, watch } from 'vue'\nimport { useRoute, useRouter } from 'vue-router'\nimport { apiRequest } from '@/api/client'\nimport AppIcon from '@/components/common/AppIcon.vue'\nimport UsageActivityHeatmap from '@/components/workspace/UsageActivityHeatmap.vue'\nimport type { WorkspaceUsageAgentRow, WorkspaceUsageDayRow, WorkspaceUsageDetails, WorkspaceUsageOverview, WorkspaceUsageRange, WorkspaceUsageTotals } from '@shared/workspace'\n\nconst router = useRouter(), route = useRoute()\nconst ranges: Array<{ id: WorkspaceUsageRange; label: string }> = [{ id: 'today', label: '今天' }, { id: '7d', label: '7 天' }, { id: '30d', label: '30 天' }, { id: 'all', label: '全部' }]\nconst range = ref<WorkspaceUsageRange>('7d')\nconst overview = ref<WorkspaceUsageOverview | null>(null), details = ref<WorkspaceUsageDetails | null>(null)\nconst calendarDays = ref<Array<Pick<WorkspaceUsageDayRow, 'date' | 'total'>>>([])\nconst loading = ref(true), error = ref(''), exporting = ref(false), page = ref(0)\nconst pageSize = 20\nconst backPath = computed(() => typeof route.query.from === 'string' && /^\\/conversations(?:\\/[a-f0-9-]{36})?(?:\\?taskId=[a-f0-9-]{36})?$/i.test(route.query.from) ? route.query.from : '/conversations')\nconst fmt = (n: number) => n.toLocaleString('zh-CN')\nconst compact = (n: number) => n >= 1e9 ? `${(n / 1e9).toFixed(2)}B` : n >= 1e6 ? `${(n / 1e6).toFixed(2)}M` : n >= 1e4 ? `${(n / 1e3).toFixed(1)}K` : fmt(n)\nconst share = (value: number, total: number) => total > 0 ? Math.round(value / total * 1000) / 10 : 0\nconst estimated = (t: WorkspaceUsageTotals) => t.estimatedCalls > 0\nconst when = (at: number) => new Date(at).toLocaleString('zh-CN', { hour12: false })\nconst liveAgents = computed(() => overview.value?.byAgent.filter(a => !a.deleted) ?? [])\nconst deletedAgents = computed(() => overview.value?.byAgent.filter(a => a.deleted) ?? [])\nconst maxDay = computed(() => Math.max(1, ...(overview.value?.byDay.map(d => d.total) ?? [0])))\nconst pages = computed(() => Math.max(1, Math.ceil((details.value?.total ?? 0) / pageSize)))\nconst sinceLabel = computed(() => overview.value?.firstRecordedAt ? `统计自 ${when(overview.value.firstRecordedAt)} 起` : '尚无记录')\n\nlet generation = 0\nasync function load() {\n const current = ++generation\n loading.value = true\n try {\n const query = `range=${range.value}`\n const [o, d, calendar] = await Promise.all([\n apiRequest<WorkspaceUsageOverview>(`/api/app/usage/overview?${query}`),\n apiRequest<WorkspaceUsageDetails>(`/api/app/usage/details?${query}&limit=${pageSize}&offset=${page.value * pageSize}`),\n // Heatmap always looks at the last ~12 months; ignore the page range filter.\n apiRequest<WorkspaceUsageOverview>('/api/app/usage/overview?range=all'),\n ])\n if (current !== generation) return\n overview.value = o; details.value = d\n calendarDays.value = calendar.byDay.map(day => ({ date: day.date, total: day.total }))\n error.value = ''\n } catch (e) { if (current === generation) error.value = e instanceof Error ? e.message : '无法读取 Token 用量' }\n finally { if (current === generation) loading.value = false }\n}\nwatch(range, () => { page.value = 0; void load() })\nfunction turn(delta: number) { page.value = Math.min(pages.value - 1, Math.max(0, page.value + delta)); void load() }\nasync function exportCsv() {\n exporting.value = true\n try {\n const text = await apiRequest<string>(`/api/app/usage/details?range=${range.value}&format=csv`)\n const url = URL.createObjectURL(new Blob([text], { type: 'text/csv;charset=utf-8' }))\n const link = document.createElement('a')\n link.href = url; link.download = `yaovi-token-usage-${range.value}.csv`; link.click()\n window.setTimeout(() => URL.revokeObjectURL(url), 1000)\n } catch (e) { error.value = e instanceof Error ? e.message : '导出失败' }\n finally { exporting.value = false }\n}\nconst agentLine = (a: WorkspaceUsageAgentRow) => `输入 ${fmt(a.input)} · 输出 ${fmt(a.output)}`\nonMounted(load)\n</script>\n\n<template>\n <main class=\"usage-page\" aria-label=\"Token 用量总览\">\n <header class=\"usage-header\">\n <button class=\"icon-button\" type=\"button\" aria-label=\"返回聊天\" @click=\"router.push(backPath)\"><AppIcon name=\"chevron-left\" :size=\"20\" /></button>\n <AppIcon name=\"history\" :size=\"23\" /><h1>Token 用量</h1>\n <div class=\"segmented\" role=\"tablist\" aria-label=\"时间范围\"><button v-for=\"item in ranges\" :key=\"item.id\" role=\"tab\" :aria-selected=\"range === item.id\" @click=\"range = item.id\">{{ item.label }}</button></div>\n <button class=\"usage-action\" type=\"button\" :disabled=\"exporting || !overview?.totals.calls\" @click=\"exportCsv\">{{ exporting ? '导出中…' : '导出 CSV' }}</button>\n <button class=\"icon-button\" type=\"button\" aria-label=\"刷新用量\" @click=\"load\"><AppIcon name=\"refresh\" :size=\"17\" /></button>\n </header>\n <div class=\"usage-body\">\n <p v-if=\"error\" class=\"usage-error\" role=\"alert\">{{ error }}</p>\n <p v-if=\"loading && !overview\" class=\"usage-note\" role=\"status\">正在读取 Token 用量…</p>\n <template v-if=\"overview\">\n <p class=\"usage-note\">{{ sinceLabel }}。合计 = 输入 + 输出;缓存和推理 Token 单独列出,不重复计入。</p>\n <section class=\"usage-cards\" aria-label=\"总量\">\n <div class=\"usage-card\"><small>合计</small><strong>{{ compact(overview.totals.total) }}</strong><span>{{ estimated(overview.totals) ? '≈ ' : '' }}{{ fmt(overview.totals.calls) }} 次调用</span></div>\n <div class=\"usage-card\"><small>输入</small><strong>{{ compact(overview.totals.input) }}</strong><span>缓存读 {{ fmt(overview.totals.cacheRead) }} · 缓存写 {{ fmt(overview.totals.cacheWrite) }}</span></div>\n <div class=\"usage-card\"><small>输出</small><strong>{{ compact(overview.totals.output) }}</strong><span>推理 {{ fmt(overview.totals.reasoning) }}</span></div>\n <div class=\"usage-card\"><small>已删除 Bot</small><strong>{{ compact(overview.deletedTotals.total) }}</strong><span>{{ share(overview.deletedTotals.total, overview.totals.total) }}% 占比</span></div>\n </section>\n <p v-if=\"overview.totals.estimatedCalls\" class=\"usage-note estimated-note\">其中 {{ fmt(overview.totals.estimatedCalls) }} 次为本机估算值(供应商未返回用量),标记 ≈。</p>\n\n <UsageActivityHeatmap :days=\"calendarDays\" />\n\n <div class=\"usage-grid\">\n <section class=\"usage-panel\" aria-label=\"按模型\">\n <h2>按模型</h2>\n <p v-if=\"!overview.byModel.length\" class=\"usage-empty\">这个时间范围内还没有记录。</p>\n <ul v-else class=\"share-list\">\n <li v-for=\"m in overview.byModel\" :key=\"m.provider + m.model\">\n <div class=\"share-head\"><strong :title=\"m.provider\">{{ m.model }}</strong><span>{{ estimated(m) ? '≈ ' : '' }}{{ fmt(m.total) }} · {{ share(m.total, overview.totals.total) }}%</span></div>\n <div class=\"share-bar\"><i :style=\"{ width: share(m.total, overview.totals.total) + '%' }\" /></div>\n <small>{{ m.provider }} · 输入 {{ fmt(m.input) }} · 输出 {{ fmt(m.output) }} · {{ fmt(m.calls) }} 次</small>\n </li>\n </ul>\n </section>\n\n <section class=\"usage-panel\" aria-label=\"按 Bot\">\n <h2>按 Bot</h2>\n <p v-if=\"!overview.byAgent.length\" class=\"usage-empty\">这个时间范围内还没有记录。</p>\n <template v-else>\n <ul class=\"share-list\">\n <li v-for=\"a in liveAgents\" :key=\"a.agentId\">\n <div class=\"share-head\"><strong>{{ a.name }}</strong><span>{{ estimated(a) ? '≈ ' : '' }}{{ fmt(a.total) }} · {{ share(a.total, overview.totals.total) }}%</span></div>\n <div class=\"share-bar\"><i :style=\"{ width: share(a.total, overview.totals.total) + '%' }\" /></div>\n <small>{{ agentLine(a) }} · {{ fmt(a.calls) }} 次</small>\n </li>\n </ul>\n <template v-if=\"deletedAgents.length\">\n <h3 class=\"deleted-title\">已删除的 Bot <span>{{ fmt(overview.deletedTotals.total) }}</span></h3>\n <ul class=\"share-list deleted\">\n <li v-for=\"a in deletedAgents\" :key=\"a.agentId\">\n <div class=\"share-head\"><strong>{{ a.name }} <em class=\"badge\">已删除</em></strong><span>{{ estimated(a) ? '≈ ' : '' }}{{ fmt(a.total) }} · {{ share(a.total, overview.totals.total) }}%</span></div>\n <div class=\"share-bar\"><i :style=\"{ width: share(a.total, overview.totals.total) + '%' }\" /></div>\n <small>{{ agentLine(a) }} · {{ fmt(a.calls) }} 次</small>\n </li>\n </ul>\n </template>\n </template>\n </section>\n </div>\n\n <section class=\"usage-panel\" aria-label=\"按天趋势\">\n <h2>按天趋势</h2>\n <p v-if=\"!overview.byDay.length\" class=\"usage-empty\">这个时间范围内还没有记录。</p>\n <div v-else class=\"trend\" role=\"img\" :aria-label=\"`按天 Token 用量,共 ${overview.byDay.length} 天`\">\n <div v-for=\"day in overview.byDay\" :key=\"day.date\" class=\"trend-col\" :title=\"`${day.date}:${fmt(day.total)}(输入 ${fmt(day.input)},输出 ${fmt(day.output)})`\">\n <i :style=\"{ height: Math.max(2, day.total / maxDay * 100) + '%' }\" /><small>{{ day.date.slice(5) }}</small>\n </div>\n </div>\n </section>\n\n <section v-if=\"overview.historical\" class=\"usage-panel\" aria-label=\"历史(未分模型)\">\n <h2>历史(未分模型)</h2>\n <p class=\"usage-note\">{{ overview.historical.before }} 之前的旧统计,只有每个 Bot 的输入和输出,没有模型信息,也没有计入上面的总量。合计 {{ fmt(overview.historical.totals.total) }}(输入 {{ fmt(overview.historical.totals.input) }} · 输出 {{ fmt(overview.historical.totals.output) }})。</p>\n <table class=\"usage-table\"><thead><tr><th>Bot</th><th>输入</th><th>输出</th><th>合计</th></tr></thead>\n <tbody><tr v-for=\"a in overview.historical.byAgent\" :key=\"a.agentId\"><td>{{ a.name }} <em v-if=\"a.deleted\" class=\"badge\">已删除</em></td><td>{{ fmt(a.input) }}</td><td>{{ fmt(a.output) }}</td><td>{{ fmt(a.total) }}</td></tr></tbody></table>\n </section>\n\n <section class=\"usage-panel\" aria-label=\"Bot 与模型明细\">\n <h2>明细:Bot × 模型</h2>\n <p v-if=\"!details?.rows.length\" class=\"usage-empty\">这个时间范围内还没有记录。</p>\n <div v-else class=\"table-scroll\"><table class=\"usage-table\">\n <thead><tr><th>Bot</th><th>模型</th><th>输入</th><th>输出</th><th>缓存读</th><th>缓存写</th><th>推理</th><th>合计</th><th>次数</th><th>最近使用</th></tr></thead>\n <tbody><tr v-for=\"r in details.rows\" :key=\"r.agentId + r.provider + r.model\" :class=\"{ deleted: r.deleted }\">\n <td>{{ r.name }} <em v-if=\"r.deleted\" class=\"badge\">已删除</em></td><td :title=\"r.provider\">{{ r.model }}</td>\n <td>{{ fmt(r.input) }}</td><td>{{ fmt(r.output) }}</td><td>{{ fmt(r.cacheRead) }}</td><td>{{ fmt(r.cacheWrite) }}</td><td>{{ fmt(r.reasoning) }}</td>\n <td>{{ estimated(r) ? '≈ ' : '' }}{{ fmt(r.total) }}</td><td>{{ fmt(r.calls) }}<span v-if=\"r.estimatedCalls\" class=\"est\"> (估算 {{ fmt(r.estimatedCalls) }})</span></td><td>{{ when(r.lastAt) }}</td>\n </tr></tbody></table></div>\n <div v-if=\"pages > 1\" class=\"pager\"><button :disabled=\"page === 0 || loading\" @click=\"turn(-1)\">上一页</button><span>{{ page + 1 }} / {{ pages }}</span><button :disabled=\"page >= pages - 1 || loading\" @click=\"turn(1)\">下一页</button></div>\n </section>\n <p class=\"usage-footer\">不含辅助模型调用(标题、压缩、复盘、记忆提炼等)。已删除 Bot 的用量会保留。日期按 Host 本机时区划分。</p>\n </template>\n </div>\n </main>\n</template>\n\n<style scoped>\n.usage-page{height:100dvh;display:flex;flex-direction:column;background:var(--canvas);color:var(--text-primary);min-width:0}\n.usage-header{display:flex;align-items:center;gap:12px;min-height:72px;box-sizing:border-box;padding:12px 24px;border-bottom:1px solid var(--line);background:var(--surface);flex-wrap:wrap}\nh1{font-size:20px;margin:0 20px 0 0;letter-spacing:-.02em}\nh2{font-size:15px;margin:0 0 12px}\n.usage-header button,.pager button{border:1px solid var(--line);background:var(--surface);color:var(--text-primary);border-radius:10px;min-height:36px;padding:0 14px;cursor:pointer}\n.usage-header button:disabled,.pager button:disabled{opacity:.5;cursor:default}\n.icon-button{padding:8px!important;width:40px;flex-shrink:0}\n.segmented{display:flex;gap:3px;border:1px solid var(--line);border-radius:10px;padding:3px;background:var(--surface);margin-left:auto}\n.segmented button{min-height:32px;border:0!important;background:transparent!important;white-space:nowrap}\n.segmented button[aria-selected=true]{background:var(--surface-soft)!important;font-weight:600}\n.usage-action{background:var(--accent)!important;color:var(--text-on-solid)!important;border-color:transparent!important}\n.usage-body{flex:1;overflow:auto;padding:20px 24px 32px;display:flex;flex-direction:column;gap:16px}\n.usage-note,.usage-footer,.usage-empty,small{color:var(--text-secondary);font-size:13px;margin:0}\n.estimated-note{color:var(--text-muted)}\n.usage-error{color:var(--danger);margin:0}\n.usage-cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px}\n.usage-card,.usage-panel{background:var(--surface);border:1px solid var(--line);border-radius:14px;padding:16px;min-width:0}\n.usage-card{display:flex;flex-direction:column;gap:4px}.usage-card strong{font-size:26px;letter-spacing:-.02em}.usage-card span{color:var(--text-secondary);font-size:12px}\n.usage-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(340px,1fr));gap:16px}\n.share-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:12px}\n.share-head{display:flex;justify-content:space-between;gap:12px;font-size:14px}.share-head strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.share-head span{color:var(--text-secondary);white-space:nowrap}\n.share-bar{height:6px;border-radius:3px;background:var(--surface-soft);margin:6px 0 4px;overflow:hidden}.share-bar i{display:block;height:100%;background:var(--accent);border-radius:3px}\n.deleted-title{font-size:13px;margin:16px 0 8px;color:var(--text-secondary);display:flex;justify-content:space-between}\n.deleted .share-bar i{background:var(--text-muted)}\n.badge{font-style:normal;font-size:11px;border:1px solid var(--line);border-radius:6px;padding:1px 6px;color:var(--text-secondary);margin-left:4px}\n.trend{display:flex;align-items:flex-end;gap:4px;height:140px;overflow-x:auto;padding-bottom:2px}\n.trend-col{flex:1 0 22px;max-width:48px;height:100%;display:flex;flex-direction:column;justify-content:flex-end;align-items:center;gap:4px}\n.trend-col i{display:block;width:100%;background:var(--accent);border-radius:4px 4px 0 0;min-height:2px}.trend-col small{font-size:10px}\n.table-scroll{overflow-x:auto}\n.usage-table{width:100%;border-collapse:collapse;font-size:13px}\n.usage-table th,.usage-table td{text-align:left;padding:8px 10px;border-bottom:1px solid var(--line);white-space:nowrap;font-variant-numeric:tabular-nums}\n.usage-table th{color:var(--text-secondary);font-weight:500}\n.usage-table tr.deleted td{color:var(--text-secondary)}\n.est{color:var(--text-muted)}\n.pager{display:flex;align-items:center;justify-content:flex-end;gap:12px;margin-top:12px;font-size:13px}\n</style>\n"],"mappings":"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"}
|