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:
Kangyang Ji
2026-04-02 07:56:35 +01:00
committed by GitHub
parent 105530a51c
commit b17e5d42b7
4 changed files with 150 additions and 60 deletions

View File

@@ -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";

View 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;
}

View File

@@ -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;

View File

@@ -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();
});