diff --git a/monitor-web/index.html b/monitor-web/index.html index 3abe5bb..c2993d4 100644 --- a/monitor-web/index.html +++ b/monitor-web/index.html @@ -1,17 +1,13 @@ - + - - Vite App - + + + flying monitor
diff --git a/monitor-web/public/icon.svg b/monitor-web/public/icon.svg index 38a7469..f3b1193 100644 --- a/monitor-web/public/icon.svg +++ b/monitor-web/public/icon.svg @@ -1 +1,5 @@ - \ No newline at end of file + + + + + diff --git a/monitor-web/src/App.vue b/monitor-web/src/App.vue index b8885b9..e3d3380 100644 --- a/monitor-web/src/App.vue +++ b/monitor-web/src/App.vue @@ -1,22 +1,9 @@ diff --git a/monitor-web/src/assert/css/element.less b/monitor-web/src/assert/css/element.less index d40937c..7103545 100644 --- a/monitor-web/src/assert/css/element.less +++ b/monitor-web/src/assert/css/element.less @@ -1,53 +1,186 @@ +:root { + color-scheme: light; + --app-shell: #061b38; + --app-shell-hover: rgba(255, 255, 255, 0.08); + --app-shell-active: #0b6bee; + --app-shell-text: #cbd9eb; + --app-canvas: #f5f7fb; + --app-surface: #ffffff; + --app-surface-soft: #f8faff; + --app-text: #10233f; + --app-text-secondary: #64748b; + --app-border: #dce4ee; + --app-primary: #0b6bee; + --app-primary-soft: #eaf3ff; + --app-success: #16a34a; + --app-warning: #f97316; + --app-danger: #dc2626; + --app-muted: #94a3b8; + --app-shadow-sm: 0 1px 2px rgba(15, 35, 63, 0.04), 0 4px 16px rgba(15, 35, 63, 0.05); + --app-shadow-md: 0 12px 32px rgba(15, 35, 63, 0.12); + --app-radius-sm: 8px; + --app-radius-md: 12px; + --app-radius-lg: 16px; + --app-transition: 180ms ease-out; + --el-color-primary: #0b6bee; + --el-color-success: #16a34a; + --el-color-warning: #f97316; + --el-color-danger: #dc2626; + --el-border-radius-base: 8px; +} + +html.dark { + color-scheme: dark; + --app-shell: #050f20; + --app-shell-hover: rgba(255, 255, 255, 0.1); + --app-shell-active: #1677ff; + --app-shell-text: #d8e4f3; + --app-canvas: #0f1726; + --app-surface: #162033; + --app-surface-soft: #19263a; + --app-text: #f1f5f9; + --app-text-secondary: #9dacbf; + --app-border: #2b3a50; + --app-primary: #5aa2ff; + --app-primary-soft: rgba(74, 144, 245, 0.16); + --app-success: #4ade80; + --app-warning: #fb923c; + --app-danger: #f87171; + --app-muted: #718096; + --app-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.18), 0 8px 22px rgba(0, 0, 0, 0.18); + --app-shadow-md: 0 16px 38px rgba(0, 0, 0, 0.34); +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html, +body, +#app { + min-width: 320px; + min-height: 100%; + margin: 0; +} + +body { + background: var(--app-canvas); + color: var(--app-text); + font-family: + Inter, + ui-sans-serif, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + 'PingFang SC', + 'Microsoft YaHei', + sans-serif; + font-size: 14px; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} + +button, +input, +textarea, +select { + font: inherit; +} + +::selection { + background: var(--app-primary-soft); + color: var(--app-text); +} + +:focus-visible { + outline: 2px solid var(--app-primary); + outline-offset: 2px; +} + +.el-button { + min-height: 36px; + border-radius: var(--app-radius-sm); + font-weight: 600; +} + +.el-button.is-circle { + min-width: 36px; +} + +.el-input__wrapper, +.el-select__wrapper, +.el-textarea__inner { + border-radius: var(--app-radius-sm); +} + +.el-drawer { + background: var(--app-surface); + color: var(--app-text); +} + +.el-overlay { + background: rgba(4, 15, 32, 0.48); +} + .is-success { .el-progress-bar__outer { - background-color: #18cb1822 !important; + background-color: rgba(22, 163, 74, 0.14) !important; } .el-progress-bar__inner { - background-color: #18cb18 !important; + background-color: var(--app-success) !important; } .el-progress-circle__track { - stroke: #18cb1822; + stroke: rgba(22, 163, 74, 0.14); } .el-progress-circle__path { - stroke: #18cb18 !important; + stroke: var(--app-success) !important; } } .is-warning { .el-progress-bar__outer { - background-color: #ffa04622; + background-color: rgba(249, 115, 22, 0.14); } .el-progress-circle__track { - stroke: #ffa04622; - } - - .el-progress-circle__path { - stroke: #ffa046 !important; + stroke: rgba(249, 115, 22, 0.14); } + .el-progress-circle__path, .el-progress-bar__inner { - background-color: #ffa046 !important; + stroke: var(--app-warning) !important; + background-color: var(--app-warning) !important; } } .is-exception { .el-progress-bar__outer { - background-color: #ef4e4e22; + background-color: rgba(220, 38, 38, 0.14); } .el-progress-circle__track { - stroke: #ef4e4e22; + stroke: rgba(220, 38, 38, 0.14); } - .el-progress-circle__path { - stroke: #ef4e4e !important; + .el-progress-circle__path, + .el-progress-bar__inner { + stroke: var(--app-danger) !important; + background-color: var(--app-danger) !important; } +} - .el-progress-bar__inner { - background-color: #ef4e4e !important; +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + scroll-behavior: auto !important; + transition-duration: 0.01ms !important; + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; } } diff --git a/monitor-web/src/component/NotificationBell.vue b/monitor-web/src/component/NotificationBell.vue index a2d0107..3bff75a 100644 --- a/monitor-web/src/component/NotificationBell.vue +++ b/monitor-web/src/component/NotificationBell.vue @@ -57,7 +57,7 @@ onMounted(() => { >
diff --git a/monitor-web/src/component/PreviewCard.vue b/monitor-web/src/component/PreviewCard.vue index d85b168..edf24f8 100644 --- a/monitor-web/src/component/PreviewCard.vue +++ b/monitor-web/src/component/PreviewCard.vue @@ -1,156 +1,457 @@ diff --git a/monitor-web/src/component/TabItem.vue b/monitor-web/src/component/TabItem.vue index 6a21429..848ed7f 100644 --- a/monitor-web/src/component/TabItem.vue +++ b/monitor-web/src/component/TabItem.vue @@ -1,33 +1,73 @@ diff --git a/monitor-web/src/component/__tests__/PreviewCard.test.js b/monitor-web/src/component/__tests__/PreviewCard.test.js new file mode 100644 index 0000000..cfdf79e --- /dev/null +++ b/monitor-web/src/component/__tests__/PreviewCard.test.js @@ -0,0 +1,103 @@ +// @vitest-environment jsdom +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { mount } from '@vue/test-utils' + +const toolMocks = vi.hoisted(() => ({ + copyIp: vi.fn(), + rename: vi.fn() +})) + +vi.mock('@/tools', () => ({ + copyIp: toolMocks.copyIp, + fitByUnit: (value, unit) => `${value} ${unit}`, + osNameToIcon: () => ({ color: '#0b6bee', icon: 'fa-linux' }), + percentageToStatus: (value) => (value >= 80 ? 'exception' : 'success'), + rename: toolMocks.rename +})) + +const stubs = { + 'el-icon': { template: '' }, + 'el-progress': { + name: 'ElProgress', + props: ['percentage', 'status', 'strokeWidth', 'showText'], + template: '
' + } +} + +import PreviewCard from '@/component/PreviewCard.vue' + +/** + * 构造主机预览卡片最小完整数据。 + * + * @returns {object} 主机预览数据 + */ +function createHost() { + return { + id: 1, + location: 'cn', + name: '生产节点 · 上海', + online: true, + ip: '10.20.0.12', + osName: 'Ubuntu', + osVersion: '24.04', + cpuName: 'AMD EPYC 7763', + cpuCore: 32, + memory: 64, + cpuUsage: 0.34, + memoryUsage: 27.8, + networkUpload: 832, + networkDownload: 1620 + } +} + +describe('PreviewCard.vue', () => { + beforeEach(() => { + toolMocks.copyIp.mockReset() + toolMocks.rename.mockReset() + }) + + it('opens host details with Enter and Space', async () => { + const wrapper = mount(PreviewCard, { + props: { data: createHost(), update: vi.fn() }, + global: { stubs } + }) + + await wrapper.trigger('keydown', { key: 'Enter' }) + await wrapper.trigger('keydown', { key: ' ' }) + + expect(wrapper.emitted('open')).toHaveLength(2) + }) + + it('keeps rename and copy actions independent from opening the card', async () => { + const update = vi.fn() + const host = createHost() + const wrapper = mount(PreviewCard, { + props: { data: host, update }, + global: { stubs } + }) + + await wrapper.get(`[aria-label="重命名主机 ${host.name}"]`).trigger('click') + await wrapper.get(`[aria-label="复制 IP ${host.ip}"]`).trigger('click') + + expect(toolMocks.rename).toHaveBeenCalledWith(host.id, host.name, update) + expect(toolMocks.copyIp).toHaveBeenCalledWith(host.ip) + expect(wrapper.emitted('open')).toBeUndefined() + }) + + it('clamps invalid utilization values to safe progress percentages', () => { + const host = { + ...createHost(), + cpuUsage: 2, + memory: 0, + memoryUsage: Number.NaN + } + const wrapper = mount(PreviewCard, { + props: { data: host, update: vi.fn() }, + global: { stubs } + }) + + const progress = wrapper.findAllComponents({ name: 'ElProgress' }) + expect(progress[0].props('percentage')).toBe(100) + expect(progress[1].props('percentage')).toBe(0) + }) +}) diff --git a/monitor-web/src/views/IndexView.vue b/monitor-web/src/views/IndexView.vue index 16139f3..ba34802 100755 --- a/monitor-web/src/views/IndexView.vue +++ b/monitor-web/src/views/IndexView.vue @@ -1,65 +1,19 @@ - - + + diff --git a/monitor-web/src/views/WelcomeView.vue b/monitor-web/src/views/WelcomeView.vue index 2068dde..8088e69 100755 --- a/monitor-web/src/views/WelcomeView.vue +++ b/monitor-web/src/views/WelcomeView.vue @@ -1,40 +1,290 @@ + + +
+
+
+
+ + flying monitor +
+
+

欢迎来到 flying monitor

+

清晰掌握每台服务器的运行状态

+
+
+ + +
- +
+ + + + + + +
+ + + + + +
+
+
+ diff --git a/monitor-web/src/views/tabs/AlertView.vue b/monitor-web/src/views/tabs/AlertView.vue index 4fcd018..ff692f4 100644 --- a/monitor-web/src/views/tabs/AlertView.vue +++ b/monitor-web/src/views/tabs/AlertView.vue @@ -46,16 +46,24 @@ const activeTab = computed({ diff --git a/monitor-web/src/views/tabs/Probes.vue b/monitor-web/src/views/tabs/Probes.vue index 924321e..cccd59b 100644 --- a/monitor-web/src/views/tabs/Probes.vue +++ b/monitor-web/src/views/tabs/Probes.vue @@ -664,18 +664,27 @@ onMounted(() => { diff --git a/monitor-web/src/views/tabs/Security.vue b/monitor-web/src/views/tabs/Security.vue index 8cd9f09..deac1f5 100644 --- a/monitor-web/src/views/tabs/Security.vue +++ b/monitor-web/src/views/tabs/Security.vue @@ -232,11 +232,9 @@ onBeforeUnmount(() => {