fix: 修复路由和页面加载问题

v3
veypi 1 week ago
parent e5308f9471
commit 983a5651a3

@ -13,7 +13,7 @@ import (
type ListRequest struct { type ListRequest struct {
Page int `json:"page" src:"query" default:"1"` Page int `json:"page" src:"query" default:"1"`
PageSize int `json:"page_size" src:"query" default:"20"` PageSize int `json:"page_size" src:"query" default:"20"`
Keyword string `json:"keyword" src:"query"` Keyword string `json:"keyword" src:"query" default:""`
} }
type ListResponse struct { type ListResponse struct {

@ -28,7 +28,7 @@ export default async ($env) => {
try { try {
await vbase.refresh(); await vbase.refresh();
} catch (e) { } catch (e) {
vbase.logout(to.fullPath); vbase.logout('/login?redirect=' + encodeURIComponent(to.fullPath));
return false; return false;
} }
} }
@ -37,7 +37,7 @@ export default async ($env) => {
try { try {
await vbase.fetchUser(); await vbase.fetchUser();
} catch (e) { } catch (e) {
vbase.logout(to.fullPath); vbase.logout('/login?redirect=' + encodeURIComponent(to.fullPath));
return false; return false;
} }
} }
@ -50,11 +50,6 @@ export default async ($env) => {
return false; return false;
} }
} }
} else if (isGuest) {
if (!vbase.isExpired()) {
next('/');
return false;
}
} }
next(); next();

@ -157,12 +157,12 @@
// Define Menu Items // Define Menu Items
menuItems = [ menuItems = [
{label: $t('nav.dashboard'), icon: "<i class='fas fa-tachometer-alt'></i>", path: "/"}, {label: () => $t('nav.dashboard'), icon: "<i class='fas fa-tachometer-alt'></i>", path: "/"},
{label: $t('nav.org'), icon: "<i class='fas fa-sitemap'></i>", path: "/org"}, {label: () => $t('nav.org'), icon: "<i class='fas fa-sitemap'></i>", path: "/org"},
{label: $t('nav.profile'), icon: "<i class='fas fa-user'></i>", path: "/profile"}, {label: () => $t('nav.profile'), icon: "<i class='fas fa-user'></i>", path: "/profile"},
// Admin only items would be filtered here ideally // Admin only items would be filtered here ideally
{label: $t('nav.users'), icon: "<i class='fas fa-users-cog'></i>", path: "/users"}, {label: () => $t('nav.users'), icon: "<i class='fas fa-users-cog'></i>", path: "/users"},
{label: $t('nav.oauth'), icon: "<i class='fas fa-key'></i>", path: "/oauth/apps"}, {label: () => $t('nav.oauth'), icon: "<i class='fas fa-key'></i>", path: "/oauth/apps"},
]; ];
currentRouteName = ""; currentRouteName = "";
@ -186,34 +186,12 @@
}; };
</script> </script>
<script> <script>
$watch(() => $env.$route.path, () => { $router.onChange(() => {
// Update breadcrumb or active item const path = $router.current.path;
// v-sidebar handles active state via path matching usually console.log(path)
// We can update title based on route name
// For now just simple mapping or rely on $route
// $data.currentRouteName = $env.$route.name || $env.$route.path
// Simple implementation:
const path = $env.$route.path;
const item = $data.menuItems.find(i => i.path === path); const item = $data.menuItems.find(i => i.path === path);
$data.currentRouteName = item ? item.label : path; $data.currentRouteName = item ? item.label : path;
}); });
// Watch global state for user/org changes
$watch(() => [$env.$vbase.user, $env.$i18n.locale], () => {
$data.user = $env.$vbase.user;
// Re-generate menu items when locale changes
$data.menuItems = [
{label: $t('nav.dashboard'), icon: "<i class='fas fa-tachometer-alt'></i>", path: "/"},
{label: $t('nav.org'), icon: "<i class='fas fa-sitemap'></i>", path: "/org"},
{label: $t('nav.profile'), icon: "<i class='fas fa-user'></i>", path: "/profile"},
// Admin only items would be filtered here ideally
{label: $t('nav.users'), icon: "<i class='fas fa-users-cog'></i>", path: "/users"},
{label: $t('nav.oauth'), icon: "<i class='fas fa-key'></i>", path: "/oauth/apps"},
];
});
$watch(() => $env.$vbase.currentOrg, () => {
$data.currentOrg = $env.$vbase.currentOrg;
});
</script> </script>
</html> </html>

