From 02b763bfb69a934f95a7e4f4df760e1861bbfa96 Mon Sep 17 00:00:00 2001
From: chakcy <947105045@qq.com>
Date: Thu, 27 Aug 2026 14:45:10 +0800
Subject: [PATCH] =?UTF-8?q?modify:=20worker=20=E7=AE=A1=E7=90=86?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
components.d.ts | 17 +
index.html | 4 +-
src/App.vue | 172 +++++++---
src/api/workerAdmin.ts | 162 ++++++++++
src/assets/base.css | 111 +++----
src/assets/main.css | 45 ++-
src/main.ts | 1 +
src/router/index.ts | 6 +
src/views/AboutView.vue | 52 +++-
src/views/DoubaoView.vue | 8 +-
src/views/WorkerView.vue | 658 +++++++++++++++++++++++++++++++++++++++
11 files changed, 1089 insertions(+), 147 deletions(-)
create mode 100644 src/api/workerAdmin.ts
create mode 100644 src/views/WorkerView.vue
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 @@
-
关于页面
-
这里是后续扩展的示例页面。
+
+
+
+
+ Chongming 是配置驱动的微服务网关平台,基于 FastAPI + Zenoh + MQTT + Vue 3 构建。
+ Web 请求经网关路由到对应的 Worker 插件(RPC 层走 Eclipse Zenoh),支持分布式、多语言插件部署。
+
-
+.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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 登录
+
+
+
+
+
+
+
+ {{ currentUser?.username || '已登录' }}
+
+
+ 超级管理员
+
+ 非超管(操作将被拒绝)
+
+ 刷新列表
+
+
+ 重启选中 ({{ selected.length }})
+
+ 退出
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ row.worker_type }}
+
+
+
+
+
+
+ {{ row.mode }}
+
+
+
+
+
+
+ {{ row.alive ? '在线' : '离线' }}
+
+
+
+
+
+
+ 重启单副本
+
+
+
+
+
+
+
+
+
插件级批量操作
+
+
+
+
+ 重启该插件全部副本
+
+
+ 更新插件配置
+
+
+
+ 副本数量:当前 {{ workerType ? countByType(workerType) : '—' }}
+
+
+ 调整数量
+
+ 调小会优雅排空:先隔离新请求,等当前请求处理完再关闭对应副本;数量将写入配置,重启网关后保持。
+
+
+
+
+
+
+
+
+ {{ workerType || '-' }}
+
+
+ row && selectConfigEntry(row)"
+ style="width: 100%"
+ >
+
+
+
+
+ {{ row.type }}
+
+
+
+ 暂无 `[workers.config]` 配置项,可直接新增
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+
+ 下发并重启
+
+
+
+
+
+
+
+
+
\ No newline at end of file