mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 00:06:08 +00:00
feat(frontend): adopt UI v0.8.0 and refine form feedback
Unify theme namespaces and plugin theme delivery, improve validation and empty states, and pin UI v0.8.0 plus plugin workbench v0.9.0. Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
+41
-36
@@ -90,12 +90,12 @@
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[Pinia 持久化配置] --> B[resolveTheme]
|
||||
A[Pinia 持久化配置] --> B[createTheme]
|
||||
B --> C[Seed Token]
|
||||
C --> D[Map Token]
|
||||
D --> E[Alias Token]
|
||||
E --> F[Component Token]
|
||||
F --> G[#cp-runtime-theme]
|
||||
F --> G[#z-runtime-theme]
|
||||
G --> H[Vue / Tailwind / ECharts]
|
||||
|
||||
B --> I[Theme Editor 草稿]
|
||||
@@ -119,7 +119,7 @@ flowchart LR
|
||||
[SDK 页面与宿主桥](../backend/crates/gateway-plugin/sdk/docs/capabilities.md#页面宿主桥-v2)。
|
||||
`theme/` 根目录只保留公开入口 `index.ts` 和唯一类型文件 `types.ts`;内部实现按 `core/`、`derive/`、`runtime/` 分层,不增加嵌套 barrel。
|
||||
纯派生模块不访问 DOM,`theme/runtime/browser.ts` 不包含派生规则,Theme Store 不复制算法。
|
||||
普通 Map 字段按 camelCase → kebab-case 统一生成 `--cp-*`;Semantic 与 Preset Color 仅维护各自的短角色表。
|
||||
普通 Map 字段按 camelCase → kebab-case 统一生成 `--z-*`;Semantic 与 Preset Color 仅维护各自的短角色表。
|
||||
`ThemeTokenName` 在 `types.ts` 中由 Map 契约推导,无需手写重复的联合类型和对象映射
|
||||
|
||||
下文未带宿主路径的 `theme/`、`styles/` 均指 `codex-proxy-ui/src/` 下的目录,联调入口见[开发文档](development.md#源码联调)
|
||||
@@ -215,20 +215,20 @@ Quaternary;主按钮白字与功能色文字遵循各自的容器配对规则
|
||||
|
||||
### Alias Token
|
||||
|
||||
Alias 按视觉角色命名,使用 `--cp-` 命名空间;整体分层对齐 Ant Design,并以 Container / On Container 表达成对的
|
||||
Alias 按视觉角色命名,使用 `--z-` 命名空间;整体分层对齐 Ant Design,并以 Container / On Container 表达成对的
|
||||
语义表面与前景:
|
||||
|
||||
| 角色 | CSS Token | 主要消费者 |
|
||||
| --- | --- | --- |
|
||||
| 品牌交互 | `--cp-color-primary*` | 主操作、选中态、焦点与强调 |
|
||||
| 链接 | `--cp-color-link*` | 普通文本链接 |
|
||||
| 表面 | `--cp-color-bg-*` | 页面、容器、浮层和遮罩 |
|
||||
| 填充 | `--cp-color-fill-*` | 轨道、弱背景和静态层级 |
|
||||
| 文字 | `--cp-color-text*` | 标题、正文和辅助信息 |
|
||||
| 选择 | `--cp-control-item-bg-active*` | Segmented、Select 和选择控件 |
|
||||
| 焦点 | `--cp-control-outline` | 键盘焦点和输入反馈 |
|
||||
| 功能色 | `--cp-color-info / success / warning / error-*` | 系统反馈与状态 |
|
||||
| 预设彩色 | `--cp-color-{blue,cyan,green,orange,purple,red}-{container,container-strong,border,solid,text,on-container}` | 分类标签与数据强调 |
|
||||
| 品牌交互 | `--z-color-primary*` | 主操作、选中态、焦点与强调 |
|
||||
| 链接 | `--z-color-link*` | 普通文本链接 |
|
||||
| 表面 | `--z-color-bg-*` | 页面、容器、浮层和遮罩 |
|
||||
| 填充 | `--z-color-fill-*` | 轨道、弱背景和静态层级 |
|
||||
| 文字 | `--z-color-text*` | 标题、正文和辅助信息 |
|
||||
| 选择 | `--z-control-item-bg-active*` | Segmented、Select 和选择控件 |
|
||||
| 焦点 | `--z-control-outline` | 键盘焦点和输入反馈 |
|
||||
| 功能色 | `--z-color-info / success / warning / error-*` | 系统反馈与状态 |
|
||||
| 预设彩色 | `--z-color-{blue,cyan,green,orange,purple,red}-{container,container-strong,border,solid,text,on-container}` | 分类标签与数据强调 |
|
||||
|
||||
Alias 不在页面中追加修色;中性与彩色文字在派生层执行各自的背景配对约束。Component
|
||||
Token 直接覆盖时不会自动重算同组件的其他状态;需要保持梯度关系时应修改 Seed,而不是逐个覆盖 Map Token
|
||||
@@ -239,16 +239,16 @@ Token 直接覆盖时不会自动重算同组件的其他状态;需要保持
|
||||
|
||||
| 组件 | 代表 Token |
|
||||
| --- | --- |
|
||||
| Button | `--cp-button-primary-color / bg / hover-bg / active-bg`、`--cp-button-secondary-bg / hover-bg / active-bg` |
|
||||
| IconButton | `--cp-icon-button-secondary-bg / hover-bg / active-bg` |
|
||||
| Input | `--cp-input-bg / hover-bg / active-bg / error-active-bg` 与对应 Shadow |
|
||||
| Menu | `--cp-menu-item-selected-bg` |
|
||||
| Popover | `--cp-popover-header-bg` |
|
||||
| Table | `--cp-table-row-bg / stripe-bg / hover-bg / selected-bg / selected-hover-bg / height` |
|
||||
| Card | `--cp-card-bg / border-radius / shadow` |
|
||||
| Layout | `--cp-layout-sider-bg / shadow` |
|
||||
| Scrollbar | `--cp-scrollbar-thumb-bg / hover-bg` |
|
||||
| BrandMark | `--cp-brand-mark-bg` |
|
||||
| Button | `--z-button-primary-color / bg / hover-bg / active-bg`、`--z-button-secondary-bg / hover-bg / active-bg` |
|
||||
| IconButton | `--z-icon-button-secondary-bg / hover-bg / active-bg` |
|
||||
| Input | `--z-input-bg / hover-bg / active-bg / error-active-bg` 与对应 Shadow |
|
||||
| Menu | `--z-menu-item-selected-bg` |
|
||||
| Popover | `--z-popover-header-bg` |
|
||||
| Table | `--z-table-row-bg / stripe-bg / hover-bg / selected-bg / selected-hover-bg / height` |
|
||||
| Card | `--z-card-bg / border-radius / shadow` |
|
||||
| Layout | `--z-layout-sider-bg / shadow` |
|
||||
| Scrollbar | `--z-scrollbar-thumb-bg / hover-bg` |
|
||||
| BrandMark | `--z-brand-mark-bg` |
|
||||
|
||||
组件状态由 UI 库的 Component 派生层统一生成,页面不维护混色系数:
|
||||
|
||||
@@ -308,13 +308,17 @@ Theme Store 统一读取持久化配置并初始化主题,首个 Vue 组件渲
|
||||
|
||||
### CSS Variables 提交
|
||||
|
||||
主题内部和持久化配置使用 `color-primary`、`input-bg` 等语义 Token 名,不包含 CSS 前缀。
|
||||
UI 构建插件 `CodexProxyUI({ themePrefix: 'z' })` 同时配置变量生成与样式引用,默认前缀为 `z`;
|
||||
它与组件导出名、DOM 类名和 utility 类名独立,修改后需重新构建。配置方式见 UI 的[主题定制](../modules/ui/playground/guide/theme.md#自定义主题前缀)
|
||||
|
||||
全局主题由 `<head>` 中唯一的运行时样式节点承载:
|
||||
|
||||
```html
|
||||
<style id="cp-runtime-theme">
|
||||
<style id="z-runtime-theme">
|
||||
:root[data-theme='dark'][data-theme-color='relay-blue'] {
|
||||
color-scheme: dark;
|
||||
--cp-color-bg-layout: #0B111C;
|
||||
--z-color-bg-layout: #0B111C;
|
||||
/* 其余 Map、Alias 与 Component Token */
|
||||
}
|
||||
</style>
|
||||
@@ -324,14 +328,15 @@ Theme Store 统一读取持久化配置并初始化主题,首个 Vue 组件渲
|
||||
全局弹窗通过 Teleport 挂到 `body` 后仍继承根变量
|
||||
|
||||
> [!NOTE]
|
||||
> Theme Editor 预览是例外:草稿 Token 以内联变量写在影子环境的局部根节点上,仅影响预览,不污染已保存主题
|
||||
> Theme Editor 预览是例外:草稿 Token 经 `createThemeVars()` 转换后以内联变量写在影子环境的局部根节点上,仅影响预览,不污染已保存主题
|
||||
|
||||
### 切换与图表
|
||||
|
||||
- 用户主动切换时,以点击位置为圆心运行 View Transition;不支持时使用 180ms 颜色过渡
|
||||
- 初始化、系统主题变化或减少动态效果时直接提交,不播放扩散动画
|
||||
- 每次有效提交只增加一次 `themeRevision`,相同签名不会重复刷新
|
||||
- `useThemeColor()` 在正式页面读取根 CSS Variables,在影子预览中优先读取注入的局部 Token
|
||||
- `useThemeColor()` 接收语义 Token 名,通过 `getThemeVarName()` 读取当前前缀下的 CSS Variables;影子预览优先读取注入的局部变量
|
||||
- 插件桥接传递语义 Token,由插件构建元数据指定的前缀生成 CSS 变量,宿主与插件可独立配置
|
||||
- 图表 Option 在主题 revision 或预览 Token 改变后重算,`BaseChart` 通过 `setOption` 更新现有 Canvas
|
||||
|
||||
## 主题编辑器
|
||||
@@ -381,24 +386,24 @@ Store 在其他入口发生变化时更新保存基线;没有未保存修改
|
||||
|
||||
### CSS Token
|
||||
|
||||
- 全局 Token:`--cp-color-bg-container`、`--cp-font-size`
|
||||
- Component Token:`--cp-table-row-hover-bg`、`--cp-input-active-shadow`
|
||||
- Preset Color Token:`--cp-color-purple-container-strong`、`--cp-color-cyan-on-container`
|
||||
- 全局 Token:`--z-color-bg-container`、`--z-font-size`
|
||||
- Component Token:`--z-table-row-hover-bg`、`--z-input-active-shadow`
|
||||
- Preset Color Token:`--z-color-purple-container-strong`、`--z-color-cyan-on-container`
|
||||
- 业务组件按[通用颜色消费](#通用颜色消费)选择角色,不在主题层声明业务域名称
|
||||
- Map 与 Component 字段由 `theme/core/tokens.ts` 统一生成 CSS Token;禁止在解析器中再写平行的逐项映射表
|
||||
- Map 与 Component 字段由 `theme/core/tokens.ts` 统一生成语义 Token,运行时通过 `createThemeVars()` 输出 CSS 变量;禁止在解析器中再写平行的逐项映射表
|
||||
- 不使用 `accent`、`soft`、`current`、`subtle` 等与现有角色重叠的平行词汇
|
||||
|
||||
### Vue 与 Tailwind CSS 4
|
||||
|
||||
组件优先使用 `bg-cp-*`、`text-cp-*`、`shadow-cp-*`、`rounded-cp-*` 等由 `@theme inline` 暴露的 utility:
|
||||
组件优先使用 `bg-z-*`、`text-z-*`、`shadow-z-*`、`rounded-z-*` 等由 `@theme inline` 暴露的 utility:
|
||||
|
||||
```vue
|
||||
<section class="rounded-cp-card bg-cp-bg-container text-cp-text shadow-cp-card">
|
||||
<section class="rounded-z-card bg-z-bg-container text-z-text shadow-z-card">
|
||||
...
|
||||
</section>
|
||||
```
|
||||
|
||||
仅当值需要在 CSS 函数、SVG 或局部派生中参与计算时,直接读取 `var(--cp-*)`。不在页面组件中重新实现色阶、
|
||||
仅当值需要在 CSS 函数、SVG 或局部派生中参与计算时,CSS 中直接读取 `var(--z-*)`,JavaScript 中通过 `getThemeVarName()` 生成变量名。不在页面组件中重新实现色阶、
|
||||
对比度或明暗算法
|
||||
|
||||
共享包的 `styles/tailwind.css` 是唯一 `@theme inline` 注册表,只注册 Tailwind 名称,不保存主题值。注册表按基础、排版、颜色、圆角、阴影、间距与尺寸排序;
|
||||
@@ -426,8 +431,8 @@ utility / arbitrary variant;Vue Transition、跨浏览器 Range、动态富文
|
||||
### 增加预置主题
|
||||
|
||||
1. 在 `ThemeColorPresetId` 增加稳定 ID
|
||||
2. 在 `THEME_COLOR_PRESETS` 声明名称、描述与主色 Seed
|
||||
3. 不新增页面特判,确认预置可通过同一 `resolveTheme()` 派生
|
||||
2. 在 `themeColorPresets` 声明名称、描述与主色 Seed
|
||||
3. 不新增页面特判,确认预置可通过同一 `createTheme()` 派生
|
||||
4. 在浅色、深色和系统模式下检查文本、浮层、Input、表格与图表
|
||||
|
||||
### 增加全局 Seed 或 Alias
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@boxicons/vue": "1.0.1",
|
||||
"@codex-proxy/ui": "github:zyycn/codex-proxy-ui#v0.7.0",
|
||||
"@codex-proxy/ui": "github:zyycn/codex-proxy-ui#v0.8.0",
|
||||
"@fontsource-variable/inter": "^5.3.0",
|
||||
"@fontsource-variable/jetbrains-mono": "^5.3.0",
|
||||
"@lucide/vue": "^1.61.0",
|
||||
|
||||
Generated
+53
-6
@@ -195,8 +195,8 @@ importers:
|
||||
specifier: 1.0.1
|
||||
version: 1.0.1(vue@3.5.44(typescript@6.0.3))
|
||||
'@codex-proxy/ui':
|
||||
specifier: github:zyycn/codex-proxy-ui#v0.7.0
|
||||
version: https://codeload.github.com/zyycn/codex-proxy-ui/tar.gz/0692ca034be88de6faf88bc76f1a6e9e7ad65225(axios@1.20.0(debug@4.4.3))(change-case@5.4.4)(vite@8.3.5(@types/node@26.6.5)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.44(typescript@6.0.3))
|
||||
specifier: github:zyycn/codex-proxy-ui#v0.8.0
|
||||
version: https://codeload.github.com/zyycn/codex-proxy-ui/tar.gz/d6100447010715c1203e67a4195fc6d875ff6249(axios@1.20.0(debug@4.4.3))(change-case@5.4.4)(jiti@2.7.0)(postcss@8.5.30)(vite@8.3.5(@types/node@26.6.5)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.44(typescript@6.0.3))(yaml@2.9.0)
|
||||
'@fontsource-variable/inter':
|
||||
specifier: ^5.3.0
|
||||
version: 5.3.0
|
||||
@@ -443,9 +443,9 @@ packages:
|
||||
resolution: {integrity: sha512-dlT1m5e/0yUL0kRNcQn7yGLVThkgbB0Ga/1AmfDDC/8ik6AIiSf2QLQO2zPYvefsHP0aFgxO93cVLCCfDp7kzQ==}
|
||||
engines: {node: '>= 20.12.0'}
|
||||
|
||||
'@codex-proxy/ui@https://codeload.github.com/zyycn/codex-proxy-ui/tar.gz/0692ca034be88de6faf88bc76f1a6e9e7ad65225':
|
||||
resolution: {gitHosted: true, integrity: sha512-JJesyJ1Y/QcdXhBtmKX6k0GyUgOSxiV8sJrUymvLyKjC8NhHLmtpXWt/evZcjYBDyJEIRll85bmSmtMF9/Wpqw==, tarball: https://codeload.github.com/zyycn/codex-proxy-ui/tar.gz/0692ca034be88de6faf88bc76f1a6e9e7ad65225}
|
||||
version: 0.7.0
|
||||
'@codex-proxy/ui@https://codeload.github.com/zyycn/codex-proxy-ui/tar.gz/d6100447010715c1203e67a4195fc6d875ff6249':
|
||||
resolution: {gitHosted: true, integrity: sha512-XNPErDXqq9OxvgLgSb0gVRGmI9Yf+Vx0dTjoZKzaAaCOX9exSirU5kEr9mJBeuo9E1DDw8817xV0B5Od9zH+5A==, tarball: https://codeload.github.com/zyycn/codex-proxy-ui/tar.gz/d6100447010715c1203e67a4195fc6d875ff6249}
|
||||
version: 0.8.0
|
||||
engines: {node: '>=24'}
|
||||
peerDependencies:
|
||||
vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||
@@ -2618,6 +2618,10 @@ packages:
|
||||
resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
|
||||
lilconfig@3.1.4:
|
||||
resolution: {integrity: sha512-zJZyBmQhArDRefuWTypZxwDpFDIYmrj8ypSBvwGD5P13Y95s6DjzKgu4DVdGbeQa5PDEsAOCAPNk4X+QOR006w==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
local-pkg@1.2.1:
|
||||
resolution: {integrity: sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==}
|
||||
engines: {node: '>=14'}
|
||||
@@ -2645,6 +2649,11 @@ packages:
|
||||
markdown-table@3.0.4:
|
||||
resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==}
|
||||
|
||||
marked-alert@2.1.2:
|
||||
resolution: {integrity: sha512-EFNRZ08d8L/iEIPLTlQMDjvwIsj03gxWCczYTht6DCiHJIZhMk4NK5gtPY9UqAYb09eV5VGT+jD4lp396E0I+w==}
|
||||
peerDependencies:
|
||||
marked: '>=7.0.0'
|
||||
|
||||
marked@18.1.0:
|
||||
resolution: {integrity: sha512-PamYXWWWg2nboG3oX5Ffzpy3EFZROqZJK9hSlMmUnekp7TxPs5DcT95vwd46m73/exfKCRVMtgXoPMI4FEJL6w==}
|
||||
engines: {node: '>= 20'}
|
||||
@@ -2973,6 +2982,24 @@ packages:
|
||||
pnpm-workspace-yaml@1.9.1:
|
||||
resolution: {integrity: sha512-Xj/T2X6DNAWbtk/9UQ0/TJRswltiHZevmcMjqoL7WrpJi67lu3qaslU4+I+zJ8wtwqmuvAE0KkB8rbv2ZdogBg==}
|
||||
|
||||
postcss-load-config@6.0.1:
|
||||
resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==}
|
||||
engines: {node: '>= 18'}
|
||||
peerDependencies:
|
||||
jiti: '>=1.21.0'
|
||||
postcss: '>=8.0.9'
|
||||
tsx: ^4.8.1
|
||||
yaml: ^2.4.2
|
||||
peerDependenciesMeta:
|
||||
jiti:
|
||||
optional: true
|
||||
postcss:
|
||||
optional: true
|
||||
tsx:
|
||||
optional: true
|
||||
yaml:
|
||||
optional: true
|
||||
|
||||
postcss-selector-parser@7.1.6:
|
||||
resolution: {integrity: sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -3525,7 +3552,7 @@ snapshots:
|
||||
fast-wrap-ansi: 0.2.2
|
||||
sisteransi: 1.0.5
|
||||
|
||||
'@codex-proxy/ui@https://codeload.github.com/zyycn/codex-proxy-ui/tar.gz/0692ca034be88de6faf88bc76f1a6e9e7ad65225(axios@1.20.0(debug@4.4.3))(change-case@5.4.4)(vite@8.3.5(@types/node@26.6.5)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.44(typescript@6.0.3))':
|
||||
'@codex-proxy/ui@https://codeload.github.com/zyycn/codex-proxy-ui/tar.gz/d6100447010715c1203e67a4195fc6d875ff6249(axios@1.20.0(debug@4.4.3))(change-case@5.4.4)(jiti@2.7.0)(postcss@8.5.30)(vite@8.3.5(@types/node@26.6.5)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.44(typescript@6.0.3))(yaml@2.9.0)':
|
||||
dependencies:
|
||||
'@ant-design/colors': 8.0.1
|
||||
'@ant-design/fast-color': 3.0.1
|
||||
@@ -3538,6 +3565,8 @@ snapshots:
|
||||
es-toolkit: 1.53.0
|
||||
gsap: 3.15.0
|
||||
marked: 18.1.0
|
||||
marked-alert: 2.1.2(marked@18.1.0)
|
||||
postcss-load-config: 6.0.1(jiti@2.7.0)(postcss@8.5.30)(yaml@2.9.0)
|
||||
sortablejs: 1.15.7
|
||||
vue: 3.5.44(typescript@6.0.3)
|
||||
optionalDependencies:
|
||||
@@ -3550,10 +3579,14 @@ snapshots:
|
||||
- focus-trap
|
||||
- fuse.js
|
||||
- idb-keyval
|
||||
- jiti
|
||||
- jwt-decode
|
||||
- nprogress
|
||||
- postcss
|
||||
- qrcode
|
||||
- tsx
|
||||
- universal-cookie
|
||||
- yaml
|
||||
|
||||
'@dprint/formatter@0.5.1': {}
|
||||
|
||||
@@ -5486,6 +5519,8 @@ snapshots:
|
||||
lightningcss-win32-arm64-msvc: 1.33.0
|
||||
lightningcss-win32-x64-msvc: 1.33.0
|
||||
|
||||
lilconfig@3.1.4: {}
|
||||
|
||||
local-pkg@1.2.1:
|
||||
dependencies:
|
||||
mlly: 1.8.2
|
||||
@@ -5514,6 +5549,10 @@ snapshots:
|
||||
|
||||
markdown-table@3.0.4: {}
|
||||
|
||||
marked-alert@2.1.2(marked@18.1.0):
|
||||
dependencies:
|
||||
marked: 18.1.0
|
||||
|
||||
marked@18.1.0: {}
|
||||
|
||||
math-intrinsics@1.1.0: {}
|
||||
@@ -6053,6 +6092,14 @@ snapshots:
|
||||
dependencies:
|
||||
yaml: 2.9.0
|
||||
|
||||
postcss-load-config@6.0.1(jiti@2.7.0)(postcss@8.5.30)(yaml@2.9.0):
|
||||
dependencies:
|
||||
lilconfig: 3.1.4
|
||||
optionalDependencies:
|
||||
jiti: 2.7.0
|
||||
postcss: 8.5.30
|
||||
yaml: 2.9.0
|
||||
|
||||
postcss-selector-parser@7.1.6:
|
||||
dependencies:
|
||||
cssesc: 3.0.0
|
||||
|
||||
@@ -35,7 +35,7 @@ function updateGroup(groupId: string, selected: boolean) {
|
||||
<div
|
||||
v-for="group in groups"
|
||||
:key="group.id"
|
||||
class="flex min-h-11 items-center justify-between gap-3 rounded-cp bg-cp-fill-quaternary px-3.5 py-2.5"
|
||||
class="flex min-h-11 items-center justify-between gap-3 rounded-z bg-z-fill-quaternary px-3.5 py-2.5"
|
||||
>
|
||||
<span class="flex min-w-0 items-center gap-2.5">
|
||||
<span class="size-3.5 shrink-0 rounded-sm" :style="{ backgroundColor: group.color }" />
|
||||
@@ -46,10 +46,10 @@ function updateGroup(groupId: string, selected: boolean) {
|
||||
@update:model-value="updateGroup(group.id, $event)"
|
||||
/>
|
||||
</span>
|
||||
<span v-if="!group.enabled" class="shrink-0 text-cp-xs font-emphasis text-cp-text-quaternary"> {{ t('common.status.disabled') }} </span>
|
||||
<span v-if="!group.enabled" class="shrink-0 text-z-xs font-emphasis text-z-text-quaternary"> {{ t('common.status.disabled') }} </span>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="m-0 rounded-cp bg-cp-fill-quaternary px-3.5 py-3 text-cp-sm font-emphasis text-cp-text-secondary">
|
||||
<p v-else class="m-0 rounded-z bg-z-fill-quaternary px-3.5 py-3 text-z-sm font-emphasis text-z-text-secondary">
|
||||
{{ loading ? t('groups.selector.loading') : t('groups.selector.empty') }}
|
||||
</p>
|
||||
</template>
|
||||
|
||||
@@ -32,5 +32,5 @@ const fullNames = computed(() =>
|
||||
:title="group.name"
|
||||
/>
|
||||
</div>
|
||||
<span v-else class="text-xs text-cp-text-quaternary">—</span>
|
||||
<span v-else class="text-xs text-z-text-quaternary">—</span>
|
||||
</template>
|
||||
|
||||
@@ -79,10 +79,10 @@ function importToClient() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<section class="overflow-hidden rounded-cp-card bg-cp-fill-quaternary shadow-cp-tertiary">
|
||||
<section class="overflow-hidden rounded-z-card bg-z-fill-quaternary shadow-z-tertiary">
|
||||
<div class="flex items-center justify-between gap-3 px-4 py-2.5">
|
||||
<span
|
||||
class="min-w-0 truncate font-mono text-cp-sm font-emphasis text-cp-text-secondary"
|
||||
class="min-w-0 truncate font-mono text-z-sm font-emphasis text-z-text-secondary"
|
||||
>
|
||||
{{ configPath }}
|
||||
</span>
|
||||
@@ -96,9 +96,9 @@ function importToClient() {
|
||||
</ZIconButton>
|
||||
</div>
|
||||
<ZScrollbar max-height="calc(100dvh - 21rem)">
|
||||
<div class="mx-3 mb-3 rounded-cp bg-cp-bg-container px-3.5 py-3 shadow-cp-tertiary">
|
||||
<div class="mx-3 mb-3 rounded-z bg-z-bg-container px-3.5 py-3 shadow-z-tertiary">
|
||||
<pre
|
||||
class="m-0 whitespace-pre-wrap wrap-break-word font-mono text-cp-sm leading-[1.65] font-emphasis text-cp-text"
|
||||
class="m-0 whitespace-pre-wrap wrap-break-word font-mono text-z-sm leading-[1.65] font-emphasis text-z-text"
|
||||
v-text="codexConfig"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,7 @@ const mistGradientId = `${id}-brand-mist`
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg :class="{ 'text-cp-text-light-solid': props.variant === 'tile' }" xmlns="http://www.w3.org/2000/svg" viewBox="48 48 416 416" focusable="false">
|
||||
<svg :class="{ 'text-z-text-light-solid': props.variant === 'tile' }" xmlns="http://www.w3.org/2000/svg" viewBox="48 48 416 416" focusable="false">
|
||||
<defs v-if="props.variant === 'tile'">
|
||||
<filter :id="noiseFilterId" x="0" y="0" width="100%" height="100%">
|
||||
<feTurbulence type="fractalNoise" baseFrequency="1.7" numOctaves="2" seed="31" result="noise" />
|
||||
@@ -35,7 +35,7 @@ const mistGradientId = `${id}-brand-mist`
|
||||
</radialGradient>
|
||||
</defs>
|
||||
|
||||
<rect v-if="props.variant === 'tile'" class="fill-(--cp-brand-mark-bg)" x="48" y="48" width="416" height="416" rx="88" />
|
||||
<rect v-if="props.variant === 'tile'" class="fill-(--z-brand-mark-bg)" x="48" y="48" width="416" height="416" rx="88" />
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="36">
|
||||
<path d="M156 307A112 112 0 0 1 307 156" />
|
||||
<path d="M356 205A112 112 0 0 1 205 356" />
|
||||
|
||||
@@ -65,7 +65,7 @@ async function selectLocale(value: Locale, close: () => void) {
|
||||
:aria-pressed="option.value === locale"
|
||||
:disabled="pendingLocale === option.value"
|
||||
:aria-busy="pendingLocale === option.value || undefined"
|
||||
class="aria-pressed:bg-cp-control-item-bg-active aria-pressed:text-cp-primary-text aria-pressed:hover:bg-cp-control-item-bg-active"
|
||||
class="aria-pressed:bg-z-control-item-bg-active aria-pressed:text-z-primary-text aria-pressed:hover:bg-z-control-item-bg-active"
|
||||
@click="selectLocale(option.value, close)"
|
||||
>
|
||||
<template #icon>
|
||||
|
||||
@@ -27,17 +27,17 @@ function select() {
|
||||
>
|
||||
<div class="grid gap-2">
|
||||
<div
|
||||
class="flex items-center gap-1 rounded-cp-sm pr-1 transition-colors hover:bg-cp-bg-text-hover has-[:focus-visible]:bg-cp-bg-text-hover motion-reduce:transition-none"
|
||||
class="flex items-center gap-1 rounded-z-sm pr-1 transition-colors hover:bg-z-bg-text-hover has-[:focus-visible]:bg-z-bg-text-hover motion-reduce:transition-none"
|
||||
>
|
||||
<ZMenuItem class="min-h-18 min-w-0 py-3 hover:bg-transparent!" @click="select">
|
||||
<template #icon>
|
||||
<span class="grid size-9 place-items-center rounded-cp bg-cp-fill-quaternary text-cp-text-secondary">
|
||||
<span class="grid size-9 place-items-center rounded-z bg-z-fill-quaternary text-z-text-secondary">
|
||||
<AppWindow class="size-5" aria-hidden="true" />
|
||||
</span>
|
||||
</template>
|
||||
<span class="grid gap-1 whitespace-normal">
|
||||
<span>CC Switch</span>
|
||||
<span class="text-xs leading-5 font-normal text-cp-text-secondary">{{ t('keys.configuration.ccSwitchDescription') }}</span>
|
||||
<span class="text-xs leading-5 font-normal text-z-text-secondary">{{ t('keys.configuration.ccSwitchDescription') }}</span>
|
||||
</span>
|
||||
</ZMenuItem>
|
||||
<a
|
||||
@@ -46,7 +46,7 @@ function select() {
|
||||
rel="noopener noreferrer"
|
||||
:aria-label="t('keys.configuration.viewOnGitHub', { name: 'CC Switch' })"
|
||||
:title="t('keys.configuration.viewOnGitHub', { name: 'CC Switch' })"
|
||||
class="inline-grid size-9 shrink-0 place-items-center rounded-cp-sm text-cp-text-tertiary outline-none transition-colors hover:text-cp-text focus-visible:ring-2 focus-visible:ring-cp-control-outline focus-visible:ring-inset motion-reduce:transition-none"
|
||||
class="inline-grid size-9 shrink-0 place-items-center rounded-z-sm text-z-text-tertiary outline-none transition-colors hover:text-z-text focus-visible:ring-2 focus-visible:ring-z-control-outline focus-visible:ring-inset motion-reduce:transition-none"
|
||||
>
|
||||
<Github class="size-6" aria-hidden="true" />
|
||||
</a>
|
||||
|
||||
@@ -12,7 +12,7 @@ defineProps<{ label: string, content: string }>()
|
||||
<CircleAlert class="size-3.5" aria-hidden="true" />
|
||||
</ZIconButton>
|
||||
</template>
|
||||
<section role="dialog" :aria-label="label" class="max-w-72 p-3 text-cp-xs leading-relaxed text-cp-text-secondary">
|
||||
<section role="dialog" :aria-label="label" class="max-w-72 p-3 text-z-xs leading-relaxed text-z-text-secondary">
|
||||
{{ content }}
|
||||
</section>
|
||||
</ZPopover>
|
||||
|
||||
@@ -53,22 +53,22 @@ const authenticationLabel = computed(() => formatAuthenticationLabel(props.authe
|
||||
<template>
|
||||
<span class="inline-flex items-center whitespace-nowrap" :class="groupGapClass">
|
||||
<span
|
||||
class="inline-flex shrink-0 items-center justify-center bg-cp-fill-secondary/80 text-cp-text"
|
||||
class="inline-flex shrink-0 items-center justify-center bg-z-fill-secondary/80 text-z-text"
|
||||
:class="iconContainerClass"
|
||||
:title="providerLabel"
|
||||
>
|
||||
<component :is="providerIconComponent" v-if="providerIconComponent" :class="iconClass" />
|
||||
<span v-else class="text-[10px] font-heavy text-cp-text-quaternary">?</span>
|
||||
<span v-else class="text-[10px] font-heavy text-z-text-quaternary">?</span>
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-if="showAuthenticationKind"
|
||||
class="inline-flex shrink-0 items-center justify-center bg-cp-fill-secondary/80 text-cp-text"
|
||||
class="inline-flex shrink-0 items-center justify-center bg-z-fill-secondary/80 text-z-text"
|
||||
:class="iconContainerClass"
|
||||
:title="authenticationLabel"
|
||||
>
|
||||
<component :is="authenticationIconComponent" v-if="authenticationIconComponent" :class="iconClass" />
|
||||
<span v-else class="text-[10px] font-heavy text-cp-text-quaternary">?</span>
|
||||
<span v-else class="text-[10px] font-heavy text-z-text-quaternary">?</span>
|
||||
</span>
|
||||
|
||||
<span class="sr-only">
|
||||
|
||||
@@ -20,7 +20,7 @@ const { t } = useI18n({ useScope: 'global' })
|
||||
:disabled="disabled || loading"
|
||||
@click="emit('detect')"
|
||||
>
|
||||
<span class="relative grid size-7 place-items-center text-cp-link">
|
||||
<span class="relative grid size-7 place-items-center text-z-link">
|
||||
<span v-if="loading" class="location-ripple" aria-hidden="true" />
|
||||
<span v-if="loading" class="location-ripple location-ripple-delayed" aria-hidden="true" />
|
||||
<LocateFixed class="relative z-10 size-4" aria-hidden="true" />
|
||||
|
||||
@@ -1,14 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormRule } from '@codex-proxy/ui'
|
||||
import type { RequestLocation } from '@/api'
|
||||
|
||||
import { ZFormItem, ZInput } from '@codex-proxy/ui'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
defineProps<{ disabled?: boolean }>()
|
||||
defineProps<{ disabled?: boolean, formProp?: string }>()
|
||||
|
||||
const { t } = useI18n({ useScope: 'global' })
|
||||
|
||||
const location = defineModel<RequestLocation>({ required: true })
|
||||
const requiredRule = computed<FormRule>(() => ({
|
||||
required: true,
|
||||
whitespace: true,
|
||||
trigger: ['blur', 'change'],
|
||||
message: t('common.location.required'),
|
||||
}))
|
||||
const countryRules = computed<FormRule[]>(() => [
|
||||
requiredRule.value,
|
||||
{
|
||||
pattern: /^[A-Z]{2}$/,
|
||||
transform: value => value.trim().toUpperCase(),
|
||||
trigger: ['blur', 'change'],
|
||||
message: t('common.location.required'),
|
||||
},
|
||||
])
|
||||
|
||||
function update(field: keyof RequestLocation, value: string) {
|
||||
location.value = { ...location.value, [field]: value }
|
||||
@@ -16,17 +33,17 @@ function update(field: keyof RequestLocation, value: string) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<ZFormItem :label="t('common.location.countryCode')" required>
|
||||
<div class="grid gap-4.5 sm:grid-cols-2">
|
||||
<ZFormItem :prop="formProp ? `${formProp}.country` : undefined" :rules="countryRules" :show-message="Boolean(location.country.trim())" :label="t('common.location.countryCode')" required>
|
||||
<ZInput :model-value="location.country" :disabled="disabled" :aria-label="t('common.location.countryCode')" :placeholder="t('common.location.countryCodePlaceholder')" maxlength="2" @update:model-value="update('country', $event)" />
|
||||
</ZFormItem>
|
||||
<ZFormItem :label="t('common.location.region')" required>
|
||||
<ZFormItem :prop="formProp ? `${formProp}.region` : undefined" :rules="requiredRule" :show-message="false" :label="t('common.location.region')" required>
|
||||
<ZInput :model-value="location.region" :disabled="disabled" :aria-label="t('common.location.region')" :placeholder="t('common.location.regionPlaceholder')" maxlength="128" @update:model-value="update('region', $event)" />
|
||||
</ZFormItem>
|
||||
<ZFormItem :label="t('common.location.city')" required>
|
||||
<ZFormItem :prop="formProp ? `${formProp}.city` : undefined" :rules="requiredRule" :show-message="false" :label="t('common.location.city')" required>
|
||||
<ZInput :model-value="location.city" :disabled="disabled" :aria-label="t('common.location.city')" :placeholder="t('common.location.cityPlaceholder')" maxlength="128" @update:model-value="update('city', $event)" />
|
||||
</ZFormItem>
|
||||
<ZFormItem :label="t('common.location.timeZone')" required>
|
||||
<ZFormItem :prop="formProp ? `${formProp}.timezone` : undefined" :rules="requiredRule" :show-message="false" :label="t('common.location.timeZone')" required>
|
||||
<ZInput :model-value="location.timezone" :disabled="disabled" :aria-label="t('common.location.timeZone')" :placeholder="t('common.location.timeZonePlaceholder')" @update:model-value="update('timezone', $event)" />
|
||||
</ZFormItem>
|
||||
</div>
|
||||
|
||||
@@ -20,18 +20,18 @@ const props = withDefaults(
|
||||
)
|
||||
|
||||
const planPalettes: Record<string, string> = {
|
||||
api: 'bg-cp-cyan-container-strong text-cp-cyan-on-container',
|
||||
free: 'bg-cp-cyan-container text-cp-cyan-on-container',
|
||||
plus: 'bg-cp-blue-container-strong text-cp-blue-on-container',
|
||||
pro: 'bg-cp-purple-container-strong text-cp-purple-on-container',
|
||||
prolite: 'bg-cp-purple-container text-cp-purple-on-container',
|
||||
api: 'bg-z-cyan-container-strong text-z-cyan-on-container',
|
||||
free: 'bg-z-cyan-container text-z-cyan-on-container',
|
||||
plus: 'bg-z-blue-container-strong text-z-blue-on-container',
|
||||
pro: 'bg-z-purple-container-strong text-z-purple-on-container',
|
||||
prolite: 'bg-z-purple-container text-z-purple-on-container',
|
||||
}
|
||||
|
||||
const fallbackPalettes = [
|
||||
'bg-cp-blue-container text-cp-blue-on-container',
|
||||
'bg-cp-green-container text-cp-green-on-container',
|
||||
'bg-cp-cyan-container text-cp-cyan-on-container',
|
||||
'bg-cp-orange-container text-cp-orange-on-container',
|
||||
'bg-z-blue-container text-z-blue-on-container',
|
||||
'bg-z-green-container text-z-green-on-container',
|
||||
'bg-z-cyan-container text-z-cyan-on-container',
|
||||
'bg-z-orange-container text-z-orange-on-container',
|
||||
] as const
|
||||
|
||||
const rawPlanType = computed(() => props.authenticationKind === 'api_key'
|
||||
@@ -45,8 +45,8 @@ const sizeClass = computed(() => {
|
||||
if (props.size === 'xs')
|
||||
return 'h-4.5 rounded-full px-1.5 text-[10px] font-bold'
|
||||
return props.size === 'sm'
|
||||
? 'h-5 rounded-full px-1.75 text-cp-xs font-bold'
|
||||
: 'h-5.5 rounded-full px-2 text-cp-xs font-heavy'
|
||||
? 'h-5 rounded-full px-1.75 text-z-xs font-bold'
|
||||
: 'h-5.5 rounded-full px-2 text-z-xs font-heavy'
|
||||
})
|
||||
|
||||
const paletteClass = computed(() => {
|
||||
@@ -63,7 +63,7 @@ const paletteClass = computed(() => {
|
||||
<AccountUpstreamBadge v-if="upstream?.discovered && upstream.observedAt" :size="size" />
|
||||
<span
|
||||
v-else
|
||||
class="inline-flex min-w-0 max-w-full items-center justify-center whitespace-nowrap leading-none shadow-cp-tertiary"
|
||||
class="inline-flex min-w-0 max-w-full items-center justify-center whitespace-nowrap leading-none shadow-z-tertiary"
|
||||
:class="[sizeClass, paletteClass]"
|
||||
:title="displayText || undefined"
|
||||
>
|
||||
|
||||
@@ -65,9 +65,9 @@ function quotaWindowCode(windowSeconds: number | null, role: AccountQuotaWindow[
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="w-84 max-w-[calc(100vw-2rem)] overflow-hidden rounded-cp-lg" role="dialog" :aria-label="t('accounts.quota.details', { detailTitle })">
|
||||
<header v-if="detailHeading" class="bg-cp-popover-header-bg px-3 py-2.5">
|
||||
<h3 class="m-0 truncate text-cp leading-5 font-heavy text-cp-text">
|
||||
<section class="w-84 max-w-[calc(100vw-2rem)] overflow-hidden rounded-z-lg" role="dialog" :aria-label="t('accounts.quota.details', { detailTitle })">
|
||||
<header v-if="detailHeading" class="bg-z-popover-header-bg px-3 py-2.5">
|
||||
<h3 class="m-0 truncate text-z leading-5 font-heavy text-z-text">
|
||||
{{ detailHeading }}
|
||||
</h3>
|
||||
</header>
|
||||
@@ -77,16 +77,16 @@ function quotaWindowCode(windowSeconds: number | null, role: AccountQuotaWindow[
|
||||
<article v-for="item in detailItems" :key="item.key">
|
||||
<div class="grid grid-cols-[32px_minmax(0,1fr)_auto] items-center gap-2.5">
|
||||
<span
|
||||
class="grid h-5 w-8 place-items-center rounded-sm bg-cp-blue-container-strong font-mono text-[9px] leading-none font-heavy tracking-[0.04em] whitespace-nowrap text-cp-blue-on-container"
|
||||
class="grid h-5 w-8 place-items-center rounded-sm bg-z-blue-container-strong font-mono text-[9px] leading-none font-heavy tracking-[0.04em] whitespace-nowrap text-z-blue-on-container"
|
||||
>
|
||||
{{ item.code }}
|
||||
</span>
|
||||
<h4 class="m-0 min-w-0 truncate text-cp-sm leading-4 font-heavy text-cp-text-secondary">
|
||||
<h4 class="m-0 min-w-0 truncate text-z-sm leading-4 font-heavy text-z-text-secondary">
|
||||
{{ item.local ? item.localLabel : item.label }}
|
||||
</h4>
|
||||
<strong
|
||||
class="font-mono text-cp-xs leading-none font-heavy tabular-nums"
|
||||
:class="item.local ? 'text-cp-text' : item.percentTextClass"
|
||||
class="font-mono text-z-xs leading-none font-heavy tabular-nums"
|
||||
:class="item.local ? 'text-z-text' : item.percentTextClass"
|
||||
>
|
||||
{{ item.local ? t('accounts.quota.requestCount', { requestDisplay: item.requestDisplay }) : item.quotaValueDisplay }}
|
||||
</strong>
|
||||
@@ -100,7 +100,7 @@ function quotaWindowCode(windowSeconds: number | null, role: AccountQuotaWindow[
|
||||
/>
|
||||
<template v-else>
|
||||
<div
|
||||
class="mt-2 h-1 w-full overflow-hidden rounded-full bg-cp-border-secondary"
|
||||
class="mt-2 h-1 w-full overflow-hidden rounded-full bg-z-border-secondary"
|
||||
role="progressbar"
|
||||
:aria-label="item.label"
|
||||
aria-valuemin="0"
|
||||
@@ -116,38 +116,38 @@ function quotaWindowCode(windowSeconds: number | null, role: AccountQuotaWindow[
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<p v-if="item.observationDisplay" class="mt-2 text-[10px] leading-4 text-cp-text-tertiary">
|
||||
<p v-if="item.observationDisplay" class="mt-2 text-[10px] leading-4 text-z-text-tertiary">
|
||||
{{ item.observationDisplay }}
|
||||
</p>
|
||||
|
||||
<dl class="mt-2.5 grid grid-cols-[auto_minmax(0,1fr)] gap-x-4 gap-y-1.5 text-[10px] leading-4">
|
||||
<template v-if="item.local">
|
||||
<dt class="font-emphasis text-cp-text-tertiary">
|
||||
<dt class="font-emphasis text-z-text-tertiary">
|
||||
{{ t('accounts.quota.statisticalMethod') }}
|
||||
</dt>
|
||||
<dd class="m-0 text-right font-mono font-heavy tabular-nums text-cp-text">
|
||||
<dd class="m-0 text-right font-mono font-heavy tabular-nums text-z-text">
|
||||
{{ t('accounts.quota.rollingWindow') }}
|
||||
</dd>
|
||||
<dt class="font-emphasis text-cp-text-tertiary">
|
||||
<dt class="font-emphasis text-z-text-tertiary">
|
||||
{{ t('accounts.quota.statisticalRange') }}
|
||||
</dt>
|
||||
<dd class="m-0 truncate text-right font-mono font-emphasis tabular-nums text-cp-text-secondary">
|
||||
<dd class="m-0 truncate text-right font-mono font-emphasis tabular-nums text-z-text-secondary">
|
||||
{{ item.durationDisplay }}
|
||||
</dd>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="showConsumption && item.consumptionDisplay">
|
||||
<dt class="font-emphasis text-cp-text-tertiary">
|
||||
<dt class="font-emphasis text-z-text-tertiary">
|
||||
{{ t('accounts.quota.windowConsumption') }}
|
||||
</dt>
|
||||
<dd class="m-0 text-right font-mono font-heavy tabular-nums text-cp-text">
|
||||
<dd class="m-0 text-right font-mono font-heavy tabular-nums text-z-text">
|
||||
{{ item.consumptionDisplay }}
|
||||
</dd>
|
||||
</template>
|
||||
<dt class="font-emphasis text-cp-text-tertiary">
|
||||
<dt class="font-emphasis text-z-text-tertiary">
|
||||
{{ t('accounts.quota.resetTime') }}
|
||||
</dt>
|
||||
<dd class="m-0 truncate text-right font-mono font-emphasis tabular-nums text-cp-text-secondary">
|
||||
<dd class="m-0 truncate text-right font-mono font-emphasis tabular-nums text-z-text-secondary">
|
||||
{{ item.resetAtDisplay }}
|
||||
</dd>
|
||||
</template>
|
||||
|
||||
@@ -9,7 +9,7 @@ const { t } = useI18n({ useScope: 'global' })
|
||||
|
||||
<template>
|
||||
<span
|
||||
class="peer-badge relative isolate inline-flex max-w-full shrink-0 cursor-default items-center gap-0.5 overflow-hidden rounded-md bg-cp-cyan-container-strong py-0.5 pr-1.5 pl-0.5 align-middle text-cp-cyan-on-container"
|
||||
class="peer-badge relative isolate inline-flex max-w-full shrink-0 cursor-default items-center gap-0.5 overflow-hidden rounded-md bg-z-cyan-container-strong py-0.5 pr-1.5 pl-0.5 align-middle text-z-cyan-on-container"
|
||||
:class="size === 'xs' ? 'h-4.5' : 'h-5'"
|
||||
:aria-label="t('accounts.upstream.link')"
|
||||
:title="t('accounts.upstream.link')"
|
||||
@@ -29,8 +29,8 @@ const { t } = useI18n({ useScope: 'global' })
|
||||
background: linear-gradient(
|
||||
110deg,
|
||||
transparent 30%,
|
||||
color-mix(in srgb, var(--cp-color-text-light-solid) 12%, transparent) 43%,
|
||||
color-mix(in srgb, var(--cp-color-text-light-solid) 36%, transparent) 50%,
|
||||
color-mix(in srgb, var(--z-color-text-light-solid) 12%, transparent) 43%,
|
||||
color-mix(in srgb, var(--z-color-text-light-solid) 36%, transparent) 50%,
|
||||
transparent 70%
|
||||
);
|
||||
transform: translateX(-120%);
|
||||
|
||||
@@ -28,8 +28,8 @@ const detail = computed(() => [
|
||||
|
||||
<template>
|
||||
<span
|
||||
class="min-w-0 truncate text-cp-xs font-emphasis"
|
||||
:class="upstream.stale || upstream.lastError ? 'text-cp-warning-text' : 'text-cp-text-tertiary'"
|
||||
class="min-w-0 truncate text-z-xs font-emphasis"
|
||||
:class="upstream.stale || upstream.lastError ? 'text-z-warning-text' : 'text-z-text-tertiary'"
|
||||
:title="detail"
|
||||
role="status"
|
||||
>
|
||||
|
||||
@@ -23,11 +23,11 @@ withDefaults(
|
||||
<span
|
||||
v-for="bar in bars"
|
||||
:key="bar.key"
|
||||
class="relative min-w-0 flex-1 bg-cp-border-secondary"
|
||||
class="relative min-w-0 flex-1 bg-z-border-secondary"
|
||||
:title="showNativeTooltip ? bar.title : undefined"
|
||||
>
|
||||
<span
|
||||
class="absolute inset-x-0 bottom-0 bg-cp-green-solid"
|
||||
class="absolute inset-x-0 bottom-0 bg-z-green-solid"
|
||||
:style="{ height: bar.height }"
|
||||
/>
|
||||
</span>
|
||||
|
||||
@@ -49,7 +49,7 @@ const view = computed(() => resolveAccountUsageWindowPresentation({
|
||||
>
|
||||
<span
|
||||
v-if="view.quota.localUsageVisible"
|
||||
class="text-right text-cp-text-quaternary"
|
||||
class="text-right text-z-text-quaternary"
|
||||
:class="view.classes.value"
|
||||
:title="showNativeTooltip ? t('accounts.quota.usageInThisWindow', { localUsageDisplay: view.quota.localUsageDisplay }) : undefined"
|
||||
>
|
||||
@@ -70,7 +70,7 @@ const view = computed(() => resolveAccountUsageWindowPresentation({
|
||||
>
|
||||
<span
|
||||
v-if="view.quota.localUsageVisible"
|
||||
class="text-cp-text-quaternary"
|
||||
class="text-z-text-quaternary"
|
||||
:class="view.classes.value"
|
||||
>
|
||||
{{ view.quota.localUsageDisplay }}
|
||||
@@ -100,7 +100,7 @@ const view = computed(() => resolveAccountUsageWindowPresentation({
|
||||
</div>
|
||||
<div
|
||||
v-if="view.quota.resetVisible"
|
||||
class="mt-3 text-cp-sm font-emphasis text-cp-text-secondary"
|
||||
class="mt-3 text-z-sm font-emphasis text-z-text-secondary"
|
||||
>
|
||||
{{ t('accounts.quota.resets', { resetAtDisplay: window.resetAtDisplay }) }}
|
||||
</div>
|
||||
@@ -113,7 +113,7 @@ const view = computed(() => resolveAccountUsageWindowPresentation({
|
||||
</span>
|
||||
<strong
|
||||
v-if="view.local.requestValueVisible"
|
||||
class="shrink-0 font-mono tabular-nums text-cp-text"
|
||||
class="shrink-0 font-mono tabular-nums text-z-text"
|
||||
:class="view.classes.value"
|
||||
>
|
||||
{{ view.local.requestDisplay }}
|
||||
|
||||
@@ -56,9 +56,9 @@ const variantDefinitions: Record<
|
||||
> = {
|
||||
metric: {
|
||||
rootClass: 'grid min-w-0 grid-rows-[14px_14px] gap-1',
|
||||
headerClass: 'flex items-center justify-between gap-2 text-cp-xs leading-[14px] font-bold',
|
||||
labelClass: 'truncate text-cp-text-quaternary',
|
||||
valueClass: 'text-cp-xs leading-none font-bold',
|
||||
headerClass: 'flex items-center justify-between gap-2 text-z-xs leading-[14px] font-bold',
|
||||
labelClass: 'truncate text-z-text-quaternary',
|
||||
valueClass: 'text-z-xs leading-none font-bold',
|
||||
trackOffsetClass: 'self-center',
|
||||
trackShapeClass: 'h-1.5 w-full overflow-hidden rounded-full',
|
||||
minimumBarWidth: '6px',
|
||||
@@ -68,8 +68,8 @@ const variantDefinitions: Record<
|
||||
},
|
||||
compact: {
|
||||
rootClass: 'flex min-w-0 flex-col',
|
||||
headerClass: 'mb-1 flex items-baseline justify-between gap-2 text-cp-xs leading-[14px] font-bold',
|
||||
labelClass: 'truncate text-cp-text-quaternary',
|
||||
headerClass: 'mb-1 flex items-baseline justify-between gap-2 text-z-xs leading-[14px] font-bold',
|
||||
labelClass: 'truncate text-z-text-quaternary',
|
||||
valueClass: 'text-[10px] leading-[14px] font-heavy',
|
||||
trackOffsetClass: 'mt-auto',
|
||||
trackShapeClass: 'h-1 w-full overflow-hidden rounded-full',
|
||||
@@ -79,10 +79,10 @@ const variantDefinitions: Record<
|
||||
showResetTime: false,
|
||||
},
|
||||
detail: {
|
||||
rootClass: 'rounded-lg bg-cp-fill-quaternary p-2',
|
||||
headerClass: 'flex items-center justify-between gap-3 text-cp-sm font-bold',
|
||||
labelClass: 'text-cp-text-secondary',
|
||||
valueClass: 'text-cp-sm font-heavy',
|
||||
rootClass: 'rounded-lg bg-z-fill-quaternary p-2',
|
||||
headerClass: 'flex items-center justify-between gap-3 text-z-sm font-bold',
|
||||
labelClass: 'text-z-text-secondary',
|
||||
valueClass: 'text-z-sm font-heavy',
|
||||
trackOffsetClass: 'mt-2',
|
||||
trackShapeClass: 'h-2 overflow-hidden rounded-full',
|
||||
minimumBarWidth: '8px',
|
||||
@@ -97,20 +97,20 @@ const quotaToneClasses: Record<
|
||||
{ bar: string, text: string }
|
||||
> = {
|
||||
unknown: {
|
||||
bar: 'bg-cp-border',
|
||||
text: 'text-cp-text-quaternary',
|
||||
bar: 'bg-z-border',
|
||||
text: 'text-z-text-quaternary',
|
||||
},
|
||||
danger: {
|
||||
bar: 'bg-cp-error',
|
||||
text: 'text-cp-error-text',
|
||||
bar: 'bg-z-error',
|
||||
text: 'text-z-error-text',
|
||||
},
|
||||
warning: {
|
||||
bar: 'bg-cp-warning',
|
||||
text: 'text-cp-warning-text',
|
||||
bar: 'bg-z-warning',
|
||||
text: 'text-z-warning-text',
|
||||
},
|
||||
success: {
|
||||
bar: 'bg-cp-success',
|
||||
text: 'text-cp-success-text',
|
||||
bar: 'bg-z-success',
|
||||
text: 'text-z-success-text',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ export function resolveAccountUsageWindowPresentation(
|
||||
value: definition.valueClass,
|
||||
trackOffset: definition.trackOffsetClass,
|
||||
trackShape: definition.trackShapeClass,
|
||||
track: `${definition.trackShapeClass} bg-cp-border-secondary`,
|
||||
track: `${definition.trackShapeClass} bg-z-border-secondary`,
|
||||
},
|
||||
quota: {
|
||||
valueVisible: typeof input.window?.usedPercent === 'number'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getThemeVarName } from '@codex-proxy/ui/theme'
|
||||
import { escape } from 'es-toolkit'
|
||||
import { isRecord } from '@/utils/data'
|
||||
|
||||
@@ -31,7 +32,7 @@ export function chartTooltipStyle(
|
||||
fontFamily: options.fontFamily ?? (getComputedStyle(document.documentElement).fontFamily),
|
||||
fontWeight: options.fontWeight ?? 650,
|
||||
},
|
||||
extraCssText: 'border-radius: 12px; box-shadow: var(--cp-box-shadow);',
|
||||
extraCssText: `border-radius: 12px; box-shadow: var(${getThemeVarName('box-shadow')});`,
|
||||
}
|
||||
|
||||
if (!options.axisPointer)
|
||||
|
||||
@@ -194,7 +194,7 @@ const userAgent = computed({
|
||||
/>
|
||||
</ZFormItem>
|
||||
</div>
|
||||
<p v-if="!custom && currentPreset?.reason" class="m-0 text-cp-sm text-cp-text-secondary">
|
||||
<p v-if="!custom && currentPreset?.reason" class="m-0 text-z-sm text-z-text-secondary">
|
||||
{{ currentPreset.reason }}
|
||||
</p>
|
||||
<div v-if="!custom" class="grid gap-4 sm:grid-cols-2">
|
||||
|
||||
@@ -14,26 +14,26 @@ const { t } = useI18n({ useScope: 'global' })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p v-if="needsVersionInput" role="status" class="m-0 text-cp-sm text-cp-text-tertiary">
|
||||
<p v-if="needsVersionInput" role="status" class="m-0 text-z-sm text-z-text-tertiary">
|
||||
{{ t('settings.clientProfile.previewAfterFillingInVersion') }}
|
||||
</p>
|
||||
<div
|
||||
v-else-if="previewing || preview"
|
||||
class="grid min-h-20 min-w-0 content-center gap-2 rounded-cp bg-cp-fill-quaternary p-4"
|
||||
class="grid min-h-20 min-w-0 content-center gap-2 rounded-z bg-z-fill-quaternary p-4"
|
||||
aria-live="polite"
|
||||
:aria-busy="previewing"
|
||||
>
|
||||
<template v-if="previewing">
|
||||
<div class="flex h-lh items-center text-cp-sm" role="status" :aria-label="t('settings.clientProfile.resolvingClientIdentity')">
|
||||
<div class="flex h-lh items-center text-z-sm" role="status" :aria-label="t('settings.clientProfile.resolvingClientIdentity')">
|
||||
<ZSkeleton shape="text" class="w-4/5" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="flex h-lh items-center text-cp-xs" aria-hidden="true">
|
||||
<div class="flex h-lh items-center text-z-xs" aria-hidden="true">
|
||||
<ZSkeleton shape="text" class="w-52 max-w-full" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="preview">
|
||||
<code class="break-all text-cp-sm text-cp-text">{{ preview.userAgent }}</code>
|
||||
<p class="m-0 text-cp-xs text-cp-text-tertiary">
|
||||
<code class="break-all text-z-sm text-z-text">{{ preview.userAgent }}</code>
|
||||
<p class="m-0 text-z-xs text-z-text-tertiary">
|
||||
{{ preview.versionSource === 'custom' ? t('settings.clientProfile.fixedIdentity') : t('settings.clientProfile.automaticUpdates') }}
|
||||
<template v-if="preview.versionSource === 'official'">
|
||||
<template v-if="preview.versionLag">
|
||||
@@ -42,7 +42,7 @@ const { t } = useI18n({ useScope: 'global' })
|
||||
· {{ preview.checkedAtDisplay ? t('settings.clientProfile.checked', { checkedAtDisplay: preview.checkedAtDisplay }) : t('settings.clientProfile.toBeChecked') }}
|
||||
</template>
|
||||
</p>
|
||||
<p v-if="preview.error && preview.versionSource !== 'custom'" :title="preview.error" class="m-0 text-cp-sm text-cp-warning">
|
||||
<p v-if="preview.error && preview.versionSource !== 'custom'" :title="preview.error" class="m-0 text-z-sm text-z-warning">
|
||||
{{ t('settings.clientProfile.updateFailedUseLastVersion') }}
|
||||
</p>
|
||||
</template>
|
||||
|
||||
@@ -150,7 +150,7 @@ defineExpose({ focusField })
|
||||
<Info class="size-3.5" />
|
||||
</ZIconButton>
|
||||
</template>
|
||||
<p class="m-0 max-w-72 p-3 text-cp-xs leading-relaxed text-cp-text-secondary">
|
||||
<p class="m-0 max-w-72 p-3 text-z-xs leading-relaxed text-z-text-secondary">
|
||||
{{ field.description }}
|
||||
</p>
|
||||
</ZPopover>
|
||||
|
||||
@@ -20,26 +20,26 @@ const outcomeMeta = computed(() => [
|
||||
{
|
||||
label: t('common.status.success'),
|
||||
value: props.point.successRequests,
|
||||
dotClass: 'bg-cp-success',
|
||||
valueClass: 'text-cp-success-text',
|
||||
dotClass: 'bg-z-success',
|
||||
valueClass: 'text-z-success-text',
|
||||
},
|
||||
{
|
||||
label: t('usage.health.point.serviceFailed'),
|
||||
value: healthServiceFailureCount(props.point),
|
||||
dotClass: 'bg-cp-error',
|
||||
valueClass: 'text-cp-error-text',
|
||||
dotClass: 'bg-z-error',
|
||||
valueClass: 'text-z-error-text',
|
||||
},
|
||||
{
|
||||
label: t('usage.health.point.clientCancel'),
|
||||
value: props.point.cancelledRequests,
|
||||
dotClass: 'bg-cp-orange-solid',
|
||||
valueClass: 'text-cp-orange-text',
|
||||
dotClass: 'bg-z-orange-solid',
|
||||
valueClass: 'text-z-orange-text',
|
||||
},
|
||||
{
|
||||
label: t('usage.health.point.callerError'),
|
||||
value: props.point.callerErrorRequests,
|
||||
dotClass: 'bg-cp-info',
|
||||
valueClass: 'text-cp-info-text',
|
||||
dotClass: 'bg-z-info',
|
||||
valueClass: 'text-z-info-text',
|
||||
},
|
||||
])
|
||||
const outcomeSegments = computed(() => {
|
||||
@@ -62,12 +62,12 @@ const metricItems = outcomeMeta
|
||||
<section
|
||||
role="dialog"
|
||||
:aria-label="t('usage.health.point.requestHealthDetailsFor', { time: point.time })"
|
||||
class="overflow-hidden rounded-cp-lg bg-cp-bg-elevated"
|
||||
class="overflow-hidden rounded-z-lg bg-z-bg-elevated"
|
||||
>
|
||||
<header class="flex items-center justify-between gap-3 bg-cp-popover-header-bg px-3.5 py-3">
|
||||
<header class="flex items-center justify-between gap-3 bg-z-popover-header-bg px-3.5 py-3">
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<span class="size-2 shrink-0 rounded-full" :class="status.cellClass" />
|
||||
<strong class="font-mono text-cp leading-none font-heavy text-cp-text">
|
||||
<strong class="font-mono text-z leading-none font-heavy text-z-text">
|
||||
{{ point.time }}
|
||||
</strong>
|
||||
<span
|
||||
@@ -77,13 +77,13 @@ const metricItems = outcomeMeta
|
||||
{{ status.label }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="shrink-0 text-[10px] leading-none font-emphasis text-cp-text-quaternary"> {{ t('usage.health.point.minutes') }} </span>
|
||||
<span class="shrink-0 text-[10px] leading-none font-emphasis text-z-text-quaternary"> {{ t('usage.health.point.minutes') }} </span>
|
||||
</header>
|
||||
|
||||
<div class="grid gap-3.5 px-3.5 py-3.5">
|
||||
<div class="flex items-end justify-between gap-4">
|
||||
<div>
|
||||
<p class="m-0 text-cp-xs leading-none font-emphasis text-cp-text-quaternary">
|
||||
<p class="m-0 text-z-xs leading-none font-emphasis text-z-text-quaternary">
|
||||
{{ t('usage.health.point.validRequestAvailability') }}
|
||||
</p>
|
||||
<strong
|
||||
@@ -94,14 +94,14 @@ const metricItems = outcomeMeta
|
||||
</strong>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<span class="block text-[10px] leading-none font-emphasis text-cp-text-quaternary"> {{ t('usage.health.point.validRequest') }} </span>
|
||||
<strong class="mt-2 block font-mono text-cp-xl leading-none font-heavy tabular-nums text-cp-text">
|
||||
<span class="block text-[10px] leading-none font-emphasis text-z-text-quaternary"> {{ t('usage.health.point.validRequest') }} </span>
|
||||
<strong class="mt-2 block font-mono text-z-xl leading-none font-heavy tabular-nums text-z-text">
|
||||
{{ formatHealthCount(eligibleRequests) }}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex h-1.5 w-full overflow-hidden rounded-full bg-cp-fill-tertiary">
|
||||
<div class="flex h-1.5 w-full overflow-hidden rounded-full bg-z-fill-tertiary">
|
||||
<span
|
||||
v-for="segment in outcomeSegments"
|
||||
:key="segment.label"
|
||||
@@ -115,13 +115,13 @@ const metricItems = outcomeMeta
|
||||
<div
|
||||
v-for="item in metricItems"
|
||||
:key="item.label"
|
||||
class="grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-2 rounded-cp bg-cp-fill-tertiary px-2.5 py-2.5"
|
||||
class="grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-2 rounded-z bg-z-fill-tertiary px-2.5 py-2.5"
|
||||
>
|
||||
<span class="size-1.5 rounded-full" :class="item.dotClass" />
|
||||
<span class="truncate text-[10px] leading-none font-emphasis text-cp-text-secondary">
|
||||
<span class="truncate text-[10px] leading-none font-emphasis text-z-text-secondary">
|
||||
{{ item.label }}
|
||||
</span>
|
||||
<strong class="font-mono text-cp-sm leading-none font-heavy tabular-nums" :class="item.valueClass">
|
||||
<strong class="font-mono text-z-sm leading-none font-heavy tabular-nums" :class="item.valueClass">
|
||||
{{ formatHealthCount(item.value) }}
|
||||
</strong>
|
||||
</div>
|
||||
|
||||
@@ -17,8 +17,8 @@ const props = defineProps<{
|
||||
const { t } = useI18n({ useScope: 'global' })
|
||||
|
||||
const healthPopoverArrowSurfaceClasses = {
|
||||
top: 'bg-cp-popover-header-bg',
|
||||
bottom: 'bg-cp-bg-elevated',
|
||||
top: 'bg-z-popover-header-bg',
|
||||
bottom: 'bg-z-bg-elevated',
|
||||
}
|
||||
|
||||
const timelineGrid = useTemplateRef<HTMLElement>('timelineGrid')
|
||||
@@ -26,6 +26,8 @@ const popoverAnchor = useTemplateRef<HTMLElement>('popoverAnchor')
|
||||
const pointPopover = useTemplateRef<InstanceType<typeof ZPopover>>('pointPopover')
|
||||
const preferredMotion = usePreferredReducedMotion()
|
||||
const points = computed(() => props.timeline.points)
|
||||
// 未取得时间桶时按常规日的 96 格保留非交互占位,不补造时间与请求数据
|
||||
const pointCount = computed(() => points.value.length || 96)
|
||||
|
||||
const activePoint = shallowRef<DashboardHealthTimelinePoint>()
|
||||
const popoverOpen = shallowRef(false)
|
||||
@@ -240,7 +242,7 @@ onBeforeUnmount(() => {
|
||||
<template #actions>
|
||||
<div class="flex w-full flex-wrap items-center justify-between gap-x-4 gap-y-2">
|
||||
<div
|
||||
class="flex max-w-full flex-wrap items-center gap-x-2 gap-y-1 text-cp-xs leading-none font-emphasis text-cp-text-quaternary"
|
||||
class="flex max-w-full flex-wrap items-center gap-x-2 gap-y-1 text-z-xs leading-none font-emphasis text-z-text-quaternary"
|
||||
>
|
||||
<span
|
||||
v-for="item in healthLegend"
|
||||
@@ -282,8 +284,15 @@ onBeforeUnmount(() => {
|
||||
<span ref="popoverAnchor" aria-hidden="true" class="pointer-events-none absolute left-0 top-0 h-5 w-px opacity-0" />
|
||||
<div
|
||||
ref="timelineGrid" class="grid w-full grid-cols-[repeat(var(--timeline-columns),minmax(0,1fr))] items-end gap-x-0.5 gap-y-1 sm:grid-cols-[repeat(var(--timeline-columns-wide),minmax(0,1fr))]"
|
||||
:style="{ '--timeline-columns': Math.max(1, Math.ceil(points.length / 2)), '--timeline-columns-wide': Math.max(1, points.length) }"
|
||||
:style="{ '--timeline-columns': Math.ceil(pointCount / 2), '--timeline-columns-wide': pointCount }"
|
||||
:role="points.length ? undefined : 'img'"
|
||||
:aria-label="points.length ? undefined : healthStatusMeta.no_data.label"
|
||||
>
|
||||
<template v-if="!points.length">
|
||||
<span v-for="index in pointCount" :key="index" aria-hidden="true" class="flex h-5 items-center">
|
||||
<span class="block h-3.5 w-full rounded-xs" :class="healthStatusMeta.no_data.cellClass" />
|
||||
</span>
|
||||
</template>
|
||||
<ZButton
|
||||
v-for="(point, pointIndex) in points"
|
||||
:key="point.bucketStart"
|
||||
@@ -306,7 +315,7 @@ onBeforeUnmount(() => {
|
||||
:class="[
|
||||
healthStatusMeta[point.status].cellClass,
|
||||
isActivePoint(point)
|
||||
? 'brightness-105 shadow-[0_5px_10px_-5px_var(--cp-color-shadow)]'
|
||||
? 'brightness-105 shadow-[0_5px_10px_-5px_var(--z-color-shadow)]'
|
||||
: isInteractivePoint(point)
|
||||
? 'group-hover:brightness-95'
|
||||
: undefined,
|
||||
@@ -316,7 +325,7 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div data-health-timeline-popover class="w-72 overflow-hidden rounded-cp-lg">
|
||||
<div data-health-timeline-popover class="w-72 overflow-hidden rounded-z-lg">
|
||||
<HealthTimelinePointPopover v-if="activePoint" :point="activePoint" />
|
||||
</div>
|
||||
</ZPopover>
|
||||
|
||||
@@ -124,23 +124,23 @@ const amountItems = computed(() => {
|
||||
|
||||
function itemValueClass(tone?: string, accent?: boolean) {
|
||||
if (tone === 'success')
|
||||
return 'text-cp-success-text'
|
||||
return 'text-z-success-text'
|
||||
if (tone === 'info' || accent)
|
||||
return 'text-cp-info-text'
|
||||
return 'text-cp-text'
|
||||
return 'text-z-info-text'
|
||||
return 'text-z-text'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center justify-end gap-1.5">
|
||||
<span class="font-mono text-cp font-heavy tabular-nums text-cp-green-text">
|
||||
<span class="font-mono text-z font-heavy tabular-nums text-z-green-text">
|
||||
{{ usageBillingText(record) }}
|
||||
</span>
|
||||
|
||||
<div v-if="billing" class="flex items-center gap-1">
|
||||
<span
|
||||
v-if="billing.serviceTierDisplay === 'Fast'"
|
||||
class="inline-flex size-4 items-center justify-center rounded-full bg-cp-warning-container text-cp-warning-on-container"
|
||||
class="inline-flex size-4 items-center justify-center rounded-full bg-z-warning-container text-z-warning-on-container"
|
||||
:title="t('usage.billing.fastMode')"
|
||||
role="img"
|
||||
:aria-label="t('usage.billing.fastMode')"
|
||||
@@ -152,7 +152,7 @@ function itemValueClass(tone?: string, accent?: boolean) {
|
||||
:trigger-label="billing.longContextBillingApplied ? t('usage.billing.viewLongContextBillingDetails') : t('usage.billing.viewCostDetails')"
|
||||
:tone="billing.longContextBillingApplied ? 'warning' : 'primary'"
|
||||
>
|
||||
<div class="grid gap-1.5 text-cp-text-secondary">
|
||||
<div class="grid gap-1.5 text-z-text-secondary">
|
||||
<div v-for="item in amountItems" :key="item.label" class="grid grid-cols-[auto_minmax(0,1fr)] items-center gap-4">
|
||||
<span class="whitespace-nowrap">{{ item.label }}</span>
|
||||
<span class="justify-self-end whitespace-nowrap font-mono font-heavy" :class="itemValueClass(undefined, item.accent)">
|
||||
@@ -160,7 +160,7 @@ function itemValueClass(tone?: string, accent?: boolean) {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-1 grid gap-1.5 rounded-cp bg-cp-fill-tertiary p-2 text-cp-text-secondary">
|
||||
<div class="mt-1 grid gap-1.5 rounded-z bg-z-fill-tertiary p-2 text-z-text-secondary">
|
||||
<div v-for="item in billingItems" :key="item.label" class="grid grid-cols-[auto_minmax(0,1fr)] items-center gap-4">
|
||||
<span class="whitespace-nowrap">{{ item.label }}</span>
|
||||
<span class="justify-self-end whitespace-nowrap font-mono font-heavy" :class="itemValueClass(item.tone)">
|
||||
|
||||
@@ -11,7 +11,7 @@ const clientIp = computed(() => usageClientIp(props.record))
|
||||
|
||||
<template>
|
||||
<span
|
||||
class="inline-flex h-6 max-w-full items-center rounded-full bg-cp-blue-container px-2.5 font-mono text-cp-sm leading-none font-bold text-cp-blue-on-container"
|
||||
class="inline-flex h-6 max-w-full items-center rounded-full bg-z-blue-container px-2.5 font-mono text-z-sm leading-none font-bold text-z-blue-on-container"
|
||||
:title="clientIp"
|
||||
>
|
||||
<span class="min-w-0 truncate">
|
||||
|
||||
@@ -19,18 +19,18 @@ defineProps<{
|
||||
<template #reference>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex size-4 items-center justify-center rounded-full bg-transparent outline-none focus-visible:ring-2 focus-visible:ring-cp-control-outline"
|
||||
class="inline-flex size-4 items-center justify-center rounded-full bg-transparent outline-none focus-visible:ring-2 focus-visible:ring-z-control-outline"
|
||||
:class="tone === 'warning'
|
||||
? 'text-cp-warning-text'
|
||||
: 'text-cp-primary-text'"
|
||||
? 'text-z-warning-text'
|
||||
: 'text-z-primary-text'"
|
||||
:aria-label="triggerLabel"
|
||||
>
|
||||
<Info class="size-3" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<div class="grid w-max min-w-60 max-w-[calc(100vw-2rem)] gap-2 p-3 text-cp-sm leading-none">
|
||||
<p v-if="title" class="m-0 font-heavy text-cp-text">
|
||||
<div class="grid w-max min-w-60 max-w-[calc(100vw-2rem)] gap-2 p-3 text-z-sm leading-none">
|
||||
<p v-if="title" class="m-0 font-heavy text-z-text">
|
||||
{{ title }}
|
||||
</p>
|
||||
<slot />
|
||||
|
||||
@@ -16,25 +16,25 @@ const { t } = useI18n({ useScope: 'global' })
|
||||
const latencyDetails = computed(() => usageLatencyDetails(props.record))
|
||||
|
||||
const latencyTones = {
|
||||
good: 'text-cp-success-text',
|
||||
warn: 'text-cp-warning-text',
|
||||
good: 'text-z-success-text',
|
||||
warn: 'text-z-warning-text',
|
||||
// 主题的 Orange 与 Warning 共用色种,混入 Error 让相邻两档可区分
|
||||
slow: 'latency-slow',
|
||||
critical: 'text-cp-error-text',
|
||||
critical: 'text-z-error-text',
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center justify-end gap-1.5">
|
||||
<div
|
||||
class="grid grid-cols-[auto_auto] items-center justify-end gap-x-2 gap-y-1.5 whitespace-nowrap font-mono text-cp-sm leading-none font-heavy tabular-nums"
|
||||
class="grid grid-cols-[auto_auto] items-center justify-end gap-x-2 gap-y-1.5 whitespace-nowrap font-mono text-z-sm leading-none font-heavy tabular-nums"
|
||||
>
|
||||
<span class="text-cp-xs text-cp-text-quaternary">{{ t('usage.ttft') }}</span>
|
||||
<span :class="latencyDetails.firstOutputSeverity === null ? 'text-cp-text-quaternary' : latencyTones[latencyDetails.firstOutputSeverity]">
|
||||
<span class="text-z-xs text-z-text-quaternary">{{ t('usage.ttft') }}</span>
|
||||
<span :class="latencyDetails.firstOutputSeverity === null ? 'text-z-text-quaternary' : latencyTones[latencyDetails.firstOutputSeverity]">
|
||||
{{ latencyDetails.firstOutputDisplay }}
|
||||
</span>
|
||||
<span class="text-cp-xs text-cp-text-quaternary">{{ t('usage.latency.total') }}</span>
|
||||
<span :class="latencyDetails.totalSeverity === null ? 'text-cp-text-quaternary' : latencyTones[latencyDetails.totalSeverity]">
|
||||
<span class="text-z-xs text-z-text-quaternary">{{ t('usage.latency.total') }}</span>
|
||||
<span :class="latencyDetails.totalSeverity === null ? 'text-z-text-quaternary' : latencyTones[latencyDetails.totalSeverity]">
|
||||
{{ latencyDetails.totalDisplay }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -43,16 +43,16 @@ const latencyTones = {
|
||||
<section
|
||||
v-for="(section, index) in latencyDetails.sections"
|
||||
:key="section.title"
|
||||
class="grid gap-1.5 text-cp-text-secondary"
|
||||
:class="{ 'border-t border-cp-split pt-2': index > 0 }"
|
||||
class="grid gap-1.5 text-z-text-secondary"
|
||||
:class="{ 'border-t border-z-split pt-2': index > 0 }"
|
||||
>
|
||||
<p class="m-0 flex items-center justify-between gap-3 text-cp-sm font-heavy text-cp-text">
|
||||
<p class="m-0 flex items-center justify-between gap-3 text-z-sm font-heavy text-z-text">
|
||||
<span>{{ section.title }}</span>
|
||||
<span
|
||||
class="rounded px-1 py-0.5 text-[10px] leading-none font-normal"
|
||||
:class="section.source === 'official'
|
||||
? 'bg-cp-primary-container text-cp-primary-on-container'
|
||||
: 'bg-cp-fill-tertiary text-cp-text-tertiary'"
|
||||
? 'bg-z-primary-container text-z-primary-on-container'
|
||||
: 'bg-z-fill-tertiary text-z-text-tertiary'"
|
||||
>
|
||||
{{ section.source === 'official' ? t('usage.latency.official') : t('usage.latency.local') }}
|
||||
</span>
|
||||
@@ -60,12 +60,12 @@ const latencyTones = {
|
||||
<div
|
||||
v-for="item in section.items"
|
||||
:key="item.label"
|
||||
class="flex justify-between gap-4 text-cp-xs"
|
||||
class="flex justify-between gap-4 text-z-xs"
|
||||
>
|
||||
<span class="whitespace-nowrap">{{ item.label }}</span>
|
||||
<span
|
||||
class="whitespace-nowrap font-mono font-heavy"
|
||||
:class="item.emphasized ? 'text-cp-info-text' : 'text-cp-text'"
|
||||
:class="item.emphasized ? 'text-z-info-text' : 'text-z-text'"
|
||||
>
|
||||
{{ item.value }}
|
||||
</span>
|
||||
@@ -77,6 +77,6 @@ const latencyTones = {
|
||||
|
||||
<style scoped>
|
||||
.latency-slow {
|
||||
color: color-mix(in oklab, var(--cp-color-warning-text) 65%, var(--cp-color-error-text));
|
||||
color: color-mix(in oklab, var(--z-color-warning-text) 65%, var(--z-color-error-text));
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -18,22 +18,22 @@ const { t } = useI18n({ useScope: 'global' })
|
||||
<template>
|
||||
<div class="inline-grid max-w-full gap-1">
|
||||
<code
|
||||
class="block max-w-full truncate font-mono text-cp-sm leading-none font-heavy text-cp-text"
|
||||
class="block max-w-full truncate font-mono text-z-sm leading-none font-heavy text-z-text"
|
||||
:title="t('usage.status.requestedModel', { model: modelDisplay.primary })"
|
||||
>
|
||||
{{ modelDisplay.primary }}
|
||||
</code>
|
||||
<div
|
||||
v-if="modelDisplay.secondary"
|
||||
class="flex min-w-0 max-w-full items-center gap-1.25 text-cp-text-secondary"
|
||||
class="flex min-w-0 max-w-full items-center gap-1.25 text-z-text-secondary"
|
||||
:title="t('usage.status.mappedModel', { secondary: modelDisplay.secondary })"
|
||||
>
|
||||
<CornerDownRight
|
||||
class="size-3.25 shrink-0 text-cp-blue-text"
|
||||
class="size-3.25 shrink-0 text-z-blue-text"
|
||||
stroke-width="2.4"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<code class="block truncate font-mono text-cp-xs leading-none font-bold">
|
||||
<code class="block truncate font-mono text-z-xs leading-none font-bold">
|
||||
{{ modelDisplay.secondary }}
|
||||
</code>
|
||||
</div>
|
||||
|
||||
@@ -16,10 +16,10 @@ const performance = computed(() => usagePerformanceDetails(props.record))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid justify-items-end gap-y-1.5 whitespace-nowrap font-mono text-cp-sm leading-none font-heavy tabular-nums">
|
||||
<span class="text-cp-text">{{ performance.throughputDisplay }}</span>
|
||||
<span class="text-cp-xs text-cp-text-secondary">
|
||||
<span :title="t('usage.performance.ttftHelp')" class="text-cp-text-quaternary">TTFT</span>
|
||||
<div class="grid justify-items-end gap-y-1.5 whitespace-nowrap font-mono text-z-sm leading-none font-heavy tabular-nums">
|
||||
<span class="text-z-text">{{ performance.throughputDisplay }}</span>
|
||||
<span class="text-z-xs text-z-text-secondary">
|
||||
<span :title="t('usage.performance.ttftHelp')" class="text-z-text-quaternary">TTFT</span>
|
||||
{{ performance.firstTokenDisplay }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -15,12 +15,12 @@ const { t } = useI18n({ useScope: 'global' })
|
||||
|
||||
<template>
|
||||
<span
|
||||
class="inline-flex items-center gap-1 whitespace-nowrap text-cp-sm font-bold text-cp-text"
|
||||
class="inline-flex items-center gap-1 whitespace-nowrap text-z-sm font-bold text-z-text"
|
||||
>
|
||||
<span>{{ usageReasoningEffort(record) }}</span>
|
||||
<span
|
||||
v-if="record.subagentKind"
|
||||
class="inline-flex shrink-0 text-cp-purple-text"
|
||||
class="inline-flex shrink-0 text-z-purple-text"
|
||||
:title="t('usage.reasoning.subagentRequest')"
|
||||
:aria-label="t('usage.reasoning.subagentRequest')"
|
||||
>
|
||||
@@ -28,7 +28,7 @@ const { t } = useI18n({ useScope: 'global' })
|
||||
</span>
|
||||
<span
|
||||
v-if="record.subagentKind === 'review'"
|
||||
class="inline-flex shrink-0 text-cp-purple-text"
|
||||
class="inline-flex shrink-0 text-z-purple-text"
|
||||
:title="t('usage.reasoning.reviewSubagentRequest')"
|
||||
:aria-label="t('usage.reasoning.reviewSubagentRequest')"
|
||||
>
|
||||
|
||||
@@ -47,7 +47,7 @@ const { t } = useI18n({ useScope: 'global' })
|
||||
>
|
||||
<template #clientApiKeyName="{ displayValue }">
|
||||
<span
|
||||
class="block max-w-full truncate font-mono text-cp-sm leading-none font-bold text-cp-text"
|
||||
class="block max-w-full truncate font-mono text-z-sm leading-none font-bold text-z-text"
|
||||
:title="String(displayValue)"
|
||||
>
|
||||
{{ displayValue }}
|
||||
@@ -63,14 +63,14 @@ const { t } = useI18n({ useScope: 'global' })
|
||||
|
||||
<template #accountEmail="{ row }">
|
||||
<span
|
||||
class="block max-w-full truncate font-mono text-cp-sm leading-none font-bold text-cp-text"
|
||||
class="block max-w-full truncate font-mono text-z-sm leading-none font-bold text-z-text"
|
||||
:title="usageAccountText(row)"
|
||||
>
|
||||
{{ usageAccountText(row) }}
|
||||
</span>
|
||||
<span
|
||||
v-if="row.accountNotes?.trim()"
|
||||
class="mt-1 block max-w-full truncate text-cp-xs font-emphasis text-cp-text-quaternary"
|
||||
class="mt-1 block max-w-full truncate text-z-xs font-emphasis text-z-text-quaternary"
|
||||
:title="row.accountNotes"
|
||||
>
|
||||
{{ row.accountNotes }}
|
||||
@@ -85,7 +85,7 @@ const { t } = useI18n({ useScope: 'global' })
|
||||
:plan-type-display="row.accountPlanTypeDisplay || row.accountPlanType || ''"
|
||||
size="sm"
|
||||
/>
|
||||
<span v-else class="text-cp-text-quaternary">—</span>
|
||||
<span v-else class="text-z-text-quaternary">—</span>
|
||||
</template>
|
||||
|
||||
<template #createdAtDisplay="{ row }">
|
||||
@@ -97,7 +97,7 @@ const { t } = useI18n({ useScope: 'global' })
|
||||
</template>
|
||||
|
||||
<template #userAgent="{ row }">
|
||||
<span class="block max-w-full wrap-break-word whitespace-normal font-mono text-cp-sm leading-[1.4] font-emphasis text-cp-text-secondary">
|
||||
<span class="block max-w-full wrap-break-word whitespace-normal font-mono text-z-sm leading-[1.4] font-emphasis text-z-text-secondary">
|
||||
{{ usageUserAgent(row) }}
|
||||
</span>
|
||||
</template>
|
||||
@@ -116,10 +116,10 @@ const { t } = useI18n({ useScope: 'global' })
|
||||
|
||||
<template #errorMessage="{ row }">
|
||||
<div class="min-w-0 py-0.5" :title="row.errorMessage || row.errorCode || undefined">
|
||||
<code class="block truncate font-mono text-cp-sm font-bold text-cp-error-text">
|
||||
<code class="block truncate font-mono text-z-sm font-bold text-z-error-text">
|
||||
{{ row.errorCode || (row.logicalOutcome === 'incomplete' ? t('usage.error.responseIsNotComplete') : t('usage.error.requestFailed')) }}
|
||||
</code>
|
||||
<p v-if="row.errorMessage" class="mt-1 mb-0 truncate text-cp-xs leading-[1.45] font-emphasis text-cp-text-secondary">
|
||||
<p v-if="row.errorMessage" class="mt-1 mb-0 truncate text-z-xs leading-[1.45] font-emphasis text-z-text-secondary">
|
||||
{{ row.errorMessage }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -10,10 +10,10 @@ const { t } = useI18n({ useScope: 'global' })
|
||||
|
||||
<template>
|
||||
<div class="inline-flex max-w-full items-center gap-1.5 whitespace-nowrap">
|
||||
<code class="font-mono text-cp-sm font-emphasis">{{ record.route || '—' }}</code>
|
||||
<code class="font-mono text-z-sm font-emphasis">{{ record.route || '—' }}</code>
|
||||
<span
|
||||
v-if="record.compact"
|
||||
class="inline-flex shrink-0 text-cp-orange-text"
|
||||
class="inline-flex shrink-0 text-z-orange-text"
|
||||
:title="t('usage.route.compressionRequest')"
|
||||
:aria-label="t('usage.route.compressionRequest')"
|
||||
>
|
||||
|
||||
@@ -3,7 +3,7 @@ defineProps<{ value: string }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="block truncate font-mono text-cp-sm leading-none font-bold text-cp-text-secondary" :title="value">
|
||||
<span class="block truncate font-mono text-z-sm leading-none font-bold text-z-text-secondary" :title="value">
|
||||
{{ value || '—' }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
@@ -43,34 +43,34 @@ const tokenItems = computed(() => [
|
||||
<template>
|
||||
<div class="flex items-center justify-end gap-1.5">
|
||||
<div
|
||||
class="grid grid-cols-[auto_auto] items-center justify-end gap-x-2 gap-y-1 font-mono text-cp-sm leading-none font-bold tabular-nums"
|
||||
class="grid grid-cols-[auto_auto] items-center justify-end gap-x-2 gap-y-1 font-mono text-z-sm leading-none font-bold tabular-nums"
|
||||
>
|
||||
<span class="inline-flex items-center gap-1 text-cp-success-text">
|
||||
<span class="inline-flex items-center gap-1 text-z-success-text">
|
||||
<ArrowDown class="size-3" />
|
||||
{{ tokenDetails.inputTokensDisplay }}
|
||||
</span>
|
||||
<span class="inline-flex items-center gap-1 text-cp-info-text">
|
||||
<span class="inline-flex items-center gap-1 text-z-info-text">
|
||||
<ArrowUp class="size-3" />
|
||||
{{ tokenDetails.outputTokensDisplay }}
|
||||
</span>
|
||||
<span class="col-span-2 inline-flex items-center justify-end gap-1 text-cp-info-text">
|
||||
<span class="col-span-2 inline-flex items-center justify-end gap-1 text-z-info-text">
|
||||
<Archive class="size-3" />
|
||||
{{ tokenDetails.cachedInputTokensDisplay }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<UsageDetailPopover :title="t('usage.tokens.tokenDetails')" :trigger-label="t('usage.tokens.viewTokenDetails')">
|
||||
<div class="grid gap-1.5 text-cp-text-secondary">
|
||||
<div class="grid gap-1.5 text-z-text-secondary">
|
||||
<div v-for="item in tokenItems" :key="item.label" class="flex justify-between gap-4">
|
||||
<span class="whitespace-nowrap">{{ item.label }}</span>
|
||||
<span class="whitespace-nowrap font-mono font-heavy text-cp-text">
|
||||
<span class="whitespace-nowrap font-mono font-heavy text-z-text">
|
||||
{{ item.value }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-1 flex justify-between border-t border-cp-split pt-2">
|
||||
<span class="whitespace-nowrap text-cp-text-secondary">{{ t('usage.tokens.totalTokens') }}</span>
|
||||
<span class="whitespace-nowrap font-mono font-heavy text-cp-info-text">
|
||||
<div class="mt-1 flex justify-between border-t border-z-split pt-2">
|
||||
<span class="whitespace-nowrap text-z-text-secondary">{{ t('usage.tokens.totalTokens') }}</span>
|
||||
<span class="whitespace-nowrap font-mono font-heavy text-z-info-text">
|
||||
{{ tokenDetails.totalTokensDisplay }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@ defineProps<{
|
||||
|
||||
<template>
|
||||
<span
|
||||
class="inline-flex h-6 min-w-12 items-center justify-center rounded-full px-2 text-cp leading-none font-bold"
|
||||
class="inline-flex h-6 min-w-12 items-center justify-center rounded-full px-2 text-z leading-none font-bold"
|
||||
:class="usageTransportTypeClass(transport)"
|
||||
>
|
||||
{{ usageTransportType(transport) }}
|
||||
|
||||
@@ -36,39 +36,39 @@ export const healthLegend = computed(() => ([
|
||||
export const healthStatusMeta = computed<Record<DashboardHealthStatus, DashboardHealthStatusMeta>>(() => ({
|
||||
future: {
|
||||
label: t('usage.future'),
|
||||
cellClass: 'bg-cp-bg-container-disabled opacity-60',
|
||||
badgeClass: 'bg-cp-fill-tertiary text-cp-text-quaternary',
|
||||
valueClass: 'text-cp-text-quaternary',
|
||||
cellClass: 'bg-z-bg-container-disabled opacity-60',
|
||||
badgeClass: 'bg-z-fill-tertiary text-z-text-quaternary',
|
||||
valueClass: 'text-z-text-quaternary',
|
||||
},
|
||||
no_data: {
|
||||
label: t('usage.noValidSample'),
|
||||
cellClass: 'bg-cp-border',
|
||||
badgeClass: 'bg-cp-fill-tertiary text-cp-text-secondary',
|
||||
valueClass: 'text-cp-text-quaternary',
|
||||
cellClass: 'bg-z-border',
|
||||
badgeClass: 'bg-z-fill-tertiary text-z-text-secondary',
|
||||
valueClass: 'text-z-text-quaternary',
|
||||
},
|
||||
unavailable: {
|
||||
label: t('usage.notReachable'),
|
||||
cellClass: 'bg-cp-error',
|
||||
badgeClass: 'bg-cp-error-container text-cp-error-on-container',
|
||||
valueClass: 'text-cp-error-text',
|
||||
cellClass: 'bg-z-error',
|
||||
badgeClass: 'bg-z-error-container text-z-error-on-container',
|
||||
valueClass: 'text-z-error-text',
|
||||
},
|
||||
unstable: {
|
||||
label: t('usage.unstable'),
|
||||
cellClass: 'bg-cp-warning',
|
||||
badgeClass: 'bg-cp-warning-container text-cp-warning-on-container',
|
||||
valueClass: 'text-cp-warning-text',
|
||||
cellClass: 'bg-z-warning',
|
||||
badgeClass: 'bg-z-warning-container text-z-warning-on-container',
|
||||
valueClass: 'text-z-warning-text',
|
||||
},
|
||||
low_sample: {
|
||||
label: t('usage.lowSample'),
|
||||
cellClass: 'bg-cp-cyan-solid',
|
||||
badgeClass: 'bg-cp-cyan-container text-cp-cyan-on-container',
|
||||
valueClass: 'text-cp-cyan-text',
|
||||
cellClass: 'bg-z-cyan-solid',
|
||||
badgeClass: 'bg-z-cyan-container text-z-cyan-on-container',
|
||||
valueClass: 'text-z-cyan-text',
|
||||
},
|
||||
stable: {
|
||||
label: t('usage.stable'),
|
||||
cellClass: 'bg-cp-success',
|
||||
badgeClass: 'bg-cp-success-container text-cp-success-on-container',
|
||||
valueClass: 'text-cp-success-text',
|
||||
cellClass: 'bg-z-success',
|
||||
badgeClass: 'bg-z-success-container text-z-success-on-container',
|
||||
valueClass: 'text-z-success-text',
|
||||
},
|
||||
}))
|
||||
|
||||
|
||||
@@ -58,10 +58,10 @@ export function usageTransportType(transport?: string | null) {
|
||||
export function usageTransportTypeClass(transport?: string | null) {
|
||||
const type = usageTransportType(transport)
|
||||
if (type === 'WS')
|
||||
return 'bg-cp-blue-container text-cp-blue-on-container'
|
||||
return 'bg-z-blue-container text-z-blue-on-container'
|
||||
if (type === 'SSE')
|
||||
return 'bg-cp-green-container text-cp-green-on-container'
|
||||
return 'bg-cp-fill-tertiary text-cp-text-secondary'
|
||||
return 'bg-z-green-container text-z-green-on-container'
|
||||
return 'bg-z-fill-tertiary text-z-text-secondary'
|
||||
}
|
||||
|
||||
export function usageAccountText(record: Pick<UsageCommonRecord, 'accountEmail' | 'accountName' | 'accountId'>) {
|
||||
|
||||
@@ -5,21 +5,21 @@ import { useThemeColor } from '@/composables/useThemeColor'
|
||||
export function useChartPalette() {
|
||||
const color = useThemeColor()
|
||||
const palette = computed(() => ({
|
||||
textPrimary: color('--cp-color-text', '#0E1726'),
|
||||
textSecondary: color('--cp-color-text-secondary', '#64748B'),
|
||||
textMuted: color('--cp-color-text-quaternary', '#94A3B8'),
|
||||
surface: color('--cp-color-bg-container', '#FFFFFF'),
|
||||
surfaceMuted: color('--cp-color-fill-tertiary', '#F1F5F9'),
|
||||
grid: color('--cp-color-fill-tertiary', '#F1F5F9'),
|
||||
divider: color('--cp-color-split', '#E2E8F0'),
|
||||
border: color('--cp-color-border-secondary', '#E2E8F0'),
|
||||
pointer: color('--cp-color-border', '#CBD5E1'),
|
||||
info: color('--cp-color-blue-solid', '#5983F4'),
|
||||
success: color('--cp-color-green-solid', '#12B981'),
|
||||
warning: color('--cp-color-orange-solid', '#F59E0B'),
|
||||
danger: color('--cp-color-red-solid', '#EF4444'),
|
||||
normal: color('--cp-color-text-tertiary', '#94A3B8'),
|
||||
reasoning: color('--cp-color-purple-solid', '#722ED1'),
|
||||
textPrimary: color('color-text', '#0E1726'),
|
||||
textSecondary: color('color-text-secondary', '#64748B'),
|
||||
textMuted: color('color-text-quaternary', '#94A3B8'),
|
||||
surface: color('color-bg-container', '#FFFFFF'),
|
||||
surfaceMuted: color('color-fill-tertiary', '#F1F5F9'),
|
||||
grid: color('color-fill-tertiary', '#F1F5F9'),
|
||||
divider: color('color-split', '#E2E8F0'),
|
||||
border: color('color-border-secondary', '#E2E8F0'),
|
||||
pointer: color('color-border', '#CBD5E1'),
|
||||
info: color('color-blue-solid', '#5983F4'),
|
||||
success: color('color-green-solid', '#12B981'),
|
||||
warning: color('color-orange-solid', '#F59E0B'),
|
||||
danger: color('color-red-solid', '#EF4444'),
|
||||
normal: color('color-text-tertiary', '#94A3B8'),
|
||||
reasoning: color('color-purple-solid', '#722ED1'),
|
||||
}))
|
||||
|
||||
return { color, palette }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { InjectionKey, Ref } from 'vue'
|
||||
import { getThemeVarName } from '@codex-proxy/ui/theme'
|
||||
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { inject, provide } from 'vue'
|
||||
@@ -20,7 +21,7 @@ export function useThemeColor() {
|
||||
const { themeRevision } = storeToRefs(useThemeStore())
|
||||
return (name: string, fallback: string) => {
|
||||
if (scopedTokens) {
|
||||
const scopedValue = scopedTokens.value[name]
|
||||
const scopedValue = scopedTokens.value[getThemeVarName(name)]
|
||||
if (typeof scopedValue === 'string' && scopedValue.trim())
|
||||
return scopedValue.trim()
|
||||
if (typeof scopedValue === 'number')
|
||||
@@ -36,5 +37,5 @@ export function useThemeColor() {
|
||||
function readCssVariable(name: string, fallback: string) {
|
||||
if (typeof document === 'undefined')
|
||||
return fallback
|
||||
return getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback
|
||||
return getComputedStyle(document.documentElement).getPropertyValue(getThemeVarName(name)).trim() || fallback
|
||||
}
|
||||
|
||||
@@ -582,6 +582,7 @@
|
||||
"fillInModelMappingCompletely": "Please fill in the model mapping completely",
|
||||
"duplicateClientModel": "Duplicate client model: {requested}",
|
||||
"requiredSettings": "Complete concurrency, queue, credential refresh, and scheduling settings",
|
||||
"invalidRefreshSettings": "Credential refresh lead time and concurrency must be non-negative integers",
|
||||
"invalidAffinityDuration": "Affinity duration must be an integer from 1 to 720 hours",
|
||||
"invalidSwitchLimit": "Maximum account switches must be an integer from 0 to 31, set to 0 to disable switching",
|
||||
"invalidConcurrency": "Default account concurrency must be an integer from 0 to 4,294,967,295, set to 0 for unlimited",
|
||||
@@ -591,13 +592,11 @@
|
||||
"invalidResetLeadTime": "Reset credit lead time must be an integer from 1 to 1,440 minutes",
|
||||
"invalidRetention": "History retention must be an integer from 1 to 31 days",
|
||||
"invalidQueueSettings": "Queue capacity must be an integer from 0 to 1,000 and timeout from 1 to 120 seconds",
|
||||
"invalidClientVersion": "Enter a valid minimum client version",
|
||||
"invalidProbeModel": "The probe model must be at most 128 characters without control characters",
|
||||
"invalidWarmupTimes": "Use HH:MM for warmup times, for example 08:00 or 08:00,13:00",
|
||||
"warmupModelRequired": "Select a model to enable warmup",
|
||||
"invalidWarmupModel": "Warmup model name must not exceed 128 characters",
|
||||
"settingsSaved": "Settings saved",
|
||||
"protectionSettingsRequired": "Complete the error protection settings",
|
||||
"invalidProtectionSettings": "Use a failure threshold of 2–1,000, count validity of 60–3,600 seconds, and a cooldown of 300–604,800 seconds"
|
||||
},
|
||||
"page": {
|
||||
|
||||
@@ -582,6 +582,7 @@
|
||||
"fillInModelMappingCompletely": "请完整填写模型映射",
|
||||
"duplicateClientModel": "存在重复的客户端模型:{requested}",
|
||||
"requiredSettings": "请完整填写并发、队列、凭据刷新参数和调度策略",
|
||||
"invalidRefreshSettings": "凭据刷新提前量和并发数须为非负整数",
|
||||
"invalidAffinityDuration": "亲和时长应为 1~720 小时的整数",
|
||||
"invalidSwitchLimit": "最大换号次数应为 0~31 的整数,0 表示不换号",
|
||||
"invalidConcurrency": "默认账号并发上限应为 0~4294967295 的整数,0 表示不限制",
|
||||
@@ -591,13 +592,11 @@
|
||||
"invalidResetLeadTime": "重置卡提前使用时间应为 1~1440 分钟的整数",
|
||||
"invalidRetention": "历史保留天数应为 1–31 的整数",
|
||||
"invalidQueueSettings": "队列容量应为 0~1000 的整数,排队超时应为 1~120 秒的整数",
|
||||
"invalidClientVersion": "请修正客户端最低版本格式",
|
||||
"invalidProbeModel": "探测模型最多 128 个字符,不能包含控制字符",
|
||||
"invalidWarmupTimes": "预激活时间格式无效,请输入 HH:MM 格式(如 08:00 或 08:00,13:00)",
|
||||
"warmupModelRequired": "启用预激活时请选择模型",
|
||||
"invalidWarmupModel": "预激活模型名称不能超过 128 个字符",
|
||||
"settingsSaved": "设置已保存",
|
||||
"protectionSettingsRequired": "请完整填写错误保护参数",
|
||||
"invalidProtectionSettings": "失败阈值应为 2~1000,计数有效期为 60~3600 秒,冷却时长为 300~604800 秒"
|
||||
},
|
||||
"page": {
|
||||
|
||||
@@ -582,6 +582,7 @@
|
||||
"fillInModelMappingCompletely": "請完整填寫模型映射",
|
||||
"duplicateClientModel": "存在重複的客戶端模型:{requested}",
|
||||
"requiredSettings": "請完整填寫併發、隊列、憑據刷新參數和調度策略",
|
||||
"invalidRefreshSettings": "憑據刷新提前量和併發數須為非負整數",
|
||||
"invalidAffinityDuration": "親和時長應爲 1~720 小時的整數",
|
||||
"invalidSwitchLimit": "最大換號次數應爲 0~31 的整數,0 表示不換號",
|
||||
"invalidConcurrency": "默認賬號併發上限應爲 0~4294967295 的整數,0 表示不限制",
|
||||
@@ -591,13 +592,11 @@
|
||||
"invalidResetLeadTime": "重置卡提前使用時間應爲 1~1440 分鐘的整數",
|
||||
"invalidRetention": "歷史保留天數應爲 1–31 的整數",
|
||||
"invalidQueueSettings": "隊列容量應爲 0~1000 的整數,排隊超時應爲 1~120 秒的整數",
|
||||
"invalidClientVersion": "請修正客戶端最低版本格式",
|
||||
"invalidProbeModel": "探測模型最多 128 個字符,不能包含控制字符",
|
||||
"invalidWarmupTimes": "預激活時間格式無效,請輸入 HH:MM 格式(如 08:00 或 08:00,13:00)",
|
||||
"warmupModelRequired": "啓用預激活時請選擇模型",
|
||||
"invalidWarmupModel": "預激活模型名稱不能超過 128 個字符",
|
||||
"settingsSaved": "設置已保存",
|
||||
"protectionSettingsRequired": "請完整填寫錯誤保護參數",
|
||||
"invalidProtectionSettings": "失敗閾值應爲 2~1000,計數有效期爲 60~3600 秒,冷卻時長爲 300~604800 秒"
|
||||
},
|
||||
"page": {
|
||||
|
||||
@@ -141,23 +141,23 @@ const activeNavIndex = computed(() => {
|
||||
const index = navItems.value.findIndex(item => isActive(item.path))
|
||||
return Math.max(0, index)
|
||||
})
|
||||
const activeNavIndicatorStyle = computed(() => ({
|
||||
transform: `translate3d(0, ${activeNavIndex.value * 58 + (activeNavIndex.value > pluginNavIndex
|
||||
? pluginChildrenHeight.value
|
||||
: 0)}px, 0)`,
|
||||
opacity: isActive('/plugins') && pluginMenuVisible.value
|
||||
? 0
|
||||
: 1,
|
||||
}))
|
||||
const activePluginIndex = computed(() => route.name === 'plugins'
|
||||
? 0
|
||||
: pluginPageViews.value.findIndex(isPluginViewActive) + 1)
|
||||
const activePluginIndicatorStyle = computed(() => ({
|
||||
transform: `translate3d(0, ${activePluginIndex.value * 44}px, 0)`,
|
||||
opacity: isActive('/plugins') && pluginMenuVisible.value
|
||||
? 1
|
||||
: 0,
|
||||
}))
|
||||
const activeNavIndicatorStyle = computed(() => {
|
||||
const inPluginMenu = isActive('/plugins') && pluginMenuVisible.value
|
||||
const offset = inPluginMenu
|
||||
? 50 + activePluginIndex.value * 44
|
||||
: activeNavIndex.value > pluginNavIndex
|
||||
? pluginChildrenHeight.value
|
||||
: 0
|
||||
// 主菜单与子菜单共享选中框,位置和尺寸在同一次过渡中变化
|
||||
return {
|
||||
transform: `translate3d(${inPluginMenu ? 24 : 0}px, ${activeNavIndex.value * 58 + offset}px, 0)`,
|
||||
width: inPluginMenu ? 'calc(100% - 24px)' : '100%',
|
||||
height: inPluginMenu ? '40px' : '46px',
|
||||
}
|
||||
})
|
||||
const navFeedbackMuted = shallowRef(false)
|
||||
const { start: restoreNavFeedback, stop: stopNavFeedbackRestore } = useTimeoutFn(
|
||||
() => {
|
||||
@@ -225,7 +225,6 @@ async function handleLogout() {
|
||||
const sidebarEl = ref<HTMLElement | null>(null)
|
||||
const brandLabelEl = ref<HTMLElement | null>(null)
|
||||
const navSignalEl = useTemplateRef<HTMLElement>('navSignal')
|
||||
const pluginNavSignalEls = useTemplateRef<HTMLElement[]>('pluginNavSignal')
|
||||
const collapsedSidebarWidth = 88
|
||||
const expandedSidebarWidth = 251
|
||||
const sidebarWidth = computed(() => (isCollapsed.value ? collapsedSidebarWidth : expandedSidebarWidth))
|
||||
@@ -349,9 +348,7 @@ function animateSidebarWidth(collapsed: boolean) {
|
||||
}
|
||||
|
||||
function animateNavSignal() {
|
||||
const signal = isActive('/plugins') && pluginMenuVisible.value
|
||||
? pluginNavSignalEls.value?.[0]
|
||||
: navSignalEl.value
|
||||
const signal = navSignalEl.value
|
||||
if (!signal)
|
||||
return
|
||||
|
||||
@@ -431,7 +428,6 @@ onBeforeUnmount(() => {
|
||||
sidebarEl.value,
|
||||
brandLabelEl.value,
|
||||
navSignalEl.value,
|
||||
...(pluginNavSignalEls.value ?? []),
|
||||
].filter((target): target is HTMLElement =>
|
||||
Boolean(target),
|
||||
)
|
||||
@@ -447,7 +443,7 @@ onBeforeUnmount(() => {
|
||||
<template>
|
||||
<aside
|
||||
ref="sidebarEl"
|
||||
class="z-20 h-dvh shrink-0 flex-col overflow-hidden bg-(--cp-layout-sider-bg) shadow-cp-layout-sider"
|
||||
class="z-20 h-dvh shrink-0 flex-col overflow-hidden bg-(--z-layout-sider-bg) shadow-z-layout-sider"
|
||||
:class="[
|
||||
mobile ? 'flex' : 'hidden min-[961px]:flex',
|
||||
isCollapsed ? 'w-22 basis-22 items-center' : 'w-62.75 basis-62.75',
|
||||
@@ -459,28 +455,28 @@ onBeforeUnmount(() => {
|
||||
>
|
||||
<ZMotionIcon
|
||||
variant="brand"
|
||||
class="inline-flex size-11 items-center justify-center relative -top-0.5 rounded-cp"
|
||||
class="inline-flex size-11 items-center justify-center relative -top-0.5 rounded-z"
|
||||
>
|
||||
<AppBrandMark class="block size-11 select-none" />
|
||||
</ZMotionIcon>
|
||||
<span v-show="brandLabelVisible" ref="brandLabelEl" class="grid min-w-33 content-center overflow-hidden">
|
||||
<strong class="text-base leading-[1.1] font-heavy text-cp-text"> Codex Proxy </strong>
|
||||
<strong class="text-base leading-[1.1] font-heavy text-z-text"> Codex Proxy </strong>
|
||||
<span class="mt-1.5 flex h-4.5 min-w-0 items-center gap-2">
|
||||
<span class="shrink-0 text-xs leading-none font-emphasis text-cp-text-secondary"> Rust build </span>
|
||||
<span class="shrink-0 text-xs leading-none font-emphasis text-z-text-secondary"> Rust build </span>
|
||||
<button
|
||||
v-if="hasVersionLabel"
|
||||
type="button"
|
||||
class="inline-flex h-4.5 min-w-0 cursor-pointer items-center gap-1 rounded-cp-sm border-0 px-1.5 font-mono text-[10px] leading-none font-bold transition-colors outline-none focus-visible:ring-2 focus-visible:ring-cp-control-outline focus-visible:ring-offset-2 focus-visible:ring-offset-cp-bg-container"
|
||||
class="inline-flex h-4.5 min-w-0 cursor-pointer items-center gap-1 rounded-z-sm border-0 px-1.5 font-mono text-[10px] leading-none font-bold transition-colors outline-none focus-visible:ring-2 focus-visible:ring-z-control-outline focus-visible:ring-offset-2 focus-visible:ring-offset-z-bg-container"
|
||||
:class="[
|
||||
hasUpdate
|
||||
? 'bg-cp-success-container text-cp-success-on-container hover:bg-cp-success-container-hover'
|
||||
: 'bg-cp-fill-quaternary text-cp-text-quaternary hover:bg-cp-fill-tertiary hover:text-cp-text-secondary',
|
||||
? 'bg-z-success-container text-z-success-on-container hover:bg-z-success-container-hover'
|
||||
: 'bg-z-fill-quaternary text-z-text-quaternary hover:bg-z-fill-tertiary hover:text-z-text-secondary',
|
||||
]"
|
||||
:title="updateButtonLabel"
|
||||
@click="openSystemUpdate"
|
||||
>
|
||||
<span>{{ versionLabel }}</span>
|
||||
<ArrowUpCircle v-if="hasUpdate" class="size-3 shrink-0 text-cp-success" />
|
||||
<ArrowUpCircle v-if="hasUpdate" class="size-3 shrink-0 text-z-success" />
|
||||
</button>
|
||||
</span>
|
||||
</span>
|
||||
@@ -491,12 +487,12 @@ onBeforeUnmount(() => {
|
||||
<!-- 焦点环绘制在按钮内侧,避免被滚动区和折叠菜单裁切 -->
|
||||
<nav class="relative grid gap-3" :class="isCollapsed ? 'mx-auto w-11.5' : 'w-full'" :aria-label="t('navigation.mainLabel')">
|
||||
<span
|
||||
class="pointer-events-none absolute inset-x-0 top-0 h-11.5 overflow-hidden rounded-cp bg-cp-menu-item-selected-bg transition-[transform,opacity] duration-260 ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none"
|
||||
class="pointer-events-none absolute left-0 top-0 overflow-hidden rounded-z bg-z-menu-item-selected-bg transition-[transform,width,height] duration-260 ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none"
|
||||
:style="activeNavIndicatorStyle"
|
||||
>
|
||||
<span
|
||||
ref="navSignal"
|
||||
class="absolute inset-y-0 left-0 w-2/3 [background:linear-gradient(90deg,transparent,color-mix(in_srgb,var(--cp-color-info)_9%,transparent),transparent)]"
|
||||
class="absolute inset-y-0 left-0 w-2/3 [background:linear-gradient(90deg,transparent,color-mix(in_srgb,var(--z-color-info)_9%,transparent),transparent)]"
|
||||
/>
|
||||
</span>
|
||||
<template v-for="item in navItems" :key="item.label">
|
||||
@@ -505,16 +501,16 @@ onBeforeUnmount(() => {
|
||||
<template #reference="{ close }">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-11.5 cursor-pointer items-center rounded-cp border-0 bg-transparent text-sm leading-[1.15] outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-cp-control-outline"
|
||||
class="inline-flex h-11.5 cursor-pointer items-center rounded-z border-0 bg-transparent text-sm leading-[1.15] outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-z-control-outline"
|
||||
:class="[
|
||||
isCollapsed ? 'w-11.5 justify-center' : 'w-full gap-3 px-4',
|
||||
isActive(item.path)
|
||||
? navFeedbackMuted
|
||||
? 'font-bold text-cp-text transition-none'
|
||||
: 'font-bold text-cp-text transition-colors duration-200'
|
||||
? 'font-bold text-z-text transition-none'
|
||||
: 'font-bold text-z-text transition-colors duration-200'
|
||||
: navFeedbackMuted
|
||||
? 'font-semibold text-cp-text-secondary transition-none'
|
||||
: 'font-semibold text-cp-text-secondary transition-colors duration-200 hover:bg-cp-fill-quaternary hover:text-cp-text',
|
||||
? 'font-semibold text-z-text-secondary transition-none'
|
||||
: 'font-semibold text-z-text-secondary transition-colors duration-200 hover:bg-z-fill-quaternary hover:text-z-text',
|
||||
]"
|
||||
:aria-expanded="isCollapsed ? undefined : pluginGroupExpanded"
|
||||
:aria-controls="isCollapsed ? undefined : pluginMenuId"
|
||||
@@ -540,7 +536,7 @@ onBeforeUnmount(() => {
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
<span class="block px-3 py-2 text-cp-sm whitespace-nowrap">{{ item.label }}</span>
|
||||
<span class="block px-3 py-2 text-z-sm whitespace-nowrap">{{ item.label }}</span>
|
||||
</ZPopover>
|
||||
|
||||
<div
|
||||
@@ -552,19 +548,12 @@ onBeforeUnmount(() => {
|
||||
>
|
||||
<div class="min-h-0 overflow-hidden">
|
||||
<div class="relative ml-3 grid gap-1 pt-1 pl-3">
|
||||
<span
|
||||
aria-hidden="true"
|
||||
class="pointer-events-none absolute top-1 right-0 left-3 h-10 overflow-hidden rounded-cp bg-cp-menu-item-selected-bg transition-[transform,opacity] duration-260 ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none"
|
||||
:style="activePluginIndicatorStyle"
|
||||
>
|
||||
<span ref="pluginNavSignal" class="absolute inset-y-0 left-0 w-2/3 opacity-0 [background:linear-gradient(90deg,transparent,color-mix(in_srgb,var(--cp-color-info)_9%,transparent),transparent)]" />
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
class="relative inline-flex h-10 min-w-0 cursor-pointer items-center gap-2 rounded-cp border-0 bg-transparent px-3 text-left text-cp-sm outline-none transition-colors focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-cp-control-outline motion-reduce:transition-none"
|
||||
class="relative inline-flex h-10 min-w-0 cursor-pointer items-center gap-2 rounded-z border-0 bg-transparent px-3 text-left text-z-sm outline-none transition-colors focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-z-control-outline motion-reduce:transition-none"
|
||||
:class="route.name === 'plugins'
|
||||
? 'font-emphasis text-cp-text'
|
||||
: navFeedbackMuted ? 'font-semibold text-cp-text-secondary' : 'font-semibold text-cp-text-secondary hover:bg-cp-fill-quaternary hover:text-cp-text'"
|
||||
? 'font-emphasis text-z-text'
|
||||
: navFeedbackMuted ? 'font-semibold text-z-text-secondary' : 'font-semibold text-z-text-secondary hover:bg-z-fill-quaternary hover:text-z-text'"
|
||||
:aria-current="route.name === 'plugins' ? 'page' : undefined"
|
||||
@click="navigate('/plugins')"
|
||||
>
|
||||
@@ -575,10 +564,10 @@ onBeforeUnmount(() => {
|
||||
v-for="view in pluginPageViews"
|
||||
:key="view.target.instanceId"
|
||||
type="button"
|
||||
class="relative inline-flex h-10 min-w-0 cursor-pointer items-center gap-2 rounded-cp border-0 bg-transparent px-3 text-left text-cp-sm outline-none transition-colors focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-cp-control-outline motion-reduce:transition-none"
|
||||
class="relative inline-flex h-10 min-w-0 cursor-pointer items-center gap-2 rounded-z border-0 bg-transparent px-3 text-left text-z-sm outline-none transition-colors focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-z-control-outline motion-reduce:transition-none"
|
||||
:class="isPluginViewActive(view)
|
||||
? 'font-emphasis text-cp-text'
|
||||
: navFeedbackMuted ? 'font-semibold text-cp-text-secondary' : 'font-semibold text-cp-text-secondary hover:bg-cp-fill-quaternary hover:text-cp-text'"
|
||||
? 'font-emphasis text-z-text'
|
||||
: navFeedbackMuted ? 'font-semibold text-z-text-secondary' : 'font-semibold text-z-text-secondary hover:bg-z-fill-quaternary hover:text-z-text'"
|
||||
:aria-current="isPluginViewActive(view) ? 'page' : undefined"
|
||||
@click="navigatePluginView(view)"
|
||||
>
|
||||
@@ -594,16 +583,16 @@ onBeforeUnmount(() => {
|
||||
<template #reference="{ close }">
|
||||
<button
|
||||
type="button"
|
||||
class="relative z-10 inline-flex h-11.5 cursor-pointer items-center rounded-cp border-0 text-sm leading-[1.15] outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-cp-control-outline"
|
||||
class="relative z-10 inline-flex h-11.5 cursor-pointer items-center rounded-z border-0 text-sm leading-[1.15] outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-z-control-outline"
|
||||
:class="[
|
||||
isCollapsed ? 'w-11.5 justify-center' : 'w-full gap-3 px-4',
|
||||
isActive(item.path)
|
||||
? navFeedbackMuted
|
||||
? 'bg-transparent font-bold text-cp-text transition-none'
|
||||
: 'bg-transparent font-bold text-cp-text transition-colors duration-200'
|
||||
? 'bg-transparent font-bold text-z-text transition-none'
|
||||
: 'bg-transparent font-bold text-z-text transition-colors duration-200'
|
||||
: navFeedbackMuted
|
||||
? 'bg-transparent font-semibold text-cp-text-secondary transition-none'
|
||||
: 'bg-transparent font-semibold text-cp-text-secondary transition-colors duration-200 hover:bg-cp-fill-quaternary hover:text-cp-text',
|
||||
? 'bg-transparent font-semibold text-z-text-secondary transition-none'
|
||||
: 'bg-transparent font-semibold text-z-text-secondary transition-colors duration-200 hover:bg-z-fill-quaternary hover:text-z-text',
|
||||
]"
|
||||
:aria-label="isCollapsed ? item.label : undefined"
|
||||
@click="close(); navigate(item.path)"
|
||||
@@ -617,7 +606,7 @@ onBeforeUnmount(() => {
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
<span class="block px-3 py-2 text-cp-sm whitespace-nowrap">{{ item.label }}</span>
|
||||
<span class="block px-3 py-2 text-z-sm whitespace-nowrap">{{ item.label }}</span>
|
||||
</ZPopover>
|
||||
</template>
|
||||
</nav>
|
||||
@@ -626,14 +615,14 @@ onBeforeUnmount(() => {
|
||||
|
||||
<div class="mx-4 mb-6 shrink-0" :class="isCollapsed ? 'w-11' : 'self-stretch'">
|
||||
<div
|
||||
class="bg-cp-fill-quaternary"
|
||||
:class="isCollapsed ? 'grid gap-1 rounded-cp p-1' : 'grid h-11 grid-cols-[4.25rem_auto] items-center justify-between rounded-cp-lg px-2'"
|
||||
class="bg-z-fill-quaternary"
|
||||
:class="isCollapsed ? 'grid gap-1 rounded-z p-1' : 'grid h-11 grid-cols-[4.25rem_auto] items-center justify-between rounded-z-lg px-2'"
|
||||
>
|
||||
<span
|
||||
v-if="!isCollapsed"
|
||||
class="inline-flex h-7 items-center justify-self-start gap-1.5 whitespace-nowrap rounded-lg bg-cp-success-container px-2 text-xs leading-none font-emphasis text-cp-success-on-container"
|
||||
class="inline-flex h-7 items-center justify-self-start gap-1.5 whitespace-nowrap rounded-lg bg-z-success-container px-2 text-xs leading-none font-emphasis text-z-success-on-container"
|
||||
>
|
||||
<i class="size-1.5 rounded-full bg-cp-success" />
|
||||
<i class="size-1.5 rounded-full bg-z-success" />
|
||||
{{ t('navigation.online') }}
|
||||
</span>
|
||||
|
||||
|
||||
@@ -233,10 +233,10 @@ onScopeDispose(() => {
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex size-12 touch-none select-none items-center justify-center rounded-cp border-0 bg-cp-fill-secondary text-cp-text shadow-cp-tertiary outline-none transition-[background-color,box-shadow,color,opacity,transform] duration-150 [-webkit-tap-highlight-color:transparent] hover:bg-cp-bg-text-active active:scale-95 active:bg-cp-control-item-bg-active focus-visible:ring-2 focus-visible:ring-cp-control-outline focus-visible:ring-offset-2 focus-visible:ring-offset-cp-bg-layout"
|
||||
class="inline-flex size-12 touch-none select-none items-center justify-center rounded-z border-0 bg-z-fill-secondary text-z-text shadow-z-tertiary outline-none transition-[background-color,box-shadow,color,opacity,transform] duration-150 [-webkit-tap-highlight-color:transparent] hover:bg-z-bg-text-active active:scale-95 active:bg-z-control-item-bg-active focus-visible:ring-2 focus-visible:ring-z-control-outline focus-visible:ring-offset-2 focus-visible:ring-offset-z-bg-layout"
|
||||
:class="
|
||||
isDragging
|
||||
? 'scale-95 cursor-grabbing shadow-[0_14px_28px_-20px_var(--cp-color-shadow)]'
|
||||
? 'scale-95 cursor-grabbing shadow-[0_14px_28px_-20px_var(--z-color-shadow)]'
|
||||
: 'cursor-grab'
|
||||
"
|
||||
:aria-label="t('navigation.sidebar.open')"
|
||||
|
||||
@@ -292,22 +292,22 @@ watch(
|
||||
:show-close="!restarting" :close-on-click-modal="!restarting" :close-on-press-escape="!restarting"
|
||||
>
|
||||
<template #icon>
|
||||
<ArrowUpCircle class="size-4.5 text-cp-success" />
|
||||
<ArrowUpCircle class="size-4.5 text-z-success" />
|
||||
</template>
|
||||
|
||||
<div class="grid gap-3.5">
|
||||
<section class="grid gap-4 rounded-cp-card bg-cp-fill-quaternary px-4 py-4">
|
||||
<section class="grid gap-4 rounded-z-card bg-z-fill-quaternary px-4 py-4">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div class="min-w-0">
|
||||
<p class="m-0 text-cp-xs leading-none font-heavy text-cp-text-quaternary">
|
||||
<p class="m-0 text-z-xs leading-none font-heavy text-z-text-quaternary">
|
||||
Codex Proxy RS
|
||||
</p>
|
||||
<p class="mt-2 mb-0 text-lg leading-none font-heavy text-cp-text">
|
||||
<p class="mt-2 mb-0 text-lg leading-none font-heavy text-z-text">
|
||||
{{ t('systemUpdate.application') }}
|
||||
</p>
|
||||
</div>
|
||||
<span
|
||||
class="inline-flex h-7 shrink-0 items-center gap-1.5 rounded-full px-2.5 text-cp-sm font-heavy"
|
||||
class="inline-flex h-7 shrink-0 items-center gap-1.5 rounded-full px-2.5 text-z-sm font-heavy"
|
||||
:class="presentation.status.badge"
|
||||
>
|
||||
<component
|
||||
@@ -323,10 +323,10 @@ watch(
|
||||
<div
|
||||
v-for="item in presentation.summaryItems"
|
||||
:key="item.key"
|
||||
class="min-w-0 rounded-cp bg-cp-bg-container px-3 py-2.5"
|
||||
class="min-w-0 rounded-z bg-z-bg-container px-3 py-2.5"
|
||||
>
|
||||
<div class="flex min-w-0 items-center justify-between gap-2">
|
||||
<p class="m-0 truncate text-cp-xs leading-none font-heavy text-cp-text-quaternary">
|
||||
<p class="m-0 truncate text-z-xs leading-none font-heavy text-z-text-quaternary">
|
||||
{{ item.label }}
|
||||
</p>
|
||||
<a
|
||||
@@ -335,14 +335,14 @@ watch(
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
:class="{ invisible: checking || changingChannel }"
|
||||
class="inline-flex shrink-0 items-center gap-1 text-cp-xs leading-none font-bold text-cp-link transition-colors hover:text-cp-link-hover"
|
||||
class="inline-flex shrink-0 items-center gap-1 text-z-xs leading-none font-bold text-z-link transition-colors hover:text-z-link-hover"
|
||||
>
|
||||
{{ t('systemUpdate.releasePage') }}
|
||||
<ExternalLink class="size-3" />
|
||||
</a>
|
||||
</div>
|
||||
<p
|
||||
class="mt-2 mb-0 h-[1em] truncate font-mono text-cp leading-none font-bold text-cp-text"
|
||||
class="mt-2 mb-0 h-[1em] truncate font-mono text-z leading-none font-bold text-z-text"
|
||||
:title="item.title || item.value"
|
||||
>
|
||||
<ZSkeleton
|
||||
@@ -357,12 +357,12 @@ watch(
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="updateInfo?.unsupportedReason" class="m-0 text-cp-sm text-cp-text-secondary">
|
||||
<p v-if="updateInfo?.unsupportedReason" class="m-0 text-z-sm text-z-text-secondary">
|
||||
{{ updateInfo.unsupportedReason }}
|
||||
</p>
|
||||
<p
|
||||
v-if="updateError || updateInfo?.warning"
|
||||
class="m-0 rounded-cp bg-cp-error-container px-3 py-2 text-cp-sm leading-normal font-bold text-cp-error-on-container"
|
||||
class="m-0 rounded-z bg-z-error-container px-3 py-2 text-z-sm leading-normal font-bold text-z-error-on-container"
|
||||
>
|
||||
{{ updateError || updateInfo?.warning }}
|
||||
</p>
|
||||
@@ -375,12 +375,12 @@ watch(
|
||||
{{ t('systemUpdate.status.lastFailed') }}
|
||||
</ZButton>
|
||||
</template>
|
||||
<div class="grid w-80 max-w-[calc(100vw-2rem)] gap-2 p-3 text-cp-sm">
|
||||
<div v-if="lastFailedOperation.targetVersion || lastFailedOperation.finishedAt" class="flex flex-wrap gap-x-3 gap-y-1 text-cp-xs text-cp-text-quaternary">
|
||||
<div class="grid w-80 max-w-[calc(100vw-2rem)] gap-2 p-3 text-z-sm">
|
||||
<div v-if="lastFailedOperation.targetVersion || lastFailedOperation.finishedAt" class="flex flex-wrap gap-x-3 gap-y-1 text-z-xs text-z-text-quaternary">
|
||||
<span v-if="lastFailedOperation.targetVersion">{{ t('systemUpdate.targetVersion', { version: lastFailedOperation.targetVersion }) }}</span>
|
||||
<span v-if="lastFailedOperation.finishedAt">{{ lastFailedOperation.finishedAtDisplay }}</span>
|
||||
</div>
|
||||
<p class="m-0 wrap-anywhere text-cp-text-secondary">
|
||||
<p class="m-0 wrap-anywhere text-z-text-secondary">
|
||||
{{ lastFailedOperation.error || lastFailedOperation.message || t('common.errors.operation') }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -389,13 +389,13 @@ watch(
|
||||
|
||||
<section
|
||||
v-if="hasReleaseNotes || releaseNotesLoading"
|
||||
class="grid gap-2 rounded-cp-card bg-cp-fill-quaternary px-4 py-3.5"
|
||||
class="grid gap-2 rounded-z-card bg-z-fill-quaternary px-4 py-3.5"
|
||||
>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<p class="m-0 text-cp font-heavy text-cp-text">
|
||||
<p class="m-0 text-z font-heavy text-z-text">
|
||||
{{ t('systemUpdate.releaseNotes') }}
|
||||
</p>
|
||||
<span class="font-mono text-cp-xs font-emphasis text-cp-text-quaternary">
|
||||
<span class="font-mono text-z-xs font-emphasis text-z-text-quaternary">
|
||||
<ZSkeleton v-if="releaseNotesLoading" shape="text" class="w-14" aria-hidden="true" />
|
||||
<template v-else>{{ presentation.releaseVersion }}</template>
|
||||
</span>
|
||||
@@ -420,22 +420,22 @@ watch(
|
||||
|
||||
<section
|
||||
v-if="showUpdateProgress"
|
||||
class="overflow-hidden rounded-cp-card bg-cp-fill-quaternary"
|
||||
class="overflow-hidden rounded-z-card bg-z-fill-quaternary"
|
||||
>
|
||||
<header class="flex items-center justify-between gap-3 px-4 pt-3.5 pb-2.5">
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<Terminal class="size-4 shrink-0 text-cp-success" />
|
||||
<p class="m-0 text-cp leading-none font-heavy text-cp-text">
|
||||
<Terminal class="size-4 shrink-0 text-z-success" />
|
||||
<p class="m-0 text-z leading-none font-heavy text-z-text">
|
||||
{{ t('systemUpdate.progress') }}
|
||||
</p>
|
||||
</div>
|
||||
<span
|
||||
class="inline-flex h-6 items-center gap-1.5 rounded-full bg-cp-fill-quaternary px-2 text-cp-xs leading-none font-bold text-cp-text-secondary"
|
||||
class="inline-flex h-6 items-center gap-1.5 rounded-full bg-z-fill-quaternary px-2 text-z-xs leading-none font-bold text-z-text-secondary"
|
||||
:title="updateStreamError || presentation.streamStatusLabel"
|
||||
>
|
||||
<i
|
||||
class="size-1.5 rounded-full"
|
||||
:class="updateStreaming ? 'bg-cp-success' : 'bg-cp-text-quaternary'"
|
||||
:class="updateStreaming ? 'bg-z-success' : 'bg-z-text-quaternary'"
|
||||
/>
|
||||
{{ presentation.streamStatusLabel }}
|
||||
</span>
|
||||
@@ -449,16 +449,16 @@ watch(
|
||||
<div
|
||||
v-for="log in updateLogRows"
|
||||
:key="log.id"
|
||||
class="grid grid-cols-[68px_14px_minmax(0,1fr)] items-start gap-2 rounded-cp bg-cp-bg-container px-3 py-2 font-mono text-cp-xs leading-[1.55]"
|
||||
class="grid grid-cols-[68px_14px_minmax(0,1fr)] items-start gap-2 rounded-z bg-z-bg-container px-3 py-2 font-mono text-z-xs leading-[1.55]"
|
||||
>
|
||||
<span class="tabular-nums text-cp-text-quaternary">{{ log.time }}</span>
|
||||
<span class="tabular-nums text-z-text-quaternary">{{ log.time }}</span>
|
||||
<Circle
|
||||
class="mt-1 size-2.5"
|
||||
:class="log.classes.marker"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<p class="m-0 min-w-0 wrap-break-word" :class="log.classes.text">
|
||||
<span v-if="log.step" class="mr-1 text-cp-text-quaternary">[{{ log.step }}]</span>
|
||||
<span v-if="log.step" class="mr-1 text-z-text-quaternary">[{{ log.step }}]</span>
|
||||
{{ log.message }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -485,12 +485,12 @@ watch(
|
||||
<CircleHelp class="size-4" />
|
||||
</ZButton>
|
||||
</template>
|
||||
<div class="grid w-72 gap-2 p-3 text-cp-sm text-cp-text-secondary">
|
||||
<div class="grid w-72 gap-2 p-3 text-z-sm text-z-text-secondary">
|
||||
<p v-for="channel in availableChannels" :key="channel" class="m-0">
|
||||
<strong class="font-heavy text-cp-text">{{ channelLabels[channel].label }}</strong>
|
||||
<strong class="font-heavy text-z-text">{{ channelLabels[channel].label }}</strong>
|
||||
· {{ channelLabels[channel].description }}
|
||||
</p>
|
||||
<p class="m-0 text-cp-text-quaternary">
|
||||
<p class="m-0 text-z-text-quaternary">
|
||||
{{ t('systemUpdate.channels.description') }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -545,20 +545,20 @@ watch(
|
||||
@confirm="handleConfirmRestart"
|
||||
>
|
||||
<div class="grid gap-4">
|
||||
<div v-if="restartPlan?.targetVersion" class="flex items-center justify-between gap-3 text-cp-sm">
|
||||
<span class="text-cp-text-secondary">{{ t('systemUpdate.versionAfterRestart') }}</span>
|
||||
<span class="font-mono font-emphasis text-cp-text">v{{ restartPlan.targetVersion }}</span>
|
||||
<div v-if="restartPlan?.targetVersion" class="flex items-center justify-between gap-3 text-z-sm">
|
||||
<span class="text-z-text-secondary">{{ t('systemUpdate.versionAfterRestart') }}</span>
|
||||
<span class="font-mono font-emphasis text-z-text">v{{ restartPlan.targetVersion }}</span>
|
||||
</div>
|
||||
<ul class="m-0 max-h-64 list-none overflow-y-auto p-0" :aria-label="t('systemUpdate.plugins.incompatible')">
|
||||
<li v-for="plugin in restartPlan?.incompatiblePlugins" :key="plugin.instanceId" class="flex items-start gap-3 py-3 first:pt-0 last:pb-0">
|
||||
<TriangleAlert class="mt-0.5 size-4 shrink-0 text-cp-warning" aria-hidden="true" />
|
||||
<TriangleAlert class="mt-0.5 size-4 shrink-0 text-z-warning" aria-hidden="true" />
|
||||
<div class="grid min-w-0 gap-1">
|
||||
<strong class="wrap-anywhere text-cp-sm font-heavy text-cp-text">{{ plugin.name }}</strong>
|
||||
<span class="wrap-anywhere text-cp-xs leading-relaxed font-normal text-cp-text-secondary">{{ plugin.reason }}</span>
|
||||
<strong class="wrap-anywhere text-z-sm font-heavy text-z-text">{{ plugin.name }}</strong>
|
||||
<span class="wrap-anywhere text-z-xs leading-relaxed font-normal text-z-text-secondary">{{ plugin.reason }}</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<p class="m-0 rounded-cp bg-cp-fill-quaternary px-3 py-2.5 text-cp-sm leading-relaxed font-normal text-cp-text-secondary">
|
||||
<p class="m-0 rounded-z bg-z-fill-quaternary px-3 py-2.5 text-z-sm leading-relaxed font-normal text-z-text-secondary">
|
||||
{{ t('systemUpdate.plugins.requestErrorHelp') }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -574,23 +574,23 @@ watch(
|
||||
@confirm="handleConfirmUpdate"
|
||||
>
|
||||
<div class="grid gap-3">
|
||||
<div class="grid gap-2 rounded-cp bg-cp-fill-quaternary p-3">
|
||||
<div class="grid gap-2 rounded-z bg-z-fill-quaternary p-3">
|
||||
<div
|
||||
v-for="item in presentation.confirmRows"
|
||||
:key="item.key"
|
||||
class="flex min-w-0 items-center justify-between gap-3"
|
||||
>
|
||||
<span class="text-cp-sm leading-none font-bold text-cp-text-quaternary">
|
||||
<span class="text-z-sm leading-none font-bold text-z-text-quaternary">
|
||||
{{ item.label }}
|
||||
</span>
|
||||
<span
|
||||
class="truncate font-mono text-cp leading-none font-heavy text-cp-text"
|
||||
class="truncate font-mono text-z leading-none font-heavy text-z-text"
|
||||
>
|
||||
{{ item.value }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="m-0 text-cp-sm leading-relaxed font-emphasis text-cp-text-quaternary">
|
||||
<p class="m-0 text-z-sm leading-relaxed font-emphasis text-z-text-quaternary">
|
||||
{{ t('systemUpdate.confirmation.description') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -38,13 +38,13 @@ export function resolveSystemUpdatePresentation(input: SystemUpdatePresentationI
|
||||
export function resolveSystemUpdateLogClasses(level: string) {
|
||||
switch (level) {
|
||||
case 'success':
|
||||
return { marker: 'text-cp-success', text: 'text-cp-success' }
|
||||
return { marker: 'text-z-success', text: 'text-z-success' }
|
||||
case 'warning':
|
||||
return { marker: 'text-cp-warning', text: 'text-cp-warning' }
|
||||
return { marker: 'text-z-warning', text: 'text-z-warning' }
|
||||
case 'error':
|
||||
return { marker: 'text-cp-error', text: 'text-cp-error' }
|
||||
return { marker: 'text-z-error', text: 'text-z-error' }
|
||||
default:
|
||||
return { marker: 'text-cp-info', text: 'text-cp-text' }
|
||||
return { marker: 'text-z-info', text: 'text-z-text' }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,16 +57,16 @@ function resolveStatus(input: SystemUpdatePresentationInput) {
|
||||
? t('systemUpdate.status.updating')
|
||||
: t('systemUpdate.status.checking'),
|
||||
icon: RefreshCw,
|
||||
badge: 'bg-cp-info-container text-cp-info-on-container',
|
||||
iconClass: 'text-cp-info',
|
||||
badge: 'bg-z-info-container text-z-info-on-container',
|
||||
iconClass: 'text-z-info',
|
||||
}
|
||||
}
|
||||
if (input.updateError || input.updateInfo?.warning) {
|
||||
return {
|
||||
label: t('common.status.error'),
|
||||
icon: XCircle,
|
||||
badge: 'bg-cp-error-container text-cp-error-on-container',
|
||||
iconClass: 'text-cp-error',
|
||||
badge: 'bg-z-error-container text-z-error-on-container',
|
||||
iconClass: 'text-z-error',
|
||||
}
|
||||
}
|
||||
if (input.needRestart || input.hasUpdate || input.updateInfo) {
|
||||
@@ -77,15 +77,15 @@ function resolveStatus(input: SystemUpdatePresentationInput) {
|
||||
? t('systemUpdate.status.available')
|
||||
: t('systemUpdate.status.upToDate'),
|
||||
icon: input.hasUpdate ? ArrowUpCircle : CheckCircle2,
|
||||
badge: 'bg-cp-success-container text-cp-success-on-container',
|
||||
iconClass: 'text-cp-success',
|
||||
badge: 'bg-z-success-container text-z-success-on-container',
|
||||
iconClass: 'text-z-success',
|
||||
}
|
||||
}
|
||||
return {
|
||||
label: t('systemUpdate.status.notChecked'),
|
||||
icon: CheckCircle2,
|
||||
badge: 'bg-cp-fill-tertiary text-cp-text-secondary',
|
||||
iconClass: 'text-cp-text-quaternary',
|
||||
badge: 'bg-z-fill-tertiary text-z-text-secondary',
|
||||
iconClass: 'text-z-text-quaternary',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ watch(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative flex h-dvh overflow-hidden bg-cp-bg-layout" :inert="mobileSidebarOpen">
|
||||
<div class="relative flex h-dvh overflow-hidden bg-z-bg-layout" :inert="mobileSidebarOpen">
|
||||
<AppSidebar
|
||||
:collapsed="sidebarCollapsed"
|
||||
@toggle="toggleSidebar"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @env browser
|
||||
|
||||
import type {
|
||||
ResolvedTheme,
|
||||
Theme,
|
||||
ThemeColorId,
|
||||
ThemeCustomization,
|
||||
ThemeMode,
|
||||
@@ -9,15 +9,15 @@ import type {
|
||||
|
||||
import { normalizeHexColor } from '@codex-proxy/ui'
|
||||
import {
|
||||
applyResolvedTheme,
|
||||
DEFAULT_CUSTOM_THEME_COLOR,
|
||||
DEFAULT_THEME_COLOR,
|
||||
DEFAULT_THEME_MODE,
|
||||
applyTheme,
|
||||
createTheme,
|
||||
defaultThemeColorId,
|
||||
defaultThemeMode,
|
||||
defaultThemePrimaryColor,
|
||||
isThemeColorId,
|
||||
isThemeMode,
|
||||
normalizeThemeCustomization,
|
||||
resolveTheme,
|
||||
resolveThemeName,
|
||||
resolveThemeScheme,
|
||||
} from '@codex-proxy/ui/theme'
|
||||
import { usePreferredDark, usePreferredReducedMotion, useTimeoutFn } from '@vueuse/core'
|
||||
|
||||
@@ -48,9 +48,9 @@ type ViewTransitionDocument = Document & {
|
||||
export const useThemeStore = defineStore(
|
||||
'theme',
|
||||
() => {
|
||||
const themeMode = shallowRef<ThemeMode>(DEFAULT_THEME_MODE)
|
||||
const themeColor = shallowRef<ThemeColorId>(DEFAULT_THEME_COLOR)
|
||||
const customThemeColor = shallowRef(DEFAULT_CUSTOM_THEME_COLOR)
|
||||
const themeMode = shallowRef<ThemeMode>(defaultThemeMode)
|
||||
const themeColor = shallowRef<ThemeColorId>(defaultThemeColorId)
|
||||
const customThemeColor = shallowRef(defaultThemePrimaryColor)
|
||||
const themeCustomization = ref<ThemeCustomization>({})
|
||||
const themeRevision = shallowRef(0)
|
||||
const themeTransitioning = shallowRef(false)
|
||||
@@ -60,14 +60,14 @@ export const useThemeStore = defineStore(
|
||||
let themeTransitionOrigin: ThemeTransitionOrigin | undefined
|
||||
let themeTransitionRequested = false
|
||||
|
||||
const effectiveTheme = computed(() => resolveThemeName(themeMode.value, preferredDark.value))
|
||||
const effectiveTheme = computed(() => resolveThemeScheme(themeMode.value, preferredDark.value))
|
||||
const resolvedTheme = computed(() =>
|
||||
resolveTheme(
|
||||
effectiveTheme.value,
|
||||
themeColor.value,
|
||||
customThemeColor.value,
|
||||
themeCustomization.value,
|
||||
),
|
||||
createTheme({
|
||||
scheme: effectiveTheme.value,
|
||||
colorId: themeColor.value,
|
||||
primaryColor: customThemeColor.value,
|
||||
customization: themeCustomization.value,
|
||||
}),
|
||||
)
|
||||
|
||||
const { start: startFallbackTransitionTimer, stop: stopFallbackTransitionTimer } = useTimeoutFn(
|
||||
@@ -79,23 +79,23 @@ export const useThemeStore = defineStore(
|
||||
{ immediate: false },
|
||||
)
|
||||
|
||||
watch(resolvedTheme, theme => applyTheme(theme))
|
||||
watch(resolvedTheme, theme => syncTheme(theme))
|
||||
|
||||
function initializeTheme(): void {
|
||||
normalizePersistedTheme()
|
||||
applyTheme(resolvedTheme.value)
|
||||
syncTheme(resolvedTheme.value)
|
||||
}
|
||||
|
||||
function normalizePersistedTheme(): void {
|
||||
if (!isThemeMode(themeMode.value))
|
||||
themeMode.value = DEFAULT_THEME_MODE
|
||||
themeMode.value = defaultThemeMode
|
||||
if (!isThemeColorId(themeColor.value))
|
||||
themeColor.value = DEFAULT_THEME_COLOR
|
||||
customThemeColor.value = normalizeHexColor(customThemeColor.value) ?? DEFAULT_CUSTOM_THEME_COLOR
|
||||
themeColor.value = defaultThemeColorId
|
||||
customThemeColor.value = normalizeHexColor(customThemeColor.value) ?? defaultThemePrimaryColor
|
||||
themeCustomization.value = normalizeThemeCustomization(themeCustomization.value)
|
||||
}
|
||||
|
||||
function applyTheme(theme: ResolvedTheme): void {
|
||||
function syncTheme(theme: Theme): void {
|
||||
if (typeof document === 'undefined')
|
||||
return
|
||||
|
||||
@@ -120,16 +120,16 @@ export const useThemeStore = defineStore(
|
||||
clearThemeTransitionRequest()
|
||||
}
|
||||
|
||||
function commitTheme(theme: ResolvedTheme): void {
|
||||
applyResolvedTheme(document.documentElement, theme)
|
||||
function commitTheme(theme: Theme): void {
|
||||
applyTheme(document.documentElement, theme)
|
||||
appliedThemeSignature = themeSignature(theme)
|
||||
themeRevision.value += 1
|
||||
}
|
||||
|
||||
function runThemeTransition(theme: ResolvedTheme): void {
|
||||
function runThemeTransition(theme: Theme): void {
|
||||
const transitionDocument = document as ViewTransitionDocument
|
||||
const shrinkingDarkLayer
|
||||
= document.documentElement.dataset.theme === 'dark' && theme.name === 'light'
|
||||
= document.documentElement.dataset.theme === 'dark' && theme.scheme === 'light'
|
||||
|
||||
if (!transitionDocument.startViewTransition) {
|
||||
runFallbackThemeTransition(theme)
|
||||
@@ -192,7 +192,7 @@ export const useThemeStore = defineStore(
|
||||
.catch(() => undefined)
|
||||
}
|
||||
|
||||
function runFallbackThemeTransition(theme: ResolvedTheme): void {
|
||||
function runFallbackThemeTransition(theme: Theme): void {
|
||||
themeTransitioning.value = true
|
||||
stopFallbackTransitionTimer()
|
||||
document.documentElement.classList.add('theme-fallback-transition')
|
||||
@@ -250,18 +250,18 @@ export const useThemeStore = defineStore(
|
||||
|
||||
function resetTheme(event?: MouseEvent): void {
|
||||
const unchanged
|
||||
= themeMode.value === DEFAULT_THEME_MODE
|
||||
&& themeColor.value === DEFAULT_THEME_COLOR
|
||||
&& customThemeColor.value === DEFAULT_CUSTOM_THEME_COLOR
|
||||
= themeMode.value === defaultThemeMode
|
||||
&& themeColor.value === defaultThemeColorId
|
||||
&& customThemeColor.value === defaultThemePrimaryColor
|
||||
&& Object.keys(themeCustomization.value).length === 0
|
||||
if (unchanged) {
|
||||
clearThemeTransitionRequest()
|
||||
return
|
||||
}
|
||||
requestThemeTransition(event)
|
||||
themeMode.value = DEFAULT_THEME_MODE
|
||||
themeColor.value = DEFAULT_THEME_COLOR
|
||||
customThemeColor.value = DEFAULT_CUSTOM_THEME_COLOR
|
||||
themeMode.value = defaultThemeMode
|
||||
themeColor.value = defaultThemeColorId
|
||||
customThemeColor.value = defaultThemePrimaryColor
|
||||
themeCustomization.value = {}
|
||||
}
|
||||
|
||||
@@ -316,6 +316,6 @@ export const useThemeStore = defineStore(
|
||||
},
|
||||
)
|
||||
|
||||
function themeSignature(theme: ResolvedTheme): string {
|
||||
return `${theme.name}:${theme.colorId}:${theme.seed}:${JSON.stringify(theme.tokens)}`
|
||||
function themeSignature(theme: Theme): string {
|
||||
return `${theme.scheme}:${theme.colorId}:${theme.seed}:${JSON.stringify(theme.tokens)}`
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ type PresetVisualTone = 'blue' | 'cyan' | 'green' | 'orange'
|
||||
|
||||
const presetVisualTones: readonly PresetVisualTone[] = ['blue', 'green', 'orange', 'cyan']
|
||||
const presetVisualToneClasses = {
|
||||
blue: 'bg-cp-blue-container-strong text-cp-blue-on-container',
|
||||
cyan: 'bg-cp-cyan-container-strong text-cp-cyan-on-container',
|
||||
green: 'bg-cp-green-container-strong text-cp-green-on-container',
|
||||
orange: 'bg-cp-orange-container-strong text-cp-orange-on-container',
|
||||
blue: 'bg-z-blue-container-strong text-z-blue-on-container',
|
||||
cyan: 'bg-z-cyan-container-strong text-z-cyan-on-container',
|
||||
green: 'bg-z-green-container-strong text-z-green-on-container',
|
||||
orange: 'bg-z-orange-container-strong text-z-orange-on-container',
|
||||
} as const satisfies Record<PresetVisualTone, string>
|
||||
|
||||
export function stableVisualIndex(value: unknown, length: number): number {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { RequestLocation } from '@/api'
|
||||
import { t } from '@/i18n'
|
||||
|
||||
export function normalizeRequestLocation(location: RequestLocation): RequestLocation {
|
||||
return {
|
||||
@@ -9,9 +8,3 @@ export function normalizeRequestLocation(location: RequestLocation): RequestLoca
|
||||
timezone: location.timezone.trim(),
|
||||
}
|
||||
}
|
||||
|
||||
export function requestLocationError(location: RequestLocation): string {
|
||||
if (!/^[A-Z]{2}$/.test(location.country) || !location.region || !location.city || !location.timezone)
|
||||
return t('common.location.required')
|
||||
return ''
|
||||
}
|
||||
|
||||
@@ -17,21 +17,19 @@ const transportOptions = [
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid gap-4">
|
||||
<ZFormItem v-if="!editing" :label="t('accounts.credentials.accountName')" required>
|
||||
<ZInput v-model="form.name" :placeholder="t('accounts.credentials.enterUpstreamName')" :disabled="disabled" />
|
||||
</ZFormItem>
|
||||
<ZFormItem :label="t('common.fields.baseUrl')" required>
|
||||
<template #extra>
|
||||
<span class="text-cp-xs text-cp-text-quaternary" :title="t('accounts.credentials.baseUrlHelp')">{{ t('accounts.credentials.apiPrefix') }}</span>
|
||||
</template>
|
||||
<ZInput v-model="form.base_url" type="url" placeholder="https://api.example.com/v1" :disabled="disabled" autocomplete="off" />
|
||||
</ZFormItem>
|
||||
<ZFormItem :label="t('common.fields.apiKey')" :required="!editing">
|
||||
<ZInput v-model="form.apiKey" type="password" :placeholder="editing ? t('accounts.credentials.apiKeyPlaceholder') : t('accounts.credentials.enterUpstreamKey')" :disabled="disabled" autocomplete="new-password" />
|
||||
</ZFormItem>
|
||||
<ZFormItem :label="t('accounts.credentials.transportLabel')">
|
||||
<ZSegmented v-model="form.transport" :aria-label="t('accounts.credentials.transportLabel')" :options="transportOptions" :disabled="disabled" :title="t('accounts.credentials.webSocketHelp')" />
|
||||
</ZFormItem>
|
||||
</div>
|
||||
<ZFormItem v-if="!editing" prop="apiKey.name" :show-message="false" :label="t('accounts.credentials.accountName')" required>
|
||||
<ZInput v-model="form.name" :placeholder="t('accounts.credentials.enterUpstreamName')" :disabled="disabled" />
|
||||
</ZFormItem>
|
||||
<ZFormItem prop="apiKey.base_url" :show-message="Boolean(form.base_url.trim())" :label="t('common.fields.baseUrl')" required>
|
||||
<template #extra>
|
||||
<span class="text-z-xs text-z-text-quaternary" :title="t('accounts.credentials.baseUrlHelp')">{{ t('accounts.credentials.apiPrefix') }}</span>
|
||||
</template>
|
||||
<ZInput v-model="form.base_url" type="url" placeholder="https://api.example.com/v1" :disabled="disabled" autocomplete="off" />
|
||||
</ZFormItem>
|
||||
<ZFormItem prop="apiKey.apiKey" :show-message="Boolean(form.apiKey)" :label="t('common.fields.apiKey')" :required="!editing">
|
||||
<ZInput v-model="form.apiKey" type="password" :placeholder="editing ? t('accounts.credentials.apiKeyPlaceholder') : t('accounts.credentials.enterUpstreamKey')" :disabled="disabled" autocomplete="new-password" />
|
||||
</ZFormItem>
|
||||
<ZFormItem prop="apiKey.transport" :show-message="false" :label="t('accounts.credentials.transportLabel')">
|
||||
<ZSegmented v-model="form.transport" :aria-label="t('accounts.credentials.transportLabel')" :options="transportOptions" :disabled="disabled" :title="t('accounts.credentials.webSocketHelp')" />
|
||||
</ZFormItem>
|
||||
</template>
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import type { AccountGroupRef, AccountModelAccess } from '@/api'
|
||||
|
||||
import { ZButton, ZDialog, ZFormItem, ZSegmented } from '@codex-proxy/ui'
|
||||
import { ZButton, ZDialog, ZForm, ZFormItem, ZSegmented } from '@codex-proxy/ui'
|
||||
|
||||
import { computed } from 'vue'
|
||||
import { computed, useTemplateRef, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAccountSettingsFormRules } from '../composables/useAccountSettingsFormRules'
|
||||
import AccountSettingsFields from './AccountSettingsFields.vue'
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
selectedCount: number
|
||||
catalogAccountId?: string
|
||||
groups: AccountGroupRef[]
|
||||
groupsLoading: boolean
|
||||
saving: boolean
|
||||
editedFields: ReadonlySet<string>
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -29,6 +31,24 @@ const weight = defineModel<string>('weight', { required: true })
|
||||
const proxyMode = defineModel<string>('proxyMode', { required: true })
|
||||
const proxyId = defineModel<string>('proxyId', { required: true })
|
||||
const selectedGroupIds = defineModel<string[]>('selectedGroupIds', { required: true })
|
||||
const formRef = useTemplateRef<InstanceType<typeof ZForm>>('formElement')
|
||||
const form = computed(() => ({
|
||||
concurrencyLimit: concurrencyLimit.value,
|
||||
weight: weight.value,
|
||||
modelAccess: modelAccess.value,
|
||||
proxyMode: proxyMode.value,
|
||||
proxyId: proxyId.value,
|
||||
}))
|
||||
const rules = useAccountSettingsFormRules(() => form.value, () => props.editedFields)
|
||||
|
||||
watch(open, () => formRef.value?.clearValidate())
|
||||
|
||||
async function requestSave() {
|
||||
if (props.saving || !props.selectedCount || props.groupsLoading || !await formRef.value?.validate().catch(() => false))
|
||||
return
|
||||
emit('save')
|
||||
}
|
||||
|
||||
const schedulingModes = computed(() => [
|
||||
{
|
||||
value: 'preserve',
|
||||
@@ -63,7 +83,7 @@ const schedulingMode = computed({
|
||||
width="48rem"
|
||||
:show-close="!saving" :close-on-click-modal="!saving" :close-on-press-escape="!saving"
|
||||
>
|
||||
<div class="grid gap-5">
|
||||
<ZForm ref="formElement" :model="form" :rules="rules" scroll-to-error focus-on-error novalidate @submit="requestSave">
|
||||
<AccountSettingsFields
|
||||
v-model:concurrency-limit="concurrencyLimit"
|
||||
v-model:weight="weight"
|
||||
@@ -72,6 +92,7 @@ const schedulingMode = computed({
|
||||
v-model:proxy-mode="proxyMode"
|
||||
v-model:proxy-id="proxyId"
|
||||
preserve-model-access
|
||||
|
||||
:account-id="catalogAccountId"
|
||||
:groups="groups"
|
||||
:groups-loading="groupsLoading"
|
||||
@@ -89,7 +110,7 @@ const schedulingMode = computed({
|
||||
</ZFormItem>
|
||||
</template>
|
||||
</AccountSettingsFields>
|
||||
</div>
|
||||
</ZForm>
|
||||
|
||||
<template #footer>
|
||||
<ZButton :disabled="saving" @click="open = false">
|
||||
@@ -99,7 +120,7 @@ const schedulingMode = computed({
|
||||
type="primary"
|
||||
:loading="saving"
|
||||
:disabled="selectedCount === 0 || groupsLoading"
|
||||
@click="emit('save')"
|
||||
@click="requestSave"
|
||||
>
|
||||
{{ t('common.actions.save') }}
|
||||
</ZButton>
|
||||
|
||||
@@ -25,7 +25,7 @@ const label = computed(() => t('accounts.capacity.concurrentRequestsLimit', { va
|
||||
variant="text" size="small" :type="full ? 'warning' : 'default'"
|
||||
:aria-label="t('accounts.capacity.viewConcurrencyDetails', { label })"
|
||||
:title="t('accounts.capacity.viewDetails', { label })"
|
||||
class="relative top-0.75 -m-0.5 h-auto! px-0.5! py-0.5! whitespace-nowrap font-mono text-[9px]! font-normal! tabular-nums hover:bg-cp-bg-text-hover!"
|
||||
class="relative top-0.75 -m-0.5 h-auto! px-0.5! py-0.5! whitespace-nowrap font-mono text-[9px]! font-normal! tabular-nums hover:bg-z-bg-text-hover!"
|
||||
@click.stop="$emit('showDetails')"
|
||||
>
|
||||
<span aria-hidden="true" class="inline-flex items-center gap-1">
|
||||
|
||||
@@ -53,27 +53,27 @@ const columns = computed(() => (defineTableColumns<AccountConcurrencyEntry>([
|
||||
|
||||
<template>
|
||||
<ZTable
|
||||
class="h-[min(60dvh,20rem)]! min-h-0 [--cp-table-row-height:3rem]"
|
||||
class="h-[min(60dvh,20rem)]! min-h-0 [--z-table-row-height:3rem]"
|
||||
:columns="columns" :data="entries" row-key="leaseId"
|
||||
:loading="loading" :empty-text="emptyText"
|
||||
>
|
||||
<template #key="{ row }">
|
||||
<span
|
||||
class="block truncate font-emphasis text-cp-text"
|
||||
class="block truncate font-emphasis text-z-text"
|
||||
:title="[row.clientKeyName, row.clientKeyId].filter(Boolean).join('\n') || undefined"
|
||||
>
|
||||
{{ row.clientKeyName ?? row.clientKeyId ?? '—' }}
|
||||
</span>
|
||||
</template>
|
||||
<template #model="{ displayValue }">
|
||||
<span class="text-cp font-normal text-cp-text-secondary" :title="String(displayValue)">{{ displayValue }}</span>
|
||||
<span class="text-z font-normal text-z-text-secondary" :title="String(displayValue)">{{ displayValue }}</span>
|
||||
</template>
|
||||
<template #startedAtDisplay="{ displayValue }">
|
||||
<span class="text-cp">{{ displayValue }}</span>
|
||||
<span class="text-z">{{ displayValue }}</span>
|
||||
</template>
|
||||
<template #clientIp="{ row }">
|
||||
<UsageClientIpCell v-if="row.clientIp" :record="row" class="text-cp!" />
|
||||
<span v-else class="text-cp-text-quaternary">—</span>
|
||||
<UsageClientIpCell v-if="row.clientIp" :record="row" class="text-z!" />
|
||||
<span v-else class="text-z-text-quaternary">—</span>
|
||||
</template>
|
||||
</ZTable>
|
||||
</template>
|
||||
|
||||
@@ -37,12 +37,12 @@ const selectedModel = defineModel<string>('selectedModel', { required: true })
|
||||
|
||||
function connectionLogClass(tone: string) {
|
||||
if (tone === 'success')
|
||||
return 'text-cp-success-text'
|
||||
return 'text-z-success-text'
|
||||
if (tone === 'danger')
|
||||
return 'text-cp-error-text'
|
||||
return 'text-z-error-text'
|
||||
if (tone === 'info')
|
||||
return 'text-cp-info-text'
|
||||
return 'text-cp-text-secondary'
|
||||
return 'text-z-info-text'
|
||||
return 'text-z-text-secondary'
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -56,7 +56,7 @@ function connectionLogClass(tone: string) {
|
||||
>
|
||||
<div v-if="account" class="flex flex-col gap-4">
|
||||
<section
|
||||
class="flex items-center justify-between gap-4 rounded-cp-card bg-cp-fill-quaternary px-4 py-3"
|
||||
class="flex items-center justify-between gap-4 rounded-z-card bg-z-fill-quaternary px-4 py-3"
|
||||
>
|
||||
<AccountIdentityCell :account="account" size="lg" show-plan show-notes />
|
||||
<AccountStatusBadge
|
||||
@@ -73,10 +73,10 @@ function connectionLogClass(tone: string) {
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section class="rounded-cp-card bg-cp-fill-quaternary px-4 py-3">
|
||||
<section class="rounded-z-card bg-z-fill-quaternary px-4 py-3">
|
||||
<div class="grid gap-2">
|
||||
<div class="flex min-h-8 items-center justify-between gap-3">
|
||||
<span class="text-cp-sm font-heavy text-cp-text-quaternary">
|
||||
<span class="text-z-sm font-heavy text-z-text-quaternary">
|
||||
{{ t('accounts.connectionTest.model') }}
|
||||
</span>
|
||||
<ZIconButton
|
||||
@@ -103,7 +103,7 @@ function connectionLogClass(tone: string) {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rounded-cp-card bg-cp-fill-quaternary p-4">
|
||||
<section class="rounded-z-card bg-z-fill-quaternary p-4">
|
||||
<div class="flex min-w-0 items-start gap-3">
|
||||
<span
|
||||
class="inline-flex size-10 shrink-0 items-center justify-center rounded-lg"
|
||||
@@ -116,11 +116,11 @@ function connectionLogClass(tone: string) {
|
||||
/>
|
||||
</span>
|
||||
<div class="min-w-0">
|
||||
<p class="m-0 text-[16px] font-heavy text-cp-text">
|
||||
<p class="m-0 text-[16px] font-heavy text-z-text">
|
||||
{{ statusView.label }}
|
||||
</p>
|
||||
<p
|
||||
class="mt-1.5 mb-0 text-cp leading-normal font-emphasis text-cp-text-secondary"
|
||||
class="mt-1.5 mb-0 text-z leading-normal font-emphasis text-z-text-secondary"
|
||||
>
|
||||
{{ statusView.description }}
|
||||
</p>
|
||||
@@ -128,59 +128,59 @@ function connectionLogClass(tone: string) {
|
||||
</div>
|
||||
|
||||
<div class="mt-4 grid gap-3 sm:grid-cols-3">
|
||||
<div class="rounded-lg bg-cp-bg-container px-3 py-2.5">
|
||||
<p class="m-0 text-cp-xs font-heavy text-cp-text-quaternary">
|
||||
<div class="rounded-lg bg-z-bg-container px-3 py-2.5">
|
||||
<p class="m-0 text-z-xs font-heavy text-z-text-quaternary">
|
||||
{{ t('accounts.concurrency.startTime') }}
|
||||
</p>
|
||||
<p class="mt-1.5 mb-0 font-mono text-cp-sm font-emphasis text-cp-text">
|
||||
<p class="mt-1.5 mb-0 font-mono text-z-sm font-emphasis text-z-text">
|
||||
{{ startedAt || '-' }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-lg bg-cp-bg-container px-3 py-2.5">
|
||||
<p class="m-0 text-cp-xs font-heavy text-cp-text-quaternary">
|
||||
<div class="rounded-lg bg-z-bg-container px-3 py-2.5">
|
||||
<p class="m-0 text-z-xs font-heavy text-z-text-quaternary">
|
||||
{{ t('accounts.connectionTest.completedAt') }}
|
||||
</p>
|
||||
<p class="mt-1.5 mb-0 font-mono text-cp-sm font-emphasis text-cp-text">
|
||||
<p class="mt-1.5 mb-0 font-mono text-z-sm font-emphasis text-z-text">
|
||||
{{ finishedAt || '-' }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-lg bg-cp-bg-container px-3 py-2.5">
|
||||
<p class="m-0 text-cp-xs font-heavy text-cp-text-quaternary">
|
||||
<div class="rounded-lg bg-z-bg-container px-3 py-2.5">
|
||||
<p class="m-0 text-z-xs font-heavy text-z-text-quaternary">
|
||||
{{ t('accounts.connectionTest.duration') }}
|
||||
</p>
|
||||
<p class="mt-1.5 mb-0 font-mono text-cp-sm font-emphasis whitespace-nowrap text-cp-text">
|
||||
<p class="mt-1.5 mb-0 font-mono text-z-sm font-emphasis whitespace-nowrap text-z-text">
|
||||
{{ durationMs !== null ? `${durationMs} ms` : '-' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 rounded-lg bg-cp-bg-container px-3 py-2.5">
|
||||
<p class="m-0 text-cp-xs font-heavy text-cp-text-quaternary">
|
||||
<div class="mt-3 rounded-lg bg-z-bg-container px-3 py-2.5">
|
||||
<p class="m-0 text-z-xs font-heavy text-z-text-quaternary">
|
||||
{{ t('accounts.connectionTest.model') }}
|
||||
</p>
|
||||
<p
|
||||
class="mt-1.5 mb-0 truncate font-mono text-cp-sm font-emphasis text-cp-text"
|
||||
class="mt-1.5 mb-0 truncate font-mono text-z-sm font-emphasis text-z-text"
|
||||
:title="model || '-'"
|
||||
>
|
||||
{{ model || '-' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 rounded-lg bg-cp-bg-container px-3 py-2.5">
|
||||
<p class="m-0 text-cp-xs font-heavy text-cp-text-quaternary">
|
||||
<div class="mt-3 rounded-lg bg-z-bg-container px-3 py-2.5">
|
||||
<p class="m-0 text-z-xs font-heavy text-z-text-quaternary">
|
||||
{{ t('accounts.connectionTest.events.title') }}
|
||||
</p>
|
||||
<div class="pt-2">
|
||||
<div v-if="logs.length === 0" class="text-cp-sm font-emphasis text-cp-text-quaternary">
|
||||
<div v-if="logs.length === 0" class="text-z-sm font-emphasis text-z-text-quaternary">
|
||||
-
|
||||
</div>
|
||||
<div v-else class="flex flex-col gap-1.5">
|
||||
<div
|
||||
v-for="item in logs"
|
||||
:key="item.key"
|
||||
class="grid grid-cols-[54px_minmax(0,1fr)] gap-2 text-cp-sm leading-[1.45] font-emphasis"
|
||||
class="grid grid-cols-[54px_minmax(0,1fr)] gap-2 text-z-sm leading-[1.45] font-emphasis"
|
||||
>
|
||||
<span class="font-mono text-cp-text-quaternary">{{ item.time }}</span>
|
||||
<span class="font-mono text-z-text-quaternary">{{ item.time }}</span>
|
||||
<div class="min-w-0">
|
||||
<p
|
||||
class="m-0 wrap-break-word"
|
||||
@@ -188,15 +188,15 @@ function connectionLogClass(tone: string) {
|
||||
>
|
||||
{{ item.text }}
|
||||
</p>
|
||||
<div v-if="item.detail" class="mt-2 rounded-lg bg-cp-fill-quaternary px-3 py-2">
|
||||
<div v-if="item.detail" class="mt-2 rounded-lg bg-z-fill-quaternary px-3 py-2">
|
||||
<p
|
||||
v-if="item.tone === 'danger'"
|
||||
class="mt-0 mb-2 text-cp-xs font-heavy text-cp-text-quaternary"
|
||||
class="mt-0 mb-2 text-z-xs font-heavy text-z-text-quaternary"
|
||||
>
|
||||
{{ t('accounts.connectionTest.diagnostics') }}
|
||||
</p>
|
||||
<pre
|
||||
class="m-0 whitespace-pre-wrap wrap-break-word font-mono text-cp-xs leading-[1.6] font-emphasis text-cp-text"
|
||||
class="m-0 whitespace-pre-wrap wrap-break-word font-mono text-z-xs leading-[1.6] font-emphasis text-z-text"
|
||||
v-text="item.detail"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormRules } from '@codex-proxy/ui'
|
||||
import type { ApiKeyAccountForm } from '../utils/upstreamApiKey'
|
||||
|
||||
import type { Account, AccountGroupRef, AccountModelAccess } from '@/api'
|
||||
|
||||
import { ZButton, ZDialog, ZFormItem, ZInput, ZSegmented, ZTextarea } from '@codex-proxy/ui'
|
||||
import { ZButton, ZDialog, ZForm, ZFormItem, ZInput, ZSegmented, ZTextarea } from '@codex-proxy/ui'
|
||||
|
||||
import { computed, useTemplateRef, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import AccountPlanBadge from '@/components/account/AccountPlanBadge.vue'
|
||||
import ProviderIconGroup from '@/components/ProviderIconGroup.vue'
|
||||
import { useAccountApiKeyFormRules } from '../composables/useAccountApiKeyFormRules'
|
||||
import { useAccountSettingsFormRules } from '../composables/useAccountSettingsFormRules'
|
||||
import { accountNameUpdate } from '../utils/accountName'
|
||||
import { isOpenAiApiKeyAccount, isOpenAiOAuthAccount } from '../utils/upstreamApiKey'
|
||||
import AccountApiKeyFields from './AccountApiKeyFields.vue'
|
||||
import AccountIdentityCell from './AccountIdentityCell.vue'
|
||||
import AccountSettingsFields from './AccountSettingsFields.vue'
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
account: Account | null
|
||||
groups: AccountGroupRef[]
|
||||
groupsLoading: boolean
|
||||
@@ -44,6 +49,41 @@ const weight = defineModel<string>('weight', { required: true })
|
||||
const proxyMode = defineModel<string>('proxyMode', { required: true })
|
||||
const proxyId = defineModel<string>('proxyId', { required: true })
|
||||
const selectedGroupIds = defineModel<string[]>('selectedGroupIds', { required: true })
|
||||
const formRef = useTemplateRef<InstanceType<typeof ZForm>>('formElement')
|
||||
const form = computed(() => ({
|
||||
name: name.value,
|
||||
apiKey: apiKey.value,
|
||||
concurrencyLimit: concurrencyLimit.value,
|
||||
weight: weight.value,
|
||||
modelAccess: modelAccess.value,
|
||||
proxyMode: proxyMode.value,
|
||||
proxyId: proxyId.value,
|
||||
}))
|
||||
const settingsRules = useAccountSettingsFormRules(() => form.value)
|
||||
const apiKeyRules = useAccountApiKeyFormRules(true)
|
||||
const rules = computed<FormRules>(() => ({
|
||||
...settingsRules.value,
|
||||
...apiKeyRules.value,
|
||||
name: {
|
||||
trigger: ['blur', 'change'],
|
||||
validator: () => {
|
||||
const updatedName = accountNameUpdate(name.value, props.account?.name ?? '')
|
||||
return updatedName === undefined
|
||||
|| (new TextEncoder().encode(updatedName).length <= 512 && !/\p{Cc}/u.test(updatedName))
|
||||
},
|
||||
message: t('accounts.editor.nameInvalid'),
|
||||
},
|
||||
}))
|
||||
const saveDisabled = computed(() => !props.account || props.groupsLoading
|
||||
|| (isOpenAiApiKeyAccount(props.account) && !props.configurationReady))
|
||||
|
||||
watch(open, () => formRef.value?.clearValidate())
|
||||
|
||||
async function requestSave() {
|
||||
if (props.saving || saveDisabled.value || !await formRef.value?.validate().catch(() => false))
|
||||
return
|
||||
emit('save')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -54,9 +94,9 @@ const selectedGroupIds = defineModel<string[]>('selectedGroupIds', { required: t
|
||||
:show-close="!saving" :close-on-click-modal="!saving" :close-on-press-escape="!saving"
|
||||
@closed="emit('afterLeave')"
|
||||
>
|
||||
<div v-if="account" class="grid min-w-0 grid-cols-1 gap-5">
|
||||
<ZForm v-if="account" ref="formElement" :model="form" :rules="rules" scroll-to-error focus-on-error novalidate @submit="requestSave">
|
||||
<div
|
||||
class="flex flex-wrap items-center justify-between gap-4 rounded-cp bg-cp-fill-quaternary px-4 py-3.5"
|
||||
class="flex flex-wrap items-center justify-between gap-4 rounded-z bg-z-fill-quaternary px-4 py-3.5"
|
||||
>
|
||||
<AccountIdentityCell
|
||||
class="min-w-0 flex-1"
|
||||
@@ -96,7 +136,7 @@ const selectedGroupIds = defineModel<string[]>('selectedGroupIds', { required: t
|
||||
:account-id="account.id"
|
||||
/>
|
||||
|
||||
<ZFormItem :label="t('accounts.credentials.accountName')">
|
||||
<ZFormItem prop="name" :label="t('accounts.credentials.accountName')">
|
||||
<ZInput
|
||||
v-model="name"
|
||||
:aria-label="t('accounts.credentials.accountName')"
|
||||
@@ -114,7 +154,7 @@ const selectedGroupIds = defineModel<string[]>('selectedGroupIds', { required: t
|
||||
:disabled="saving"
|
||||
/>
|
||||
</ZFormItem>
|
||||
</div>
|
||||
</ZForm>
|
||||
|
||||
<template #footer>
|
||||
<ZButton :disabled="saving" @click="open = false">
|
||||
@@ -123,8 +163,8 @@ const selectedGroupIds = defineModel<string[]>('selectedGroupIds', { required: t
|
||||
<ZButton
|
||||
type="primary"
|
||||
:loading="saving"
|
||||
:disabled="!account || groupsLoading || (isOpenAiApiKeyAccount(account) && !configurationReady)"
|
||||
@click="emit('save')"
|
||||
:disabled="saveDisabled"
|
||||
@click="requestSave"
|
||||
>
|
||||
{{ t('common.actions.save') }}
|
||||
</ZButton>
|
||||
|
||||
@@ -68,7 +68,7 @@ const groupOptions = computed(() => [
|
||||
class="col-span-2 min-w-0 sm:col-span-3 xl:w-80 xl:flex-none"
|
||||
>
|
||||
<template #prefix>
|
||||
<Search class="size-4.5 text-cp-text-tertiary" />
|
||||
<Search class="size-4.5 text-z-text-tertiary" />
|
||||
</template>
|
||||
</ZInput>
|
||||
|
||||
@@ -112,7 +112,7 @@ const groupOptions = computed(() => [
|
||||
<div class="w-32 p-1.5">
|
||||
<ZMenuItem @click="(close(), emit('editSelected'))">
|
||||
<template #icon>
|
||||
<Pencil class="size-3.5 text-cp-text-quaternary" />
|
||||
<Pencil class="size-3.5 text-z-text-quaternary" />
|
||||
</template>
|
||||
{{ t('accounts.filters.editSelected', { count: selectedCount }) }}
|
||||
</ZMenuItem>
|
||||
@@ -124,7 +124,7 @@ const groupOptions = computed(() => [
|
||||
@click="(close(), emit('exportSelected'))"
|
||||
>
|
||||
<template #icon>
|
||||
<Download class="size-3.5 text-cp-text-quaternary" />
|
||||
<Download class="size-3.5 text-z-text-quaternary" />
|
||||
</template>
|
||||
{{ t('accounts.filters.exportSelected', { count: selectedCount }) }}
|
||||
</ZMenuItem>
|
||||
@@ -143,7 +143,7 @@ const groupOptions = computed(() => [
|
||||
</ZPopover>
|
||||
<ZButton v-if="hasImportTasks" class="whitespace-nowrap" @click="emit('importTasks')">
|
||||
<ListTodo class="size-4" />
|
||||
{{ t('accounts.filters.importTasks') }} <span v-if="activeImportCount" class="font-mono text-cp-link">{{ activeImportCount }}</span>
|
||||
{{ t('accounts.filters.importTasks') }} <span v-if="activeImportCount" class="font-mono text-z-link">{{ activeImportCount }}</span>
|
||||
</ZButton>
|
||||
<slot name="actions" />
|
||||
<ZButton type="primary" class="whitespace-nowrap" @click="emit('create')">
|
||||
|
||||
@@ -44,8 +44,8 @@ const initial = computed(() => displayTitle.value.slice(0, 1).toUpperCase())
|
||||
|
||||
const avatarSizeClass = computed(() =>
|
||||
props.size === 'lg'
|
||||
? 'size-10 text-cp-xl'
|
||||
: 'size-9 text-cp',
|
||||
? 'size-10 text-z-xl'
|
||||
: 'size-9 text-z',
|
||||
)
|
||||
|
||||
const metaGapClass = computed(() => props.metaSize === 'xs'
|
||||
@@ -68,7 +68,7 @@ const avatarToneClass = computed(() => {
|
||||
</span>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<span class="min-w-0 truncate text-cp font-heavy text-cp-text" :title="identityTitle">
|
||||
<span class="min-w-0 truncate text-z font-heavy text-z-text" :title="identityTitle">
|
||||
{{ displayTitle }}
|
||||
</span>
|
||||
<span
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { AccountModelAccess } from '@/api'
|
||||
import { ZCheckbox, ZEmpty, ZFormItem, ZIconButton, ZInput, ZPopover, ZScrollbar, ZSegmented } from '@codex-proxy/ui'
|
||||
|
||||
import { CircleAlert, RefreshCw, Search, TriangleAlert } from '@lucide/vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { computed, ref, useTemplateRef, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { getAccountModels, refreshAccountModels } from '@/api'
|
||||
import { useRequestState } from '@/composables/useRequestState'
|
||||
@@ -14,11 +14,16 @@ const props = withDefaults(defineProps<{
|
||||
accountId?: string
|
||||
disabled?: boolean
|
||||
allowPreserve?: boolean
|
||||
formProp: string
|
||||
}>(), { disabled: false, allowPreserve: false })
|
||||
|
||||
const { t } = useI18n({ useScope: 'global' })
|
||||
|
||||
const model = defineModel<AccountModelAccess | undefined>({ required: true })
|
||||
const formItem = useTemplateRef<InstanceType<typeof ZFormItem>>('modelAccessItem')
|
||||
watch(model, () => {
|
||||
void formItem.value?.validate('change').catch(() => undefined)
|
||||
})
|
||||
// 未取得目录时不能判断缺失,成功返回空目录则可以
|
||||
const catalog = ref<Array<{ id: string, label: string }>>()
|
||||
const catalogObservedAtDisplay = ref<string>()
|
||||
@@ -165,12 +170,12 @@ watch(search, () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid gap-3">
|
||||
<ZFormItem>
|
||||
<div class="grid gap-4.5">
|
||||
<ZFormItem ref="modelAccessItem" :prop="formProp">
|
||||
<template #label>
|
||||
<span class="inline-flex flex-wrap items-end gap-x-2 gap-y-1">
|
||||
<span>{{ t('accounts.modelAccess.modelCatalog') }}</span>
|
||||
<span v-if="restricted && catalogObservedAtDisplay" class="text-cp-xs leading-none font-normal text-cp-text-tertiary tabular-nums">
|
||||
<span v-if="restricted && catalogObservedAtDisplay" class="text-z-xs leading-none font-normal text-z-text-tertiary tabular-nums">
|
||||
{{ catalogObservedAtDisplay }}
|
||||
</span>
|
||||
</span>
|
||||
@@ -187,10 +192,10 @@ watch(search, () => {
|
||||
:title="undefined"
|
||||
@keydown="handleCatalogHelpKeydown($event, open, close)"
|
||||
>
|
||||
<CircleAlert class="size-[1em] text-cp-xs" aria-hidden="true" />
|
||||
<CircleAlert class="size-[1em] text-z-xs" aria-hidden="true" />
|
||||
</ZIconButton>
|
||||
</template>
|
||||
<div class="max-w-72 space-y-2 px-3 py-2 text-cp-sm leading-relaxed text-cp-text-secondary">
|
||||
<div class="max-w-72 space-y-2 px-3 py-2 text-z-sm leading-relaxed text-z-text-secondary">
|
||||
<p class="m-0">
|
||||
{{ t('accounts.modelAccess.catalogObservedAtHelp') }}
|
||||
</p>
|
||||
@@ -216,7 +221,7 @@ watch(search, () => {
|
||||
<Search class="size-4" aria-hidden="true" />
|
||||
</template>
|
||||
</ZInput>
|
||||
<p v-if="inputError" class="m-0 text-cp-xs text-cp-error-text" role="alert">
|
||||
<p v-if="inputError" class="m-0 text-z-xs text-z-error-text" role="alert">
|
||||
{{ inputError }}
|
||||
</p>
|
||||
<ZScrollbar v-if="models.length" max-height="15rem" class="min-w-0 -m-1">
|
||||
@@ -224,8 +229,8 @@ watch(search, () => {
|
||||
<div
|
||||
v-for="item in models"
|
||||
:key="item.id"
|
||||
class="flex min-w-0 items-center rounded-cp transition-colors duration-150 motion-reduce:transition-none"
|
||||
:class="model?.models.includes(item.id) ? 'bg-cp-primary-container text-cp-primary-on-container' : 'bg-cp-fill-quaternary text-cp-text-secondary hover:bg-cp-fill-tertiary hover:text-cp-text'"
|
||||
class="flex min-w-0 items-center rounded-z transition-colors duration-150 motion-reduce:transition-none"
|
||||
:class="model?.models.includes(item.id) ? 'bg-z-primary-container text-z-primary-on-container' : 'bg-z-fill-quaternary text-z-text-secondary hover:bg-z-fill-tertiary hover:text-z-text'"
|
||||
>
|
||||
<ZCheckbox
|
||||
class="min-h-11 min-w-0 flex-1 px-3 py-2.5"
|
||||
@@ -249,14 +254,14 @@ watch(search, () => {
|
||||
:aria-expanded="open"
|
||||
:title="undefined"
|
||||
>
|
||||
<TriangleAlert class="size-3.5 text-cp-warning-text" aria-hidden="true" />
|
||||
<TriangleAlert class="size-3.5 text-z-warning-text" aria-hidden="true" />
|
||||
</ZIconButton>
|
||||
</template>
|
||||
<div class="max-w-64 space-y-1 px-3 py-2 text-cp-sm wrap-anywhere">
|
||||
<p class="m-0 font-emphasis text-cp-text">
|
||||
<div class="max-w-64 space-y-1 px-3 py-2 text-z-sm wrap-anywhere">
|
||||
<p class="m-0 font-emphasis text-z-text">
|
||||
{{ item.id }}
|
||||
</p>
|
||||
<p class="m-0 text-cp-text-secondary">
|
||||
<p class="m-0 text-z-text-secondary">
|
||||
{{ t('accounts.modelAccess.modelCatalogMissingHint') }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -264,7 +269,7 @@ watch(search, () => {
|
||||
</div>
|
||||
</div>
|
||||
</ZScrollbar>
|
||||
<p v-else-if="loading" class="m-0 py-4 text-center text-cp-sm text-cp-text-tertiary" role="status">
|
||||
<p v-else-if="loading" class="m-0 py-4 text-center text-z-sm text-z-text-tertiary" role="status">
|
||||
{{ t('accounts.modelAccess.loadingModel') }}
|
||||
</p>
|
||||
<ZEmpty v-else-if="!error" :title="search ? t('accounts.modelAccess.noMatchingModels') : t('accounts.modelAccess.noModels')" :icon="search ? Search : undefined" size="small" surface="none" />
|
||||
|
||||
@@ -45,7 +45,7 @@ watch(() => props.notes, recheckOpen, { flush: 'post' })
|
||||
<template #reference>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex min-w-0 max-w-full cursor-default touch-manipulation items-center rounded-cp-sm border-0 bg-transparent p-0 text-left font-mono text-cp-xs leading-4 font-emphasis text-cp-text-quaternary outline-none focus-visible:ring-2 focus-visible:ring-cp-control-outline"
|
||||
class="inline-flex min-w-0 max-w-full cursor-default touch-manipulation items-center rounded-z-sm border-0 bg-transparent p-0 text-left font-mono text-z-xs leading-4 font-emphasis text-z-text-quaternary outline-none focus-visible:ring-2 focus-visible:ring-z-control-outline"
|
||||
:aria-label="t('accounts.notes.viewAccountNotes')"
|
||||
aria-haspopup="dialog"
|
||||
:aria-expanded="open"
|
||||
@@ -55,9 +55,9 @@ watch(() => props.notes, recheckOpen, { flush: 'post' })
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<section :id="detailId" class="w-max max-w-[min(20rem,calc(100vw-1rem))] overflow-hidden rounded-cp-lg" role="dialog" :aria-label="t('accounts.notes.title')">
|
||||
<section :id="detailId" class="w-max max-w-[min(20rem,calc(100vw-1rem))] overflow-hidden rounded-z-lg" role="dialog" :aria-label="t('accounts.notes.title')">
|
||||
<ZScrollbar max-height="min(240px, calc(100dvh - 2rem))">
|
||||
<div class="px-3 py-2.5 text-cp-sm leading-5 whitespace-pre-wrap text-cp-text select-text wrap-anywhere">
|
||||
<div class="px-3 py-2.5 text-z-sm leading-5 whitespace-pre-wrap text-z-text select-text wrap-anywhere">
|
||||
{{ notes }}
|
||||
</div>
|
||||
</ZScrollbar>
|
||||
|
||||
@@ -47,15 +47,15 @@ const overviewItems = computed(() => [
|
||||
|
||||
function overviewIconClass(tone: string) {
|
||||
if (tone === 'success') {
|
||||
return 'bg-cp-green-container text-cp-green-on-container'
|
||||
return 'bg-z-green-container text-z-green-on-container'
|
||||
}
|
||||
if (tone === 'warning') {
|
||||
return 'bg-cp-orange-container text-cp-orange-on-container'
|
||||
return 'bg-z-orange-container text-z-orange-on-container'
|
||||
}
|
||||
if (tone === 'danger') {
|
||||
return 'bg-cp-error-container text-cp-error-on-container'
|
||||
return 'bg-z-error-container text-z-error-on-container'
|
||||
}
|
||||
return 'bg-cp-blue-container text-cp-blue-on-container'
|
||||
return 'bg-z-blue-container text-z-blue-on-container'
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -64,13 +64,13 @@ function overviewIconClass(tone: string) {
|
||||
<ZCard v-for="item in overviewItems" :key="item.label" as="article" padding="compact">
|
||||
<div class="flex items-stretch justify-between gap-3">
|
||||
<div class="flex min-w-0 flex-col">
|
||||
<p class="m-0 text-cp-sm leading-none font-heavy text-cp-text-secondary">
|
||||
<p class="m-0 text-z-sm leading-none font-heavy text-z-text-secondary">
|
||||
{{ item.label }}
|
||||
</p>
|
||||
<strong class="my-2 block font-mono text-[26px] leading-none font-extrabold text-cp-text">
|
||||
<strong class="my-2 block font-mono text-[26px] leading-none font-extrabold text-z-text">
|
||||
{{ item.value }}
|
||||
</strong>
|
||||
<p class="m-0 truncate text-cp-sm leading-none font-emphasis text-cp-text-quaternary">
|
||||
<p class="m-0 truncate text-z-sm leading-none font-emphasis text-z-text-quaternary">
|
||||
{{ item.caption }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useProxyCatalog } from '@/composables/useProxyCatalog'
|
||||
|
||||
const props = withDefaults(defineProps<{ accountId?: string, currentProxyId?: string | null, endpoint?: string | null, disabled?: boolean, preserve?: boolean, error?: string }>(), { preserve: true })
|
||||
const props = withDefaults(defineProps<{ accountId?: string, currentProxyId?: string | null, endpoint?: string | null, disabled?: boolean, preserve?: boolean, formProp: string }>(), { preserve: true })
|
||||
|
||||
const { t } = useI18n({ useScope: 'global' })
|
||||
|
||||
@@ -71,7 +71,7 @@ const selection = computed({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ZFormItem :label="t('accounts.proxy.outboundTunnel')" :error="error">
|
||||
<ZFormItem :prop="formProp" :show-message="false" :label="t('accounts.proxy.outboundTunnel')">
|
||||
<ZSelect v-model="selection" class="w-full" :options="options" :disabled="disabled || loading" :aria-label="t('accounts.proxy.outboundTunnel')" />
|
||||
</ZFormItem>
|
||||
</template>
|
||||
|
||||
@@ -17,7 +17,6 @@ withDefaults(defineProps<{
|
||||
preserveProxy?: boolean
|
||||
preserveModelAccess?: boolean
|
||||
showScheduling?: boolean
|
||||
proxyError?: string
|
||||
}>(), { preserveProxy: true, showScheduling: true })
|
||||
|
||||
const { t } = useI18n({ useScope: 'global' })
|
||||
@@ -32,11 +31,11 @@ const selectedGroupIds = defineModel<string[]>('selectedGroupIds', { required: t
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid gap-5">
|
||||
<AccountModelAccessField v-model="modelAccess" :account-id="accountId" :disabled="disabled" :allow-preserve="preserveModelAccess" />
|
||||
<div class="grid gap-4.5">
|
||||
<AccountModelAccessField v-model="modelAccess" form-prop="modelAccess" :account-id="accountId" :disabled="disabled" :allow-preserve="preserveModelAccess" />
|
||||
<slot name="scheduling">
|
||||
<div v-if="showScheduling" class="flex min-h-6 items-center justify-between gap-3">
|
||||
<span class="text-cp leading-none font-medium text-cp-text-secondary">{{ t('accounts.scheduling.scheduling') }}</span>
|
||||
<span class="text-z leading-none font-medium text-z-text-secondary">{{ t('accounts.scheduling.scheduling') }}</span>
|
||||
<ZSwitch
|
||||
v-model="enabled"
|
||||
:aria-label="t('accounts.scheduling.switchAccountSchedule')"
|
||||
@@ -45,8 +44,8 @@ const selectedGroupIds = defineModel<string[]>('selectedGroupIds', { required: t
|
||||
</div>
|
||||
</slot>
|
||||
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<ZFormItem :label="t('accounts.scheduling.concurrencyLimit')">
|
||||
<div class="grid gap-4.5 sm:grid-cols-2">
|
||||
<ZFormItem prop="concurrencyLimit" :label="t('accounts.scheduling.concurrencyLimit')">
|
||||
<ZInput
|
||||
v-model="concurrencyLimit"
|
||||
:aria-label="t('accounts.scheduling.accountConcurrencyLimit')"
|
||||
@@ -57,7 +56,7 @@ const selectedGroupIds = defineModel<string[]>('selectedGroupIds', { required: t
|
||||
:disabled="disabled"
|
||||
/>
|
||||
</ZFormItem>
|
||||
<ZFormItem :label="t('accounts.scheduling.weight')">
|
||||
<ZFormItem prop="weight" :show-message="Boolean(weight.trim())" :label="t('accounts.scheduling.weight')">
|
||||
<ZInput
|
||||
v-model="weight"
|
||||
:aria-label="t('accounts.scheduling.accountSchedulingWeight')"
|
||||
@@ -78,6 +77,6 @@ const selectedGroupIds = defineModel<string[]>('selectedGroupIds', { required: t
|
||||
:disabled="disabled"
|
||||
/>
|
||||
</ZFormItem>
|
||||
<AccountProxyField v-model:mode="proxyMode" v-model:proxy-id="proxyId" :preserve="preserveProxy" :error="proxyError" :current-proxy-id="currentProxyId" :endpoint="endpoint" :account-id="accountId" :disabled="disabled" />
|
||||
<AccountProxyField v-model:mode="proxyMode" v-model:proxy-id="proxyId" form-prop="proxyId" :preserve="preserveProxy" :current-proxy-id="currentProxyId" :endpoint="endpoint" :account-id="accountId" :disabled="disabled" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -41,7 +41,7 @@ const credentialEligible = computed(() => props.account.authenticationKind === '
|
||||
:aria-label="t('accounts.editor.title')"
|
||||
@click.stop="emit('edit', account)"
|
||||
>
|
||||
<Pencil class="size-3.5 text-cp-link" />
|
||||
<Pencil class="size-3.5 text-z-link" />
|
||||
</ZIconButton>
|
||||
|
||||
<ZIconButton
|
||||
@@ -50,7 +50,7 @@ const credentialEligible = computed(() => props.account.authenticationKind === '
|
||||
:disabled="deleting"
|
||||
@click.stop="emit('delete', account)"
|
||||
>
|
||||
<Trash2 class="size-3.5 text-cp-error" />
|
||||
<Trash2 class="size-3.5 text-z-error" />
|
||||
</ZIconButton>
|
||||
|
||||
<ZPopover placement="bottom-end">
|
||||
@@ -68,7 +68,7 @@ const credentialEligible = computed(() => props.account.authenticationKind === '
|
||||
@click.stop="(close(), emit('test', account))"
|
||||
>
|
||||
<template #icon>
|
||||
<Wifi class="size-3.5 text-cp-text-quaternary" />
|
||||
<Wifi class="size-3.5 text-z-text-quaternary" />
|
||||
</template>
|
||||
{{ t('accounts.connectionTest.title') }}
|
||||
</ZMenuItem>
|
||||
@@ -78,7 +78,7 @@ const credentialEligible = computed(() => props.account.authenticationKind === '
|
||||
@click.stop="(close(), emit('toggleScheduling', account))"
|
||||
>
|
||||
<template #icon>
|
||||
<Power class="size-3.5 text-cp-text-quaternary" />
|
||||
<Power class="size-3.5 text-z-text-quaternary" />
|
||||
</template>
|
||||
{{ account.enabled ? t('accounts.actions.disableScheduling') : t('accounts.actions.enableScheduling') }}
|
||||
</ZMenuItem>
|
||||
@@ -89,16 +89,16 @@ const credentialEligible = computed(() => props.account.authenticationKind === '
|
||||
@click.stop="(close(), emit('refresh', account.id))"
|
||||
>
|
||||
<template #loading>
|
||||
<RefreshCw class="size-3.5 animate-spin text-cp-text-quaternary motion-reduce:animate-none" />
|
||||
<RefreshCw class="size-3.5 animate-spin text-z-text-quaternary motion-reduce:animate-none" />
|
||||
</template>
|
||||
<template #icon>
|
||||
<RefreshCw class="size-3.5 text-cp-text-quaternary" />
|
||||
<RefreshCw class="size-3.5 text-z-text-quaternary" />
|
||||
</template>
|
||||
{{ t('accounts.actions.refreshToken') }}
|
||||
</ZMenuItem>
|
||||
<ZMenuItem v-if="credentialEligible" @click.stop="(close(), emit('reauthorize', account))">
|
||||
<template #icon>
|
||||
<KeyRound class="size-3.5 text-cp-text-quaternary" />
|
||||
<KeyRound class="size-3.5 text-z-text-quaternary" />
|
||||
</template>
|
||||
{{ t('accounts.actions.reauthorize') }}
|
||||
</ZMenuItem>
|
||||
@@ -109,10 +109,10 @@ const credentialEligible = computed(() => props.account.authenticationKind === '
|
||||
@click.stop="(close(), emit('downloadModelCatalog', account))"
|
||||
>
|
||||
<template #loading>
|
||||
<RefreshCw class="size-3.5 animate-spin text-cp-text-quaternary motion-reduce:animate-none" />
|
||||
<RefreshCw class="size-3.5 animate-spin text-z-text-quaternary motion-reduce:animate-none" />
|
||||
</template>
|
||||
<template #icon>
|
||||
<Download class="size-3.5 text-cp-text-quaternary" />
|
||||
<Download class="size-3.5 text-z-text-quaternary" />
|
||||
</template>
|
||||
{{ t('accounts.actions.downloadModelCatalog') }}
|
||||
</ZMenuItem>
|
||||
@@ -122,7 +122,7 @@ const credentialEligible = computed(() => props.account.authenticationKind === '
|
||||
@click.stop="(close(), emit('recover', account.id))"
|
||||
>
|
||||
<template #icon>
|
||||
<RotateCcw class="size-3.5 text-cp-text-quaternary" />
|
||||
<RotateCcw class="size-3.5 text-z-text-quaternary" />
|
||||
</template>
|
||||
{{ t('accounts.actions.restoreState') }}
|
||||
</ZMenuItem>
|
||||
|
||||
@@ -81,42 +81,42 @@ const modelUsageColumns = computed(() => (defineTableColumns<AccountModelUsage>(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="grid gap-4 rounded-lg bg-cp-bg-container p-4 shadow-cp-tertiary xl:min-h-0 xl:grid-cols-[0.52fr_1.48fr]">
|
||||
<section class="grid gap-4 rounded-lg bg-z-bg-container p-4 shadow-z-tertiary xl:min-h-0 xl:grid-cols-[0.52fr_1.48fr]">
|
||||
<div class="xl:flex xl:min-h-0 xl:flex-col">
|
||||
<div class="mb-3 flex shrink-0 items-center justify-between gap-3">
|
||||
<h3 class="m-0 text-cp-lg font-heavy text-cp-text">
|
||||
<h3 class="m-0 text-z-lg font-heavy text-z-text">
|
||||
{{ t('accounts.usage.tokensStructure') }}
|
||||
</h3>
|
||||
<span class="text-cp-xs font-emphasis text-cp-text-quaternary">{{ usageWindowLabels[account.usage.windowKind] }}</span>
|
||||
<span class="text-z-xs font-emphasis text-z-text-quaternary">{{ usageWindowLabels[account.usage.windowKind] }}</span>
|
||||
</div>
|
||||
<div class="grid gap-2 xl:min-h-0 xl:flex-1 xl:grid-rows-5">
|
||||
<div class="flex items-center justify-between rounded-lg bg-cp-green-container px-3 py-2">
|
||||
<span class="text-cp-sm font-bold text-cp-green-on-container">{{ t('accounts.usage.input') }}</span>
|
||||
<strong class="font-mono text-cp text-cp-text">
|
||||
<div class="flex items-center justify-between rounded-lg bg-z-green-container px-3 py-2">
|
||||
<span class="text-z-sm font-bold text-z-green-on-container">{{ t('accounts.usage.input') }}</span>
|
||||
<strong class="font-mono text-z text-z-text">
|
||||
{{ account.usage.inputTokensDisplay }}
|
||||
</strong>
|
||||
</div>
|
||||
<div class="flex items-center justify-between rounded-lg bg-cp-orange-container px-3 py-2">
|
||||
<span class="text-cp-sm font-bold text-cp-orange-on-container">{{ t('accounts.usage.output') }}</span>
|
||||
<strong class="font-mono text-cp text-cp-text">
|
||||
<div class="flex items-center justify-between rounded-lg bg-z-orange-container px-3 py-2">
|
||||
<span class="text-z-sm font-bold text-z-orange-on-container">{{ t('accounts.usage.output') }}</span>
|
||||
<strong class="font-mono text-z text-z-text">
|
||||
{{ account.usage.outputTokensDisplay }}
|
||||
</strong>
|
||||
</div>
|
||||
<div class="flex items-center justify-between rounded-lg bg-cp-cyan-container px-3 py-2">
|
||||
<span class="text-cp-sm font-bold text-cp-cyan-on-container">{{ t('accounts.usage.cacheRead') }}</span>
|
||||
<strong class="font-mono text-cp text-cp-text">
|
||||
<div class="flex items-center justify-between rounded-lg bg-z-cyan-container px-3 py-2">
|
||||
<span class="text-z-sm font-bold text-z-cyan-on-container">{{ t('accounts.usage.cacheRead') }}</span>
|
||||
<strong class="font-mono text-z text-z-text">
|
||||
{{ account.usage.cachedInputTokensDisplay }}
|
||||
</strong>
|
||||
</div>
|
||||
<div class="flex items-center justify-between rounded-lg bg-cp-blue-container px-3 py-2">
|
||||
<span class="text-cp-sm font-bold text-cp-blue-on-container">{{ t('accounts.usage.reasoning') }}</span>
|
||||
<strong class="font-mono text-cp text-cp-text">
|
||||
<div class="flex items-center justify-between rounded-lg bg-z-blue-container px-3 py-2">
|
||||
<span class="text-z-sm font-bold text-z-blue-on-container">{{ t('accounts.usage.reasoning') }}</span>
|
||||
<strong class="font-mono text-z text-z-text">
|
||||
{{ account.usage.reasoningOutputTokensDisplay }}
|
||||
</strong>
|
||||
</div>
|
||||
<div class="flex items-center justify-between rounded-lg bg-cp-blue-container px-3 py-2">
|
||||
<span class="text-cp-sm font-bold text-cp-blue-on-container">{{ t('accounts.usage.cacheWrites') }}</span>
|
||||
<strong class="font-mono text-cp text-cp-text">
|
||||
<div class="flex items-center justify-between rounded-lg bg-z-blue-container px-3 py-2">
|
||||
<span class="text-z-sm font-bold text-z-blue-on-container">{{ t('accounts.usage.cacheWrites') }}</span>
|
||||
<strong class="font-mono text-z text-z-text">
|
||||
{{ account.usage.cacheWriteInputTokensDisplay }}
|
||||
</strong>
|
||||
</div>
|
||||
@@ -125,28 +125,28 @@ const modelUsageColumns = computed(() => (defineTableColumns<AccountModelUsage>(
|
||||
|
||||
<div class="min-w-0 pt-4 xl:flex xl:min-h-0 xl:flex-col xl:pt-0 xl:pl-4">
|
||||
<div class="mb-3 flex shrink-0 flex-wrap items-center gap-x-4 gap-y-2">
|
||||
<h3 class="m-0 shrink-0 text-cp-lg font-heavy text-cp-text">
|
||||
<h3 class="m-0 shrink-0 text-z-lg font-heavy text-z-text">
|
||||
{{ t('accounts.usage.modelUsageRanking') }}
|
||||
</h3>
|
||||
|
||||
<div class="ml-auto flex items-baseline gap-4">
|
||||
<div v-if="hasUsageSummary" class="flex items-baseline gap-1.5 whitespace-nowrap">
|
||||
<Sigma class="size-3.5 self-center text-cp-text-tertiary" :stroke-width="1.75" />
|
||||
<Sigma class="size-3.5 self-center text-z-text-tertiary" :stroke-width="1.75" />
|
||||
<span :title="t('accounts.usage.totalTokens')">
|
||||
<span class="sr-only">{{ t('accounts.usage.totalTokens') }}</span>
|
||||
<span class="font-mono text-cp-sm font-emphasis tabular-nums text-cp-text">
|
||||
<span class="font-mono text-z-sm font-emphasis tabular-nums text-z-text">
|
||||
{{ account.usage.totalTokensDisplay }}
|
||||
</span>
|
||||
</span>
|
||||
<span class="mx-0.5 text-[10px] leading-none font-emphasis text-cp-text-quaternary"> / </span>
|
||||
<span class="mx-0.5 text-[10px] leading-none font-emphasis text-z-text-quaternary"> / </span>
|
||||
<span :title="t('accounts.usage.totalCharge')">
|
||||
<span class="sr-only">{{ t('accounts.usage.totalCharges') }}</span>
|
||||
<span class="font-mono text-cp-sm font-heavy tabular-nums text-cp-green-text">
|
||||
<span class="font-mono text-z-sm font-heavy tabular-nums text-z-green-text">
|
||||
{{ totalBillingDisplay }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<span class="whitespace-nowrap text-cp-xs font-emphasis text-cp-text-quaternary">{{ usageWindowLabels[account.usage.windowKind] }}</span>
|
||||
<span class="whitespace-nowrap text-z-xs font-emphasis text-z-text-quaternary">{{ usageWindowLabels[account.usage.windowKind] }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -36,18 +36,18 @@ const statusText = computed(() => {
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="rounded-cp bg-cp-fill-quaternary px-4 py-3">
|
||||
<div class="rounded-z bg-z-fill-quaternary px-4 py-3">
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
class="mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-cp bg-cp-bg-container text-cp-primary-text"
|
||||
class="mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-z bg-z-bg-container text-z-primary-text"
|
||||
>
|
||||
<KeyRound class="size-4" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="m-0 text-cp font-bold text-cp-text">
|
||||
<p class="m-0 text-z font-bold text-z-text">
|
||||
{{ panelTitle }}
|
||||
</p>
|
||||
<p class="m-0 mt-1 text-cp-sm leading-[1.55] font-medium text-cp-text-secondary">
|
||||
<p class="m-0 mt-1 text-z-sm leading-[1.55] font-medium text-z-text-secondary">
|
||||
{{ panelDescription }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -79,9 +79,9 @@ const statusText = computed(() => {
|
||||
</ZIconButton>
|
||||
</template>
|
||||
<ZScrollbar max-height="92px">
|
||||
<div class="rounded-cp bg-(--cp-input-bg) px-3.5 py-3 shadow-cp-tertiary">
|
||||
<div class="rounded-z bg-(--z-input-bg) px-3.5 py-3 shadow-z-tertiary">
|
||||
<pre
|
||||
class="m-0 whitespace-pre-wrap wrap-break-word font-mono text-cp-sm leading-[1.6] font-emphasis text-cp-text-secondary"
|
||||
class="m-0 whitespace-pre-wrap wrap-break-word font-mono text-z-sm leading-[1.6] font-emphasis text-z-text-secondary"
|
||||
v-text="authUrl"
|
||||
/>
|
||||
</div>
|
||||
@@ -89,10 +89,10 @@ const statusText = computed(() => {
|
||||
</ZFormItem>
|
||||
</ZForm>
|
||||
|
||||
<p v-if="statusText" class="m-0 text-cp-sm text-cp-text-secondary" role="status">
|
||||
<p v-if="statusText" class="m-0 text-z-sm text-z-text-secondary" role="status">
|
||||
{{ statusText }}
|
||||
</p>
|
||||
<p v-if="authorization.error" class="m-0 text-cp-sm text-cp-error" role="alert">
|
||||
<p v-if="authorization.error" class="m-0 text-z-sm text-z-error" role="alert">
|
||||
{{ authorization.error }}
|
||||
</p>
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ defineProps<{
|
||||
groups: AccountGroupRef[]
|
||||
groupsLoading: boolean
|
||||
disabled: boolean
|
||||
proxyError?: string
|
||||
}>()
|
||||
|
||||
const { t } = useI18n({ useScope: 'global' })
|
||||
@@ -22,7 +21,7 @@ const form = defineModel<AccountCreateForm>({ required: true })
|
||||
<template>
|
||||
<div class="grid gap-6">
|
||||
<fieldset class="m-0 min-w-0 border-0 p-0">
|
||||
<legend class="mb-3 p-0 text-cp font-medium text-cp-text-secondary">
|
||||
<legend class="mb-3 p-0 text-z font-medium text-z-text-secondary">
|
||||
{{ t('accounts.onboarding.accountPlatform') }}
|
||||
</legend>
|
||||
<AccountProviderChooser v-model="form.source" :disabled="disabled" />
|
||||
@@ -36,11 +35,11 @@ const form = defineModel<AccountCreateForm>({ required: true })
|
||||
v-model:proxy-mode="form.proxyMode"
|
||||
v-model:proxy-id="form.proxyId"
|
||||
preserve-model-access
|
||||
|
||||
:groups="groups"
|
||||
:groups-loading="groupsLoading"
|
||||
:preserve-proxy="false"
|
||||
:disabled="disabled"
|
||||
:proxy-error="proxyError"
|
||||
/>
|
||||
<ZFormItem :label="t('common.fields.notes')">
|
||||
<ZTextarea
|
||||
|
||||
@@ -5,16 +5,16 @@ import type { AccountCreateForm, AccountImportMode } from '../../utils/accountCr
|
||||
import type { Account, AccountGroupRef } from '@/api'
|
||||
|
||||
import { Openai, Xai } from '@boxicons/vue'
|
||||
import { ZButton, ZDialog, ZIconButton, ZSegmented } from '@codex-proxy/ui'
|
||||
import { ZButton, ZDialog, ZForm, ZIconButton, ZSegmented } from '@codex-proxy/ui'
|
||||
import { Copy, LayoutGrid, Settings2 } from '@lucide/vue'
|
||||
import { computed } from 'vue'
|
||||
import { computed, useTemplateRef, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import AccountPlanBadge from '@/components/account/AccountPlanBadge.vue'
|
||||
import ProviderIconGroup from '@/components/ProviderIconGroup.vue'
|
||||
import { useCopyText } from '@/composables/useCopyText'
|
||||
import { accountCreateProvider, accountProxyError } from '../../utils/accountCreate'
|
||||
import { accountModelAccessError } from '../../utils/modelAccess'
|
||||
import { parseAccountSchedulingForm } from '../../utils/schedulingForm'
|
||||
import { useAccountApiKeyFormRules } from '../../composables/useAccountApiKeyFormRules'
|
||||
import { useAccountSettingsFormRules } from '../../composables/useAccountSettingsFormRules'
|
||||
import { accountCreateProvider } from '../../utils/accountCreate'
|
||||
import AccountApiKeyFields from '../AccountApiKeyFields.vue'
|
||||
import AccountIdentityCell from '../AccountIdentityCell.vue'
|
||||
import AccountImportFields from './AccountImportFields.vue'
|
||||
@@ -26,13 +26,11 @@ const props = withDefaults(defineProps<{
|
||||
groups: AccountGroupRef[]
|
||||
groupsLoading: boolean
|
||||
saving?: boolean
|
||||
oauthLoading?: boolean
|
||||
reauthorizing?: boolean
|
||||
account?: Account | null
|
||||
authorization: AccountAuthorizationView
|
||||
}>(), {
|
||||
saving: false,
|
||||
oauthLoading: false,
|
||||
reauthorizing: false,
|
||||
account: null,
|
||||
})
|
||||
@@ -51,11 +49,13 @@ const accountCopyValue = computed(() =>
|
||||
|| props.account?.id
|
||||
|| '',
|
||||
)
|
||||
const busy = computed(() => props.saving || props.oauthLoading)
|
||||
const busy = computed(() => props.saving
|
||||
|| props.authorization.status === 'starting'
|
||||
|| props.authorization.status === 'completing')
|
||||
const canSelect = computed(() => form.value.source?.kind === 'bundle' || Boolean(accountCreateProvider(form.value)))
|
||||
const proxyError = computed(() => accountProxyError(form.value))
|
||||
const modelError = computed(() => accountModelAccessError(form.value.modelAccess))
|
||||
const scheduling = computed(() => parseAccountSchedulingForm(form.value.concurrencyLimit, form.value.weight))
|
||||
const formRef = useTemplateRef<InstanceType<typeof ZForm>>('formElement')
|
||||
const settingsRules = useAccountSettingsFormRules(() => form.value)
|
||||
const apiKeyRules = useAccountApiKeyFormRules()
|
||||
const view = computed(() => resolveAccountCreatePresentation({
|
||||
form: form.value,
|
||||
provider: accountCreateProvider(form.value),
|
||||
@@ -81,9 +81,24 @@ const importText = computed({
|
||||
},
|
||||
})
|
||||
|
||||
function continueToImport() {
|
||||
if (canSelect.value && !modelError.value && scheduling.value.valid && !props.groupsLoading && !proxyError.value && !busy.value)
|
||||
form.value.step = 'import'
|
||||
watch([
|
||||
open,
|
||||
() => form.value.step,
|
||||
mode,
|
||||
], () => formRef.value?.clearValidate())
|
||||
|
||||
async function continueToImport() {
|
||||
if (!canSelect.value || props.groupsLoading || busy.value || !await formRef.value?.validate().catch(() => false))
|
||||
return
|
||||
form.value.step = 'import'
|
||||
}
|
||||
|
||||
async function requestCreate() {
|
||||
if (!view.value.canSubmit)
|
||||
return
|
||||
if (mode.value === 'api_key' && !await formRef.value?.validate().catch(() => false))
|
||||
return
|
||||
emit('create')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -98,16 +113,16 @@ function continueToImport() {
|
||||
@closed="emit('afterLeave')"
|
||||
>
|
||||
<template #icon>
|
||||
<Settings2 v-if="view.configuring" class="text-cp-text" :size="20" aria-hidden="true" />
|
||||
<LayoutGrid v-else-if="view.isBatch" class="text-cp-text" :size="20" aria-hidden="true" />
|
||||
<Xai v-else-if="view.provider === 'xai'" class="text-cp-text" :width="20" :height="20" aria-hidden="true" />
|
||||
<Openai v-else-if="view.provider === 'openai'" class="text-cp-text" :width="20" :height="20" aria-hidden="true" />
|
||||
<Settings2 v-if="view.configuring" class="text-z-text" :size="20" aria-hidden="true" />
|
||||
<LayoutGrid v-else-if="view.isBatch" class="text-z-text" :size="20" aria-hidden="true" />
|
||||
<Xai v-else-if="view.provider === 'xai'" class="text-z-text" :width="20" :height="20" aria-hidden="true" />
|
||||
<Openai v-else-if="view.provider === 'openai'" class="text-z-text" :width="20" :height="20" aria-hidden="true" />
|
||||
</template>
|
||||
|
||||
<div class="grid gap-4">
|
||||
<div
|
||||
v-if="reauthorizing && account"
|
||||
class="flex flex-wrap items-center justify-between gap-4 rounded-cp bg-cp-fill-quaternary px-4 py-3.5"
|
||||
class="flex flex-wrap items-center justify-between gap-4 rounded-z bg-z-fill-quaternary px-4 py-3.5"
|
||||
>
|
||||
<AccountIdentityCell
|
||||
class="min-w-0 flex-1"
|
||||
@@ -138,17 +153,9 @@ function continueToImport() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AccountSetupFields
|
||||
v-if="view.configuring"
|
||||
v-model="form"
|
||||
:disabled="busy"
|
||||
:groups="groups"
|
||||
:groups-loading="groupsLoading"
|
||||
:proxy-error="form.proxyId.trim() ? proxyError : undefined"
|
||||
/>
|
||||
<p v-if="view.configuring && !scheduling.valid" class="m-0 text-xs text-cp-error" role="alert">
|
||||
{{ scheduling.message }}
|
||||
</p>
|
||||
<ZForm v-if="view.configuring" ref="formElement" :model="form" :rules="settingsRules" scroll-to-error focus-on-error novalidate @submit="continueToImport">
|
||||
<AccountSetupFields v-model="form" :disabled="busy" :groups="groups" :groups-loading="groupsLoading" />
|
||||
</ZForm>
|
||||
<template v-if="!view.configuring">
|
||||
<ZSegmented
|
||||
v-if="!reauthorizing && !view.isBatch"
|
||||
@@ -166,13 +173,15 @@ function continueToImport() {
|
||||
:can-start="true"
|
||||
:panel-title="`${view.label} ${reauthorizing ? t('accounts.actions.reauthorize') : t('accounts.onboarding.authorize')}`"
|
||||
:panel-description="view.oauth.description"
|
||||
:loading="oauthLoading"
|
||||
:loading="authorization.status === 'starting'"
|
||||
:callback-label="view.oauth.callbackLabel"
|
||||
:callback-placeholder="view.oauth.callbackPlaceholder"
|
||||
:disabled="busy"
|
||||
@regenerate="emit('generateOauth')"
|
||||
/>
|
||||
<AccountApiKeyFields v-else-if="mode === 'api_key'" v-model="form.apiKey" :disabled="busy" />
|
||||
<ZForm v-else-if="mode === 'api_key'" ref="formElement" :model="form" :rules="apiKeyRules" scroll-to-error focus-on-error novalidate @submit="requestCreate">
|
||||
<AccountApiKeyFields v-model="form.apiKey" :disabled="busy" />
|
||||
</ZForm>
|
||||
<AccountImportFields
|
||||
v-else
|
||||
:key="mode"
|
||||
@@ -192,10 +201,10 @@ function continueToImport() {
|
||||
<ZButton v-else class="mr-auto" :disabled="busy" @click="form.step = 'settings'">
|
||||
{{ t('common.actions.previous') }}
|
||||
</ZButton>
|
||||
<ZButton v-if="view.configuring" type="primary" :disabled="!canSelect || !scheduling.valid || Boolean(modelError) || groupsLoading || Boolean(proxyError) || busy" @click="continueToImport">
|
||||
<ZButton v-if="view.configuring" type="primary" :disabled="!canSelect || groupsLoading || busy" @click="continueToImport">
|
||||
{{ t('accounts.onboarding.continueImporting') }}
|
||||
</ZButton>
|
||||
<ZButton v-else type="primary" :loading="saving || oauthLoading" :disabled="!view.canSubmit" @click="emit('create')">
|
||||
<ZButton v-else type="primary" :loading="saving || authorization.status === 'completing'" :disabled="!view.canSubmit" @click="requestCreate">
|
||||
{{ view.submitLabel }}
|
||||
</ZButton>
|
||||
</template>
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { AccountCreateForm } from '../../utils/accountCreate'
|
||||
import { t } from '@/i18n'
|
||||
import { formatProviderLabel } from '@/utils/providers'
|
||||
import { accountCreateProvider, accountImportModes } from '../../utils/accountCreate'
|
||||
import { apiKeyAccountError } from '../../utils/upstreamApiKey'
|
||||
|
||||
export function resolveAccountCreatePresentation(input: {
|
||||
form: AccountCreateForm
|
||||
@@ -35,9 +34,7 @@ export function resolveAccountCreatePresentation(input: {
|
||||
let canSubmit = available && !input.busy
|
||||
if (form.mode === 'oauth')
|
||||
canSubmit &&= Boolean(authorization.flow && input.callback.trim())
|
||||
else if (form.mode === 'api_key')
|
||||
canSubmit &&= !apiKeyAccountError(form.apiKey)
|
||||
else
|
||||
else if (form.mode !== 'api_key')
|
||||
canSubmit &&= Boolean(form.importTexts[form.mode].trim())
|
||||
|
||||
return {
|
||||
|
||||
@@ -45,10 +45,10 @@ watch(hasAttention, (value) => {
|
||||
<section class="min-w-0 space-y-5" :aria-label="t('accounts.importTasks.importTaskDetails')">
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<h3 class="text-base font-bold text-cp-text">
|
||||
<h3 class="text-base font-bold text-z-text">
|
||||
{{ taskLabel(task) }}
|
||||
</h3>
|
||||
<p class="mt-1 text-xs text-cp-text-secondary">
|
||||
<p class="mt-1 text-xs text-z-text-secondary">
|
||||
{{ t('accounts.importTasks.created', { createdAtDisplay: task.createdAtDisplay }) }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -60,11 +60,11 @@ watch(hasAttention, (value) => {
|
||||
@click="emit('stop')"
|
||||
>
|
||||
<template #icon>
|
||||
<Square class="size-3 text-cp-error" />
|
||||
<Square class="size-3 text-z-error" />
|
||||
</template>
|
||||
{{ task.stopRequested || task.counts.pending === 0 ? t('accounts.importTasks.waitForCurrentEntryToEnd') : t('accounts.importTasks.stopUnstartedEntries') }}
|
||||
</ZButton>
|
||||
<p v-if="task.counts.pending > 0 || task.stopRequested" class="m-0 max-w-64 text-right text-cp-xs text-cp-text-secondary">
|
||||
<p v-if="task.counts.pending > 0 || task.stopRequested" class="m-0 max-w-64 text-right text-z-xs text-z-text-secondary">
|
||||
{{ t('accounts.importTasks.stopHelp') }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -75,11 +75,11 @@ watch(hasAttention, (value) => {
|
||||
|
||||
<div>
|
||||
<div class="mb-2 flex items-baseline justify-between gap-3 text-xs">
|
||||
<span class="text-cp-text-secondary">{{ t('accounts.importTasks.entryProgress') }} <strong class="font-mono font-medium text-cp-text">{{ processed(task) }} / {{ task.total }}</strong></span>
|
||||
<span class="font-semibold text-cp-text">{{ t('accounts.importTasks.accountsImported', { importedAccounts: task.counts.importedAccounts }) }}</span>
|
||||
<span class="text-z-text-secondary">{{ t('accounts.importTasks.entryProgress') }} <strong class="font-mono font-medium text-z-text">{{ processed(task) }} / {{ task.total }}</strong></span>
|
||||
<span class="font-semibold text-z-text">{{ t('accounts.importTasks.accountsImported', { importedAccounts: task.counts.importedAccounts }) }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="flex h-2 overflow-hidden rounded-full bg-cp-fill-alter"
|
||||
class="flex h-2 overflow-hidden rounded-full bg-z-fill-alter"
|
||||
role="progressbar"
|
||||
:aria-label="t('accounts.importTasks.itemProcessingProgress')"
|
||||
:aria-valuenow="processed(task)"
|
||||
@@ -88,17 +88,17 @@ watch(hasAttention, (value) => {
|
||||
>
|
||||
<div v-for="status in outcomeOrder" :key="status" :class="itemStates[status].fill" :style="{ width: `${task.counts[status] / task.total * 100}%` }" />
|
||||
</div>
|
||||
<div class="mt-3 flex flex-wrap gap-x-4 gap-y-2 text-xs text-cp-text-secondary">
|
||||
<div class="mt-3 flex flex-wrap gap-x-4 gap-y-2 text-xs text-z-text-secondary">
|
||||
<span v-for="status in outcomeOrder" :key="status" class="inline-flex items-center gap-1.5">
|
||||
<span class="size-1.5 rounded-full" :class="itemStates[status].fill" aria-hidden="true" />
|
||||
{{ itemStates[status].label }} <span class="font-mono text-cp-text">{{ task.counts[status] }}</span>
|
||||
{{ itemStates[status].label }} <span class="font-mono text-z-text">{{ task.counts[status] }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div v-if="hasAttention" class="mb-3 flex flex-wrap items-center gap-x-4 gap-y-3">
|
||||
<p v-if="task.counts.unknown" class="flex min-w-0 items-start gap-2 text-xs leading-relaxed text-cp-warning-text">
|
||||
<p v-if="task.counts.unknown" class="flex min-w-0 items-start gap-2 text-xs leading-relaxed text-z-warning-text">
|
||||
<CircleAlert class="mt-0.5 size-3.5 shrink-0" />
|
||||
{{ t('accounts.importTasks.itemsToBeCheckedMayHaveBeen') }}
|
||||
</p>
|
||||
@@ -117,20 +117,20 @@ watch(hasAttention, (value) => {
|
||||
<li
|
||||
v-for="item in visibleItems"
|
||||
:key="item.index"
|
||||
class="grid grid-cols-[1.75rem_minmax(0,1fr)_4.5rem] items-center gap-x-3 gap-y-1 rounded-cp-sm px-3 py-2.5 odd:bg-cp-fill-quaternary sm:grid-cols-[2rem_2rem_minmax(0,1fr)_4.5rem]"
|
||||
class="grid grid-cols-[1.75rem_minmax(0,1fr)_4.5rem] items-center gap-x-3 gap-y-1 rounded-z-sm px-3 py-2.5 odd:bg-z-fill-quaternary sm:grid-cols-[2rem_2rem_minmax(0,1fr)_4.5rem]"
|
||||
>
|
||||
<span class="font-mono text-xs text-cp-text-secondary">{{ String(item.index).padStart(2, '0') }}</span>
|
||||
<span class="font-mono text-xs text-z-text-secondary">{{ String(item.index).padStart(2, '0') }}</span>
|
||||
<ProviderIconGroup :provider="item.provider" size="sm" />
|
||||
<p class="col-span-2 col-start-2 row-start-2 min-w-0 text-xs leading-relaxed wrap-anywhere text-cp-text-secondary sm:col-span-1 sm:col-start-3 sm:row-start-1">
|
||||
<p class="col-span-2 col-start-2 row-start-2 min-w-0 text-xs leading-relaxed wrap-anywhere text-z-text-secondary sm:col-span-1 sm:col-start-3 sm:row-start-1">
|
||||
{{ itemDescription(item) }}
|
||||
</p>
|
||||
<span class="col-start-3 row-start-1 inline-flex items-center justify-self-end sm:col-start-4" :title="itemStates[item.status].label">
|
||||
<template v-if="item.status === 'succeeded' || item.status === 'failed'">
|
||||
<Check v-if="item.status === 'succeeded'" class="size-4 text-cp-success-text" aria-hidden="true" />
|
||||
<X v-else class="size-4 text-cp-error-text" aria-hidden="true" />
|
||||
<Check v-if="item.status === 'succeeded'" class="size-4 text-z-success-text" aria-hidden="true" />
|
||||
<X v-else class="size-4 text-z-error-text" aria-hidden="true" />
|
||||
<span class="sr-only">{{ itemStates[item.status].label }}</span>
|
||||
</template>
|
||||
<span v-else class="rounded-cp-sm px-2 py-0.5 text-[11px] font-semibold" :class="itemStates[item.status].color">
|
||||
<span v-else class="rounded-z-sm px-2 py-0.5 text-[11px] font-semibold" :class="itemStates[item.status].color">
|
||||
{{ itemStates[item.status].label }}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
@@ -80,7 +80,7 @@ function handleRetentionKeydown(event: KeyboardEvent) {
|
||||
<CircleAlert class="size-4" />
|
||||
</ZIconButton>
|
||||
</template>
|
||||
<div :id="retentionId" role="tooltip" class="grid w-72 max-w-[calc(100vw-2rem)] gap-2 p-4 text-cp-xs leading-relaxed text-cp-text-secondary">
|
||||
<div :id="retentionId" role="tooltip" class="grid w-72 max-w-[calc(100vw-2rem)] gap-2 p-4 text-z-xs leading-relaxed text-z-text-secondary">
|
||||
<p class="m-0">
|
||||
{{ t('accounts.importTasks.resultsRetainedForHour') }}
|
||||
</p>
|
||||
|
||||
@@ -5,33 +5,33 @@ import { t } from '@/i18n'
|
||||
export const itemStates = computed<Record<ImportItemStatus, { label: string, color: string, fill: string }>>(() => ({
|
||||
pending: {
|
||||
label: t('common.status.pending'),
|
||||
color: 'bg-cp-fill-alter text-cp-text-secondary',
|
||||
fill: 'bg-cp-fill-secondary',
|
||||
color: 'bg-z-fill-alter text-z-text-secondary',
|
||||
fill: 'bg-z-fill-secondary',
|
||||
},
|
||||
running: {
|
||||
label: t('accounts.importTasks.processing'),
|
||||
color: 'bg-cp-primary-container text-cp-primary-on-container',
|
||||
fill: 'bg-cp-primary',
|
||||
color: 'bg-z-primary-container text-z-primary-on-container',
|
||||
fill: 'bg-z-primary',
|
||||
},
|
||||
succeeded: {
|
||||
label: t('accounts.importTasks.alreadyInStock'),
|
||||
color: 'bg-cp-success-container text-cp-success-on-container',
|
||||
fill: 'bg-cp-success',
|
||||
color: 'bg-z-success-container text-z-success-on-container',
|
||||
fill: 'bg-z-success',
|
||||
},
|
||||
failed: {
|
||||
label: t('common.status.failed'),
|
||||
color: 'bg-cp-error-container text-cp-error-on-container',
|
||||
fill: 'bg-cp-error',
|
||||
color: 'bg-z-error-container text-z-error-on-container',
|
||||
fill: 'bg-z-error',
|
||||
},
|
||||
unknown: {
|
||||
label: t('accounts.importTasks.toBeVerified'),
|
||||
color: 'bg-cp-warning-container text-cp-warning-on-container',
|
||||
fill: 'bg-cp-warning',
|
||||
color: 'bg-z-warning-container text-z-warning-on-container',
|
||||
fill: 'bg-z-warning',
|
||||
},
|
||||
skipped: {
|
||||
label: t('accounts.importTasks.notExecuted'),
|
||||
color: 'bg-cp-fill-tertiary text-cp-text-secondary',
|
||||
fill: 'bg-cp-text-quaternary',
|
||||
color: 'bg-z-fill-tertiary text-z-text-secondary',
|
||||
fill: 'bg-z-text-quaternary',
|
||||
},
|
||||
}))
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ function invocationView(invocation: AccountProfileInvocation) {
|
||||
<template>
|
||||
<section class="grid gap-8 sm:grid-cols-2 sm:gap-12" aria-labelledby="profile-activity-insights-title">
|
||||
<div>
|
||||
<h3 id="profile-activity-insights-title" class="m-0 text-cp-lg font-heavy text-cp-text">
|
||||
<h3 id="profile-activity-insights-title" class="m-0 text-z-lg font-heavy text-z-text">
|
||||
{{ t('accounts.profile.activity.activityInsights') }}
|
||||
</h3>
|
||||
<dl class="mt-4 mb-0 grid gap-3.5">
|
||||
@@ -86,10 +86,10 @@ function invocationView(invocation: AccountProfileInvocation) {
|
||||
:key="row.label"
|
||||
class="grid grid-cols-[minmax(0,1fr)_auto] items-baseline gap-4"
|
||||
>
|
||||
<dt class="min-w-0 text-cp font-semibold text-cp-text-secondary">
|
||||
<dt class="min-w-0 text-z font-semibold text-z-text-secondary">
|
||||
{{ row.label }}
|
||||
</dt>
|
||||
<dd class="m-0 font-mono font-heavy tabular-nums text-cp-text">
|
||||
<dd class="m-0 font-mono font-heavy tabular-nums text-z-text">
|
||||
{{ row.value }}
|
||||
</dd>
|
||||
</div>
|
||||
@@ -97,7 +97,7 @@ function invocationView(invocation: AccountProfileInvocation) {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="m-0 text-cp-lg font-heavy text-cp-text">
|
||||
<h3 class="m-0 text-z-lg font-heavy text-z-text">
|
||||
{{ t('accounts.profile.activity.pluginsAndSkills') }}
|
||||
</h3>
|
||||
<ol v-if="invocations.length > 0" class="mt-3 mb-0 grid list-none gap-1.5 p-0">
|
||||
@@ -106,16 +106,16 @@ function invocationView(invocation: AccountProfileInvocation) {
|
||||
:key="invocation.key"
|
||||
class="grid grid-cols-[24px_minmax(0,1fr)_auto] items-center gap-2.5 py-1.5"
|
||||
>
|
||||
<span class="grid size-6 place-items-center rounded-cp bg-cp-fill-quaternary text-cp-text-secondary">
|
||||
<span class="grid size-6 place-items-center rounded-z bg-z-fill-quaternary text-z-text-secondary">
|
||||
<Package class="size-3.5" :stroke-width="2" />
|
||||
</span>
|
||||
<span class="truncate text-cp font-bold text-cp-text">{{ invocation.name }}</span>
|
||||
<span class="font-mono text-cp-sm font-semibold tabular-nums text-cp-text-secondary">
|
||||
<span class="truncate text-z font-bold text-z-text">{{ invocation.name }}</span>
|
||||
<span class="font-mono text-z-sm font-semibold tabular-nums text-z-text-secondary">
|
||||
{{ t('accounts.profile.activity.runs', { count: formatInteger(invocation.count) }) }}
|
||||
</span>
|
||||
</li>
|
||||
</ol>
|
||||
<p v-else class="mt-4 mb-0 text-cp-sm font-semibold text-cp-text-quaternary">
|
||||
<p v-else class="mt-4 mb-0 text-z-sm font-semibold text-z-text-quaternary">
|
||||
{{ t('accounts.profile.activity.noToolCalls') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -87,13 +87,13 @@ function formatDuration(value: number | null | undefined) {
|
||||
<section class="@container min-w-0 py-1" :aria-label="t('accounts.profile.metrics.cumulativeActivitySummary')" :aria-busy="loading">
|
||||
<dl class="grid grid-cols-2 gap-y-4 @[24rem]:grid-cols-3 @[34rem]:grid-cols-5">
|
||||
<div v-for="metric in metrics" :key="metric.label" class="min-w-0 px-2 text-center sm:px-3">
|
||||
<dd class="m-0 font-mono text-cp-lg leading-tight font-heavy whitespace-nowrap tabular-nums text-cp-text" :title="metric.title">
|
||||
<dd class="m-0 font-mono text-z-lg leading-tight font-heavy whitespace-nowrap tabular-nums text-z-text" :title="metric.title">
|
||||
<ZSkeleton v-if="loading && !profile" shape="text" class="mx-auto h-4 w-14" />
|
||||
<template v-else>
|
||||
{{ metric.value }}
|
||||
</template>
|
||||
</dd>
|
||||
<dt class="mt-1.5 text-cp-xs font-semibold text-cp-text-secondary">
|
||||
<dt class="mt-1.5 text-z-xs font-semibold text-z-text-secondary">
|
||||
{{ metric.label }}
|
||||
</dt>
|
||||
</div>
|
||||
|
||||
@@ -67,14 +67,14 @@ watch(
|
||||
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<h3 id="profile-display-name" class="m-0 min-w-0 truncate text-cp-lg leading-tight font-heavy text-cp-text">
|
||||
<h3 id="profile-display-name" class="m-0 min-w-0 truncate text-z-lg leading-tight font-heavy text-z-text">
|
||||
{{ displayName }}
|
||||
</h3>
|
||||
<AccountPlanBadge :upstream="account.upstream" :authentication-kind="account.authenticationKind" :plan-type="account.planType" :plan-type-display="account.planTypeDisplay" size="sm" />
|
||||
</div>
|
||||
<div
|
||||
v-if="username || accountIdentity"
|
||||
class="mt-1.5 grid min-w-0 gap-0.5 text-cp-xs font-semibold text-cp-text-secondary"
|
||||
class="mt-1.5 grid min-w-0 gap-0.5 text-z-xs font-semibold text-z-text-secondary"
|
||||
>
|
||||
<span v-if="username" class="truncate">{{ username }}</span>
|
||||
<span v-if="accountIdentity" class="truncate" :title="accountIdentity">{{ accountIdentity }}</span>
|
||||
|
||||
@@ -13,7 +13,7 @@ const { t } = useI18n({ useScope: 'global' })
|
||||
<section class="grid min-w-0 gap-4 overflow-hidden">
|
||||
<div class="flex items-center justify-between">
|
||||
<ZSkeleton shape="text" class="h-4 w-20" />
|
||||
<ZSkeleton class="h-7 w-32 rounded-cp opacity-70" />
|
||||
<ZSkeleton class="h-7 w-32 rounded-z opacity-70" />
|
||||
</div>
|
||||
<div class="grid min-w-190 gap-1" style="grid-template-columns: repeat(52, minmax(0, 1fr))">
|
||||
<div v-for="week in 52" :key="week" class="grid gap-1">
|
||||
|
||||
@@ -46,30 +46,30 @@ const dates = computed(() => [
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="min-w-0 rounded-cp-lg bg-cp-fill-alter p-4 sm:p-5" :aria-label="t('accounts.profile.subscription.label')">
|
||||
<section class="min-w-0 rounded-z-lg bg-z-fill-alter p-4 sm:p-5" :aria-label="t('accounts.profile.subscription.label')">
|
||||
<dl class="m-0 grid grid-cols-2 gap-x-6 gap-y-4 sm:grid-cols-[1.3fr_1.3fr_1fr_1fr]">
|
||||
<div v-for="date in dates" :key="date.label" class="min-w-0">
|
||||
<dt class="text-cp-xs text-cp-text-secondary">
|
||||
<dt class="text-z-xs text-z-text-secondary">
|
||||
{{ date.label }}
|
||||
</dt>
|
||||
<dd class="m-0 mt-1.5 font-mono text-cp-sm leading-relaxed tabular-nums text-cp-text">
|
||||
<dd class="m-0 mt-1.5 font-mono text-z-sm leading-relaxed tabular-nums text-z-text">
|
||||
<time v-if="date.value" :datetime="date.value">{{ date.display ?? t('common.values.unknown') }}</time>
|
||||
<span v-else>{{ t('common.values.unknown') }}</span>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<dt class="text-cp-xs text-cp-text-secondary">
|
||||
<dt class="text-z-xs text-z-text-secondary">
|
||||
{{ t('accounts.profile.subscription.renewalMethod') }}
|
||||
</dt>
|
||||
<dd class="m-0 mt-1.5 text-cp-sm leading-relaxed" :class="subscription.willRenew === true ? 'text-cp-success' : 'text-cp-text'">
|
||||
<dd class="m-0 mt-1.5 text-z-sm leading-relaxed" :class="subscription.willRenew === true ? 'text-z-success' : 'text-z-text'">
|
||||
{{ renewal }}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<dt class="text-cp-xs text-cp-text-secondary">
|
||||
<dt class="text-z-xs text-z-text-secondary">
|
||||
{{ t('accounts.usage.billing') }}
|
||||
</dt>
|
||||
<dd class="m-0 mt-1.5 text-cp-sm leading-relaxed wrap-break-word text-cp-text">
|
||||
<dd class="m-0 mt-1.5 text-z-sm leading-relaxed wrap-break-word text-z-text">
|
||||
{{ billing }}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
@@ -30,11 +30,11 @@ const modeOptions = computed(() => ([
|
||||
},
|
||||
]))
|
||||
const levelClasses: Record<ProfileActivityLevel, string> = {
|
||||
0: 'bg-cp-activity-level-0',
|
||||
1: 'bg-cp-activity-level-1',
|
||||
2: 'bg-cp-activity-level-2',
|
||||
3: 'bg-cp-activity-level-3',
|
||||
4: 'bg-cp-activity-level-4',
|
||||
0: 'bg-z-activity-level-0',
|
||||
1: 'bg-z-activity-level-1',
|
||||
2: 'bg-z-activity-level-2',
|
||||
3: 'bg-z-activity-level-3',
|
||||
4: 'bg-z-activity-level-4',
|
||||
}
|
||||
const grid = computed(() => buildProfileActivityGrid(props.calendar, mode.value))
|
||||
const rangeLabel = computed(() => grid.value.rangeLabel)
|
||||
@@ -53,7 +53,7 @@ const monthLabels = computed(() =>
|
||||
<section class="min-w-0" aria-labelledby="profile-token-activity-title">
|
||||
<div class="mb-4 flex flex-wrap items-end justify-between gap-3">
|
||||
<div>
|
||||
<h3 id="profile-token-activity-title" class="m-0 text-cp-lg font-heavy text-cp-text">
|
||||
<h3 id="profile-token-activity-title" class="m-0 text-z-lg font-heavy text-z-text">
|
||||
{{ t('accounts.profile.activity.tokenActivity') }}
|
||||
</h3>
|
||||
</div>
|
||||
@@ -75,7 +75,7 @@ const monthLabels = computed(() =>
|
||||
<span
|
||||
v-for="(month, monthIndex) in monthLabels"
|
||||
:key="`${month.weekIndex}-${month.label}`"
|
||||
class="row-start-1 h-4 whitespace-nowrap text-[10px] leading-4 font-semibold text-cp-text-quaternary"
|
||||
class="row-start-1 h-4 whitespace-nowrap text-[10px] leading-4 font-semibold text-z-text-quaternary"
|
||||
:class="monthIndex === monthLabels.length - 1 ? 'text-right' : undefined"
|
||||
:style="{
|
||||
gridColumn: `${month.weekIndex + (monthIndex === monthLabels.length - 1 ? 0 : 1)} / span 2`,
|
||||
|
||||
@@ -31,13 +31,13 @@ const { profile, subscription, loading, error, load } = useAccountPersonalInfo({
|
||||
<template>
|
||||
<ZDialog v-model="open" :title="t('accounts.profile.personalInformation')" width="64rem">
|
||||
<div class="flex min-h-0 min-w-0 flex-col gap-6 pb-2">
|
||||
<div class="grid min-w-0 grid-cols-1 gap-4 rounded-cp-lg bg-cp-fill-alter p-4 sm:p-5 lg:items-center" :class="account.capabilities.profile ? 'lg:grid-cols-[minmax(310px,1.05fr)_minmax(0,1.95fr)]' : undefined">
|
||||
<div class="grid min-w-0 grid-cols-1 gap-4 rounded-z-lg bg-z-fill-alter p-4 sm:p-5 lg:items-center" :class="account.capabilities.profile ? 'lg:grid-cols-[minmax(310px,1.05fr)_minmax(0,1.95fr)]' : undefined">
|
||||
<AccountProfileHero :account="account" :profile="profile" />
|
||||
<AccountProfileMetrics v-if="account.capabilities.profile" :profile="profile" :loading="loading" />
|
||||
</div>
|
||||
|
||||
<section v-if="subscription" class="grid min-w-0 grid-cols-1 gap-4" aria-labelledby="profile-subscription-title">
|
||||
<h3 id="profile-subscription-title" class="m-0 text-cp font-heavy text-cp-text">
|
||||
<h3 id="profile-subscription-title" class="m-0 text-z font-heavy text-z-text">
|
||||
{{ t('accounts.profile.subscriptionInformation') }}
|
||||
</h3>
|
||||
<AccountSubscription :subscription="subscription" />
|
||||
@@ -47,7 +47,7 @@ const { profile, subscription, loading, error, load } = useAccountPersonalInfo({
|
||||
<AccountProfileSkeleton v-if="account.capabilities.profile" />
|
||||
<div v-else role="status" aria-busy="true">
|
||||
<span class="sr-only">{{ t('accounts.profile.loadingSubscription') }}</span>
|
||||
<ZSkeleton class="h-24 w-full rounded-cp-lg" />
|
||||
<ZSkeleton class="h-24 w-full rounded-z-lg" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="profile">
|
||||
|
||||
+17
-17
@@ -15,60 +15,60 @@ const metrics = computed(() => [
|
||||
label: t('accounts.quotaForecast.tokenCapacity'),
|
||||
observed: source.value?.tokensDisplay ?? '—',
|
||||
estimated: props.forecast.estimatedTokensDisplay,
|
||||
tone: 'text-cp-cyan-text',
|
||||
tone: 'text-z-cyan-text',
|
||||
},
|
||||
{
|
||||
label: t('accounts.quotaForecast.equivalentFeeUsd'),
|
||||
observed: source.value?.usdDisplay ?? '—',
|
||||
estimated: props.forecast.estimatedUsdDisplay,
|
||||
tone: 'text-cp-green-text',
|
||||
tone: 'text-z-green-text',
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section v-if="source" :aria-label="t('accounts.quotaForecast.capacityForecast')" class="flex flex-col gap-5 rounded-cp-card bg-cp-fill-tertiary/70 p-4 [html[data-theme=light]_&]:bg-cp-fill-quaternary/70">
|
||||
<section v-if="source" :aria-label="t('accounts.quotaForecast.capacityForecast')" class="flex flex-col gap-5 rounded-z-card bg-z-fill-tertiary/70 p-4 [html[data-theme=light]_&]:bg-z-fill-quaternary/70">
|
||||
<div>
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<span class="text-cp-xs text-cp-text-secondary">{{ t('accounts.quotaForecast.usedThisCycle') }}</span>
|
||||
<span class="inline-flex items-center rounded-cp-sm bg-cp-info-container px-2 py-1 text-cp-xs font-emphasis text-cp-info-on-container">
|
||||
<span class="text-z-xs text-z-text-secondary">{{ t('accounts.quotaForecast.usedThisCycle') }}</span>
|
||||
<span class="inline-flex items-center rounded-z-sm bg-z-info-container px-2 py-1 text-z-xs font-emphasis text-z-info-on-container">
|
||||
{{ forecast.lowSample ? t('accounts.quotaForecast.preliminary') : t('accounts.quotaForecast.estimateLabel') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-2 flex items-end justify-between gap-4">
|
||||
<strong class="font-mono text-3xl font-heavy tabular-nums text-cp-text [html[data-theme=light]_&]:font-emphasis">{{ source.usedPercentDisplay }}</strong>
|
||||
<span class="pb-1 text-cp-xs text-cp-text-secondary">{{ t('accounts.quotaForecast.thisCycle') }} <span class="font-mono">100%</span></span>
|
||||
<strong class="font-mono text-3xl font-heavy tabular-nums text-z-text [html[data-theme=light]_&]:font-emphasis">{{ source.usedPercentDisplay }}</strong>
|
||||
<span class="pb-1 text-z-xs text-z-text-secondary">{{ t('accounts.quotaForecast.thisCycle') }} <span class="font-mono">100%</span></span>
|
||||
</div>
|
||||
|
||||
<!-- 实心为已观测比例,虚线为待推算容量,不伪造历史趋势或置信区间。 -->
|
||||
<div
|
||||
class="relative mt-3 h-3 overflow-hidden rounded-full bg-cp-bg-container"
|
||||
class="relative mt-3 h-3 overflow-hidden rounded-full bg-z-bg-container"
|
||||
role="img"
|
||||
:aria-label="t('accounts.quotaForecast.usedDescription', { rate: source.usedPercentDisplay })"
|
||||
>
|
||||
<span class="absolute inset-x-0 top-1/2 border-t-2 border-dashed border-cp-primary/50" />
|
||||
<span class="absolute inset-y-0 left-0 rounded-full bg-cp-primary" :style="{ width: `${source.usedPercent ?? 0}%` }" />
|
||||
<span class="absolute inset-x-0 top-1/2 border-t-2 border-dashed border-z-primary/50" />
|
||||
<span class="absolute inset-y-0 left-0 rounded-full bg-z-primary" :style="{ width: `${source.usedPercent ?? 0}%` }" />
|
||||
</div>
|
||||
<div class="mt-2 flex justify-between font-mono text-[10px] text-cp-text-tertiary" aria-hidden="true">
|
||||
<div class="mt-2 flex justify-between font-mono text-[10px] text-z-text-tertiary" aria-hidden="true">
|
||||
<span>0</span><span>25</span><span>50</span><span>75</span><span>100%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<div class="flex flex-wrap justify-between gap-x-4 gap-y-1 text-cp-xs text-cp-text-secondary">
|
||||
<div class="flex flex-wrap justify-between gap-x-4 gap-y-1 text-z-xs text-z-text-secondary">
|
||||
<span>{{ t('accounts.quotaForecast.alreadyRecordedThisCycle') }}</span>
|
||||
<span>{{ forecast.extrapolated ? t('accounts.quotaForecast.equivalentCapacityOverDays', { targetDays: forecast.targetDays }) : t('accounts.quotaForecast.projectedTotal') }}</span>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<div v-for="metric in metrics" :key="metric.label" class="rounded-cp bg-cp-bg-container px-3 py-3">
|
||||
<p class="m-0 text-cp-xs text-cp-text-secondary">
|
||||
<div v-for="metric in metrics" :key="metric.label" class="rounded-z bg-z-bg-container px-3 py-3">
|
||||
<p class="m-0 text-z-xs text-z-text-secondary">
|
||||
{{ metric.label }}
|
||||
</p>
|
||||
<div class="mt-2 grid grid-cols-[1fr_20px_1.2fr] items-center gap-2">
|
||||
<span class="font-mono text-cp-sm font-emphasis tabular-nums text-cp-text [html[data-theme=light]_&]:font-medium">{{ metric.observed }}</span>
|
||||
<ArrowRight class="size-3.5 text-cp-text-tertiary" />
|
||||
<span class="font-mono text-z-sm font-emphasis tabular-nums text-z-text [html[data-theme=light]_&]:font-medium">{{ metric.observed }}</span>
|
||||
<ArrowRight class="size-3.5 text-z-text-tertiary" />
|
||||
<strong class="text-right font-mono text-xl font-heavy tabular-nums" :class="metric.tone">
|
||||
<span v-if="metric.estimated !== '—'" class="mr-1 text-cp-sm font-normal text-cp-text-tertiary">≈</span>{{ metric.estimated }}
|
||||
<span v-if="metric.estimated !== '—'" class="mr-1 text-z-sm font-normal text-z-text-tertiary">≈</span>{{ metric.estimated }}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@ const { t } = useI18n({ useScope: 'global' })
|
||||
<div role="status" aria-busy="true">
|
||||
<span class="sr-only">{{ t('accounts.quotaForecast.loadingQuotaForecast') }}</span>
|
||||
<div aria-hidden="true">
|
||||
<div class="flex flex-col gap-5 rounded-cp-card bg-cp-fill-tertiary/70 p-4 [html[data-theme=light]_&]:bg-cp-fill-quaternary/70">
|
||||
<div class="flex flex-col gap-5 rounded-z-card bg-z-fill-tertiary/70 p-4 [html[data-theme=light]_&]:bg-z-fill-quaternary/70">
|
||||
<div>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<ZSkeleton shape="text" class="w-16" />
|
||||
@@ -28,7 +28,7 @@ const { t } = useI18n({ useScope: 'global' })
|
||||
<ZSkeleton shape="text" class="w-20" />
|
||||
<ZSkeleton shape="text" class="w-20" />
|
||||
</div>
|
||||
<div v-for="metric in 2" :key="metric" class="rounded-cp bg-cp-bg-container px-3 py-3">
|
||||
<div v-for="metric in 2" :key="metric" class="rounded-z bg-z-bg-container px-3 py-3">
|
||||
<ZSkeleton shape="text" class="w-20" />
|
||||
<div class="mt-3 grid grid-cols-[1fr_20px_1.2fr] items-center gap-2">
|
||||
<ZSkeleton shape="text" class="w-14" />
|
||||
|
||||
@@ -84,7 +84,7 @@ function handleExplanationKeydown(event: KeyboardEvent) {
|
||||
type="info"
|
||||
>
|
||||
<template #icon>
|
||||
<ChartNoAxesCombined class="size-5 text-cp-primary-text" :stroke-width="1.75" />
|
||||
<ChartNoAxesCombined class="size-5 text-z-primary-text" :stroke-width="1.75" />
|
||||
</template>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
@@ -100,7 +100,7 @@ function handleExplanationKeydown(event: KeyboardEvent) {
|
||||
<div aria-live="polite" :aria-busy="loading || refreshing">
|
||||
<ForecastSkeleton v-if="loading || refreshing" />
|
||||
<template v-else-if="forecast">
|
||||
<div v-if="unavailableReason" class="grid rounded-cp-card bg-cp-fill-tertiary/70 [html[data-theme=light]_&]:bg-cp-fill-quaternary/70">
|
||||
<div v-if="unavailableReason" class="grid rounded-z-card bg-z-fill-tertiary/70 [html[data-theme=light]_&]:bg-z-fill-quaternary/70">
|
||||
<ZEmpty :title="t('accounts.quotaForecast.unpredictableAtMoment')" :description="unavailableReason" :icon="ChartNoAxesCombined" surface="none" class="min-h-72 content-center" />
|
||||
</div>
|
||||
<ForecastCapacity v-else :forecast="forecast" />
|
||||
@@ -110,7 +110,7 @@ function handleExplanationKeydown(event: KeyboardEvent) {
|
||||
|
||||
<template #footer>
|
||||
<div class="mr-auto flex min-w-0 items-center gap-1">
|
||||
<span v-if="forecast?.source?.observedAt" class="hidden text-cp-xs text-cp-text-tertiary sm:block">
|
||||
<span v-if="forecast?.source?.observedAt" class="hidden text-z-xs text-z-text-tertiary sm:block">
|
||||
{{ t('accounts.quotaForecast.updated', { observedAtDisplay: forecast.source.observedAtDisplay }) }}
|
||||
</span>
|
||||
<ZPopover v-model="explanationOpen" trigger="hover-click" placement="top-start" :hover-delay="240">
|
||||
@@ -127,8 +127,8 @@ function handleExplanationKeydown(event: KeyboardEvent) {
|
||||
<CircleAlert class="size-3.5" aria-hidden="true" />
|
||||
</ZIconButton>
|
||||
</template>
|
||||
<section :id="explanationId" role="tooltip" class="grid w-96 max-w-[calc(100vw-2rem)] gap-3 p-4 text-cp-xs leading-relaxed text-cp-text-secondary">
|
||||
<h4 class="m-0 font-heavy text-cp-text">
|
||||
<section :id="explanationId" role="tooltip" class="grid w-96 max-w-[calc(100vw-2rem)] gap-3 p-4 text-z-xs leading-relaxed text-z-text-secondary">
|
||||
<h4 class="m-0 font-heavy text-z-text">
|
||||
{{ t('accounts.quotaForecast.forReferenceOnly') }}
|
||||
</h4>
|
||||
<p class="m-0">
|
||||
|
||||
@@ -37,15 +37,15 @@ const hasBalance = computed(() => !props.credits?.unlimited && balanceDisplay.va
|
||||
|
||||
<template>
|
||||
<div class="flex min-w-0 flex-wrap items-baseline justify-between gap-x-4 gap-y-1 px-4 pb-4" :aria-label="t('accounts.quota.quotaPoints')">
|
||||
<span class="text-cp-sm font-heavy text-cp-text">{{ t('accounts.quota.quotaPoints') }}</span>
|
||||
<span class="text-z-sm font-heavy text-z-text">{{ t('accounts.quota.quotaPoints') }}</span>
|
||||
<span class="flex min-w-0 items-baseline gap-1.5">
|
||||
<strong
|
||||
class="min-w-0 text-right text-cp-sm font-heavy wrap-anywhere"
|
||||
:class="hasBalance ? 'font-mono tabular-nums text-cp-text' : 'text-cp-text-secondary'"
|
||||
class="min-w-0 text-right text-z-sm font-heavy wrap-anywhere"
|
||||
:class="hasBalance ? 'font-mono tabular-nums text-z-text' : 'text-z-text-secondary'"
|
||||
>
|
||||
{{ value }}
|
||||
</strong>
|
||||
<span v-if="hasBalance" class="shrink-0 text-cp-xs text-cp-text-tertiary">{{ t('accounts.quota.point') }}</span>
|
||||
<span v-if="hasBalance" class="shrink-0 text-z-xs text-z-text-tertiary">{{ t('accounts.quota.point') }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -53,7 +53,7 @@ const items = computed(() => props.windows.map((window) => {
|
||||
<section class="grid min-w-0 gap-2">
|
||||
<h4
|
||||
v-if="grouped && label && windows[0]?.limitId !== 'codex'"
|
||||
class="m-0 truncate text-cp-xs leading-4 font-heavy text-cp-text-secondary"
|
||||
class="m-0 truncate text-z-xs leading-4 font-heavy text-z-text-secondary"
|
||||
:title="label"
|
||||
>
|
||||
{{ label }}
|
||||
@@ -65,12 +65,12 @@ const items = computed(() => props.windows.map((window) => {
|
||||
>
|
||||
<div v-for="item in items" :key="item.key" class="grid min-w-0 gap-1.5">
|
||||
<template v-if="item.local">
|
||||
<div class="flex min-w-0 items-baseline justify-between gap-2 text-cp-xs leading-3.5">
|
||||
<span class="min-w-0 truncate font-bold text-cp-text-quaternary">
|
||||
<div class="flex min-w-0 items-baseline justify-between gap-2 text-z-xs leading-3.5">
|
||||
<span class="min-w-0 truncate font-bold text-z-text-quaternary">
|
||||
{{ item.localLabel }}
|
||||
</span>
|
||||
<strong
|
||||
class="shrink-0 font-mono font-heavy tabular-nums text-cp-text"
|
||||
class="shrink-0 font-mono font-heavy tabular-nums text-z-text"
|
||||
:title="t('accounts.quota.requestsInThisWindow', { requestDisplay: item.requestDisplay })"
|
||||
>
|
||||
{{ t('accounts.quota.requestCount', { requestDisplay: item.requestDisplay }) }}
|
||||
@@ -83,7 +83,7 @@ const items = computed(() => props.windows.map((window) => {
|
||||
:label="item.timelineTitle"
|
||||
/>
|
||||
|
||||
<p class="m-0 flex min-w-0 items-center justify-between gap-2 text-[10px] leading-3.5 text-cp-text-tertiary">
|
||||
<p class="m-0 flex min-w-0 items-center justify-between gap-2 text-[10px] leading-3.5 text-z-text-tertiary">
|
||||
<span class="shrink-0 font-emphasis">{{ t('accounts.quota.statisticalRange') }}</span>
|
||||
<span
|
||||
class="min-w-0 truncate font-mono font-emphasis tabular-nums"
|
||||
@@ -94,20 +94,20 @@ const items = computed(() => props.windows.map((window) => {
|
||||
</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="flex min-w-0 items-baseline justify-between gap-2 text-cp-xs leading-3.5">
|
||||
<span class="min-w-0 truncate font-bold text-cp-text-quaternary" :title="item.observationDisplay || undefined">
|
||||
<div class="flex min-w-0 items-baseline justify-between gap-2 text-z-xs leading-3.5">
|
||||
<span class="min-w-0 truncate font-bold text-z-text-quaternary" :title="item.observationDisplay || undefined">
|
||||
{{ item.label }}
|
||||
</span>
|
||||
<span class="flex shrink-0 items-baseline gap-1.5 font-mono font-heavy tabular-nums">
|
||||
<template v-if="item.amountUsedDisplay">
|
||||
<strong class="text-cp-text-quaternary">
|
||||
<strong class="text-z-text-quaternary">
|
||||
{{ item.amountUsedDisplay }}<template v-if="item.usedPercent === null"> / {{ item.quotaValueDisplay }}</template>
|
||||
</strong>
|
||||
<span v-if="item.localUsageDisplay" class="font-normal text-cp-text-quaternary" aria-hidden="true">·</span>
|
||||
<span v-if="item.localUsageDisplay" class="font-normal text-z-text-quaternary" aria-hidden="true">·</span>
|
||||
</template>
|
||||
<strong
|
||||
v-if="item.localUsageDisplay"
|
||||
class="text-cp-text-quaternary"
|
||||
class="text-z-text-quaternary"
|
||||
:title="t('accounts.quota.usageInThisWindow', { localUsageDisplay: item.localUsageDisplay })"
|
||||
>
|
||||
{{ item.localUsageDisplay }}
|
||||
@@ -123,7 +123,7 @@ const items = computed(() => props.windows.map((window) => {
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="h-1.5 w-full overflow-hidden rounded-full bg-cp-border-secondary"
|
||||
class="h-1.5 w-full overflow-hidden rounded-full bg-z-border-secondary"
|
||||
role="progressbar"
|
||||
:aria-label="item.ariaLabel"
|
||||
aria-valuemin="0"
|
||||
@@ -138,7 +138,7 @@ const items = computed(() => props.windows.map((window) => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p class="m-0 flex min-w-0 items-center justify-between gap-2 text-[10px] leading-3.5 text-cp-text-tertiary">
|
||||
<p class="m-0 flex min-w-0 items-center justify-between gap-2 text-[10px] leading-3.5 text-z-text-tertiary">
|
||||
<span class="shrink-0 font-emphasis">{{ t('common.actions.reset') }}</span>
|
||||
<span class="min-w-0 truncate font-mono font-emphasis tabular-nums" :title="item.resetAtDisplay">
|
||||
{{ item.resetAtDisplay }}
|
||||
|
||||
@@ -43,10 +43,10 @@ const { start: scheduleOpen, stop: cancelOpen } = useTimeoutFn(() => {
|
||||
|
||||
const option = computed<EChartsOption>(() => {
|
||||
const colors = {
|
||||
success: color('--cp-color-success', ''),
|
||||
warning: color('--cp-color-warning', ''),
|
||||
danger: color('--cp-color-error', ''),
|
||||
unknown: color('--cp-color-border', ''),
|
||||
success: color('color-success', ''),
|
||||
warning: color('color-warning', ''),
|
||||
danger: color('color-error', ''),
|
||||
unknown: color('color-border', ''),
|
||||
}
|
||||
const nodes = props.nodes
|
||||
return {
|
||||
@@ -264,7 +264,7 @@ onBeforeUnmount(() => chart?.off('datazoom', onDataZoom))
|
||||
<ZPopover ref="popover" v-model="open" class="w-full" placement="top" trigger="hover-click" :reference-element="anchor" :disabled="!activeNode?.windows.length" :hover-delay="180" animate-position>
|
||||
<template #reference>
|
||||
<div
|
||||
class="relative w-full rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-cp-control-outline"
|
||||
class="relative w-full rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-z-control-outline"
|
||||
role="slider" tabindex="0" :aria-valuemin="1" :aria-valuemax="nodes.length" :aria-valuenow="(activeIndex ?? 0) + 1" :aria-valuetext="t('accounts.upstream.account', { number: (activeIndex ?? 0) + 1 })" :aria-label="t('accounts.upstream.poolChart')" :aria-description="t('accounts.upstream.poolKeyboard')"
|
||||
@pointermove="onPointerMove" @pointerleave="onPointerLeave" @click="onChartClick" @dblclick="resetZoom" @keydown="onKeydown"
|
||||
>
|
||||
|
||||
+6
-6
@@ -65,10 +65,10 @@ watch(() => props.accountId, () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="grid gap-2 rounded-cp bg-cp-fill-quaternary px-4 py-3" :aria-label="t('accounts.quotaReset.automaticallyUseResetCard')">
|
||||
<section class="grid gap-2 rounded-z bg-z-fill-quaternary px-4 py-3" :aria-label="t('accounts.quotaReset.automaticallyUseResetCard')">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-1">
|
||||
<h3 class="m-0 text-cp-sm font-heavy text-cp-text">
|
||||
<h3 class="m-0 text-z-sm font-heavy text-z-text">
|
||||
{{ t('accounts.quotaReset.automaticallyUsed') }}
|
||||
</h3>
|
||||
<ZPopover trigger="click" placement="bottom-start" role="dialog">
|
||||
@@ -77,7 +77,7 @@ watch(() => props.accountId, () => {
|
||||
<CircleHelp class="size-3.5" />
|
||||
</ZIconButton>
|
||||
</template>
|
||||
<div class="grid max-w-64 gap-2 p-3 text-cp-xs leading-relaxed text-cp-text-secondary">
|
||||
<div class="grid max-w-64 gap-2 p-3 text-z-xs leading-relaxed text-z-text-secondary">
|
||||
<p>{{ t('accounts.quotaReset.automation.scheduleHelp') }}</p>
|
||||
<p>{{ t('accounts.quotaReset.automation.verifyHelp') }}</p>
|
||||
</div>
|
||||
@@ -99,10 +99,10 @@ watch(() => props.accountId, () => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="status?.enabled && status.nextConsumeAtDisplay" class="text-cp-xs text-cp-text-secondary">
|
||||
<p v-if="status?.enabled && status.nextConsumeAtDisplay" class="text-z-xs text-z-text-secondary">
|
||||
{{ t('accounts.quotaReset.nextUse', { nextConsumeAtDisplay: status.nextConsumeAtDisplay }) }}
|
||||
</p>
|
||||
<p v-if="resultText" class="text-cp-xs text-cp-warning" role="status">
|
||||
<p v-if="resultText" class="text-z-xs text-z-warning" role="status">
|
||||
{{ resultText }}
|
||||
</p>
|
||||
</section>
|
||||
@@ -110,6 +110,6 @@ watch(() => props.accountId, () => {
|
||||
|
||||
<style scoped>
|
||||
.reset-credit-switch :deep(input:not(:checked) + span) {
|
||||
background-color: var(--cp-switch-unchecked-bg);
|
||||
background-color: var(--z-switch-unchecked-bg);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -125,8 +125,8 @@ function handleRequestConsume(creditId: string) {
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex shrink-0 touch-manipulation items-center justify-center rounded-cp border-0 bg-transparent text-cp-text-secondary outline-none transition-[background-color,color,opacity,transform] duration-150 hover:bg-cp-fill-quaternary hover:text-cp-text active:bg-cp-fill-tertiary focus-visible:ring-2 focus-visible:ring-cp-control-outline focus-visible:ring-offset-2 focus-visible:ring-offset-cp-bg-container motion-safe:active:scale-[0.96] motion-reduce:transition-none"
|
||||
:class="showTriggerCount ? 'h-cp-control-sm gap-1 px-2' : 'size-cp-control-sm'"
|
||||
class="inline-flex shrink-0 touch-manipulation items-center justify-center rounded-z border-0 bg-transparent text-z-text-secondary outline-none transition-[background-color,color,opacity,transform] duration-150 hover:bg-z-fill-quaternary hover:text-z-text active:bg-z-fill-tertiary focus-visible:ring-2 focus-visible:ring-z-control-outline focus-visible:ring-offset-2 focus-visible:ring-offset-z-bg-container motion-safe:active:scale-[0.96] motion-reduce:transition-none"
|
||||
:class="showTriggerCount ? 'h-z-control-sm gap-1 px-2' : 'size-z-control-sm'"
|
||||
:aria-label="triggerLabel"
|
||||
:aria-pressed="panelOpen || undefined"
|
||||
:title="triggerLabel"
|
||||
@@ -146,16 +146,16 @@ function handleRequestConsume(creditId: string) {
|
||||
:show-close="!consuming" :close-on-click-modal="!consuming" :close-on-press-escape="!consuming"
|
||||
>
|
||||
<div v-if="confirmation" class="grid gap-3">
|
||||
<section class="rounded-cp bg-cp-fill-quaternary px-4 py-3.5">
|
||||
<p class="m-0 text-cp-xs font-heavy text-cp-text-quaternary">
|
||||
<section class="rounded-z bg-z-fill-quaternary px-4 py-3.5">
|
||||
<p class="m-0 text-z-xs font-heavy text-z-text-quaternary">
|
||||
{{ t('accounts.quotaReset.thisTimeUse') }}
|
||||
</p>
|
||||
<p class="mt-1.5 mb-0 text-cp-lg leading-snug font-heavy text-cp-text">
|
||||
<p class="mt-1.5 mb-0 text-z-lg leading-snug font-heavy text-z-text">
|
||||
{{ confirmCreditTitle }}
|
||||
</p>
|
||||
<p
|
||||
v-if="confirmation.credit"
|
||||
class="mt-1 mb-0 font-mono text-[10px] leading-normal font-emphasis text-cp-text-quaternary"
|
||||
class="mt-1 mb-0 font-mono text-[10px] leading-normal font-emphasis text-z-text-quaternary"
|
||||
>
|
||||
{{ confirmation.credit.expiresAtDisplay ? t('accounts.quotaReset.expires', { expiresAtDisplay: confirmation.credit.expiresAtDisplay }) : t('accounts.quotaReset.expiryHelp') }}
|
||||
</p>
|
||||
@@ -163,20 +163,20 @@ function handleRequestConsume(creditId: string) {
|
||||
</div>
|
||||
|
||||
<div v-else class="grid gap-4">
|
||||
<section class="overflow-hidden rounded-cp bg-cp-fill-quaternary" :aria-label="t('accounts.quotaReset.currentQuota')">
|
||||
<section class="overflow-hidden rounded-z bg-z-fill-quaternary" :aria-label="t('accounts.quotaReset.currentQuota')">
|
||||
<UsageLimits :windows="account.quota.windows" />
|
||||
<AccountQuotaCredits :credits="account.quota.credits" />
|
||||
</section>
|
||||
|
||||
<ResetCreditAutomation v-if="account.capabilities.consumeResetCredit" :key="account.id" :account-id="account.id" />
|
||||
|
||||
<section v-if="ambiguous" class="flex items-start gap-3 rounded-cp bg-cp-warning-container px-4 py-3.5" role="status">
|
||||
<AlertTriangle class="mt-0.5 size-4 shrink-0 text-cp-warning-on-container" />
|
||||
<section v-if="ambiguous" class="flex items-start gap-3 rounded-z bg-z-warning-container px-4 py-3.5" role="status">
|
||||
<AlertTriangle class="mt-0.5 size-4 shrink-0 text-z-warning-on-container" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="m-0 text-cp-sm font-heavy text-cp-warning-on-container">
|
||||
<p class="m-0 text-z-sm font-heavy text-z-warning-on-container">
|
||||
{{ t('accounts.quotaReset.operationUnconfirmed') }}
|
||||
</p>
|
||||
<p class="mt-1 mb-0 text-cp-xs leading-normal font-emphasis text-cp-text-secondary">
|
||||
<p class="mt-1 mb-0 text-z-xs leading-normal font-emphasis text-z-text-secondary">
|
||||
{{ t('accounts.quotaReset.confirmPrevious') }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -187,7 +187,7 @@ function handleRequestConsume(creditId: string) {
|
||||
|
||||
<section class="grid gap-3" :aria-label="t('accounts.quotaReset.availableResetCards')">
|
||||
<div class="flex items-center gap-2">
|
||||
<h3 class="m-0 min-w-0 flex-1 text-cp-sm font-heavy text-cp-text">
|
||||
<h3 class="m-0 min-w-0 flex-1 text-z-sm font-heavy text-z-text">
|
||||
{{ t('accounts.quotaReset.availableResetCards') }}
|
||||
</h3>
|
||||
<ZTag size="small" :type="hasSnapshot && availableCount > 0 ? 'success' : 'default'" role="status">
|
||||
@@ -210,7 +210,7 @@ function handleRequestConsume(creditId: string) {
|
||||
<div v-if="!loadError" :aria-busy="loading || consuming">
|
||||
<p
|
||||
v-if="loading && !hasSnapshot"
|
||||
class="m-0 px-4 py-4 text-cp-xs font-emphasis text-cp-text-secondary"
|
||||
class="m-0 px-4 py-4 text-z-xs font-emphasis text-z-text-secondary"
|
||||
role="status"
|
||||
>
|
||||
{{ t('accounts.quotaReset.queryingAvailableResetTimes') }}
|
||||
@@ -219,13 +219,13 @@ function handleRequestConsume(creditId: string) {
|
||||
<li
|
||||
v-for="credit in creditItems"
|
||||
:key="credit.id"
|
||||
class="flex items-center gap-4 rounded-cp bg-cp-fill-quaternary px-4 py-3.5"
|
||||
class="flex items-center gap-4 rounded-z bg-z-fill-quaternary px-4 py-3.5"
|
||||
>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="m-0 text-cp-sm leading-normal font-heavy wrap-anywhere text-cp-text">
|
||||
<p class="m-0 text-z-sm leading-normal font-heavy wrap-anywhere text-z-text">
|
||||
{{ credit.title }}
|
||||
</p>
|
||||
<p class="mt-1 mb-0 text-cp-xs leading-normal font-emphasis text-cp-text-secondary">
|
||||
<p class="mt-1 mb-0 text-z-xs leading-normal font-emphasis text-z-text-secondary">
|
||||
{{ credit.expiry }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -16,13 +16,13 @@ const nodes = computed(() => poolNodes(props.windows, props.pool?.accountCount))
|
||||
<PoolChart v-if="nodes.length" :nodes="nodes" />
|
||||
<div v-if="pool" class="flex min-h-5 min-w-0 items-center justify-between gap-x-3">
|
||||
<div class="flex shrink-0 items-baseline gap-1 whitespace-nowrap" :aria-label="`${pool.states.ready} / ${pool.accountCount} ${t('accounts.upstream.ready')}`">
|
||||
<strong class="font-mono text-cp-xs leading-[normal] font-heavy tabular-nums" :class="pool.states.ready > 0 ? 'text-cp-success-text' : 'text-cp-text'">{{ pool.states.ready }}</strong>
|
||||
<span class="font-mono text-cp-xs leading-[normal] tabular-nums text-cp-text-quaternary">/ {{ pool.accountCount }}</span>
|
||||
<strong class="font-mono text-z-xs leading-[normal] font-heavy tabular-nums" :class="pool.states.ready > 0 ? 'text-z-success-text' : 'text-z-text'">{{ pool.states.ready }}</strong>
|
||||
<span class="font-mono text-z-xs leading-[normal] tabular-nums text-z-text-quaternary">/ {{ pool.accountCount }}</span>
|
||||
</div>
|
||||
<div v-if="pool.plans.length" class="flex min-w-0 flex-wrap items-center justify-end gap-x-3 gap-y-1.5">
|
||||
<span v-for="plan in pool.plans" :key="plan.name" class="inline-flex min-w-0 items-center gap-1.5">
|
||||
<AccountPlanBadge :plan-type="plan.name" :plan-type-display="plan.name" size="xs" />
|
||||
<span class="font-mono text-cp-xs leading-[normal] font-emphasis tabular-nums text-cp-text-secondary">{{ plan.count }}</span>
|
||||
<span class="font-mono text-z-xs leading-[normal] font-emphasis tabular-nums text-z-text-secondary">{{ plan.count }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -43,16 +43,16 @@ const items = computed(() => orderedPanelQuotaWindows(props.windows)
|
||||
class="grid gap-3 sm:grid-cols-[minmax(0,1fr)_minmax(0,1fr)] sm:items-center sm:gap-5"
|
||||
>
|
||||
<div class="min-w-0">
|
||||
<h3 class="m-0 text-cp-sm font-heavy text-cp-text">
|
||||
<h3 class="m-0 text-z-sm font-heavy text-z-text">
|
||||
{{ item.label }}
|
||||
</h3>
|
||||
<p class="mt-1 mb-0 text-cp-xs leading-normal font-emphasis text-cp-text-secondary">
|
||||
<p class="mt-1 mb-0 text-z-xs leading-normal font-emphasis text-z-text-secondary">
|
||||
{{ t('accounts.quota.reset') }}<span class="font-mono tabular-nums">{{ item.resetAtDisplay }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex min-w-0 items-center gap-3">
|
||||
<div
|
||||
class="h-1.5 min-w-0 flex-1 overflow-hidden rounded-full bg-cp-border-secondary"
|
||||
class="h-1.5 min-w-0 flex-1 overflow-hidden rounded-full bg-z-border-secondary"
|
||||
role="progressbar"
|
||||
:aria-label="t('accounts.quota.remainingQuota', { label: item.label })"
|
||||
aria-valuemin="0"
|
||||
@@ -66,12 +66,12 @@ const items = computed(() => orderedPanelQuotaWindows(props.windows)
|
||||
:style="item.barStyle"
|
||||
/>
|
||||
</div>
|
||||
<span class="shrink-0 text-cp-xs font-heavy tabular-nums" :class="item.percentTextClass">
|
||||
<span class="shrink-0 text-z-xs font-heavy tabular-nums" :class="item.percentTextClass">
|
||||
{{ item.remainingDisplay }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="items.length === 0" class="m-0 text-cp-sm font-emphasis text-cp-text-secondary">
|
||||
<p v-if="items.length === 0" class="m-0 text-z-sm font-emphasis text-z-text-secondary">
|
||||
{{ t('accounts.quota.unobserved') }}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@@ -65,14 +65,14 @@ watch(hasPersonalInfo, (available) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="flex min-h-0 min-w-0 flex-col rounded-lg bg-cp-bg-container p-4 shadow-cp-tertiary">
|
||||
<section class="flex min-h-0 min-w-0 flex-col rounded-lg bg-z-bg-container p-4 shadow-z-tertiary">
|
||||
<div class="mb-3 grid shrink-0 grid-cols-[minmax(0,1fr)_auto] items-start gap-x-3 gap-y-1">
|
||||
<h3 class="m-0 text-cp-lg font-heavy text-cp-text">
|
||||
<h3 class="m-0 text-z-lg font-heavy text-z-text">
|
||||
{{ account.upstream?.discovered ? t('accounts.upstream.quota') : t('accounts.quota.accountLimit') }}
|
||||
</h3>
|
||||
<p
|
||||
v-if="account.capabilities.quota || quotaEntries.length > 0 || account.upstream?.discovered"
|
||||
class="col-span-2 row-start-2 m-0 flex min-w-0 items-center gap-1.5 text-cp-xs font-emphasis text-cp-text-secondary"
|
||||
class="col-span-2 row-start-2 m-0 flex min-w-0 items-center gap-1.5 text-z-xs font-emphasis text-z-text-secondary"
|
||||
>
|
||||
<span class="shrink-0 whitespace-nowrap">{{ t('accounts.quota.quota', { provider: formatProviderLabel(account.provider) }) }}</span>
|
||||
<template v-if="account.planType || account.authenticationKind === 'api_key'">
|
||||
|
||||
@@ -36,7 +36,7 @@ const hasQuotaWindow = computed(() => props.windows.some(window => typeof window
|
||||
<template #reference="{ open }">
|
||||
<button
|
||||
type="button"
|
||||
class="col-span-full grid w-full min-w-0 grid-cols-subgrid cursor-pointer rounded-sm border-0 bg-transparent p-0 text-left outline-none focus-visible:ring-2 focus-visible:ring-cp-control-outline"
|
||||
class="col-span-full grid w-full min-w-0 grid-cols-subgrid cursor-pointer rounded-sm border-0 bg-transparent p-0 text-left outline-none focus-visible:ring-2 focus-visible:ring-z-control-outline"
|
||||
:aria-label="t('accounts.quota.viewDetails', { detailTitle })"
|
||||
:aria-expanded="open"
|
||||
aria-haspopup="dialog"
|
||||
|
||||
@@ -32,7 +32,7 @@ const highestUsageWindow = computed(() => props.windows.reduce((highest, window)
|
||||
const highestUsageDisplay = computed(() => highestUsageWindow.value?.usedPercentDisplay ?? '—')
|
||||
const highestUsageTextClass = computed(() => highestUsageWindow.value
|
||||
? quotaWindowPresentation(highestUsageWindow.value, '2px').percentTextClass
|
||||
: 'text-cp-text-quaternary')
|
||||
: 'text-z-text-quaternary')
|
||||
|
||||
const trackGridStyle = computed(() => ({
|
||||
gridTemplateColumns: `repeat(${Math.max(windowItems.value.length, 1)}, minmax(0, 1fr))`,
|
||||
@@ -42,7 +42,7 @@ const trackGridStyle = computed(() => ({
|
||||
<template>
|
||||
<div class="col-span-full grid min-w-0 grid-cols-subgrid gap-y-1.5">
|
||||
<div class="col-span-full grid min-w-0 grid-cols-subgrid items-baseline text-[10px] leading-3 font-bold">
|
||||
<span class="min-w-0 truncate text-cp-text-quaternary">
|
||||
<span class="min-w-0 truncate text-z-text-quaternary">
|
||||
{{ label }}
|
||||
</span>
|
||||
<strong
|
||||
@@ -58,7 +58,7 @@ const trackGridStyle = computed(() => ({
|
||||
<div
|
||||
v-for="item in windowItems"
|
||||
:key="item.key"
|
||||
class="h-1 w-full overflow-hidden rounded-full bg-cp-border-secondary"
|
||||
class="h-1 w-full overflow-hidden rounded-full bg-z-border-secondary"
|
||||
role="progressbar"
|
||||
:aria-label="item.labelTooltip"
|
||||
aria-valuemin="0"
|
||||
|
||||
@@ -50,7 +50,7 @@ const summaryValue = computed(() => {
|
||||
})
|
||||
const currentUsageTextClass = computed(() => currentUsageWindow.value
|
||||
? quotaWindowPresentation(currentUsageWindow.value, '2px').percentTextClass
|
||||
: 'text-cp-text-quaternary')
|
||||
: 'text-z-text-quaternary')
|
||||
const additionalEntryCount = computed(() => Math.max(summaryEntries.value.length - 1, 0))
|
||||
</script>
|
||||
|
||||
@@ -65,16 +65,16 @@ const additionalEntryCount = computed(() => Math.max(summaryEntries.value.length
|
||||
class="flex min-w-0 items-baseline gap-1 font-mono tabular-nums"
|
||||
:title="summaryValue.title"
|
||||
>
|
||||
<strong class="truncate text-cp-xs font-heavy text-cp-text">
|
||||
<strong class="truncate text-z-xs font-heavy text-z-text">
|
||||
{{ summaryValue.value }}
|
||||
</strong>
|
||||
<span class="shrink-0 text-[9px] font-emphasis tracking-[0.02em] text-cp-text-quaternary">
|
||||
<span class="shrink-0 text-[9px] font-emphasis tracking-[0.02em] text-z-text-quaternary">
|
||||
{{ summaryValue.label }}
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
v-if="currentUsageWindow"
|
||||
class="ml-auto flex shrink-0 items-baseline gap-1 text-[9px] font-emphasis text-cp-text-quaternary"
|
||||
class="ml-auto flex shrink-0 items-baseline gap-1 text-[9px] font-emphasis text-z-text-quaternary"
|
||||
:title="t('accounts.quota.usageRateHelp')"
|
||||
>
|
||||
<span>{{ t('accounts.quota.usageRate') }}</span>
|
||||
@@ -100,7 +100,7 @@ const additionalEntryCount = computed(() => Math.max(summaryEntries.value.length
|
||||
</div>
|
||||
<span
|
||||
v-if="additionalEntryCount > 0"
|
||||
class="grid h-5 min-w-5 shrink-0 place-items-center rounded-cp bg-cp-fill-quaternary px-1.5 font-mono text-[9px] font-heavy tabular-nums text-cp-text-tertiary"
|
||||
class="grid h-5 min-w-5 shrink-0 place-items-center rounded-z bg-z-fill-quaternary px-1.5 font-mono text-[9px] font-heavy tabular-nums text-z-text-tertiary"
|
||||
:title="t('accounts.quota.moreGroups', { count: additionalEntryCount })"
|
||||
>
|
||||
+{{ additionalEntryCount }}
|
||||
|
||||
@@ -66,14 +66,14 @@ const presentation = computed(() =>
|
||||
>
|
||||
<span
|
||||
v-if="variant === 'pill'"
|
||||
class="inline-flex h-7 shrink-0 items-center rounded-full px-2.5 text-cp-sm leading-none font-heavy transition-colors duration-150 motion-reduce:transition-none"
|
||||
class="inline-flex h-7 shrink-0 items-center rounded-full px-2.5 text-z-sm leading-none font-heavy transition-colors duration-150 motion-reduce:transition-none"
|
||||
:class="presentation.statusStyle.text"
|
||||
>
|
||||
{{ presentation.label }}
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
class="inline-flex items-center gap-1.5 text-cp-sm leading-none font-emphasis transition-colors duration-150 motion-reduce:transition-none"
|
||||
class="inline-flex items-center gap-1.5 text-z-sm leading-none font-emphasis transition-colors duration-150 motion-reduce:transition-none"
|
||||
:class="presentation.statusStyle.text"
|
||||
>
|
||||
<span class="size-1.5 rounded-full" :class="presentation.statusStyle.dot" />
|
||||
@@ -82,20 +82,20 @@ const presentation = computed(() =>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<section class="w-88 overflow-hidden rounded-cp-lg">
|
||||
<header class="flex items-start gap-3 bg-cp-popover-header-bg px-4 py-3">
|
||||
<section class="w-88 overflow-hidden rounded-z-lg">
|
||||
<header class="flex items-start gap-3 bg-z-popover-header-bg px-4 py-3">
|
||||
<span
|
||||
class="inline-flex size-9 shrink-0 items-center justify-center rounded-cp"
|
||||
class="inline-flex size-9 shrink-0 items-center justify-center rounded-z"
|
||||
:class="presentation.statusStyle.icon"
|
||||
>
|
||||
<component :is="presentation.icon" class="size-4.5" />
|
||||
</span>
|
||||
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="m-0 text-cp-xs leading-none font-heavy tracking-[0.08em] text-cp-text-tertiary">
|
||||
<p class="m-0 text-z-xs leading-none font-heavy tracking-[0.08em] text-z-text-tertiary">
|
||||
{{ t('accounts.status.accountStatus') }}
|
||||
</p>
|
||||
<h3 class="mt-1.5 mb-0 text-cp-lg leading-5 font-heavy text-cp-text text-balance">
|
||||
<h3 class="mt-1.5 mb-0 text-z-lg leading-5 font-heavy text-z-text text-balance">
|
||||
{{ presentation.title }}
|
||||
</h3>
|
||||
</div>
|
||||
@@ -108,14 +108,14 @@ const presentation = computed(() =>
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<div class="grid gap-3 px-4 py-3 text-cp-sm leading-5">
|
||||
<p class="m-0 text-pretty font-emphasis text-cp-text-secondary">
|
||||
<div class="grid gap-3 px-4 py-3 text-z-sm leading-5">
|
||||
<p class="m-0 text-pretty font-emphasis text-z-text-secondary">
|
||||
{{ presentation.description }}
|
||||
</p>
|
||||
|
||||
<div v-if="presentation.protectionRules.length" class="grid gap-1.5">
|
||||
<span class="text-cp-xs font-emphasis text-cp-text-tertiary">{{ t('accounts.status.matchedProtectionRules') }}</span>
|
||||
<ul class="m-0 list-none space-y-1 p-0 text-cp-text">
|
||||
<span class="text-z-xs font-emphasis text-z-text-tertiary">{{ t('accounts.status.matchedProtectionRules') }}</span>
|
||||
<ul class="m-0 list-none space-y-1 p-0 text-z-text">
|
||||
<li v-for="rule in presentation.protectionRules" :key="rule.id" class="wrap-anywhere">
|
||||
{{ rule.name }}
|
||||
</li>
|
||||
@@ -124,42 +124,42 @@ const presentation = computed(() =>
|
||||
|
||||
<div
|
||||
v-if="presentation.rateLimitRecovery"
|
||||
class="flex items-center justify-between gap-3 rounded-cp bg-cp-fill-quaternary px-3 py-2"
|
||||
class="flex items-center justify-between gap-3 rounded-z bg-z-fill-quaternary px-3 py-2"
|
||||
>
|
||||
<span class="font-heavy text-cp-text-tertiary">{{ presentation.recoveryTimeLabel }}</span>
|
||||
<span class="font-mono font-emphasis tabular-nums text-cp-text">
|
||||
<span class="font-heavy text-z-text-tertiary">{{ presentation.recoveryTimeLabel }}</span>
|
||||
<span class="font-mono font-emphasis tabular-nums text-z-text">
|
||||
{{ presentation.rateLimitRecovery }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="presentation.nextRefreshDisplay"
|
||||
class="flex items-center justify-between gap-3 rounded-cp bg-cp-fill-quaternary px-3 py-2"
|
||||
class="flex items-center justify-between gap-3 rounded-z bg-z-fill-quaternary px-3 py-2"
|
||||
>
|
||||
<span class="font-heavy text-cp-text-tertiary">{{ t('accounts.status.tryNextTime') }}</span>
|
||||
<span class="font-mono font-emphasis tabular-nums text-cp-text">
|
||||
<span class="font-heavy text-z-text-tertiary">{{ t('accounts.status.tryNextTime') }}</span>
|
||||
<span class="font-mono font-emphasis tabular-nums text-z-text">
|
||||
{{ presentation.nextRefreshDisplay }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="rounded-cp bg-cp-fill-quaternary px-3 py-2.5">
|
||||
<p class="m-0 text-cp-xs leading-none font-heavy text-cp-text-tertiary">
|
||||
<div class="rounded-z bg-z-fill-quaternary px-3 py-2.5">
|
||||
<p class="m-0 text-z-xs leading-none font-heavy text-z-text-tertiary">
|
||||
{{ t('accounts.status.recommendedAction') }}
|
||||
</p>
|
||||
<p class="mt-1.5 mb-0 text-pretty font-emphasis text-cp-text">
|
||||
<p class="mt-1.5 mb-0 text-pretty font-emphasis text-z-text">
|
||||
{{ presentation.recoveryHint }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="presentation.errorText" class="overflow-hidden rounded-cp bg-cp-fill-tertiary">
|
||||
<div v-if="presentation.errorText" class="overflow-hidden rounded-z bg-z-fill-tertiary">
|
||||
<div class="flex items-center justify-between gap-3 px-3 pt-2.5 pb-1.5">
|
||||
<span class="text-cp-xs leading-none font-heavy text-cp-text-tertiary">{{ t('accounts.status.upstreamOriginalFeedback') }}</span>
|
||||
<span class="shrink-0 text-[10px] leading-none font-emphasis text-cp-text-quaternary">{{ t('accounts.status.forTroubleshootingOnly') }}</span>
|
||||
<span class="text-z-xs leading-none font-heavy text-z-text-tertiary">{{ t('accounts.status.upstreamOriginalFeedback') }}</span>
|
||||
<span class="shrink-0 text-[10px] leading-none font-emphasis text-z-text-quaternary">{{ t('accounts.status.forTroubleshootingOnly') }}</span>
|
||||
</div>
|
||||
<ZScrollbar class="bg-cp-fill-quaternary" max-height="124px">
|
||||
<ZScrollbar class="bg-z-fill-quaternary" max-height="124px">
|
||||
<div class="px-3 py-2">
|
||||
<pre
|
||||
class="m-0 whitespace-pre-wrap wrap-break-word font-mono text-cp-xs leading-[1.55] font-emphasis text-cp-text-secondary"
|
||||
class="m-0 whitespace-pre-wrap wrap-break-word font-mono text-z-xs leading-[1.55] font-emphasis text-z-text-secondary"
|
||||
>{{ presentation.errorText }}</pre>
|
||||
</div>
|
||||
</ZScrollbar>
|
||||
|
||||
@@ -58,34 +58,34 @@ interface AccountStatusPresentationInput {
|
||||
|
||||
const statusStyles: Record<AccountStatusTone, AccountStatusStyle> = {
|
||||
success: {
|
||||
text: 'text-cp-success-text',
|
||||
dot: 'bg-cp-success',
|
||||
badge: 'bg-cp-success-container text-cp-success-on-container',
|
||||
icon: 'bg-cp-success-container text-cp-success-on-container',
|
||||
text: 'text-z-success-text',
|
||||
dot: 'bg-z-success',
|
||||
badge: 'bg-z-success-container text-z-success-on-container',
|
||||
icon: 'bg-z-success-container text-z-success-on-container',
|
||||
},
|
||||
danger: {
|
||||
text: 'text-cp-error-text',
|
||||
dot: 'bg-cp-error',
|
||||
badge: 'bg-cp-error-container text-cp-error-on-container',
|
||||
icon: 'bg-cp-error-container text-cp-error-on-container',
|
||||
text: 'text-z-error-text',
|
||||
dot: 'bg-z-error',
|
||||
badge: 'bg-z-error-container text-z-error-on-container',
|
||||
icon: 'bg-z-error-container text-z-error-on-container',
|
||||
},
|
||||
warning: {
|
||||
text: 'text-cp-warning-text',
|
||||
dot: 'bg-cp-warning',
|
||||
badge: 'bg-cp-warning-container text-cp-warning-on-container',
|
||||
icon: 'bg-cp-warning-container text-cp-warning-on-container',
|
||||
text: 'text-z-warning-text',
|
||||
dot: 'bg-z-warning',
|
||||
badge: 'bg-z-warning-container text-z-warning-on-container',
|
||||
icon: 'bg-z-warning-container text-z-warning-on-container',
|
||||
},
|
||||
info: {
|
||||
text: 'text-cp-info-text',
|
||||
dot: 'bg-cp-info',
|
||||
badge: 'bg-cp-info-container text-cp-info-on-container',
|
||||
icon: 'bg-cp-info-container text-cp-info-on-container',
|
||||
text: 'text-z-info-text',
|
||||
dot: 'bg-z-info',
|
||||
badge: 'bg-z-info-container text-z-info-on-container',
|
||||
icon: 'bg-z-info-container text-z-info-on-container',
|
||||
},
|
||||
normal: {
|
||||
text: 'text-cp-text-secondary',
|
||||
dot: 'bg-cp-text-quaternary',
|
||||
badge: 'bg-cp-fill-quaternary text-cp-text-secondary',
|
||||
icon: 'bg-cp-fill-quaternary text-cp-text-secondary',
|
||||
text: 'text-z-text-secondary',
|
||||
dot: 'bg-z-text-quaternary',
|
||||
badge: 'bg-z-fill-quaternary text-z-text-secondary',
|
||||
icon: 'bg-z-fill-quaternary text-z-text-secondary',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import type { FormRules } from '@codex-proxy/ui'
|
||||
import type { ApiKeyAccountForm } from '../utils/upstreamApiKey'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
export function useAccountApiKeyFormRules(editing = false) {
|
||||
const { t } = useI18n({ useScope: 'global' })
|
||||
return computed<FormRules<{ apiKey: ApiKeyAccountForm }>>(() => ({
|
||||
'apiKey.name': {
|
||||
required: !editing,
|
||||
whitespace: true,
|
||||
trigger: ['blur', 'change'],
|
||||
message: t('accounts.credentials.nameRequired'),
|
||||
},
|
||||
'apiKey.transport': {
|
||||
required: true,
|
||||
trigger: 'change',
|
||||
message: t('accounts.credentials.validation.transportRequired'),
|
||||
},
|
||||
'apiKey.base_url': [
|
||||
{
|
||||
required: true,
|
||||
whitespace: true,
|
||||
trigger: ['blur', 'change'],
|
||||
message: t('accounts.credentials.validation.urlRequired'),
|
||||
},
|
||||
{
|
||||
max: 2048,
|
||||
transform: value => value.trim(),
|
||||
trigger: ['blur', 'change'],
|
||||
message: t('accounts.credentials.validation.urlLength'),
|
||||
},
|
||||
{
|
||||
trigger: ['blur', 'change'],
|
||||
validator: (_rule, value) => {
|
||||
try {
|
||||
const url = new URL(value)
|
||||
return ['https:', 'http:'].includes(url.protocol) && !!url.hostname
|
||||
&& !url.username && !url.password && !url.search && !url.hash
|
||||
}
|
||||
catch {
|
||||
return false
|
||||
}
|
||||
},
|
||||
message: t('accounts.credentials.validation.urlFormat'),
|
||||
},
|
||||
],
|
||||
'apiKey.apiKey': [
|
||||
{
|
||||
required: !editing,
|
||||
trigger: ['blur', 'change'],
|
||||
message: t('accounts.credentials.apiKeyRequired'),
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
pattern: /^[\x21-\x7E]+$/,
|
||||
max: 16384,
|
||||
trigger: ['blur', 'change'],
|
||||
message: t('accounts.credentials.validation.apiKeyFormat'),
|
||||
},
|
||||
],
|
||||
}))
|
||||
}
|
||||
@@ -6,8 +6,7 @@ import { batchUpdateAccounts } from '@/api'
|
||||
import { useAsyncAction } from '@/composables/useAsyncAction'
|
||||
import { t } from '@/i18n'
|
||||
import { notification as ZNotification } from '@/utils/notification'
|
||||
import { accountModelAccessError } from '../utils/modelAccess'
|
||||
import { concurrencyLimitInput, parseAccountSchedulingForm } from '../utils/schedulingForm'
|
||||
import { accountSchedulingValues, concurrencyLimitInput } from '../utils/schedulingForm'
|
||||
|
||||
export function useAccountBatchEditor(options: {
|
||||
accounts: Ref<Account[]>
|
||||
@@ -67,26 +66,11 @@ export function useAccountBatchEditor(options: {
|
||||
showBatchEditModal.value = false
|
||||
return
|
||||
}
|
||||
const modelError = accountModelAccessError(modelAccess.value)
|
||||
if (modelError) {
|
||||
ZNotification.warning({ message: modelError })
|
||||
return
|
||||
}
|
||||
const scheduling = parseAccountSchedulingForm(editedFields.value.has('concurrencyLimit')
|
||||
const scheduling = accountSchedulingValues(editedFields.value.has('concurrencyLimit')
|
||||
? concurrencyLimit.value
|
||||
: '', editedFields.value.has('weight')
|
||||
? weight.value
|
||||
: '1')
|
||||
if (proxyMode.value === 'proxy' && !proxyId.value.trim()) {
|
||||
ZNotification.warning({
|
||||
message: t('accounts.batchEdit.proxyRequired'),
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!scheduling.valid) {
|
||||
ZNotification.warning({ message: scheduling.message })
|
||||
return
|
||||
}
|
||||
|
||||
await saveAction.run(async () => {
|
||||
const accountIds = selectedAccounts().map(account => account.id)
|
||||
@@ -100,10 +84,10 @@ export function useAccountBatchEditor(options: {
|
||||
: proxyId.value.trim(),
|
||||
enabled: schedulingEnabled.value,
|
||||
concurrencyLimit: editedFields.value.has('concurrencyLimit')
|
||||
? scheduling.values.concurrencyLimit
|
||||
? scheduling.concurrencyLimit
|
||||
: undefined,
|
||||
weight: editedFields.value.has('weight')
|
||||
? scheduling.values.weight
|
||||
? scheduling.weight
|
||||
: undefined,
|
||||
groupIds: editedFields.value.has('groupIds')
|
||||
? [
|
||||
@@ -156,6 +140,7 @@ export function useAccountBatchEditor(options: {
|
||||
concurrencyLimit,
|
||||
weight,
|
||||
modelAccess,
|
||||
editedFields,
|
||||
catalogAccountId,
|
||||
proxyMode,
|
||||
proxyId,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user