@ -73,7 +73,6 @@
</head> </head>
<body> <body>
123
<h2 class="login-title">{{ $t('auth.login') }}</h2> <h2 class="login-title">{{ $t('auth.login') }}</h2>
<div v-if="error" class="error-msg">{{ error }}</div> <div v-if="error" class="error-msg">{{ error }}</div>
@ -106,7 +105,8 @@
error = ""; error = "";
try { try {
await $env.$vbase.login(username, password); await $env.$vbase.login(username, password);
$router.push('/'); const redirect = $router.query.redirect || '/';
$router.push(redirect);
} catch (err) { } catch (err) {
error = err.message || "Login failed"; error = err.message || "Login failed";
} }

@ -1,115 +1,123 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta name="description" content="Dashboard"> <meta name="description" content="Dashboard">
<title>{{ $t('nav.dashboard') }}</title> <title>{{ $t('nav.dashboard') }}</title>
<style> <style>
.dashboard-container { .dashboard-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 20px; gap: 20px;
} }
.stats-grid {
display: grid; .stats-grid {
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); display: grid;
gap: 20px; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
} gap: 20px;
.stat-card { }
background: #fff;
padding: 20px; .stat-card {
border-radius: var(--radius-md); background: #fff;
box-shadow: var(--shadow-sm); padding: 20px;
display: flex; border-radius: var(--radius-md);
flex-direction: column; box-shadow: var(--shadow-sm);
gap: 10px; display: flex;
} flex-direction: column;
.stat-title { gap: 10px;
font-size: 14px; }
color: var(--text-color-secondary);
} .stat-title {
.stat-value { font-size: 14px;
font-size: 24px; color: var(--text-color-secondary);
font-weight: bold; }
color: var(--color-primary);
} .stat-value {
.chart-container { font-size: 24px;
background: #fff; font-weight: bold;
padding: 20px; color: var(--color-primary);
border-radius: var(--radius-md); }
box-shadow: var(--shadow-sm);
height: 400px; .chart-container {
} background: #fff;
</style> padding: 20px;
border-radius: var(--radius-md);
box-shadow: var(--shadow-sm);
height: 400px;
}
</style>
</head> </head>
<body> <body>
<div class="dashboard-container"> <div class="dashboard-container">
<h1>{{ $t('nav.dashboard') }}</h1> <h1>{{ $t('nav.dashboard') }}</h1>
<div class="stats-grid"> <div class="stats-grid">
<div class="stat-card" v-for="stat in stats"> <div class="stat-card" v-for="stat in stats">
<div class="stat-title">{{ stat.title }}</div> <div class="stat-title">{{ stat.title }}</div>
<div class="stat-value">{{ stat.value }}</div> <div class="stat-value">{{ stat.value }}</div>
</div> </div>
</div>
<div class="chart-container" id="main-chart"></div>
</div> </div>
<div class="chart-container" id="main-chart"></div>
</div>
</body> </body>
<script setup> <script setup>
stats = [ stats = [
{ title: "Total Users", value: "1,234" }, {title: "Total Users", value: "1,234"},
{ title: "Active Orgs", value: "56" }, {title: "Active Orgs", value: "56"},
{ title: "API Calls", value: "89.2k" }, {title: "API Calls", value: "89.2k"},
{ title: "Revenue", value: "$12,340" } {title: "Revenue", value: "$12,340"}
]; ];
// Mock data fetch // Mock data fetch
fetchStats = async () => { fetchStats = async () => {
// In real app, call API // In real app, call API
// const res = await $axios.get('/api/stats/dashboard'); // const res = await $axios.get('/api/stats/dashboard');
// stats = res; // stats = res;
}; };
initChart = () => { initChart = () => {
const chartDom = $node.querySelector('#main-chart'); const chartDom = $node.querySelector('#main-chart');
if (!chartDom) return; if (!chartDom) return;
const myChart = echarts.init(chartDom); const myChart = echarts.init(chartDom);
const option = { const option = {
title: { title: {
text: 'Activity Trend' text: 'Activity Trend'
}, },
tooltip: { tooltip: {
trigger: 'axis' trigger: 'axis'
}, },
xAxis: { xAxis: {
type: 'category', type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
}, },
yAxis: { yAxis: {
type: 'value' type: 'value'
}, },
series: [ series: [
{ {
data: [820, 932, 901, 934, 1290, 1330, 1320], data: [820, 932, 901, 934, 1290, 1330, 1320],
type: 'line', type: 'line',
smooth: true, smooth: true,
itemStyle: { itemStyle: {
color: getComputedStyle(document.documentElement).getPropertyValue('--color-primary').trim() color: getComputedStyle(document.documentElement).getPropertyValue('--color-primary').trim()
} }
} }
] ]
};
myChart.setOption(option);
// Resize chart on window resize
window.addEventListener('resize', () => {
myChart.resize();
});
}; };
myChart.setOption(option);
// Resize chart on window resize
window.addEventListener('resize', () => {
myChart.resize();
});
};
</script> </script>
<script> <script>
// Run after mount // Run after mount
$data.initChart(); $data.initChart();
</script> </script>
</html> </html>

