diff --git a/components.d.ts b/components.d.ts index e33237f..523721d 100644 --- a/components.d.ts +++ b/components.d.ts @@ -16,6 +16,7 @@ declare module 'vue' { ElButton: typeof import('element-plus/es')['ElButton'] ElCard: typeof import('element-plus/es')['ElCard'] ElContainer: typeof import('element-plus/es')['ElContainer'] + ElDialog: typeof import('element-plus/es')['ElDialog'] ElDivider: typeof import('element-plus/es')['ElDivider'] ElFooter: typeof import('element-plus/es')['ElFooter'] ElForm: typeof import('element-plus/es')['ElForm'] @@ -23,9 +24,15 @@ declare module 'vue' { ElHeader: typeof import('element-plus/es')['ElHeader'] ElIcon: typeof import('element-plus/es')['ElIcon'] ElInput: typeof import('element-plus/es')['ElInput'] + ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] ElMain: typeof import('element-plus/es')['ElMain'] ElMenu: typeof import('element-plus/es')['ElMenu'] ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] + ElOption: typeof import('element-plus/es')['ElOption'] + ElPagination: typeof import('element-plus/es')['ElPagination'] + ElSelect: typeof import('element-plus/es')['ElSelect'] + ElTable: typeof import('element-plus/es')['ElTable'] + ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] ElTag: typeof import('element-plus/es')['ElTag'] ElUpload: typeof import('element-plus/es')['ElUpload'] HelloWorld: typeof import('./src/components/HelloWorld.vue')['default'] @@ -33,6 +40,9 @@ declare module 'vue' { RouterView: typeof import('vue-router')['RouterView'] WelcomeItem: typeof import('./src/components/WelcomeItem.vue')['default'] } + export interface GlobalDirectives { + vLoading: typeof import('element-plus/es')['ElLoadingDirective'] + } } // For TSX support @@ -41,6 +51,7 @@ declare global { const ElButton: typeof import('element-plus/es')['ElButton'] const ElCard: typeof import('element-plus/es')['ElCard'] const ElContainer: typeof import('element-plus/es')['ElContainer'] + const ElDialog: typeof import('element-plus/es')['ElDialog'] const ElDivider: typeof import('element-plus/es')['ElDivider'] const ElFooter: typeof import('element-plus/es')['ElFooter'] const ElForm: typeof import('element-plus/es')['ElForm'] @@ -48,9 +59,15 @@ declare global { const ElHeader: typeof import('element-plus/es')['ElHeader'] const ElIcon: typeof import('element-plus/es')['ElIcon'] const ElInput: typeof import('element-plus/es')['ElInput'] + const ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] const ElMain: typeof import('element-plus/es')['ElMain'] const ElMenu: typeof import('element-plus/es')['ElMenu'] const ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] + const ElOption: typeof import('element-plus/es')['ElOption'] + const ElPagination: typeof import('element-plus/es')['ElPagination'] + const ElSelect: typeof import('element-plus/es')['ElSelect'] + const ElTable: typeof import('element-plus/es')['ElTable'] + const ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] const ElTag: typeof import('element-plus/es')['ElTag'] const ElUpload: typeof import('element-plus/es')['ElUpload'] const HelloWorld: typeof import('./src/components/HelloWorld.vue')['default'] diff --git a/index.html b/index.html index 9e5fc8f..025a60d 100644 --- a/index.html +++ b/index.html @@ -1,10 +1,10 @@ - + - Vite App + Chongming 平台
diff --git a/src/App.vue b/src/App.vue index aded721..5283bbb 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,87 +1,163 @@ \ No newline at end of file diff --git a/src/api/workerAdmin.ts b/src/api/workerAdmin.ts new file mode 100644 index 0000000..54da4df --- /dev/null +++ b/src/api/workerAdmin.ts @@ -0,0 +1,162 @@ +/** + * Worker 管理 API(手动封装,管理端点在网关 /admin/workers 下) + * + * 管理端点(重启)仅超级管理员可调用,故所有请求需携带管理员的 Bearer token。 + * 未列入自动生成 SDK(openapi 之后可重新 gen 合并),此处用原生 fetch 保持轻量。 + */ +const API_BASE: string = (import.meta.env.VITE_API_BASE_URL as string) || '' + +export interface WorkerInstance { + worker_type: string + worker_id: string + mode: 'inline' | 'external' + alive: boolean +} + +export interface AdminCmdReply { + status?: string + cmd?: string + worker_type?: string + worker_id?: string + exit_request?: string +} + +export interface RestartAllResult { + worker_id: string + ok: boolean + result?: AdminCmdReply + error?: string +} + +async function request(path: string, token: string, init: RequestInit = {}): Promise { + const res = await fetch(`${API_BASE}${path}`, { + ...init, + headers: { + 'Content-Type': 'application/json', + ...(token ? { Authorization: `Bearer ${token}` } : {}), + ...(init.headers || {}), + }, + }) + if (!res.ok) { + let detail = `HTTP ${res.status}` + try { + const body = await res.json() + if (body?.detail) { + detail = typeof body.detail === 'string' ? body.detail : JSON.stringify(body.detail) + } + } catch { + /* ignore non-JSON error body */ + } + throw new Error(detail) + } + return res.json() as Promise +} + +/** 盘点全部 Worker 实例 */ +export function getWorkers(token: string) { + return request<{ success: boolean; workers: WorkerInstance[] }>('/api/v1/admin/workers', token) +} + +/** 精细化重启单个 Worker 副本 */ +export function restartWorker(token: string, workerType: string, workerId: string) { + const qs = new URLSearchParams({ worker_type: workerType, worker_id: workerId }) + return request<{ success: boolean; result: AdminCmdReply }>( + `/api/v1/admin/workers/restart?${qs.toString()}`, + token, + { method: 'POST' }, + ) +} + +/** 重启某插件全部副本 */ +export function restartAll(token: string, workerType: string) { + return request<{ success: boolean; total: number; failed: number; results: RestartAllResult[] }>( + `/api/v1/admin/workers/${encodeURIComponent(workerType)}/restart`, + token, + { method: 'POST' }, + ) +} + +export interface ConfigUpdateResult { + total: number + failed: number + ok: boolean + results: Array<{ worker_id: string; ok: boolean; reply?: AdminCmdReply; error?: string }> + errors: string[] +} + +export interface AuthUser { + username: string + is_superuser: boolean +} + +export interface LoginResult { + success: boolean + result: string + message: string + token: string + timestamp: number +} + +/** 通过 auth 插件登录接口换取管理 Token(无需手动填 JWT,返回体只有 token,用户信息需从 JWT 解析) */ +export function login(username: string, password: string) { + return request('/api/v1/auth/login', '', { + method: 'POST', + body: JSON.stringify({ username, password }), + }) +} + +/** 从 JWT payload 解析用户名与超管标识(仅本地展示用,权限校验以后端为准) */ +export function parseJwt(token: string): AuthUser | null { + try { + const part = token.split('.')[1] + if (!part) return null + const b64 = part.replace(/-/g, '+').replace(/_/g, '/') + const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0)) + const payload = JSON.parse(new TextDecoder().decode(bytes)) as Record + return { + username: String(payload.username ?? 'unknown'), + is_superuser: !!payload.is_superuser, + } + } catch { + return null + } +} + +/** 读取某插件的现有运行时配置([workers.config]) */ +export function getWorkerConfig(token: string, workerType: string) { + return request<{ success: boolean; worker_type: string; config: Record }>( + `/api/v1/admin/workers/${encodeURIComponent(workerType)}/config`, + token, + ) +} + +/** 更新某插件的运行时配置([workers.config],路由配置除外),由 worker 写回并重启 */ +export function updateWorkerConfig(token: string, workerType: string, updates: Record) { + return request( + `/api/v1/admin/workers/${encodeURIComponent(workerType)}/config`, + token, + { method: 'POST', body: JSON.stringify({ updates }) }, + ) +} + +export interface ScaleResult { + success: boolean + result: { + worker_type: string + target: number + mode: 'inline' | 'external' + sent_to?: string + reply?: AdminCmdReply & { persist?: string } + } +} + +/** 调整某类型 worker 副本数量(扩缩容,缩容走优雅排空),结果含持久化信息 */ +export function scaleWorker(token: string, workerType: string, count: number) { + return request( + `/api/v1/admin/workers/${encodeURIComponent(workerType)}/scale`, + token, + { method: 'POST', body: JSON.stringify({ count }) }, + ) +} + +export const TOKEN_KEY = 'cm_admin_token' \ No newline at end of file diff --git a/src/assets/base.css b/src/assets/base.css index 8816868..09536d5 100644 --- a/src/assets/base.css +++ b/src/assets/base.css @@ -1,53 +1,39 @@ -/* color palette from */ -:root { - --vt-c-white: #ffffff; - --vt-c-white-soft: #f8f8f8; - --vt-c-white-mute: #f2f2f2; +/* ============================================================ + Chongming 深色主题基础样式 + 始终使用深色配色(html.dark 已启用),并微调 Element Plus + 深色变量以获得更统一的观感。 + ============================================================ */ - --vt-c-black: #181818; - --vt-c-black-soft: #222222; - --vt-c-black-mute: #282828; +:root, +html.dark { + /* 背景层级 */ + --cm-bg: #0d1117; /* 页面最底层 */ + --cm-bg-soft: #161b22; /* 卡片/面板 */ + --cm-bg-elevated: #1c2129; /* 悬浮/表头 */ + --cm-border: #2a303a; /* 分割线 */ + --cm-text: #e6edf3; /* 主文字 */ + --cm-text-muted: #8b949e; /* 次要文字 */ - --vt-c-indigo: #2c3e50; + /* Element Plus 深色变量覆盖:统一背景与文字 */ + --el-bg-color: #161b22; + --el-bg-color-page: #0d1117; + --el-bg-color-overlay: #1c2129; + --el-fill-color-blank: #161b22; + --el-fill-color: #21262d; + --el-fill-color-light: #262c34; + --el-fill-color-lighter: #2a303a; + --el-fill-color-lightest: #2f3640; + --el-border-color: #2a303a; + --el-border-color-light: #2a303a; + --el-border-color-lighter: #30363d; + --el-border-color-extra-light: #353b45; + --el-text-color-primary: #e6edf3; + --el-text-color-regular: #c9d1d9; + --el-text-color-secondary: #8b949e; + --el-text-color-placeholder: #6e7681; + --el-color-primary: #4f8cff; - --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); - --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); - --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); - --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); - - --vt-c-text-light-1: var(--vt-c-indigo); - --vt-c-text-light-2: rgba(60, 60, 60, 0.66); - --vt-c-text-dark-1: var(--vt-c-white); - --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); -} - -/* semantic color variables for this project */ -:root { - --color-background: var(--vt-c-white); - --color-background-soft: var(--vt-c-white-soft); - --color-background-mute: var(--vt-c-white-mute); - - --color-border: var(--vt-c-divider-light-2); - --color-border-hover: var(--vt-c-divider-light-1); - - --color-heading: var(--vt-c-text-light-1); - --color-text: var(--vt-c-text-light-1); - - --section-gap: 160px; -} - -@media (prefers-color-scheme: dark) { - :root { - --color-background: var(--vt-c-black); - --color-background-soft: var(--vt-c-black-soft); - --color-background-mute: var(--vt-c-black-mute); - - --color-border: var(--vt-c-divider-dark-2); - --color-border-hover: var(--vt-c-divider-dark-1); - - --color-heading: var(--vt-c-text-dark-1); - --color-text: var(--vt-c-text-dark-2); - } + color-scheme: dark; } *, @@ -58,29 +44,34 @@ font-weight: normal; } -body { +html, +body, +#app { min-height: 100vh; - color: var(--color-text); - background: var(--color-background); - transition: - color 0.5s, - background-color 0.5s; + width: 100%; +} + +body { + margin: 0; + color: var(--cm-text); + background: var(--cm-bg); + background-image: + radial-gradient(1200px 600px at 10% -10%, rgba(79, 140, 255, 0.10), transparent 60%), + radial-gradient(1000px 500px at 110% 10%, rgba(79, 140, 255, 0.06), transparent 55%); + background-attachment: fixed; line-height: 1.6; font-family: - Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, - Oxygen, - Ubuntu, - Cantarell, - 'Fira Sans', - 'Droid Sans', 'Helvetica Neue', + Arial, + 'PingFang SC', + 'Microsoft YaHei', sans-serif; font-size: 15px; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; -} +} \ No newline at end of file diff --git a/src/assets/main.css b/src/assets/main.css index 36fb845..e56c0cb 100644 --- a/src/assets/main.css +++ b/src/assets/main.css @@ -1,35 +1,32 @@ @import './base.css'; +/* 应用全局兜底:字号与滚动条 */ #app { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; font-weight: normal; } -a, -.green { +a { text-decoration: none; - color: hsla(160, 100%, 37%, 1); - transition: 0.4s; - padding: 3px; + color: var(--el-color-primary); + transition: 0.2s; } -@media (hover: hover) { - a:hover { - background-color: hsla(160, 100%, 37%, 0.2); - } +/* 全局深色细滚动条 */ +* { + scrollbar-width: thin; + scrollbar-color: var(--cm-border) transparent; } - -@media (min-width: 1024px) { - body { - display: flex; - place-items: center; - } - - #app { - display: grid; - grid-template-columns: 1fr 1fr; - padding: 0 2rem; - } +*::-webkit-scrollbar { + width: 10px; + height: 10px; } +*::-webkit-scrollbar-thumb { + background: var(--cm-border); + border-radius: 6px; +} +*::-webkit-scrollbar-thumb:hover { + background: #3d4550; +} +*::-webkit-scrollbar-track { + background: transparent; +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 24c59d3..6112174 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,6 +3,7 @@ import './assets/main.css' import { createApp } from 'vue' import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' +import 'element-plus/theme-chalk/dark/css-vars.css' import { createPinia } from 'pinia' import App from './App.vue' diff --git a/src/router/index.ts b/src/router/index.ts index 3349613..71d3116 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -17,6 +17,12 @@ const router = createRouter({ // which is lazy-loaded when the route is visited. component: () => import('../views/AboutView.vue'), }, + { + path: '/workers', + name: 'workers', + // 懒加载 Worker 管理页面(实例盘点 + 单/批量重启) + component: () => import('../views/WorkerView.vue'), + }, ], }) diff --git a/src/views/AboutView.vue b/src/views/AboutView.vue index 2f5e3a6..1cecfe0 100644 --- a/src/views/AboutView.vue +++ b/src/views/AboutView.vue @@ -1,16 +1,48 @@ - +.about-card { + border-radius: 12px; + border: 1px solid var(--cm-border); + background: var(--cm-bg-soft); +} +.card-header { + display: flex; + align-items: center; + gap: 8px; +} +.title { + font-weight: 700; + font-size: 17px; + color: var(--cm-text); +} +.about-card p { + margin-bottom: 12px; + color: var(--cm-text-regular, var(--cm-text)); + line-height: 1.8; +} +.about-card b { + color: var(--cm-text); +} + \ No newline at end of file diff --git a/src/views/DoubaoView.vue b/src/views/DoubaoView.vue index e706577..69edfa5 100644 --- a/src/views/DoubaoView.vue +++ b/src/views/DoubaoView.vue @@ -239,14 +239,15 @@ const copyResult = () => { max-width: 100%; max-height: 400px; border-radius: 8px; - border: 1px solid #dcdfe6; + border: 1px solid var(--cm-border); } .preview-actions { position: absolute; top: 8px; right: 8px; - background: rgba(255, 255, 255, 0.8); + background: rgba(22, 27, 34, 0.75); border-radius: 50%; + backdrop-filter: blur(4px); } .action-bar { margin-top: 16px; @@ -255,11 +256,12 @@ const copyResult = () => { flex-wrap: wrap; } .result-content { - background: #f5f7fa; + background: var(--el-fill-color-lighter); padding: 16px; border-radius: 8px; white-space: pre-wrap; word-break: break-word; line-height: 1.8; + color: var(--cm-text); } diff --git a/src/views/WorkerView.vue b/src/views/WorkerView.vue new file mode 100644 index 0000000..57bd00f --- /dev/null +++ b/src/views/WorkerView.vue @@ -0,0 +1,658 @@ + + + + + \ No newline at end of file