From 886c9c2fdbbcd8f1d5ba75fe0cb0f12b999a9df1 Mon Sep 17 00:00:00 2001 From: fatedier Date: Sat, 31 Jan 2026 12:43:31 +0800 Subject: [PATCH] web/frpc: redesign dashboard (#5145) --- web/frpc/components.d.ts | 10 +- web/frpc/src/App.vue | 358 +++++++++---------- web/frpc/src/api/http.ts | 60 ++-- web/frpc/src/assets/css/custom.css | 88 +++-- web/frpc/src/assets/css/dark.css | 132 ++++++- web/frpc/src/assets/icons/logo.svg | 15 + web/frpc/src/components/ProxyCard.vue | 236 ++++++++++++ web/frpc/src/components/StatCard.vue | 202 +++++++++++ web/frpc/src/main.ts | 2 +- web/frpc/src/router/index.ts | 2 +- web/frpc/src/types/proxy.ts | 16 +- web/frpc/src/utils/format.ts | 2 +- web/frpc/src/views/ClientConfigure.vue | 358 +++++++++++++++++-- web/frpc/src/views/Overview.vue | 475 ++++++++++++++++++------- web/frpc/vite.config.mts | 6 +- web/frps/components.d.ts | 7 - web/frps/src/App.vue | 12 + 17 files changed, 1540 insertions(+), 441 deletions(-) create mode 100644 web/frpc/src/assets/icons/logo.svg create mode 100644 web/frpc/src/components/ProxyCard.vue create mode 100644 web/frpc/src/components/StatCard.vue diff --git a/web/frpc/components.d.ts b/web/frpc/components.d.ts index 08d9bce2..f9d4522a 100644 --- a/web/frpc/components.d.ts +++ b/web/frpc/components.d.ts @@ -9,16 +9,18 @@ declare module 'vue' { export interface GlobalComponents { ElButton: typeof import('element-plus/es')['ElButton'] ElCard: typeof import('element-plus/es')['ElCard'] + ElCol: typeof import('element-plus/es')['ElCol'] + ElEmpty: typeof import('element-plus/es')['ElEmpty'] + ElIcon: typeof import('element-plus/es')['ElIcon'] ElInput: typeof import('element-plus/es')['ElInput'] - ElMenu: typeof import('element-plus/es')['ElMenu'] - ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] + ElRow: typeof import('element-plus/es')['ElRow'] ElSwitch: typeof import('element-plus/es')['ElSwitch'] - ElTable: typeof import('element-plus/es')['ElTable'] - ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] ElTag: typeof import('element-plus/es')['ElTag'] ElTooltip: typeof import('element-plus/es')['ElTooltip'] + ProxyCard: typeof import('./src/components/ProxyCard.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] + StatCard: typeof import('./src/components/StatCard.vue')['default'] } export interface ComponentCustomProperties { vLoading: typeof import('element-plus/es')['ElLoadingDirective'] diff --git a/web/frpc/src/App.vue b/web/frpc/src/App.vue index 0746c980..2bfded92 100644 --- a/web/frpc/src/App.vue +++ b/web/frpc/src/App.vue @@ -1,42 +1,50 @@ \ No newline at end of file + diff --git a/web/frpc/src/api/http.ts b/web/frpc/src/api/http.ts index e9a22f8d..5f5b8c64 100644 --- a/web/frpc/src/api/http.ts +++ b/web/frpc/src/api/http.ts @@ -19,7 +19,11 @@ async function request(url: string, options: RequestInit = {}): Promise { const response = await fetch(url, { ...defaultOptions, ...options }) if (!response.ok) { - throw new HTTPError(response.status, response.statusText, `HTTP ${response.status}`) + throw new HTTPError( + response.status, + response.statusText, + `HTTP ${response.status}`, + ) } // Handle empty response (e.g. 204 No Content) @@ -35,42 +39,54 @@ async function request(url: string, options: RequestInit = {}): Promise { } export const http = { - get: (url: string, options?: RequestInit) => request(url, { ...options, method: 'GET' }), + get: (url: string, options?: RequestInit) => + request(url, { ...options, method: 'GET' }), post: (url: string, body?: any, options?: RequestInit) => { const headers: HeadersInit = { ...options?.headers } let requestBody = body - if (body && typeof body === 'object' && !(body instanceof FormData) && !(body instanceof Blob)) { - if (!('Content-Type' in headers)) { - (headers as any)['Content-Type'] = 'application/json' - } - requestBody = JSON.stringify(body) + if ( + body && + typeof body === 'object' && + !(body instanceof FormData) && + !(body instanceof Blob) + ) { + if (!('Content-Type' in headers)) { + ;(headers as any)['Content-Type'] = 'application/json' + } + requestBody = JSON.stringify(body) } - return request(url, { - ...options, - method: 'POST', + return request(url, { + ...options, + method: 'POST', headers, - body: requestBody + body: requestBody, }) }, put: (url: string, body?: any, options?: RequestInit) => { const headers: HeadersInit = { ...options?.headers } let requestBody = body - if (body && typeof body === 'object' && !(body instanceof FormData) && !(body instanceof Blob)) { - if (!('Content-Type' in headers)) { - (headers as any)['Content-Type'] = 'application/json' - } - requestBody = JSON.stringify(body) + if ( + body && + typeof body === 'object' && + !(body instanceof FormData) && + !(body instanceof Blob) + ) { + if (!('Content-Type' in headers)) { + ;(headers as any)['Content-Type'] = 'application/json' + } + requestBody = JSON.stringify(body) } - return request(url, { - ...options, - method: 'PUT', + return request(url, { + ...options, + method: 'PUT', headers, - body: requestBody + body: requestBody, }) }, - delete: (url: string, options?: RequestInit) => request(url, { ...options, method: 'DELETE' }), -} \ No newline at end of file + delete: (url: string, options?: RequestInit) => + request(url, { ...options, method: 'DELETE' }), +} diff --git a/web/frpc/src/assets/css/custom.css b/web/frpc/src/assets/css/custom.css index 6ff997a5..ed128996 100644 --- a/web/frpc/src/assets/css/custom.css +++ b/web/frpc/src/assets/css/custom.css @@ -1,32 +1,9 @@ -.el-form-item span { - margin-left: 15px; -} - -.proxy-table-expand { - font-size: 0; -} - -.proxy-table-expand .el-form-item__label{ - width: 90px; - color: #99a9bf; -} - -.proxy-table-expand .el-form-item { - margin-right: 0; - margin-bottom: 0; - width: 50%; -} - -.el-table .el-table__expanded-cell { - padding: 20px 50px; -} - -/* Modern styles */ +/* Modern Base Styles */ * { box-sizing: border-box; } -/* Smooth transitions */ +/* Smooth transitions for Element Plus components */ .el-button, .el-card, .el-input, @@ -37,7 +14,7 @@ /* Card hover effects */ .el-card:hover { - box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1); + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.08); } /* Better scrollbar */ @@ -60,16 +37,6 @@ background: #a8a8a8; } -/* Page headers */ -.el-page-header { - padding: 16px 0; -} - -.el-page-header__title { - font-size: 20px; - font-weight: 600; -} - /* Better form layouts */ .el-form-item { margin-bottom: 18px; @@ -87,3 +54,52 @@ padding-right: 10px !important; } } + +/* Input enhancements */ +.el-input__wrapper { + transition: all 0.2s ease; +} + +.el-input__wrapper:hover { + box-shadow: 0 0 0 1px var(--el-border-color-hover) inset; +} + +/* Button enhancements */ +.el-button { + font-weight: 500; +} + +/* Tag enhancements */ +.el-tag { + font-weight: 500; +} + +/* Card enhancements */ +.el-card__header { + padding: 16px 20px; + border-bottom: 1px solid var(--el-border-color-lighter); +} + +.el-card__body { + padding: 20px; +} + +/* Table enhancements */ +.el-table { + font-size: 14px; +} + +.el-table th { + font-weight: 600; +} + +/* Empty state */ +.el-empty__description { + margin-top: 16px; + font-size: 14px; +} + +/* Loading state */ +.el-loading-mask { + border-radius: 12px; +} diff --git a/web/frpc/src/assets/css/dark.css b/web/frpc/src/assets/css/dark.css index 557e7829..7c118fc3 100644 --- a/web/frpc/src/assets/css/dark.css +++ b/web/frpc/src/assets/css/dark.css @@ -1,11 +1,14 @@ +/* Dark Mode Theme */ html.dark { --el-bg-color: #1e1e2e; + --el-bg-color-page: #1a1a2e; + --el-bg-color-overlay: #27293d; --el-fill-color-blank: #1e1e2e; - background-color: #1e1e2e; + background-color: #1a1a2e; } html.dark body { - background-color: #1e1e2e; + background-color: #1a1a2e; color: #e5e7eb; } @@ -28,23 +31,50 @@ html.dark .el-card { border-color: #3a3d5c; } +html.dark .el-card__header { + border-bottom-color: #3a3d5c; +} + /* Dark mode inputs */ html.dark .el-input__wrapper { background-color: #27293d; - border-color: #3a3d5c; + box-shadow: 0 0 0 1px #3a3d5c inset; +} + +html.dark .el-input__wrapper:hover { + box-shadow: 0 0 0 1px #4a4d6c inset; +} + +html.dark .el-input__wrapper.is-focus { + box-shadow: 0 0 0 1px var(--el-color-primary) inset; } html.dark .el-input__inner { color: #e5e7eb; } +html.dark .el-input__inner::placeholder { + color: #6b7280; +} + +/* Dark mode textarea */ +html.dark .el-textarea__inner { + background-color: #1e1e2d; + border-color: #3a3d5c; + color: #e5e7eb; +} + +html.dark .el-textarea__inner::placeholder { + color: #6b7280; +} + /* Dark mode table */ html.dark .el-table { background-color: #27293d; color: #e5e7eb; } -html.dark .el-table th { +html.dark .el-table th.el-table__cell { background-color: #1e1e2e; color: #e5e7eb; } @@ -53,6 +83,98 @@ html.dark .el-table tr { background-color: #27293d; } -html.dark .el-table--striped .el-table__body tr.el-table__row--striped td { +html.dark .el-table--striped .el-table__body tr.el-table__row--striped td.el-table__cell { background-color: #1e1e2e; } + +html.dark .el-table__row:hover > td.el-table__cell { + background-color: #2a2a3c !important; +} + +/* Dark mode tags */ +html.dark .el-tag--info { + background-color: #3a3d5c; + border-color: #3a3d5c; + color: #e5e7eb; +} + +/* Dark mode buttons */ +html.dark .el-button--default { + background-color: #27293d; + border-color: #3a3d5c; + color: #e5e7eb; +} + +html.dark .el-button--default:hover { + background-color: #2a2a3c; + border-color: #4a4d6c; + color: #fff; +} + +/* Dark mode select */ +html.dark .el-select .el-input__wrapper { + background-color: #27293d; +} + +html.dark .el-select-dropdown { + background-color: #27293d; + border-color: #3a3d5c; +} + +html.dark .el-select-dropdown__item { + color: #e5e7eb; +} + +html.dark .el-select-dropdown__item:hover { + background-color: #2a2a3c; +} + +/* Dark mode dialog */ +html.dark .el-dialog { + background-color: #27293d; +} + +html.dark .el-dialog__header { + border-bottom-color: #3a3d5c; +} + +html.dark .el-dialog__title { + color: #e5e7eb; +} + +html.dark .el-dialog__body { + color: #e5e7eb; +} + +/* Dark mode message box */ +html.dark .el-message-box { + background-color: #27293d; + border-color: #3a3d5c; +} + +html.dark .el-message-box__title { + color: #e5e7eb; +} + +html.dark .el-message-box__message { + color: #e5e7eb; +} + +/* Dark mode empty */ +html.dark .el-empty__description { + color: #9ca3af; +} + +/* Dark mode loading */ +html.dark .el-loading-mask { + background-color: rgba(30, 30, 46, 0.9); +} + +html.dark .el-loading-text { + color: #e5e7eb; +} + +/* Dark mode tooltip */ +html.dark .el-tooltip__trigger { + color: #e5e7eb; +} diff --git a/web/frpc/src/assets/icons/logo.svg b/web/frpc/src/assets/icons/logo.svg new file mode 100644 index 00000000..fee4a82a --- /dev/null +++ b/web/frpc/src/assets/icons/logo.svg @@ -0,0 +1,15 @@ + + + + + + diff --git a/web/frpc/src/components/ProxyCard.vue b/web/frpc/src/components/ProxyCard.vue new file mode 100644 index 00000000..70246f8c --- /dev/null +++ b/web/frpc/src/components/ProxyCard.vue @@ -0,0 +1,236 @@ + + + + + diff --git a/web/frpc/src/components/StatCard.vue b/web/frpc/src/components/StatCard.vue new file mode 100644 index 00000000..7ceca5f6 --- /dev/null +++ b/web/frpc/src/components/StatCard.vue @@ -0,0 +1,202 @@ + + + + + diff --git a/web/frpc/src/main.ts b/web/frpc/src/main.ts index 4eb32bff..e8ef7195 100644 --- a/web/frpc/src/main.ts +++ b/web/frpc/src/main.ts @@ -10,4 +10,4 @@ const app = createApp(App) app.use(router) -app.mount('#app') \ No newline at end of file +app.mount('#app') diff --git a/web/frpc/src/router/index.ts b/web/frpc/src/router/index.ts index 426d555e..bf4feb5f 100644 --- a/web/frpc/src/router/index.ts +++ b/web/frpc/src/router/index.ts @@ -18,4 +18,4 @@ const router = createRouter({ ], }) -export default router \ No newline at end of file +export default router diff --git a/web/frpc/src/types/proxy.ts b/web/frpc/src/types/proxy.ts index d7e2d4bf..d768c3b8 100644 --- a/web/frpc/src/types/proxy.ts +++ b/web/frpc/src/types/proxy.ts @@ -1,12 +1,12 @@ export interface ProxyStatus { - name: string - type: string - status: string - err: string - local_addr: string - plugin: string - remote_addr: string - [key: string]: any + name: string + type: string + status: string + err: string + local_addr: string + plugin: string + remote_addr: string + [key: string]: any } export type StatusResponse = Record diff --git a/web/frpc/src/utils/format.ts b/web/frpc/src/utils/format.ts index e7e72fcf..11cd398f 100644 --- a/web/frpc/src/utils/format.ts +++ b/web/frpc/src/utils/format.ts @@ -28,6 +28,6 @@ export function formatFileSize(bytes: number): string { // Prevent index out of bounds for extremely large numbers const unit = sizes[i] || sizes[sizes.length - 1] const val = bytes / Math.pow(k, i) - + return parseFloat(val.toFixed(2)) + ' ' + unit } diff --git a/web/frpc/src/views/ClientConfigure.vue b/web/frpc/src/views/ClientConfigure.vue index 9972e1a3..c649b5c7 100644 --- a/web/frpc/src/views/ClientConfigure.vue +++ b/web/frpc/src/views/ClientConfigure.vue @@ -1,33 +1,120 @@ \ No newline at end of file + +.editor-wrapper { + position: relative; +} + +.code-editor :deep(.el-textarea__inner) { + font-family: + ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Monaco, Consolas, monospace; + font-size: 13px; + line-height: 1.6; + padding: 16px; + border-radius: 8px; + background: #f8f9fa; + border: 1px solid #e4e7ed; + resize: none; +} + +html.dark .code-editor :deep(.el-textarea__inner) { + background: #1e1e2d; + border-color: #3a3d5c; + color: #e5e7eb; +} + +.code-editor :deep(.el-textarea__inner:focus) { + border-color: var(--el-color-primary); + box-shadow: 0 0 0 1px var(--el-color-primary-light-5); +} + +/* Help Card */ +.help-content { + display: flex; + flex-direction: column; + gap: 20px; +} + +.help-section { + display: flex; + flex-direction: column; + gap: 10px; +} + +.help-section-title { + font-size: 13px; + font-weight: 600; + color: var(--el-text-color-primary); + margin: 0; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.help-items { + display: flex; + flex-direction: column; + gap: 8px; +} + +.help-item { + display: flex; + align-items: center; + gap: 12px; + padding: 8px 12px; + background: #f8f9fa; + border-radius: 6px; + font-size: 13px; +} + +html.dark .help-item { + background: #1e1e2d; +} + +.help-item code { + font-family: + ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Monaco, Consolas, monospace; + font-size: 12px; + color: var(--el-color-primary); + background: var(--el-color-primary-light-9); + padding: 2px 6px; + border-radius: 4px; + font-weight: 500; +} + +.help-item span { + color: var(--el-text-color-secondary); + flex: 1; +} + +.proxy-type-tags { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.code-example { + font-family: + ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Monaco, Consolas, monospace; + font-size: 12px; + line-height: 1.6; + padding: 12px; + background: #f8f9fa; + border-radius: 8px; + border: 1px solid #e4e7ed; + margin: 0; + overflow-x: auto; + white-space: pre; +} + +html.dark .code-example { + background: #1e1e2d; + border-color: #3a3d5c; + color: #e5e7eb; +} + +.docs-link { + display: flex; + align-items: center; + gap: 8px; + color: var(--el-color-primary); + text-decoration: none; + font-size: 14px; + font-weight: 500; + padding: 12px 16px; + background: var(--el-color-primary-light-9); + border-radius: 8px; + transition: all 0.2s; +} + +.docs-link:hover { + background: var(--el-color-primary-light-8); +} + +@media (max-width: 768px) { + .card-header { + flex-direction: column; + align-items: stretch; + } + + .header-left { + justify-content: space-between; + } + + .header-actions { + justify-content: flex-end; + } +} + +@media (max-width: 992px) { + .help-card { + margin-top: 20px; + } +} + diff --git a/web/frpc/src/views/Overview.vue b/web/frpc/src/views/Overview.vue index 1d7cef1f..2cdbfd93 100644 --- a/web/frpc/src/views/Overview.vue +++ b/web/frpc/src/views/Overview.vue @@ -1,92 +1,132 @@