feat(ops-web): add operations console frontend

Vue 3 + Ant Design Vue SPA scaffold for the internal ops console. Ships
the auth, account list, and audit log views that consume the new ops-api
endpoints, plus the nginx/vite/typecheck plumbing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-28 11:33:17 +08:00
parent f63e800f21
commit f5254f6a27
20 changed files with 1830 additions and 7 deletions
+86
View File
@@ -0,0 +1,86 @@
import { defineStore } from "pinia";
import { computed, ref } from "vue";
import { http } from "@/lib/http";
import {
type OperatorView,
clearStoredSession,
readStoredSession,
writeStoredSession,
} from "@/lib/storage";
interface LoginResponse {
access_token: string;
expires_at: number;
operator: OperatorView;
}
export const useAuthStore = defineStore("ops-auth", () => {
const accessToken = ref<string | null>(null);
const expiresAt = ref<number | null>(null);
const operator = ref<OperatorView | null>(null);
const initialized = ref(false);
function hydrate(): void {
const stored = readStoredSession();
accessToken.value = stored.accessToken;
expiresAt.value = stored.expiresAt;
operator.value = stored.operator;
initialized.value = true;
}
async function login(username: string, password: string): Promise<void> {
const result = await http.post<LoginResponse>("/auth/login", { username, password });
accessToken.value = result.access_token;
expiresAt.value = result.expires_at;
operator.value = result.operator;
writeStoredSession({
accessToken: result.access_token,
expiresAt: result.expires_at,
operator: result.operator,
});
}
async function refreshSelf(): Promise<void> {
if (!accessToken.value) return;
try {
const me = await http.get<OperatorView & { last_login_at: string | null }>("/auth/me");
operator.value = {
id: me.id,
username: me.username,
display_name: me.display_name,
email: me.email,
role: me.role,
status: me.status,
};
writeStoredSession({
accessToken: accessToken.value,
expiresAt: expiresAt.value,
operator: operator.value,
});
} catch {
logout();
}
}
function logout(): void {
accessToken.value = null;
expiresAt.value = null;
operator.value = null;
clearStoredSession();
}
const isAuthenticated = computed(() => !!accessToken.value);
return {
accessToken,
expiresAt,
operator,
initialized,
isAuthenticated,
hydrate,
login,
refreshSelf,
logout,
};
});
+3
View File
@@ -0,0 +1,3 @@
import { createPinia } from "pinia";
export const pinia = createPinia();