mirror of https://github.com/veypi/OneAuth.git
fix: 修复路由和页面加载问题
parent
e5308f9471
commit
983a5651a3
@ -1,115 +1,123 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta name="description" content="Dashboard">
|
||||
<title>{{ $t('nav.dashboard') }}</title>
|
||||
<style>
|
||||
.dashboard-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
.stat-card {
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-sm);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.stat-title {
|
||||
font-size: 14px;
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
.chart-container {
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-sm);
|
||||
height: 400px;
|
||||
}
|
||||
</style>
|
||||
<meta name="description" content="Dashboard">
|
||||
<title>{{ $t('nav.dashboard') }}</title>
|
||||
<style>
|
||||
.dashboard-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-sm);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.stat-title {
|
||||
font-size: 14px;
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-sm);
|
||||
height: 400px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="dashboard-container">
|
||||
<h1>{{ $t('nav.dashboard') }}</h1>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card" v-for="stat in stats">
|
||||
<div class="stat-title">{{ stat.title }}</div>
|
||||
<div class="stat-value">{{ stat.value }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-container" id="main-chart"></div>
|
||||
<div class="dashboard-container">
|
||||
<h1>{{ $t('nav.dashboard') }}</h1>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card" v-for="stat in stats">
|
||||
<div class="stat-title">{{ stat.title }}</div>
|
||||
<div class="stat-value">{{ stat.value }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-container" id="main-chart"></div>
|
||||
</div>
|
||||
</body>
|
||||
<script setup>
|
||||
stats = [
|
||||
{ title: "Total Users", value: "1,234" },
|
||||
{ title: "Active Orgs", value: "56" },
|
||||
{ title: "API Calls", value: "89.2k" },
|
||||
{ title: "Revenue", value: "$12,340" }
|
||||
];
|
||||
|
||||
// Mock data fetch
|
||||
fetchStats = async () => {
|
||||
// In real app, call API
|
||||
// const res = await $axios.get('/api/stats/dashboard');
|
||||
// stats = res;
|
||||
};
|
||||
|
||||
initChart = () => {
|
||||
const chartDom = $node.querySelector('#main-chart');
|
||||
if (!chartDom) return;
|
||||
|
||||
const myChart = echarts.init(chartDom);
|
||||
const option = {
|
||||
title: {
|
||||
text: 'Activity Trend'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: [820, 932, 901, 934, 1290, 1330, 1320],
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
itemStyle: {
|
||||
color: getComputedStyle(document.documentElement).getPropertyValue('--color-primary').trim()
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
myChart.setOption(option);
|
||||
|
||||
// Resize chart on window resize
|
||||
window.addEventListener('resize', () => {
|
||||
myChart.resize();
|
||||
});
|
||||
stats = [
|
||||
{title: "Total Users", value: "1,234"},
|
||||
{title: "Active Orgs", value: "56"},
|
||||
{title: "API Calls", value: "89.2k"},
|
||||
{title: "Revenue", value: "$12,340"}
|
||||
];
|
||||
|
||||
// Mock data fetch
|
||||
fetchStats = async () => {
|
||||
// In real app, call API
|
||||
// const res = await $axios.get('/api/stats/dashboard');
|
||||
// stats = res;
|
||||
};
|
||||
|
||||
initChart = () => {
|
||||
const chartDom = $node.querySelector('#main-chart');
|
||||
if (!chartDom) return;
|
||||
|
||||
const myChart = echarts.init(chartDom);
|
||||
const option = {
|
||||
title: {
|
||||
text: 'Activity Trend'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: [820, 932, 901, 934, 1290, 1330, 1320],
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
itemStyle: {
|
||||
color: getComputedStyle(document.documentElement).getPropertyValue('--color-primary').trim()
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
myChart.setOption(option);
|
||||
|
||||
// Resize chart on window resize
|
||||
window.addEventListener('resize', () => {
|
||||
myChart.resize();
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<script>
|
||||
// Run after mount
|
||||
$data.initChart();
|
||||
// Run after mount
|
||||
$data.initChart();
|
||||
</script>
|
||||
|
||||
</html>
|
||||
|
||||
@ -1,217 +1,220 @@
|
||||
|
||||
class VBase {
|
||||
constructor(baseURL) {
|
||||
this.baseURL = baseURL || '';
|
||||
this.tokenKey = 'vbase_access_token';
|
||||
this.refreshTokenKey = 'vbase_refresh_token';
|
||||
this.userKey = 'vbase_user_info';
|
||||
this.orgKey = 'vbase_current_org';
|
||||
|
||||
this._token = localStorage.getItem(this.tokenKey) || '';
|
||||
this._refreshToken = localStorage.getItem(this.refreshTokenKey) || '';
|
||||
this._user = JSON.parse(localStorage.getItem(this.userKey) || 'null');
|
||||
this._currentOrg = JSON.parse(localStorage.getItem(this.orgKey) || 'null');
|
||||
}
|
||||
|
||||
// Getters
|
||||
get token() { return this._token; }
|
||||
get refreshToken() { return this._refreshToken; }
|
||||
get user() { return this._user; }
|
||||
get currentOrg() { return this._currentOrg; }
|
||||
|
||||
// Setters
|
||||
set token(val) {
|
||||
this._token = val;
|
||||
if (val) localStorage.setItem(this.tokenKey, val);
|
||||
else localStorage.removeItem(this.tokenKey);
|
||||
}
|
||||
|
||||
set refreshToken(val) {
|
||||
this._refreshToken = val;
|
||||
if (val) localStorage.setItem(this.refreshTokenKey, val);
|
||||
else localStorage.removeItem(this.refreshTokenKey);
|
||||
}
|
||||
|
||||
set user(val) {
|
||||
this._user = val;
|
||||
if (val) localStorage.setItem(this.userKey, JSON.stringify(val));
|
||||
else localStorage.removeItem(this.userKey);
|
||||
}
|
||||
|
||||
set currentOrg(val) {
|
||||
this._currentOrg = val;
|
||||
if (val) localStorage.setItem(this.orgKey, JSON.stringify(val));
|
||||
else localStorage.removeItem(this.orgKey);
|
||||
}
|
||||
|
||||
// API Helpers
|
||||
async request(method, path, data = null, headers = {}) {
|
||||
const url = `${this.baseURL}${path}`;
|
||||
const config = {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...this.getAuthHeaders(),
|
||||
...headers
|
||||
}
|
||||
};
|
||||
if (data) config.body = JSON.stringify(data);
|
||||
|
||||
const response = await fetch(url, config);
|
||||
const resData = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw resData || new Error(`Request failed: ${response.status}`);
|
||||
}
|
||||
|
||||
if (resData.code && resData.code !== 200) {
|
||||
throw new Error(resData.message || 'API Error');
|
||||
}
|
||||
|
||||
return resData.data || resData;
|
||||
}
|
||||
|
||||
// Auth Actions
|
||||
async login(username, password) {
|
||||
constructor(baseURL) {
|
||||
this.baseURL = baseURL || '';
|
||||
this.tokenKey = 'vbase_access_token';
|
||||
this.refreshTokenKey = 'vbase_refresh_token';
|
||||
this.userKey = 'vbase_user_info';
|
||||
this.orgKey = 'vbase_current_org';
|
||||
|
||||
this._token = localStorage.getItem(this.tokenKey) || '';
|
||||
this._refreshToken = localStorage.getItem(this.refreshTokenKey) || '';
|
||||
this._user = JSON.parse(localStorage.getItem(this.userKey) || 'null');
|
||||
this._currentOrg = JSON.parse(localStorage.getItem(this.orgKey) || 'null');
|
||||
}
|
||||
|
||||
// Getters
|
||||
get token() { return this._token; }
|
||||
get refreshToken() { return this._refreshToken; }
|
||||
get user() { return this._user; }
|
||||
get currentOrg() { return this._currentOrg; }
|
||||
|
||||
// Setters
|
||||
set token(val) {
|
||||
this._token = val;
|
||||
if (val) localStorage.setItem(this.tokenKey, val);
|
||||
else localStorage.removeItem(this.tokenKey);
|
||||
}
|
||||
|
||||
set refreshToken(val) {
|
||||
this._refreshToken = val;
|
||||
if (val) localStorage.setItem(this.refreshTokenKey, val);
|
||||
else localStorage.removeItem(this.refreshTokenKey);
|
||||
}
|
||||
|
||||
set user(val) {
|
||||
this._user = val;
|
||||
if (val) localStorage.setItem(this.userKey, JSON.stringify(val));
|
||||
else localStorage.removeItem(this.userKey);
|
||||
}
|
||||
|
||||
set currentOrg(val) {
|
||||
this._currentOrg = val;
|
||||
if (val) localStorage.setItem(this.orgKey, JSON.stringify(val));
|
||||
else localStorage.removeItem(this.orgKey);
|
||||
}
|
||||
|
||||
// API Helpers
|
||||
async request(method, path, data = null, headers = {}) {
|
||||
const url = `${this.baseURL}${path}`;
|
||||
const config = {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...this.getAuthHeaders(),
|
||||
...headers
|
||||
}
|
||||
};
|
||||
if (data) config.body = JSON.stringify(data);
|
||||
|
||||
const response = await fetch(url, config);
|
||||
const resData = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
// 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);
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (resData.code && resData.code !== 200) {
|
||||
throw new Error(resData.message || 'API Error');
|
||||
}
|
||||
|
||||
return resData.data || resData;
|
||||
}
|
||||
|
||||
// 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 {
|
||||
const data = await this.request('POST', '/api/auth/login', { username, password });
|
||||
if (data.access) {
|
||||
this.token = data.access;
|
||||
if (data.refresh) this.refreshToken = data.refresh;
|
||||
await this.fetchUser();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
await this.refresh();
|
||||
const headers = this.getAuthHeaders();
|
||||
originalRequest.headers['Authorization'] = headers['Authorization'];
|
||||
return axiosInstance(originalRequest);
|
||||
} catch (e) {
|
||||
throw e;
|
||||
this.logout('/login?redirect=' + encodeURIComponent(window.location.pathname));
|
||||
return Promise.reject(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: 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
return Promise.reject(error?.response?.data || error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default VBase;
|
||||
|
||||
Loading…
Reference in New Issue