mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-15 17:30:13 +08:00
feat(dashboard): Enhance Types in Settings.vue (#7241)
* feat: Strict Types for Settings.vue, Enhance Error Handling feat: Fixed Type in Theme Constant for better TypeScript in Settings.vue * fix: add other error handling and improve api create type behavier * fix error messages
This commit is contained in:
@@ -1,2 +1,5 @@
|
||||
export const LIGHT_THEME_NAME = "BlueBusinessTheme";
|
||||
export const DARK_THEME_NAME = "BlueBusinessDarkTheme";
|
||||
|
||||
// Theme related types
|
||||
export type ThemeMode = "light" | "dark" | "auto";
|
||||
37
dashboard/src/types/api.ts
Normal file
37
dashboard/src/types/api.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
// API Key related types
|
||||
export interface ApiKey {
|
||||
key_id: string;
|
||||
name: string;
|
||||
key_prefix: string;
|
||||
scopes: string[];
|
||||
is_revoked: boolean;
|
||||
is_expired: boolean;
|
||||
last_used_at: string | null;
|
||||
created_at: string;
|
||||
expires_at: string | null;
|
||||
}
|
||||
|
||||
export type ApiKeyExpiresDays = 1 | 7 | 30 | 90 | "permanent";
|
||||
|
||||
export interface ApiKeyCreatePayload {
|
||||
name: string;
|
||||
scopes: string[];
|
||||
expires_in_days?: number;
|
||||
}
|
||||
|
||||
export interface ApiKeyListResponse {
|
||||
status: "ok" | "error";
|
||||
data: ApiKey[];
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface ApiKeyCreateResponse {
|
||||
status: "ok" | "error";
|
||||
data?: ApiKey & { api_key: string }; // Include the full API key in the response
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface ApiKeyActionResponse {
|
||||
status: "ok" | "error";
|
||||
message?: string;
|
||||
}
|
||||
@@ -3,8 +3,8 @@ export type ThemeTypes = {
|
||||
dark: boolean;
|
||||
variables?: object;
|
||||
colors: {
|
||||
primary?: string;
|
||||
secondary?: string;
|
||||
primary: string;
|
||||
secondary: string;
|
||||
tertiary?: string;
|
||||
info?: string;
|
||||
success?: string;
|
||||
|
||||
@@ -338,7 +338,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import axios from "@/utils/request";
|
||||
import axios, { AxiosError } from "@/utils/request";
|
||||
import WaitingForRestart from "@/components/shared/WaitingForRestart.vue";
|
||||
import ProxySelector from "@/components/shared/ProxySelector.vue";
|
||||
import MigrationDialog from "@/components/shared/MigrationDialog.vue";
|
||||
@@ -349,9 +349,17 @@ import { restartAstrBot as restartAstrBotRuntime } from "@/utils/restartAstrBot"
|
||||
import { useModuleI18n } from "@/i18n/composables";
|
||||
import { useTheme } from "vuetify";
|
||||
import { BlueBusinessLightTheme } from "@/theme/BlueBusinessLightTheme";
|
||||
import { LIGHT_THEME_NAME, DARK_THEME_NAME } from "@/theme/constants";
|
||||
import { LIGHT_THEME_NAME, DARK_THEME_NAME, ThemeMode } from "@/theme/constants";
|
||||
import { useToastStore } from "@/stores/toast";
|
||||
import { useCustomizerStore } from "@/stores/customizer";
|
||||
import {
|
||||
ApiKey,
|
||||
ApiKeyActionResponse,
|
||||
ApiKeyCreatePayload,
|
||||
ApiKeyCreateResponse,
|
||||
ApiKeyExpiresDays,
|
||||
ApiKeyListResponse
|
||||
} from "@/types/api";
|
||||
|
||||
const { tm } = useModuleI18n("features/settings");
|
||||
const toastStore = useToastStore();
|
||||
@@ -364,7 +372,7 @@ const themeMode = computed({
|
||||
if (customizer.autoSwitchTheme) return "auto";
|
||||
return customizer.isDarkTheme ? "dark" : "light";
|
||||
},
|
||||
set(mode: string) {
|
||||
set(mode: ThemeMode) {
|
||||
if (mode === "auto") {
|
||||
customizer.SET_AUTO_SYNC(true);
|
||||
customizer.APPLY_SYSTEM_THEME();
|
||||
@@ -377,7 +385,7 @@ const themeMode = computed({
|
||||
},
|
||||
});
|
||||
|
||||
const getStoredColor = (key, fallback) => {
|
||||
const getStoredColor = (key: string, fallback: string) => {
|
||||
const stored =
|
||||
typeof window !== "undefined" ? localStorage.getItem(key) : null;
|
||||
return stored || fallback;
|
||||
@@ -476,7 +484,7 @@ const resolveThemes = () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
const applyThemeColors = (primary, secondary) => {
|
||||
const applyThemeColors = (primary: string, secondary: string) => {
|
||||
const themes = resolveThemes();
|
||||
if (!themes) return;
|
||||
[LIGHT_THEME_NAME, DARK_THEME_NAME].forEach((name) => {
|
||||
@@ -505,13 +513,21 @@ watch(secondaryColor, (value) => {
|
||||
applyThemeColors(primaryColor.value, value);
|
||||
});
|
||||
|
||||
const resetThemeColors = () => {
|
||||
primaryColor.value = BlueBusinessLightTheme.colors.primary;
|
||||
secondaryColor.value = BlueBusinessLightTheme.colors.secondary;
|
||||
localStorage.removeItem("themePrimary");
|
||||
localStorage.removeItem("themeSecondary");
|
||||
applyThemeColors(primaryColor.value, secondaryColor.value);
|
||||
};
|
||||
|
||||
const wfr = ref(null);
|
||||
const migrationDialog = ref(null);
|
||||
const backupDialog = ref(null);
|
||||
const apiKeys = ref([]);
|
||||
const apiKeys = ref<ApiKey[]>([]);
|
||||
const apiKeyCreating = ref(false);
|
||||
const newApiKeyName = ref("");
|
||||
const newApiKeyExpiresInDays = ref(30);
|
||||
const newApiKeyExpiresInDays = ref<ApiKeyExpiresDays>(30);
|
||||
const newApiKeyScopes = ref(["chat", "config", "file", "im"]);
|
||||
const createdApiKeyPlaintext = ref("");
|
||||
const apiKeyExpiryOptions = computed(() => [
|
||||
@@ -529,7 +545,7 @@ const availableScopes = [
|
||||
{ value: "im", label: "im" },
|
||||
];
|
||||
|
||||
const showToast = (message, color = "success") => {
|
||||
const showToast = (message: string, color = "success") => {
|
||||
toastStore.add({
|
||||
message,
|
||||
color,
|
||||
@@ -537,7 +553,7 @@ const showToast = (message, color = "success") => {
|
||||
});
|
||||
};
|
||||
|
||||
const formatDate = (value) => {
|
||||
const formatDate = (value: string | null) => {
|
||||
if (!value) return "-";
|
||||
const dt = new Date(value);
|
||||
if (Number.isNaN(dt.getTime())) return "-";
|
||||
@@ -546,22 +562,27 @@ const formatDate = (value) => {
|
||||
|
||||
const loadApiKeys = async () => {
|
||||
try {
|
||||
const res = await axios.get("/api/apikey/list");
|
||||
const res = await axios.get<ApiKeyListResponse>("/api/apikey/list");
|
||||
if (res.data.status !== "ok") {
|
||||
showToast(res.data.message || tm("apiKey.messages.loadFailed"), "error");
|
||||
return;
|
||||
}
|
||||
apiKeys.value = res.data.data || [];
|
||||
} catch (e) {
|
||||
showToast(
|
||||
e?.response?.data?.message || tm("apiKey.messages.loadFailed"),
|
||||
"error",
|
||||
);
|
||||
apiKeys.value = res.data.data;
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof AxiosError) {
|
||||
showToast(
|
||||
e?.response?.data?.message || tm("apiKey.messages.loadFailed"),
|
||||
"error",
|
||||
);
|
||||
} else {
|
||||
console.error("An unexpected error occurred while loading API keys:", e);
|
||||
showToast(tm("apiKey.messages.loadFailed"), "error");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const tryExecCommandCopy = (text) => {
|
||||
let textArea = null;
|
||||
const tryExecCommandCopy = (text: string): boolean => {
|
||||
let textArea: HTMLTextAreaElement | null = null;
|
||||
try {
|
||||
if (typeof document === "undefined" || !document.body) return false;
|
||||
textArea = document.createElement("textarea");
|
||||
@@ -575,6 +596,7 @@ const tryExecCommandCopy = (text) => {
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
textArea.setSelectionRange(0, text.length);
|
||||
// Fallback to deprecated execCommand for older browsers
|
||||
return document.execCommand("copy");
|
||||
} catch (_) {
|
||||
return false;
|
||||
@@ -589,17 +611,21 @@ const tryExecCommandCopy = (text) => {
|
||||
}
|
||||
};
|
||||
|
||||
const copyTextToClipboard = async (text) => {
|
||||
const copyTextToClipboard = async (text: string): Promise<boolean> => {
|
||||
if (!text) return false;
|
||||
if (tryExecCommandCopy(text)) return true;
|
||||
if (typeof navigator === "undefined" || !navigator.clipboard?.writeText)
|
||||
return false;
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
|
||||
// 由于execCommand被弃用,改用推荐的Clipboard API,但仍保留execCommand作为兼容性回退
|
||||
if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
return true;
|
||||
} catch (_) {
|
||||
// Clipboard API failed, try fallback method
|
||||
}
|
||||
}
|
||||
|
||||
// 回退到execCommand方法,兼容不支持Clipboard API的环境
|
||||
return tryExecCommandCopy(text);
|
||||
};
|
||||
|
||||
const copyCreatedApiKey = async () => {
|
||||
@@ -623,14 +649,17 @@ const createApiKey = async () => {
|
||||
}
|
||||
apiKeyCreating.value = true;
|
||||
try {
|
||||
const payload = {
|
||||
const payload: ApiKeyCreatePayload = {
|
||||
name: newApiKeyName.value,
|
||||
scopes: selectedScopes,
|
||||
};
|
||||
if (newApiKeyExpiresInDays.value !== "permanent") {
|
||||
payload.expires_in_days = Number(newApiKeyExpiresInDays.value);
|
||||
}
|
||||
const res = await axios.post("/api/apikey/create", payload);
|
||||
const res = await axios.post<ApiKeyCreateResponse>(
|
||||
"/api/apikey/create",
|
||||
payload,
|
||||
);
|
||||
if (res.data.status !== "ok") {
|
||||
showToast(
|
||||
res.data.message || tm("apiKey.messages.createFailed"),
|
||||
@@ -643,19 +672,27 @@ const createApiKey = async () => {
|
||||
newApiKeyExpiresInDays.value = 30;
|
||||
showToast(tm("apiKey.messages.createSuccess"), "success");
|
||||
await loadApiKeys();
|
||||
} catch (e) {
|
||||
showToast(
|
||||
e?.response?.data?.message || tm("apiKey.messages.createFailed"),
|
||||
"error",
|
||||
);
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof AxiosError) {
|
||||
showToast(
|
||||
e?.response?.data?.message || tm("apiKey.messages.createFailed"),
|
||||
"error",
|
||||
);
|
||||
} else {
|
||||
console.error("An unexpected error occurred while creating API key:", e);
|
||||
showToast(tm("apiKey.messages.createFailed"), "error");
|
||||
}
|
||||
} finally {
|
||||
apiKeyCreating.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const revokeApiKey = async (keyId) => {
|
||||
const revokeApiKey = async (keyId: string) => {
|
||||
try {
|
||||
const res = await axios.post("/api/apikey/revoke", { key_id: keyId });
|
||||
const res = await axios.post<ApiKeyActionResponse>(
|
||||
"/api/apikey/revoke",
|
||||
{ key_id: keyId },
|
||||
);
|
||||
if (res.data.status !== "ok") {
|
||||
showToast(
|
||||
res.data.message || tm("apiKey.messages.revokeFailed"),
|
||||
@@ -665,17 +702,25 @@ const revokeApiKey = async (keyId) => {
|
||||
}
|
||||
showToast(tm("apiKey.messages.revokeSuccess"), "success");
|
||||
await loadApiKeys();
|
||||
} catch (e) {
|
||||
showToast(
|
||||
e?.response?.data?.message || tm("apiKey.messages.revokeFailed"),
|
||||
"error",
|
||||
);
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof AxiosError) {
|
||||
showToast(
|
||||
e?.response?.data?.message || tm("apiKey.messages.revokeFailed"),
|
||||
"error",
|
||||
);
|
||||
} else {
|
||||
console.error("An unexpected error occurred while revoking API key:", e);
|
||||
showToast(tm("apiKey.messages.revokeFailed"), "error");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const deleteApiKey = async (keyId) => {
|
||||
const deleteApiKey = async (keyId: string) => {
|
||||
try {
|
||||
const res = await axios.post("/api/apikey/delete", { key_id: keyId });
|
||||
const res = await axios.post<ApiKeyActionResponse>(
|
||||
"/api/apikey/delete",
|
||||
{ key_id: keyId },
|
||||
);
|
||||
if (res.data.status !== "ok") {
|
||||
showToast(
|
||||
res.data.message || tm("apiKey.messages.deleteFailed"),
|
||||
@@ -685,19 +730,32 @@ const deleteApiKey = async (keyId) => {
|
||||
}
|
||||
showToast(tm("apiKey.messages.deleteSuccess"), "success");
|
||||
await loadApiKeys();
|
||||
} catch (e) {
|
||||
showToast(
|
||||
e?.response?.data?.message || tm("apiKey.messages.deleteFailed"),
|
||||
"error",
|
||||
);
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof AxiosError) {
|
||||
showToast(
|
||||
e?.response?.data?.message || tm("apiKey.messages.deleteFailed"),
|
||||
"error",
|
||||
);
|
||||
} else {
|
||||
console.error("An unexpected error occurred while deleting API key:", e);
|
||||
showToast(tm("apiKey.messages.deleteFailed"), "error");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const restartAstrBot = async () => {
|
||||
try {
|
||||
await restartAstrBotRuntime(wfr.value);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof AxiosError) {
|
||||
showToast(
|
||||
error?.response?.data?.message || tm("apiKey.messages.restartFailed"),
|
||||
"error",
|
||||
);
|
||||
} else {
|
||||
console.error("An unexpected error occurred while restarting AstrBot:", error);
|
||||
showToast(tm("apiKey.messages.restartFailed"), "error");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -720,14 +778,6 @@ const openBackupDialog = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const resetThemeColors = () => {
|
||||
primaryColor.value = BlueBusinessLightTheme.colors.primary;
|
||||
secondaryColor.value = BlueBusinessLightTheme.colors.secondary;
|
||||
localStorage.removeItem("themePrimary");
|
||||
localStorage.removeItem("themeSecondary");
|
||||
applyThemeColors(primaryColor.value, secondaryColor.value);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadApiKeys();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user