diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a30b29..50b45d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,12 +8,15 @@ - AI 网关端点列表恢复展示 OpenAI Chat Completions 接口(`POST /ai/v1/chat/completions`),标明支持流式与非流式调用 - 租户列表分页支持 10 / 20 / 50 / 100 条每页切换 +- 设置 · 关于新增「运行状态」面板:运行时长(随查看自增)与 CPU 均值 / 内存(堆)/ 存储占用 / Goroutines 四格指标,明暗主题与窄屏两列自适应;旧版后端缺运行时字段时面板自动隐藏 +- 代理列表新增「关联租户」管理:弹窗回显当前关联名单(支持点 ✕ 移除),多选增删保存即设置全集,选中已关联其他代理的租户时提示保存后改挂 ### Changed - 回传日志告警统一改由「通知管理 → 云端事件」分类推送,租户删除提示同步移除已废弃的告警规则清理项 - 总览页租户测活列表改为限高滚动,「查看全部租户」入口固定展示 - 系统日志、OCI 审计日志与配额表格取消固定高度,分页内容随页面完整展开 +- 代理列表「认证」列由用户名明文拼接改为「是 / 否」徽标,悬停「是」显示用户名(无用户名提示仅密码认证) ### Removed @@ -22,6 +25,7 @@ ### Fixed - 修复租户列表分页状态在页面重绘时丢失,以及筛选后停留在越界页码的问题 +- 修复「引导卷(GB)」等可清空数字输入框的步进箭头挤出输入框右缘的错位;清空图标改为悬浮在步进竖栏左侧,输入文本自动避让 ## [0.2.0] - 2026-07-12 diff --git a/src/api/mock.ts b/src/api/mock.ts index b56bfc6..305d0e4 100644 --- a/src/api/mock.ts +++ b/src/api/mock.ts @@ -113,8 +113,9 @@ export const mockConfigs: OciConfig[] = [ promotionStatus: 'EXPIRED', multiRegion: true, multiCompartment: true, - proxyId: null, - proxyName: '', + // 与 mockProxies[0].usedBy = 1 呼应:主号经东京 socks5 出站 + proxyId: 1, + proxyName: '东京 socks5', }), cfg({ id: 2, diff --git a/src/api/proxies.ts b/src/api/proxies.ts index 29e57c1..e339015 100644 --- a/src/api/proxies.ts +++ b/src/api/proxies.ts @@ -1,4 +1,4 @@ -import { mockProxies } from './mock' +import { mockConfigs, mockProxies } from './mock' import { mockOn, mocked, request } from './request' import type { ProxyImportResult, ProxyInfo, ProxyInput } from '@/types/api' @@ -78,6 +78,22 @@ export function importProxies(text: string): Promise { return request('/proxies/import', { method: 'POST', body: { text } }) } +/** 代理侧设置关联租户全集:选中即关联(含从其他代理改挂),未选中即解除 */ +export function setProxyTenants(id: number, ociConfigIds: number[]): Promise<{ usedBy: number }> { + if (mockOn) { + const found = mockProxies.find((p) => p.id === id) + if (!found) return Promise.reject(new Error('代理不存在')) + for (const c of mockConfigs) { + if (ociConfigIds.includes(c.id)) Object.assign(c, { proxyId: id, proxyName: found.name }) + else if (c.proxyId === id) Object.assign(c, { proxyId: null, proxyName: '' }) + } + // 改挂会影响其他代理的引用数,全量重算保持一致 + for (const p of mockProxies) p.usedBy = mockConfigs.filter((c) => c.proxyId === p.id).length + return mocked({ usedBy: found.usedBy }) + } + return request(`/proxies/${id}/tenants`, { method: 'PUT', body: { ociConfigIds } }) +} + /** 手动重测出口地区,同步返回最新视图 */ export function probeProxy(id: number): Promise { if (mockOn) { diff --git a/src/api/settings.ts b/src/api/settings.ts index c574d7a..b8088ca 100644 --- a/src/api/settings.ts +++ b/src/api/settings.ts @@ -181,6 +181,17 @@ export function getAbout(): Promise { buildTime: '2026-07-09T12:00Z', goVersion: 'go1.26.4', platform: 'linux/amd64', + startedAt: '2026-07-01T08:23:00+08:00', + uptimeSeconds: 1053360, // 12 天 4 小时 36 分 + resources: { + cpuAvgPercent: 1.4, + numCpu: 4, + goroutines: 36, + memHeapBytes: 50537000, + memSysBytes: 101187000, + dbEngine: 'sqlite', + dbBytes: 13212000, + }, }) return request('/about') } diff --git a/src/assets/main.css b/src/assets/main.css index 0168434..0a30c7c 100644 --- a/src/assets/main.css +++ b/src/assets/main.css @@ -168,6 +168,19 @@ body { background: var(--color-wash); color: var(--color-ink); } +/* clearable 时 naive-ui 在 suffix 首位多渲染 .n-base-clear,会被上面的 + 2 行网格自动排进隐式第 2 列、把 26px 竖栏撑爆(箭头溢出框外)—— + 绝对定位使其脱离网格流,悬浮在竖栏左侧;文本区同步加宽避让清空图标 */ +.app-input-number .n-input__suffix .n-base-clear { + position: absolute; + right: 100%; + top: 50%; + transform: translateY(-50%); + margin-right: 5px; +} +.app-input-number .n-input:has(.n-base-clear) .n-input-wrapper { + padding-right: 52px; +} /* useToast 可展开详情:toast 内嵌代码块(标签 + 复制 + 内滚 pre)。 message 经 teleport 渲染于 body,组件 scoped 样式不可达,故置于全局。 */ diff --git a/src/components/proxy/ProxyPanel.vue b/src/components/proxy/ProxyPanel.vue index 27e9a0a..0f07bc3 100644 --- a/src/components/proxy/ProxyPanel.vue +++ b/src/components/proxy/ProxyPanel.vue @@ -22,6 +22,8 @@ import { import AppInputNumber from '@/components/AppInputNumber.vue' import FootNote from '@/components/FootNote.vue' import FormField from '@/components/FormField.vue' +import ProxyTenantsModal from '@/components/proxy/ProxyTenantsModal.vue' +import StatusBadge from '@/components/StatusBadge.vue' import { useAsync } from '@/composables/useAsync' import type { ProxyImportResult, ProxyInfo } from '@/types/api' import { useToast } from '@/composables/useToast' @@ -120,6 +122,15 @@ async function remove(row: ProxyInfo) { } } +// ---- 关联租户管理 ---- +const showTenants = ref(false) +const tenantsFor = ref(null) + +function openTenants(row: ProxyInfo) { + tenantsFor.value = row + showTenants.value = true +} + // ---- 手动重测出口地区 ---- const probingIds = ref(new Set()) @@ -207,13 +218,15 @@ const columns: DataTableColumns = [ { title: '认证', key: 'username', - width: 110, - render: (row) => - h( - 'span', - { class: 'text-[12.5px] text-ink-2' }, - row.username ? `${row.username}${row.passwordSet ? ' / ●●●' : ''}` : row.passwordSet ? '●●●' : '无', - ), + width: 76, + render: (row) => { + const has = !!row.username || row.passwordSet + // 悬停「是」可见用户名(无用户名则提示仅密码) + const title = has ? (row.username ? `用户名 ${row.username}` : '仅密码认证') : undefined + return h('span', { title }, [ + h(StatusBadge, { kind: has ? 'run' : 'check', label: has ? '是' : '否', dot: false }), + ]) + }, }, { title: '关联租户', @@ -225,7 +238,7 @@ const columns: DataTableColumns = [ { title: '操作', key: 'actions', - width: 168, + width: 236, render: (row) => h('div', { class: 'flex items-center gap-1' }, [ h( @@ -233,6 +246,7 @@ const columns: DataTableColumns = [ { size: 'tiny', quaternary: true, loading: probingIds.value.has(row.id), onClick: () => probe(row) }, () => '重测', ), + h(NButton, { size: 'tiny', quaternary: true, onClick: () => openTenants(row) }, () => '关联租户'), h(NButton, { size: 'tiny', quaternary: true, onClick: () => openEdit(row) }, () => '编辑'), h( NPopconfirm, @@ -261,6 +275,12 @@ const columns: DataTableColumns = [ 支持 SOCKS5 / HTTP / HTTPS;密码 AES 加密存储、不回显明文;地区为经代理实测的出口位置(ip-api.com),创建后自动检测;被租户关联的代理不可删除,请先解除关联 + + +import { NButton, NModal } from 'naive-ui' +import { computed, ref, watch } from 'vue' + +import { listConfigs } from '@/api/configs' +import { setProxyTenants } from '@/api/proxies' +import FormField from '@/components/FormField.vue' +import TenantPicker from '@/components/TenantPicker.vue' +import { useAsync } from '@/composables/useAsync' +import type { ProxyInfo } from '@/types/api' +import { useToast } from '@/composables/useToast' + +/** 代理侧关联租户管理:打开时回显当前关联名单,多选增删,保存即设置全集 + * (选中即关联,含从其他代理改挂;取消选中即解除)。 */ +const props = defineProps<{ show: boolean; proxy: ProxyInfo | null }>() +const emit = defineEmits<{ 'update:show': [boolean]; saved: [] }>() + +const message = useToast() +const configs = useAsync(listConfigs, false) +const selected = ref([]) +const saving = ref(false) + +watch( + () => props.show, + async (v) => { + if (!v || !props.proxy) return + selected.value = [] + await configs.run() + const pid = props.proxy.id + selected.value = (configs.data.value ?? []).filter((c) => c.proxyId === pid).map((c) => c.id) + }, +) + +const all = computed(() => configs.data.value ?? []) + +/** 当前选中名单(打开时即已关联名单),chips 展示并支持点 × 移除 */ +const picked = computed(() => all.value.filter((c) => selected.value.includes(c.id))) + +/** 选中但已挂其他代理的租户:保存即改挂,给出警示 */ +const rebinding = computed(() => + picked.value.filter((c) => c.proxyId !== null && c.proxyId !== props.proxy?.id), +) + +function removeOne(id: number) { + selected.value = selected.value.filter((v) => v !== id) +} + +async function save() { + if (!props.proxy) return + saving.value = true + try { + const res = await setProxyTenants(props.proxy.id, selected.value) + message.success(`已更新关联,当前 ${res.usedBy} 个租户经此代理出站`) + emit('update:show', false) + emit('saved') + } catch (e) { + message.error(e instanceof Error ? e.message : '保存失败') + } finally { + saving.value = false + } +} + + + diff --git a/src/components/settings/AboutTab.vue b/src/components/settings/AboutTab.vue index a4794ef..dc31ebc 100644 --- a/src/components/settings/AboutTab.vue +++ b/src/components/settings/AboutTab.vue @@ -1,9 +1,10 @@