implement UPS
This commit is contained in:
@@ -11,7 +11,7 @@
|
|||||||
"type-check": "vue-tsc --noEmit"
|
"type-check": "vue-tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.8.4",
|
"axios": "1.15.0",
|
||||||
"less": "^4.3.0",
|
"less": "^4.3.0",
|
||||||
"echarts": "^6.0.0",
|
"echarts": "^6.0.0",
|
||||||
"vue-echarts": "^8.0.1",
|
"vue-echarts": "^8.0.1",
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
"pinia": "^3.0.2",
|
"pinia": "^3.0.2",
|
||||||
"tdesign-mobile-vue": "^1.13.2",
|
"tdesign-mobile-vue": "^1.13.2",
|
||||||
"timi-tdesign-mobile": "0.0.9",
|
"timi-tdesign-mobile": "0.0.9",
|
||||||
"timi-web": "0.0.15",
|
"timi-web": "0.0.16",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"vue": "^3.5.16",
|
"vue": "^3.5.16",
|
||||||
"vue-router": "4.5.1",
|
"vue-router": "4.5.1",
|
||||||
|
|||||||
20
pnpm-lock.yaml
generated
20
pnpm-lock.yaml
generated
@@ -9,8 +9,8 @@ importers:
|
|||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
axios:
|
axios:
|
||||||
specifier: ^1.8.4
|
specifier: 1.15.0
|
||||||
version: 1.14.0
|
version: 1.15.0
|
||||||
echarts:
|
echarts:
|
||||||
specifier: ^6.0.0
|
specifier: ^6.0.0
|
||||||
version: 6.0.0
|
version: 6.0.0
|
||||||
@@ -30,8 +30,8 @@ importers:
|
|||||||
specifier: 0.0.9
|
specifier: 0.0.9
|
||||||
version: 0.0.9(typescript@5.8.3)
|
version: 0.0.9(typescript@5.8.3)
|
||||||
timi-web:
|
timi-web:
|
||||||
specifier: 0.0.15
|
specifier: 0.0.16
|
||||||
version: 0.0.15
|
version: 0.0.16
|
||||||
ts-node:
|
ts-node:
|
||||||
specifier: ^10.9.2
|
specifier: ^10.9.2
|
||||||
version: 10.9.2(@types/node@24.12.0)(typescript@5.8.3)
|
version: 10.9.2(@types/node@24.12.0)(typescript@5.8.3)
|
||||||
@@ -778,8 +778,8 @@ packages:
|
|||||||
axios@1.13.5:
|
axios@1.13.5:
|
||||||
resolution: {integrity: sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==}
|
resolution: {integrity: sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==}
|
||||||
|
|
||||||
axios@1.14.0:
|
axios@1.15.0:
|
||||||
resolution: {integrity: sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==}
|
resolution: {integrity: sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==}
|
||||||
|
|
||||||
balanced-match@1.0.2:
|
balanced-match@1.0.2:
|
||||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||||
@@ -1832,8 +1832,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-SV8m/FsLsEabSmJ/NvI650fLyb/JCLz7JUNQrkWeoo5xICElsN4x39L3n4lTzu6ARidjpT6uA+XEmA4wnyQmuA==}
|
resolution: {integrity: sha512-SV8m/FsLsEabSmJ/NvI650fLyb/JCLz7JUNQrkWeoo5xICElsN4x39L3n4lTzu6ARidjpT6uA+XEmA4wnyQmuA==}
|
||||||
engines: {node: '>=16.0.0'}
|
engines: {node: '>=16.0.0'}
|
||||||
|
|
||||||
timi-web@0.0.15:
|
timi-web@0.0.16:
|
||||||
resolution: {integrity: sha512-j5CU8Byd9qdK7AWUGM5vgQ5Tix20m/WrD5Sv4h77aFV0G/rqdKi8uoRg1uUXUZLqsIy1zjI7kbIV3DnRplBYog==}
|
resolution: {integrity: sha512-Tyma/szWvAmPTq7HRtXtRA0Of+YI1Acko96wsosJFRyO+tN1gDEUrwMoY1l2Uw8Q1g6Yv7huS6E6DLd9fA28og==}
|
||||||
engines: {node: '>=16.0.0'}
|
engines: {node: '>=16.0.0'}
|
||||||
|
|
||||||
tinycolor2@1.6.0:
|
tinycolor2@1.6.0:
|
||||||
@@ -2743,7 +2743,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- debug
|
- debug
|
||||||
|
|
||||||
axios@1.14.0:
|
axios@1.15.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
follow-redirects: 1.15.11
|
follow-redirects: 1.15.11
|
||||||
form-data: 4.0.5
|
form-data: 4.0.5
|
||||||
@@ -3875,7 +3875,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- typescript
|
- typescript
|
||||||
|
|
||||||
timi-web@0.0.15:
|
timi-web@0.0.16:
|
||||||
dependencies:
|
dependencies:
|
||||||
axios: 1.13.5
|
axios: 1.13.5
|
||||||
less: 4.5.1
|
less: 4.5.1
|
||||||
|
|||||||
@@ -1,116 +1,27 @@
|
|||||||
import axios, { AxiosError } from "axios";
|
import { axios } from "timi-web";
|
||||||
import type { ServerFile } from "@/types/File";
|
import type { ServerFile } from "@/types/File";
|
||||||
import { useSettingStore } from "@/store/settingStore";
|
import { useSettingStore } from "@/store/settingStore";
|
||||||
|
|
||||||
interface ApiResponse<T> {
|
async function list(pathSegments: string[]): Promise<ServerFile[]> {
|
||||||
code?: number | string;
|
const settingStore = useSettingStore();
|
||||||
data?: T;
|
const path = pathSegments.length ? `/${pathSegments.map((segment) => encodeURIComponent(segment)).join("/")}` : "";
|
||||||
list?: T;
|
return await axios.get(`${settingStore.resolveBaseURL()}/system/file/list${path}`);
|
||||||
msg?: string;
|
|
||||||
message?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listServerFiles(pathSegments: string[]): Promise<ServerFile[]> {
|
function buildURL(path: string, action = "download"): string {
|
||||||
const response = await axios.get<ApiResponse<ServerFile[]> | ServerFile[]>(buildListURL(pathSegments), {
|
const settingStore = useSettingStore();
|
||||||
params: buildQueryParams(),
|
const normalizedPath = path.trim();
|
||||||
timeout: 15000
|
|
||||||
});
|
|
||||||
|
|
||||||
return unwrapServerFileList(response.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildServerFileURL(path: string, action = "download"): string {
|
|
||||||
const normalizedPath = typeof path === "string" ? path.trim() : "";
|
|
||||||
const segments = normalizedPath.split("/").filter((segment) => !!segment);
|
const segments = normalizedPath.split("/").filter((segment) => !!segment);
|
||||||
const requestPath = segments.length ? `/${segments.map((segment) => encodeURIComponent(segment)).join("/")}` : "";
|
const requestPath = segments.length ? `/${segments.map((segment) => encodeURIComponent(segment)).join("/")}` : "";
|
||||||
const url = new URL(`${resolveBaseURL()}/system/file/${action}${requestPath}`);
|
const url = new URL(`${settingStore.resolveBaseURL()}/system/file/${action}${requestPath}`);
|
||||||
const token = useSettingStore().connect.token.trim();
|
const token = settingStore.connect.token.trim();
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
url.searchParams.set("token", token);
|
url.searchParams.set("token", token);
|
||||||
}
|
}
|
||||||
|
|
||||||
return url.toString();
|
return url.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveRequestErrorMessage(error: unknown): string {
|
export default {
|
||||||
if (error instanceof AxiosError) {
|
list,
|
||||||
const message = resolveApiMessage(error.response?.data);
|
buildURL
|
||||||
if (message) {
|
};
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error.message) {
|
|
||||||
return error.message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error instanceof Error && error.message) {
|
|
||||||
return error.message;
|
|
||||||
}
|
|
||||||
|
|
||||||
return "请求失败,请稍后重试";
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildListURL(pathSegments: string[]): string {
|
|
||||||
const path = pathSegments.length ? `/${pathSegments.map((segment) => encodeURIComponent(segment)).join("/")}` : "";
|
|
||||||
return `${resolveBaseURL()}/system/file/list${path}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveBaseURL(): string {
|
|
||||||
const settingStore = useSettingStore();
|
|
||||||
const connect = settingStore.connect;
|
|
||||||
const envBaseURL = typeof import.meta.env.VITE_API === "string" ? import.meta.env.VITE_API.trim() : "";
|
|
||||||
|
|
||||||
if (connect.host && connect.port) {
|
|
||||||
return `${connect.protocol}://${connect.host}:${connect.port}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return envBaseURL.replace(/\/+$/, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildQueryParams(): Record<string, string> {
|
|
||||||
const settingStore = useSettingStore();
|
|
||||||
const token = settingStore.connect.token.trim();
|
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
token
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function unwrapServerFileList(payload: ApiResponse<ServerFile[]> | ServerFile[]): ServerFile[] {
|
|
||||||
if (Array.isArray(payload)) {
|
|
||||||
return payload;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(payload.data)) {
|
|
||||||
return payload.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(payload.list)) {
|
|
||||||
return payload.list;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(resolveApiMessage(payload) || "文件列表返回格式不正确");
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveApiMessage(payload: unknown): string {
|
|
||||||
if (!payload || typeof payload !== "object") {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const { msg, message } = payload as ApiResponse<unknown>;
|
|
||||||
if (typeof msg === "string" && msg.trim()) {
|
|
||||||
return msg.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof message === "string" && message.trim()) {
|
|
||||||
return message.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,114 +1,29 @@
|
|||||||
import axios, { AxiosError } from "axios";
|
import { axios, Text } from "timi-web";
|
||||||
import { useSettingStore } from "@/store/settingStore";
|
import { useSettingStore } from "@/store/settingStore";
|
||||||
import type { SystemStatusHistoryView, SystemStatusSnapshotView } from "@/types/System";
|
import type { SystemStatusHistoryView, SystemStatusSnapshotView } from "@/types/System";
|
||||||
|
|
||||||
interface ApiResponse<T> {
|
async function getStatus(metrics?: string): Promise<SystemStatusSnapshotView> {
|
||||||
code?: number | string;
|
const settingStore = useSettingStore();
|
||||||
data?: T;
|
return await axios.get(`${settingStore.resolveBaseURL()}/system/server/status?${Text.urlArgs({
|
||||||
msg?: string;
|
metrics,
|
||||||
message?: string;
|
})}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSystemStatus(metrics?: string): Promise<SystemStatusSnapshotView> {
|
async function getStatusHistory(params?: {
|
||||||
const response = await axios.get<ApiResponse<SystemStatusSnapshotView> | SystemStatusSnapshotView>(`${resolveBaseURL()}/system/server/status`, {
|
|
||||||
params: {
|
|
||||||
...buildQueryParams(),
|
|
||||||
...(metrics ? { metrics } : {})
|
|
||||||
},
|
|
||||||
timeout: 15000
|
|
||||||
});
|
|
||||||
|
|
||||||
return unwrapResponse(response.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getSystemStatusHistory(params?: {
|
|
||||||
window?: string;
|
window?: string;
|
||||||
metrics?: string;
|
metrics?: string;
|
||||||
}): Promise<SystemStatusHistoryView> {
|
}): Promise<SystemStatusHistoryView> {
|
||||||
const response = await axios.get<ApiResponse<SystemStatusHistoryView> | SystemStatusHistoryView>(`${resolveBaseURL()}/system/server/status/history`, {
|
const settingStore = useSettingStore();
|
||||||
|
return await axios.get(`${settingStore.resolveBaseURL()}/system/server/status/history`, {
|
||||||
params: {
|
params: {
|
||||||
...buildQueryParams(),
|
|
||||||
...(params?.window ? { window: params.window } : {}),
|
...(params?.window ? { window: params.window } : {}),
|
||||||
...(params?.metrics ? { metrics: params.metrics } : {})
|
...(params?.metrics ? { metrics: params.metrics } : {})
|
||||||
},
|
},
|
||||||
timeout: 15000
|
timeout: 15000
|
||||||
});
|
});
|
||||||
|
|
||||||
return unwrapResponse(response.data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveSystemRequestErrorMessage(error: unknown): string {
|
export default {
|
||||||
if (error instanceof AxiosError) {
|
getStatus,
|
||||||
const message = resolveApiMessage(error.response?.data);
|
getStatusHistory
|
||||||
if (message) {
|
};
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error.message) {
|
|
||||||
return error.message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error instanceof Error && error.message) {
|
|
||||||
return error.message;
|
|
||||||
}
|
|
||||||
|
|
||||||
return "请求失败,请稍后重试";
|
|
||||||
}
|
|
||||||
|
|
||||||
function unwrapResponse<T>(payload: ApiResponse<T> | T): T {
|
|
||||||
if (payload && typeof payload === "object" && "data" in payload) {
|
|
||||||
const data = (payload as ApiResponse<T>).data;
|
|
||||||
if (data !== undefined) {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload && typeof payload === "object" && ("msg" in payload || "message" in payload)) {
|
|
||||||
throw new Error(resolveApiMessage(payload) || "接口返回为空");
|
|
||||||
}
|
|
||||||
|
|
||||||
return payload as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveBaseURL(): string {
|
|
||||||
const settingStore = useSettingStore();
|
|
||||||
const connect = settingStore.connect;
|
|
||||||
const envBaseURL = typeof import.meta.env.VITE_API === "string" ? import.meta.env.VITE_API.trim() : "";
|
|
||||||
|
|
||||||
if (connect.host && connect.port) {
|
|
||||||
return `${connect.protocol}://${connect.host}:${connect.port}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return envBaseURL.replace(/\/+$/, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildQueryParams(): Record<string, string> {
|
|
||||||
const settingStore = useSettingStore();
|
|
||||||
const token = settingStore.connect.token.trim();
|
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
token
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveApiMessage(payload: unknown): string {
|
|
||||||
if (!payload || typeof payload !== "object") {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const { msg, message } = payload as ApiResponse<unknown>;
|
|
||||||
if (typeof msg === "string" && msg.trim()) {
|
|
||||||
return msg.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof message === "string" && message.trim()) {
|
|
||||||
return message.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|||||||
27
src/api/UpsAPI.ts
Normal file
27
src/api/UpsAPI.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { axios } from "timi-web";
|
||||||
|
import type { UpsHistoryView, UpsStatusView } from "@/types/Ups";
|
||||||
|
import { useSettingStore } from "@/store/settingStore";
|
||||||
|
|
||||||
|
async function getStatus(): Promise<UpsStatusView | null> {
|
||||||
|
const settingStore = useSettingStore();
|
||||||
|
return await axios.get(`${settingStore.resolveBaseURL()}/system/ups/status`, {
|
||||||
|
timeout: 15000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getStatusHistory(params?: {
|
||||||
|
window?: string;
|
||||||
|
}): Promise<UpsHistoryView> {
|
||||||
|
const settingStore = useSettingStore();
|
||||||
|
return await axios.get(`${settingStore.resolveBaseURL()}/system/ups/history`, {
|
||||||
|
params: {
|
||||||
|
...(params?.window ? { window: params.window } : {})
|
||||||
|
},
|
||||||
|
timeout: 15000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getStatus,
|
||||||
|
getStatusHistory
|
||||||
|
};
|
||||||
19
src/main.ts
19
src/main.ts
@@ -8,6 +8,25 @@ import "tdesign-mobile-vue/es/style/index.css";
|
|||||||
import "timi-web/style.css";
|
import "timi-web/style.css";
|
||||||
import "timi-tdesign-mobile/style.css";
|
import "timi-tdesign-mobile/style.css";
|
||||||
import "vue-virtual-scroller/dist/vue-virtual-scroller.css";
|
import "vue-virtual-scroller/dist/vue-virtual-scroller.css";
|
||||||
|
import { axios } from "timi-web";
|
||||||
|
import { useSettingStore } from "@/store/settingStore.ts";
|
||||||
|
|
||||||
|
axios.interceptors.request.use((config) => {
|
||||||
|
const settingStore = useSettingStore();
|
||||||
|
const token = settingStore.connect.token.trim();
|
||||||
|
if (token) {
|
||||||
|
if (config.method === "get") {
|
||||||
|
config.params = {
|
||||||
|
token,
|
||||||
|
...config.params
|
||||||
|
};
|
||||||
|
}
|
||||||
|
config.headers.set({ "Token": token });
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
}, (error: any) => {
|
||||||
|
return Promise.reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
export const pinia = createPinia();
|
export const pinia = createPinia();
|
||||||
|
|
||||||
|
|||||||
@@ -119,11 +119,7 @@ import { GridComponent, LegendComponent, TooltipComponent } from "echarts/compon
|
|||||||
import VChart from "vue-echarts";
|
import VChart from "vue-echarts";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import type { EChartsOption, SeriesOption } from "echarts";
|
import type { EChartsOption, SeriesOption } from "echarts";
|
||||||
import {
|
import SystemAPI from "@/api/SystemAPI";
|
||||||
getSystemStatus,
|
|
||||||
getSystemStatusHistory,
|
|
||||||
resolveSystemRequestErrorMessage
|
|
||||||
} from "@/api/SystemAPI";
|
|
||||||
import type { SystemStatusHistoryPoint, SystemStatusSnapshotView } from "@/types/System";
|
import type { SystemStatusHistoryPoint, SystemStatusSnapshotView } from "@/types/System";
|
||||||
import type { DashboardHistoryMetric } from "@/store/settingStore";
|
import type { DashboardHistoryMetric } from "@/store/settingStore";
|
||||||
import { useSettingStore } from "@/store/settingStore";
|
import { useSettingStore } from "@/store/settingStore";
|
||||||
@@ -624,11 +620,11 @@ async function refreshSnapshot(): Promise<void> {
|
|||||||
isSnapshotLoading.value = true;
|
isSnapshotLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const metrics = settingStore.dashboard.server.snapshotMetrics.join(",");
|
const metrics = settingStore.dashboard.server.snapshotMetrics.join(",");
|
||||||
snapshotView.value = await getSystemStatus(metrics);
|
snapshotView.value = await SystemAPI.getStatus(metrics);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Toast({
|
Toast({
|
||||||
theme: "error",
|
theme: "error",
|
||||||
message: resolveSystemRequestErrorMessage(error)
|
message: error instanceof Error ? error.message : "请求失败,请稍后重试"
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
isSnapshotLoading.value = false;
|
isSnapshotLoading.value = false;
|
||||||
@@ -644,7 +640,7 @@ async function refreshHistory(): Promise<void> {
|
|||||||
isHistoryLoading.value = true;
|
isHistoryLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const metrics = settingStore.dashboard.server.historyMetrics.join(",");
|
const metrics = settingStore.dashboard.server.historyMetrics.join(",");
|
||||||
const historyView = await getSystemStatusHistory({
|
const historyView = await SystemAPI.getStatusHistory({
|
||||||
window: "1h",
|
window: "1h",
|
||||||
metrics
|
metrics
|
||||||
});
|
});
|
||||||
@@ -652,7 +648,7 @@ async function refreshHistory(): Promise<void> {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
Toast({
|
Toast({
|
||||||
theme: "error",
|
theme: "error",
|
||||||
message: resolveSystemRequestErrorMessage(error)
|
message: error instanceof Error ? error.message : "请求失败,请稍后重试"
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
isHistoryLoading.value = false;
|
isHistoryLoading.value = false;
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Toast } from "tdesign-mobile-vue";
|
import { Toast } from "tdesign-mobile-vue";
|
||||||
import { getSystemStatus, resolveSystemRequestErrorMessage } from "@/api/SystemAPI";
|
import SystemAPI from "@/api/SystemAPI";
|
||||||
import type { SystemStatusSnapshotView } from "@/types/System";
|
import type { SystemStatusSnapshotView } from "@/types/System";
|
||||||
import { Time } from "timi-web";
|
import { Time } from "timi-web";
|
||||||
|
|
||||||
@@ -65,11 +65,11 @@ async function refreshDetail(): Promise<void> {
|
|||||||
}
|
}
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
snapshotView.value = await getSystemStatus("os,hardware");
|
snapshotView.value = await SystemAPI.getStatus("os,hardware");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Toast({
|
Toast({
|
||||||
theme: "error",
|
theme: "error",
|
||||||
message: resolveSystemRequestErrorMessage(error)
|
message: error instanceof Error ? error.message : "请求失败,请稍后重试"
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Toast } from "tdesign-mobile-vue";
|
import { Toast } from "tdesign-mobile-vue";
|
||||||
import { getSystemStatus, resolveSystemRequestErrorMessage } from "@/api/SystemAPI";
|
import SystemAPI from "@/api/SystemAPI";
|
||||||
import type { SystemStatusSnapshot, SystemStatusSnapshotView } from "@/types/System";
|
import type { SystemStatusSnapshot, SystemStatusSnapshotView } from "@/types/System";
|
||||||
import { IOSize, Text } from "timi-web";
|
import { IOSize, Text } from "timi-web";
|
||||||
|
|
||||||
@@ -98,11 +98,11 @@ async function refreshDetail(): Promise<void> {
|
|||||||
}
|
}
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
snapshotView.value = await getSystemStatus("storage");
|
snapshotView.value = await SystemAPI.getStatus("storage");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Toast({
|
Toast({
|
||||||
theme: "error",
|
theme: "error",
|
||||||
message: resolveSystemRequestErrorMessage(error)
|
message: error instanceof Error ? error.message : "请求失败,请稍后重试"
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Toast } from "tdesign-mobile-vue";
|
import { Toast } from "tdesign-mobile-vue";
|
||||||
import { getSystemStatus, resolveSystemRequestErrorMessage } from "@/api/SystemAPI";
|
import SystemAPI from "@/api/SystemAPI";
|
||||||
import type { SystemStatusSnapshotView } from "@/types/System";
|
import type { SystemStatusSnapshotView } from "@/types/System";
|
||||||
import { IOSize, Text, Time } from "timi-web";
|
import { IOSize, Text, Time } from "timi-web";
|
||||||
|
|
||||||
@@ -96,11 +96,11 @@ async function refreshDetail(): Promise<void> {
|
|||||||
}
|
}
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
snapshotView.value = await getSystemStatus("cpu,memory,jvm,network");
|
snapshotView.value = await SystemAPI.getStatus("cpu,memory,jvm,network");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Toast({
|
Toast({
|
||||||
theme: "error",
|
theme: "error",
|
||||||
message: resolveSystemRequestErrorMessage(error)
|
message: error instanceof Error ? error.message : "请求失败,请稍后重试"
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
|
|||||||
@@ -1,57 +1,646 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="ups-dashboard">
|
<div class="ups-dashboard">
|
||||||
<section class="card">
|
<div v-if="isStatusLoading && !statusView" class="loading-wrap">
|
||||||
<p class="tag">UPS</p>
|
<t-loading text="加载 UPS 状态..." />
|
||||||
<h2 class="header">供电监控</h2>
|
</div>
|
||||||
<p class="desc">这里用于展示 UPS 电池状态、负载功率、剩余续航和告警信息。</p>
|
<template v-else-if="statusView">
|
||||||
</section>
|
<t-cell-group title="UPS 概览" theme="card">
|
||||||
|
<t-cell title="主机" :note="Text.display(statusView?.hostName)" />
|
||||||
|
<t-cell title="厂商" :note="Text.display(statusView?.customer)" />
|
||||||
|
<t-cell title="设备 ID" :note="Text.display(statusView?.deviceId)" />
|
||||||
|
<t-cell title="UPS 类型" :note="Text.display(statusView?.upsType)" />
|
||||||
|
<t-cell title="工作模式" :note="Text.display(statusView?.workMode)" />
|
||||||
|
<t-cell title="数据时间" :note="Time.toPassedDateTime(statusView?.upsTime)" />
|
||||||
|
<t-cell title="输出状态" :note="Text.displayBool(statusView?.outputOn, '开启', '关闭')" />
|
||||||
|
<t-cell title="充电状态" :note="Text.displayBool(statusView?.charging, '充电中', '未充电')" />
|
||||||
|
<t-cell title="旁路状态" :note="Text.displayBool(statusView?.bypassActive, '旁路运行', '正常供电')" />
|
||||||
|
<t-cell title="关机状态" :note="Text.displayBool(statusView?.shutdownActive, '执行中', '未执行')" />
|
||||||
|
</t-cell-group>
|
||||||
|
<t-cell-group class="section energy" title="电池与负载" theme="card">
|
||||||
|
<t-cell-info label="电池容量" :value="Text.unit(statusView?.batteryCapacity, '%')">
|
||||||
|
<progress-group mode="heap" :progress="batteryProgressList" />
|
||||||
|
</t-cell-info>
|
||||||
|
<t-cell-info label="输出负载" :value="Text.unit(statusView?.outputLoadPercent, '%')">
|
||||||
|
<progress-group mode="heap" :progress="loadProgressList" />
|
||||||
|
</t-cell-info>
|
||||||
|
<t-cell title="剩余时间" :note="Text.unit(statusView?.batteryRemainTime, '分钟')" />
|
||||||
|
</t-cell-group>
|
||||||
|
<t-cell-group class="section electric" title="电气参数" theme="card">
|
||||||
|
<t-cell title="输入电压" :note="Text.unit(statusView?.inputVoltage, 'V')" />
|
||||||
|
<t-cell title="输入频率" :note="Text.unit(statusView?.inputFrequency, 'Hz')" />
|
||||||
|
<t-cell title="输出电压" :note="Text.unit(statusView?.outputVoltage, 'V')" />
|
||||||
|
<t-cell title="输出频率" :note="Text.unit(statusView?.outputFrequency, 'Hz')" />
|
||||||
|
<t-cell title="电池电压" :note="Text.unit(statusView?.batteryVoltage, 'V')" />
|
||||||
|
<t-cell title="温度" :note="Text.unit(statusView?.temperature, '℃')" />
|
||||||
|
</t-cell-group>
|
||||||
|
<t-cell-group v-if="faultList.length" class="section warning" title="告警与故障" theme="card">
|
||||||
|
<t-cell v-for="(item, index) in faultList" :key="`${item}-${index}`" :title="item" />
|
||||||
|
</t-cell-group>
|
||||||
|
</template>
|
||||||
|
<t-empty v-else description="暂无 UPS 状态数据" />
|
||||||
|
<t-cell-group class="section history" title="历史采样" theme="card">
|
||||||
|
<t-tabs class="metric-tabs" :value="historyMetric" @change="handleHistoryMetricChange">
|
||||||
|
<t-tab-panel v-for="item in historyMetricTabs" :key="item.value" :value="item.value" :label="item.label" />
|
||||||
|
</t-tabs>
|
||||||
|
<div v-if="isHistoryLoading && !historyItems.length" class="loading-wrap">
|
||||||
|
<t-loading text="加载 UPS 历史..." />
|
||||||
|
</div>
|
||||||
|
<template v-else-if="historyItems.length">
|
||||||
|
<v-chart
|
||||||
|
ref="chartRef"
|
||||||
|
class="chart"
|
||||||
|
:option="historyChartOption"
|
||||||
|
:update-options="historyChartUpdateOptions"
|
||||||
|
:autoresize="true"
|
||||||
|
/>
|
||||||
|
<div class="slider bg-white">
|
||||||
|
<span class="label">较早</span>
|
||||||
|
<t-slider
|
||||||
|
class="control"
|
||||||
|
:min="0"
|
||||||
|
:max="Math.max(historyItems.length - historyVisibleCount, 0)"
|
||||||
|
:step="1"
|
||||||
|
:value="historySliderValue"
|
||||||
|
@change="handleHistorySliderChange"
|
||||||
|
/>
|
||||||
|
<span class="label">最新</span>
|
||||||
|
</div>
|
||||||
|
<t-cell-group>
|
||||||
|
<t-cell v-for="item in historySummary" :key="item.label" :title="item.label" :note="item.value" />
|
||||||
|
</t-cell-group>
|
||||||
|
</template>
|
||||||
|
<t-empty v-else description="暂无 UPS 历史数据" />
|
||||||
|
</t-cell-group>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts"></script>
|
<script setup lang="ts">
|
||||||
|
import { Toast } from "tdesign-mobile-vue";
|
||||||
|
import { use } from "echarts/core";
|
||||||
|
import { SVGRenderer } from "echarts/renderers";
|
||||||
|
import { LineChart } from "echarts/charts";
|
||||||
|
import { GridComponent, LegendComponent, TooltipComponent } from "echarts/components";
|
||||||
|
import VChart from "vue-echarts";
|
||||||
|
import type { EChartsOption, SeriesOption } from "echarts";
|
||||||
|
import UpsAPI from "@/api/UpsAPI";
|
||||||
|
import type {
|
||||||
|
UpsHistoryPointView,
|
||||||
|
UpsStatusView
|
||||||
|
} from "@/types/Ups";
|
||||||
|
import { useSettingStore } from "@/store/settingStore";
|
||||||
|
import { Text, Time } from "timi-web";
|
||||||
|
import type { LabelValue } from "timi-web";
|
||||||
|
import TCellInfo from "@/components/TCellInfo.vue";
|
||||||
|
import type { ProgressItem } from "@/components/ProgressGroup.vue";
|
||||||
|
import ProgressGroup from "@/components/ProgressGroup.vue";
|
||||||
|
|
||||||
|
type UpsHistoryMetric = "battery" | "load" | "voltage" | "temperature";
|
||||||
|
|
||||||
|
use([
|
||||||
|
SVGRenderer,
|
||||||
|
LineChart,
|
||||||
|
GridComponent,
|
||||||
|
TooltipComponent,
|
||||||
|
LegendComponent
|
||||||
|
]);
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "UPSDashboard"
|
||||||
|
});
|
||||||
|
|
||||||
|
const settingStore = useSettingStore();
|
||||||
|
|
||||||
|
const isStatusLoading = ref(false);
|
||||||
|
const isHistoryLoading = ref(false);
|
||||||
|
const statusView = ref<UpsStatusView | null>(null);
|
||||||
|
const historyPoints = ref<UpsHistoryPointView[]>([]);
|
||||||
|
const chartRef = ref<InstanceType<typeof VChart> | null>(null);
|
||||||
|
const historyChartUpdateOptions = Object.freeze({
|
||||||
|
notMerge: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------- 当前状态 ----------
|
||||||
|
|
||||||
|
const batteryProgressList = computed<ProgressItem[]>(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
color: "var(--td-brand-color)",
|
||||||
|
value: statusView.value?.batteryCapacity! / 100,
|
||||||
|
}
|
||||||
|
];
|
||||||
|
});
|
||||||
|
const loadProgressList = computed<ProgressItem[]>(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
color: "var(--td-warning-color)",
|
||||||
|
value: statusView.value?.outputLoadPercent! / 100,
|
||||||
|
}
|
||||||
|
];
|
||||||
|
});
|
||||||
|
const faultList = computed<string[]>(() => {
|
||||||
|
const list: string[] = [];
|
||||||
|
const warningList = statusView.value?.warnings || [];
|
||||||
|
if (statusView.value?.faultType) {
|
||||||
|
list.push(`故障类型:${statusView.value.faultType}`);
|
||||||
|
}
|
||||||
|
if (statusView.value?.faultKind) {
|
||||||
|
list.push(`故障明细:${statusView.value.faultKind}`);
|
||||||
|
}
|
||||||
|
for (const item of warningList) {
|
||||||
|
list.push(`告警:${item}`);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------- 历史采样 ----------
|
||||||
|
|
||||||
|
const historyMetric = ref<UpsHistoryMetric>("battery");
|
||||||
|
|
||||||
|
const historyMetricTabs = computed(() => {
|
||||||
|
const labelMap: Record<UpsHistoryMetric, string> = {
|
||||||
|
battery: "电池",
|
||||||
|
load: "负载",
|
||||||
|
voltage: "电压",
|
||||||
|
temperature: "温度"
|
||||||
|
};
|
||||||
|
return (Object.keys(labelMap) as UpsHistoryMetric[]).map((item) => ({
|
||||||
|
value: item,
|
||||||
|
label: labelMap[item]
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleHistoryMetricChange(value: string): void {
|
||||||
|
const nextValue = value as UpsHistoryMetric;
|
||||||
|
if (nextValue === "battery" || nextValue === "load" || nextValue === "voltage" || nextValue === "temperature") {
|
||||||
|
historyMetric.value = nextValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const historyItems = computed<UpsHistoryPointView[]>(() => {
|
||||||
|
return historyPoints.value.slice().sort((left, right) => left.at - right.at);
|
||||||
|
});
|
||||||
|
const historySummary = computed<LabelValue<string, any>[]>(() => {
|
||||||
|
const lastPoint = historyItems.value[historyItems.value.length - 1];
|
||||||
|
if (!lastPoint) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (historyMetric.value === "battery") {
|
||||||
|
return [
|
||||||
|
{ label: "容量", value: Text.unit(lastPoint.batteryCapacity, '%') },
|
||||||
|
{ label: "剩余时间", value: Text.unit(lastPoint.batteryRemainTime, '分钟') }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (historyMetric.value === "load") {
|
||||||
|
return [
|
||||||
|
{ label: "负载", value: Text.unit(lastPoint.outputLoadPercent, '%') },
|
||||||
|
{ label: "输出状态", value: Text.displayBool(lastPoint.outputOn, '开启', '关闭') }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (historyMetric.value === "voltage") {
|
||||||
|
return [
|
||||||
|
{ label: "输入电压", value: Text.unit(lastPoint.inputVoltage, 'V') },
|
||||||
|
{ label: "输出电压", value: Text.unit(lastPoint.outputVoltage, 'V') }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{ label: "温度", value: Text.unit(lastPoint.temperature, '℃') },
|
||||||
|
{ label: "工作模式", value: Text.display(lastPoint.workMode) }
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
const isHistorySliderTouched = ref(false);
|
||||||
|
const historySliderValue = ref(0);
|
||||||
|
const HISTORY_DEFAULT_VISIBLE_COUNT = 24;
|
||||||
|
|
||||||
|
const historyVisibleCount = computed(() => {
|
||||||
|
if (historyItems.value.length < 1) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return Math.min(HISTORY_DEFAULT_VISIBLE_COUNT, historyItems.value.length);
|
||||||
|
});
|
||||||
|
const historySliderMaxStart = computed(() => {
|
||||||
|
return Math.max(historyItems.value.length - historyVisibleCount.value, 0);
|
||||||
|
});
|
||||||
|
const currentHistoryRange = computed(() => {
|
||||||
|
const length = historyItems.value.length;
|
||||||
|
if (length < 1) {
|
||||||
|
return { startValue: 0, endValue: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const startValue = Math.min(Math.max(Math.floor(historySliderValue.value), 0), historySliderMaxStart.value);
|
||||||
|
const endValue = Math.min(startValue + historyVisibleCount.value - 1, length - 1);
|
||||||
|
return { startValue, endValue };
|
||||||
|
});
|
||||||
|
const visibleHistoryItems = computed<UpsHistoryPointView[]>(() => {
|
||||||
|
if (!historyItems.value.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return historyItems.value.slice(currentHistoryRange.value.startValue, currentHistoryRange.value.endValue + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => historyItems.value.length, (length) => {
|
||||||
|
if (length < 1) {
|
||||||
|
historySliderValue.value = 0;
|
||||||
|
isHistorySliderTouched.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isHistorySliderTouched.value) {
|
||||||
|
historySliderValue.value = historySliderMaxStart.value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
historySliderValue.value = Math.min(historySliderValue.value, historySliderMaxStart.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleHistorySliderChange(value: number): void {
|
||||||
|
historySliderValue.value = Math.min(Math.max(Math.floor(value), 0), historySliderMaxStart.value);
|
||||||
|
isHistorySliderTouched.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 图表 ----------
|
||||||
|
|
||||||
|
const isDarkTheme = ref(false);
|
||||||
|
let themeObserver: MutationObserver | null = null;
|
||||||
|
|
||||||
|
const historyChartOption = computed((): EChartsOption => {
|
||||||
|
const axisColor = isDarkTheme.value ? "#9aa4b2" : "#7e8a9a";
|
||||||
|
const lineColor = isDarkTheme.value ? "rgba(255, 255, 255, .12)" : "rgba(18, 42, 66, .08)";
|
||||||
|
const legendColor = isDarkTheme.value ? "#d7dee8" : "#334155";
|
||||||
|
|
||||||
|
const commonOption: EChartsOption = {
|
||||||
|
tooltip: {
|
||||||
|
trigger: "axis",
|
||||||
|
confine: true,
|
||||||
|
position: (point, _params, _dom, _rect, size) => {
|
||||||
|
const gap = 6;
|
||||||
|
const fingerGap = 24;
|
||||||
|
const contentSize = size?.contentSize || [0, 0];
|
||||||
|
const viewSize = size?.viewSize || [0, 0];
|
||||||
|
const maxLeft = Math.max(viewSize[0] - contentSize[0] - gap, gap);
|
||||||
|
const maxTop = Math.max(viewSize[1] - contentSize[1] - gap, gap);
|
||||||
|
const clamp = (currentValue: number, min: number, max: number): number => {
|
||||||
|
return Math.min(Math.max(currentValue, min), max);
|
||||||
|
};
|
||||||
|
const preferLeft = point[0] - contentSize[0] - gap;
|
||||||
|
const preferRight = point[0] + gap;
|
||||||
|
const left = point[0] < viewSize[0] / 2
|
||||||
|
? clamp(preferRight, gap, maxLeft)
|
||||||
|
: clamp(preferLeft, gap, maxLeft);
|
||||||
|
const preferTop = point[1] - contentSize[1] - fingerGap;
|
||||||
|
const preferBottom = point[1] + fingerGap;
|
||||||
|
const top = preferTop >= gap
|
||||||
|
? clamp(preferTop, gap, maxTop)
|
||||||
|
: clamp(preferBottom, gap, maxTop);
|
||||||
|
return [left, top];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
top: 10,
|
||||||
|
textStyle: {
|
||||||
|
color: legendColor
|
||||||
|
}
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
top: 40,
|
||||||
|
left: 8,
|
||||||
|
right: 8,
|
||||||
|
bottom: 8,
|
||||||
|
containLabel: true
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: "category",
|
||||||
|
boundaryGap: false,
|
||||||
|
data: visibleHistoryItems.value.map((item) => item.at),
|
||||||
|
axisLabel: {
|
||||||
|
color: axisColor,
|
||||||
|
formatter: val => Time.toShortTime(Number(val))
|
||||||
|
},
|
||||||
|
axisLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: lineColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!historyItems.value.length) {
|
||||||
|
return commonOption;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (historyMetric.value === "battery") {
|
||||||
|
return {
|
||||||
|
...commonOption,
|
||||||
|
tooltip: {
|
||||||
|
...commonOption.tooltip,
|
||||||
|
formatter: (params) => {
|
||||||
|
const list = Array.isArray(params) ? params : [params];
|
||||||
|
const title = `时间:${Time.toDateTime(Number(list[0]?.name))}`;
|
||||||
|
const lines = list.map((item) => {
|
||||||
|
const seriesName = item.seriesName || "";
|
||||||
|
if (seriesName === "容量") {
|
||||||
|
return `${item.marker}${seriesName}: ${Text.unit(Number(item.value), " %")}`;
|
||||||
|
}
|
||||||
|
return `${item.marker}${seriesName}: ${Text.unit(Number(item.value), '分钟')}`;
|
||||||
|
});
|
||||||
|
return [title, ...lines].join("<br />");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
yAxis: [
|
||||||
|
{
|
||||||
|
type: "value",
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
axisLabel: {
|
||||||
|
color: axisColor,
|
||||||
|
formatter: "{value} %"
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: lineColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "value",
|
||||||
|
axisLabel: {
|
||||||
|
color: axisColor,
|
||||||
|
formatter: "{value} min"
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
show: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
series: [
|
||||||
|
resolveLineSeries("容量", visibleHistoryItems.value.map((item) => item.batteryCapacity!)),
|
||||||
|
resolveLineSeries("剩余时间", visibleHistoryItems.value.map((item) => item.batteryRemainTime || 0), 1)
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (historyMetric.value === "load") {
|
||||||
|
return {
|
||||||
|
...commonOption,
|
||||||
|
tooltip: {
|
||||||
|
...commonOption.tooltip,
|
||||||
|
formatter: (params) => {
|
||||||
|
const list = Array.isArray(params) ? params : [params];
|
||||||
|
const title = `时间:${Time.toDateTime(Number(list[0]?.name))}`;
|
||||||
|
const lines = list.map((item) => `${item.marker}${item.seriesName}: ${Text.unit(Number(item.value), " %")}`);
|
||||||
|
return [title, ...lines].join("<br />");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: "value",
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
axisLabel: {
|
||||||
|
color: axisColor,
|
||||||
|
formatter: "{value} %"
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: lineColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
resolveLineSeries("负载", visibleHistoryItems.value.map((item) => item.outputLoadPercent))
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (historyMetric.value === "voltage") {
|
||||||
|
return {
|
||||||
|
...commonOption,
|
||||||
|
tooltip: {
|
||||||
|
...commonOption.tooltip,
|
||||||
|
formatter: (params) => {
|
||||||
|
const list = Array.isArray(params) ? params : [params];
|
||||||
|
const title = `时间:${Time.toDateTime(Number(list[0]?.name))}`;
|
||||||
|
const lines = list.map((item) => `${item.marker}${item.seriesName}: ${Text.unit(Number(item.value), " V")}`);
|
||||||
|
return [title, ...lines].join("<br />");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: "value",
|
||||||
|
axisLabel: {
|
||||||
|
color: axisColor,
|
||||||
|
formatter: "{value} V"
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: lineColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
resolveLineSeries("输入电压", visibleHistoryItems.value.map((item) => item.inputVoltage || 0)),
|
||||||
|
resolveLineSeries("输出电压", visibleHistoryItems.value.map((item) => item.outputVoltage || 0))
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...commonOption,
|
||||||
|
tooltip: {
|
||||||
|
...commonOption.tooltip,
|
||||||
|
formatter: (params) => {
|
||||||
|
const list = Array.isArray(params) ? params : [params];
|
||||||
|
const title = `时间:${Time.toDateTime(Number(list[0]?.name))}`;
|
||||||
|
const lines = list.map((item) => `${item.marker}${item.seriesName}: ${Text.unit(Number(item.value), " ℃")}`);
|
||||||
|
return [title, ...lines].join("<br />");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: "value",
|
||||||
|
axisLabel: {
|
||||||
|
color: axisColor,
|
||||||
|
formatter: "{value} ℃"
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: lineColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
resolveLineSeries("温度", visibleHistoryItems.value.map((item) => item.temperature || 0))
|
||||||
|
]
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function resolveLineSeries(name: string, data: Array<number | null>, yAxisIndex?: number): SeriesOption {
|
||||||
|
const series: SeriesOption = {
|
||||||
|
type: "line",
|
||||||
|
name,
|
||||||
|
showSymbol: false,
|
||||||
|
smooth: true,
|
||||||
|
lineStyle: {
|
||||||
|
width: 2
|
||||||
|
},
|
||||||
|
areaStyle: {
|
||||||
|
opacity: .18
|
||||||
|
},
|
||||||
|
data
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof yAxisIndex === "number") {
|
||||||
|
series.yAxisIndex = yAxisIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
return series;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 加载与自动刷新 ----------
|
||||||
|
|
||||||
|
let statusTimer: number | null = null;
|
||||||
|
let historyTimer: number | null = null;
|
||||||
|
|
||||||
|
watch(() => settingStore.dashboard.server, restartAutoRefresh, { deep: true });
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
updateThemeState();
|
||||||
|
restartAutoRefresh();
|
||||||
|
themeObserver = new MutationObserver(() => {
|
||||||
|
updateThemeState();
|
||||||
|
});
|
||||||
|
themeObserver.observe(document.documentElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ["class"]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearAutoRefreshTimer();
|
||||||
|
if (themeObserver) {
|
||||||
|
themeObserver.disconnect();
|
||||||
|
themeObserver = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateThemeState(): void {
|
||||||
|
isDarkTheme.value = document.documentElement.classList.contains("theme-dark");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshStatus(): Promise<void> {
|
||||||
|
if (isStatusLoading.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isStatusLoading.value = true;
|
||||||
|
try {
|
||||||
|
statusView.value = await UpsAPI.getStatus();
|
||||||
|
} catch (error) {
|
||||||
|
Toast({
|
||||||
|
theme: "error",
|
||||||
|
message: error instanceof Error ? error.message : "请求失败,请稍后重试"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
isStatusLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshHistory(): Promise<void> {
|
||||||
|
if (isHistoryLoading.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isHistoryLoading.value = true;
|
||||||
|
try {
|
||||||
|
const historyView = await UpsAPI.getStatusHistory({
|
||||||
|
window: "24h"
|
||||||
|
});
|
||||||
|
historyPoints.value = historyView.points || [];
|
||||||
|
} catch (error) {
|
||||||
|
Toast({
|
||||||
|
theme: "error",
|
||||||
|
message: error instanceof Error ? error.message : "请求失败,请稍后重试"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
isHistoryLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshDashboard(): Promise<void> {
|
||||||
|
await Promise.all([
|
||||||
|
refreshStatus(),
|
||||||
|
refreshHistory()
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function restartAutoRefresh(): void {
|
||||||
|
void refreshDashboard();
|
||||||
|
clearAutoRefreshTimer();
|
||||||
|
|
||||||
|
const snapshotIntervalMs = Math.max(settingStore.dashboard.server.snapshotRefreshSeconds, 1) * 1000;
|
||||||
|
const historyIntervalMs = Math.max(settingStore.dashboard.server.historyRefreshSeconds, 1) * 1000;
|
||||||
|
|
||||||
|
statusTimer = window.setInterval(() => {
|
||||||
|
void refreshStatus();
|
||||||
|
}, snapshotIntervalMs);
|
||||||
|
|
||||||
|
historyTimer = window.setInterval(() => {
|
||||||
|
void refreshHistory();
|
||||||
|
}, historyIntervalMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAutoRefreshTimer(): void {
|
||||||
|
if (statusTimer !== null) {
|
||||||
|
window.clearInterval(statusTimer);
|
||||||
|
statusTimer = null;
|
||||||
|
}
|
||||||
|
if (historyTimer !== null) {
|
||||||
|
window.clearInterval(historyTimer);
|
||||||
|
historyTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
.ups-dashboard {
|
.ups-dashboard {
|
||||||
padding: 1rem;
|
gap: 1rem;
|
||||||
|
|
||||||
.card {
|
|
||||||
gap: .6rem;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 1rem;
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
border: 1px solid var(--app-line);
|
|
||||||
background: var(--app-card);
|
.section {
|
||||||
box-shadow: 0 .35rem 1rem rgba(17, 32, 56, .05);
|
|
||||||
|
&.history {
|
||||||
|
.metric-tabs {
|
||||||
|
width: 100%;
|
||||||
|
--td-tab-track-width: 4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag,
|
.chart {
|
||||||
.header,
|
width: 100%;
|
||||||
.desc {
|
height: 16rem;
|
||||||
margin: 0;
|
background: #FFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag,
|
.slider {
|
||||||
.desc {
|
gap: .5rem;
|
||||||
|
display: flex;
|
||||||
|
padding: .5rem 2rem;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
flex: none;
|
||||||
|
font-size: .75rem;
|
||||||
color: var(--app-sub);
|
color: var(--app-sub);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag {
|
.control {
|
||||||
font-size: .875rem;
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.loading-wrap {
|
||||||
font-size: 1.1rem;
|
display: flex;
|
||||||
}
|
min-height: 4rem;
|
||||||
|
align-items: center;
|
||||||
.desc {
|
justify-content: center;
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.theme-dark) .ups-dashboard {
|
|
||||||
.card {
|
|
||||||
box-shadow: 0 .35rem 1rem rgba(0, 0, 0, .2);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Toast } from "tdesign-mobile-vue";
|
import { Toast } from "tdesign-mobile-vue";
|
||||||
import { listServerFiles, resolveRequestErrorMessage } from "@/api/FileAPI";
|
import FileAPI from "@/api/FileAPI";
|
||||||
import FileExplorerGrid from "./FileExplorerGrid.vue";
|
import FileExplorerGrid from "./FileExplorerGrid.vue";
|
||||||
import FileExplorerList from "./FileExplorerList.vue";
|
import FileExplorerList from "./FileExplorerList.vue";
|
||||||
import { useNavBarStore } from "@/store/navBarStore";
|
import { useNavBarStore } from "@/store/navBarStore";
|
||||||
@@ -212,7 +212,7 @@ async function openItem(item: ExplorerItem): Promise<void> {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
Toast({
|
Toast({
|
||||||
theme: "error",
|
theme: "error",
|
||||||
message: resolveRequestErrorMessage(error)
|
message: "操作失败"
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
pendingFolderPath.value = "";
|
pendingFolderPath.value = "";
|
||||||
@@ -259,7 +259,7 @@ async function hydrateCurrentDirectory(pathSegments: string[]): Promise<void> {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
Toast({
|
Toast({
|
||||||
theme: "error",
|
theme: "error",
|
||||||
message: resolveRequestErrorMessage(error)
|
message: "操作失败"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -280,7 +280,7 @@ async function ensureDirectoryLoaded(pathSegments: string[], updatePageLoading:
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const files = await listServerFiles(pathSegments);
|
const files = await FileAPI.list(pathSegments);
|
||||||
const items = sortExplorerItems(files.map((item) => mapServerFileToExplorerItem(item)));
|
const items = sortExplorerItems(files.map((item) => mapServerFileToExplorerItem(item)));
|
||||||
directoryCache.value = {
|
directoryCache.value = {
|
||||||
...directoryCache.value,
|
...directoryCache.value,
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { parseBlob } from "music-metadata-browser";
|
import { parseBlob } from "music-metadata-browser";
|
||||||
import { Toast } from "tdesign-mobile-vue";
|
import { Toast } from "tdesign-mobile-vue";
|
||||||
import { buildServerFileURL } from "@/api/FileAPI";
|
|
||||||
import { isBrowserSupportedAudio } from "@/pages/file/fileAudio.shared";
|
import { isBrowserSupportedAudio } from "@/pages/file/fileAudio.shared";
|
||||||
import type { ExplorerItem } from "@/pages/file/fileExplorer.shared";
|
import type { ExplorerItem } from "@/pages/file/fileExplorer.shared";
|
||||||
import Storage from "@/utils/Storage";
|
import Storage from "@/utils/Storage";
|
||||||
|
import FileAPI from "@/api/FileAPI.ts";
|
||||||
|
|
||||||
const MUSIC_PLAYER_STORAGE_KEY = "timi-server.music-player";
|
const MUSIC_PLAYER_STORAGE_KEY = "timi-server.music-player";
|
||||||
|
|
||||||
@@ -212,7 +212,7 @@ export const useMusicPlayerStore = defineStore("music-player", () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetURL = buildServerFileURL(item.file.path, "download");
|
const targetURL = FileAPI.buildURL(item.file.path, "download");
|
||||||
if (audio.src !== targetURL) {
|
if (audio.src !== targetURL) {
|
||||||
audio.src = targetURL;
|
audio.src = targetURL;
|
||||||
audio.load();
|
audio.load();
|
||||||
@@ -276,7 +276,7 @@ export const useMusicPlayerStore = defineStore("music-player", () => {
|
|||||||
metadataLoadedMap.add(item.id);
|
metadataLoadedMap.add(item.id);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(buildServerFileURL(item.file.path, "download"));
|
const response = await fetch(FileAPI.buildURL(item.file.path, "download"));
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,12 +141,25 @@ export const useSettingStore = defineStore("setting", () => {
|
|||||||
persist();
|
persist();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveBaseURL(): string {
|
||||||
|
const settingStore = useSettingStore();
|
||||||
|
const connect = settingStore.connect;
|
||||||
|
const envBaseURL = typeof import.meta.env.VITE_API === "string" ? import.meta.env.VITE_API.trim() : "";
|
||||||
|
|
||||||
|
if (connect.host && connect.port) {
|
||||||
|
return `${connect.protocol}://${connect.host}:${connect.port}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return envBaseURL.replace(/\/+$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
connect,
|
connect,
|
||||||
dashboard,
|
dashboard,
|
||||||
hasConnectConfig,
|
hasConnectConfig,
|
||||||
setConnect,
|
setConnect,
|
||||||
resetConnect,
|
resetConnect,
|
||||||
setServerDashboard
|
setServerDashboard,
|
||||||
|
resolveBaseURL
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
54
src/types/Ups.ts
Normal file
54
src/types/Ups.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
export interface UpsStatusView {
|
||||||
|
serverTime: number;
|
||||||
|
upsTime: number;
|
||||||
|
hostName: string;
|
||||||
|
customer: string;
|
||||||
|
version: string;
|
||||||
|
deviceId: string;
|
||||||
|
upsType: string;
|
||||||
|
morphological: string;
|
||||||
|
ioPhase: string;
|
||||||
|
workMode: string;
|
||||||
|
inputVoltage: number;
|
||||||
|
inputFrequency?: number;
|
||||||
|
outputVoltage: number;
|
||||||
|
outputFrequency: number;
|
||||||
|
outputLoadPercent: number;
|
||||||
|
batteryVoltage: number;
|
||||||
|
batteryCapacity: number;
|
||||||
|
batteryRemainTime: number;
|
||||||
|
temperature: number;
|
||||||
|
bypassActive: boolean;
|
||||||
|
shutdownActive: boolean;
|
||||||
|
outputOn: boolean;
|
||||||
|
charging: boolean;
|
||||||
|
faultType?: string;
|
||||||
|
faultKind?: string;
|
||||||
|
warnings: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpsHistoryView {
|
||||||
|
serverTime: number;
|
||||||
|
sampleRateMs: number;
|
||||||
|
from: number;
|
||||||
|
to: number;
|
||||||
|
points: UpsHistoryPointView[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpsHistoryPointView {
|
||||||
|
at: number;
|
||||||
|
upsTime: number;
|
||||||
|
workMode: string;
|
||||||
|
inputVoltage: number;
|
||||||
|
inputFrequency?: number;
|
||||||
|
outputVoltage: number;
|
||||||
|
outputFrequency: number;
|
||||||
|
outputLoadPercent: number;
|
||||||
|
batteryVoltage: number;
|
||||||
|
batteryCapacity: number;
|
||||||
|
batteryRemainTime: number;
|
||||||
|
temperature?: number;
|
||||||
|
bypassActive: boolean;
|
||||||
|
shutdownActive: boolean;
|
||||||
|
outputOn: boolean;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user