@ -1,7 +1,7 @@
const routes = [ const routes = [
// Public // Public
{ path: '/login', component: '/page/auth/login.html', layout: 'public', meta: { guest: true } }, { path: '/login', component: '/page/auth/login.html', layout: 'public' },
{ path: '/register', component: '/page/auth/register.html', layout: 'public', meta: { guest: true } }, { path: '/register', component: '/page/auth/register.html', layout: 'public' },
// Dashboard (Default Layout) // Dashboard (Default Layout)
{ {

@ -1,217 +1,220 @@
class VBase { class VBase {
constructor(baseURL) { constructor(baseURL) {
this.baseURL = baseURL || ''; this.baseURL = baseURL || '';
this.tokenKey = 'vbase_access_token'; this.tokenKey = 'vbase_access_token';
this.refreshTokenKey = 'vbase_refresh_token'; this.refreshTokenKey = 'vbase_refresh_token';
this.userKey = 'vbase_user_info'; this.userKey = 'vbase_user_info';
this.orgKey = 'vbase_current_org'; this.orgKey = 'vbase_current_org';
this._token = localStorage.getItem(this.tokenKey) || ''; this._token = localStorage.getItem(this.tokenKey) || '';
this._refreshToken = localStorage.getItem(this.refreshTokenKey) || ''; this._refreshToken = localStorage.getItem(this.refreshTokenKey) || '';
this._user = JSON.parse(localStorage.getItem(this.userKey) || 'null'); this._user = JSON.parse(localStorage.getItem(this.userKey) || 'null');
this._currentOrg = JSON.parse(localStorage.getItem(this.orgKey) || 'null'); this._currentOrg = JSON.parse(localStorage.getItem(this.orgKey) || 'null');
} }
// Getters // Getters
get token() { return this._token; } get token() { return this._token; }
get refreshToken() { return this._refreshToken; } get refreshToken() { return this._refreshToken; }
get user() { return this._user; } get user() { return this._user; }
get currentOrg() { return this._currentOrg; } get currentOrg() { return this._currentOrg; }
// Setters // Setters
set token(val) { set token(val) {
this._token = val; this._token = val;
if (val) localStorage.setItem(this.tokenKey, val); if (val) localStorage.setItem(this.tokenKey, val);
else localStorage.removeItem(this.tokenKey); else localStorage.removeItem(this.tokenKey);
} }
set refreshToken(val) { set refreshToken(val) {
this._refreshToken = val; this._refreshToken = val;
if (val) localStorage.setItem(this.refreshTokenKey, val); if (val) localStorage.setItem(this.refreshTokenKey, val);
else localStorage.removeItem(this.refreshTokenKey); else localStorage.removeItem(this.refreshTokenKey);
} }
set user(val) { set user(val) {
this._user = val; this._user = val;
if (val) localStorage.setItem(this.userKey, JSON.stringify(val)); if (val) localStorage.setItem(this.userKey, JSON.stringify(val));
else localStorage.removeItem(this.userKey); else localStorage.removeItem(this.userKey);
} }
set currentOrg(val) { set currentOrg(val) {
this._currentOrg = val; this._currentOrg = val;
if (val) localStorage.setItem(this.orgKey, JSON.stringify(val)); if (val) localStorage.setItem(this.orgKey, JSON.stringify(val));
else localStorage.removeItem(this.orgKey); else localStorage.removeItem(this.orgKey);
} }
// API Helpers // API Helpers
async request(method, path, data = null, headers = {}) { async request(method, path, data = null, headers = {}) {
const url = `${this.baseURL}${path}`; const url = `${this.baseURL}${path}`;
const config = { const config = {
method, method,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...this.getAuthHeaders(), ...this.getAuthHeaders(),
...headers ...headers
} }
}; };
if (data) config.body = JSON.stringify(data); if (data) config.body = JSON.stringify(data);
const response = await fetch(url, config); const response = await fetch(url, config);
const resData = await response.json(); const resData = await response.json();
if (!response.ok) { if (!response.ok) {
throw resData || new Error(`Request failed: ${response.status}`); // Include resData in the error so caller can access the response body
} const error = new Error(resData.message || `Request failed: ${response.status}`);
Object.assign(error, resData);
if (resData.code && resData.code !== 200) { throw error;
throw new Error(resData.message || 'API Error'); }
}
if (resData.code && resData.code !== 200) {
return resData.data || resData; throw new Error(resData.message || 'API Error');
} }
// Auth Actions return resData.data || resData;
async login(username, password) { }
// Auth Actions
async login(username, password) {
try {
const data = await this.request('POST', '/api/auth/login', { username, password });
if (data.access_token) {
this.token = data.access_token;
if (data.refresh_token) this.refreshToken = data.refresh_token;
this.user = data.user; // Set user directly from login response
return true;
}
return false;
} catch (e) {
throw e;
}
}
async logout(redirect) {
try {
// Optional: Call server logout
// await this.request('POST', '/api/auth/logout');
} catch (e) {
console.warn('Logout API failed', e);
} finally {
this.clear();
if (redirect) {
location.href = redirect;
} else {
location.reload();
}
}
}
async refresh() {
if (!this.refreshToken) throw new Error("No refresh token");
try {
const data = await this.request('POST', '/api/auth/refresh', { refresh_token: this.refreshToken });
if (data.access_token) {
this.token = data.access_token;
if (data.refresh_token) this.refreshToken = data.refresh_token;
return true;
}
return false;
} catch (e) {
this.logout();
throw e;
}
}
async fetchUser() {
const user = await this.request('GET', '/api/auth/me');
this.user = user;
return user;
}
// Auth Headers
getAuthHeaders() {
const headers = {};
if (this.token) {
headers['Authorization'] = `Bearer ${this.token}`;
}
if (this.currentOrg && this.currentOrg.id) {
headers['X-Org-ID'] = this.currentOrg.id;
}
return headers;
}
// Permission Check
hasPermission(permission) {
if (!this.user) return false;
if (this.user.is_admin) return true;
if (!permission) return true;
const userPerms = this.user.permissions || [];
return userPerms.includes(permission);
}
hasRole(role) {
if (!this.user) return false;
if (this.user.is_admin) return true;
const userRoles = this.user.roles || [];
return userRoles.includes(role);
}
// State Management
clear() {
this.token = '';
this.refreshToken = '';
this.user = null;
this.currentOrg = null;
}
isExpired(token) {
if (!token) token = this.token;
if (!token) return true;
try {
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const payload = JSON.parse(window.atob(base64));
const now = Math.floor(Date.now() / 1000);
return payload.exp && payload.exp < now;
} catch (e) {
return true;
}
}
wrapAxios(axiosInstance) {
// Request Interceptor
axiosInstance.interceptors.request.use(config => {
const headers = this.getAuthHeaders();
for (const key in headers) {
config.headers[key] = headers[key];
}
return config;
}, error => Promise.reject(error));
// Response Interceptor
axiosInstance.interceptors.response.use(response => {
const res = response.data;
if (res && res.code === 200) {
return res.data;
}
if (res && res.code && res.code !== 200) {
return Promise.reject(new Error(res.message || 'Error'));
}
return res || response;
}, async error => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try { try {
const data = await this.request('POST', '/api/auth/login', { username, password }); await this.refresh();
if (data.access) { const headers = this.getAuthHeaders();
this.token = data.access; originalRequest.headers['Authorization'] = headers['Authorization'];
if (data.refresh) this.refreshToken = data.refresh; return axiosInstance(originalRequest);
await this.fetchUser();
return true;
}
return false;
} catch (e) { } catch (e) {
throw e; this.logout('/login?redirect=' + encodeURIComponent(window.location.pathname));
return Promise.reject(e);
} }
} }
return Promise.reject(error?.response?.data || error);
async logout(redirect) { });
try { }
// Optional: Call server logout
// await this.request('POST', '/api/auth/logout');
} catch (e) {
console.warn('Logout API failed', e);
} finally {
this.clear();
if (redirect) {
location.href = redirect;
} else {
location.reload();
}
}
}
async refresh() {
if (!this.refreshToken) throw new Error("No refresh token");
try {
const data = await this.request('POST', '/api/auth/refresh', { refresh: this.refreshToken });
if (data.access) {
this.token = data.access;
if (data.refresh) this.refreshToken = data.refresh;
return true;
}
return false;
} catch (e) {
this.logout();
throw e;
}
}
async fetchUser() {
const user = await this.request('GET', '/api/auth/me');
this.user = user;
return user;
}
// Auth Headers
getAuthHeaders() {
const headers = {};
if (this.token) {
headers['Authorization'] = `Bearer ${this.token}`;
}
if (this.currentOrg && this.currentOrg.id) {
headers['X-Org-ID'] = this.currentOrg.id;
}
return headers;
}
// Permission Check
hasPermission(permission) {
if (!this.user) return false;
if (this.user.is_admin) return true;
if (!permission) return true;
const userPerms = this.user.permissions || [];
return userPerms.includes(permission);
}
hasRole(role) {
if (!this.user) return false;
if (this.user.is_admin) return true;
const userRoles = this.user.roles || [];
return userRoles.includes(role);
}
// State Management
clear() {
this.token = '';
this.refreshToken = '';
this.user = null;
this.currentOrg = null;
}
isExpired(token) {
if (!token) token = this.token;
if (!token) return true;
try {
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const payload = JSON.parse(window.atob(base64));
const now = Math.floor(Date.now() / 1000);
return payload.exp && payload.exp < now;
} catch (e) {
return true;
}
}
wrapAxios(axiosInstance) {
// Request Interceptor
axiosInstance.interceptors.request.use(config => {
const headers = this.getAuthHeaders();
for (const key in headers) {
config.headers[key] = headers[key];
}
return config;
}, error => Promise.reject(error));
// Response Interceptor
axiosInstance.interceptors.response.use(response => {
const res = response.data;
if (res && res.code === 200) {
return res.data;
}
if (res && res.code && res.code !== 200) {
return Promise.reject(new Error(res.message || 'Error'));
}
return res || response;
}, async error => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
await this.refresh();
const headers = this.getAuthHeaders();
originalRequest.headers['Authorization'] = headers['Authorization'];
return axiosInstance(originalRequest);
} catch (e) {
this.logout(window.location.pathname);
return Promise.reject(e);
}
}
return Promise.reject(error?.response?.data || error);
});
}
} }
export default VBase; export default VBase;

Loading…
Cancel
Save