Compare commits
No commits in common. "7738abbbfd3ddad6401a53a4cdaab643b1cc7908" and "9730b9fd5e0bf75e53bfb24b4822c9ea68985165" have entirely different histories.
7738abbbfd
...
9730b9fd5e
2
.env
2
.env
@ -1,5 +1,3 @@
|
||||
VITE_APP_NAME=demo_default
|
||||
VITE_NUMBER_DEMO=3000
|
||||
VITE_BOOLEAN_DEMO=true
|
||||
|
||||
VITE_API_BASE_URL=http://localhost:3000/api/v1
|
||||
|
||||
158
pnpm-lock.yaml
generated
158
pnpm-lock.yaml
generated
@ -1,161 +1,3 @@
|
||||
---
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
configDependencies: {}
|
||||
packageManagerDependencies:
|
||||
pnpm:
|
||||
specifier: 12.10.1
|
||||
version: 12.10.1
|
||||
|
||||
packages:
|
||||
|
||||
'@pnpm/exe.android-arm64@12.10.1':
|
||||
resolution: {integrity: sha512-CVTlHPwNvadQCJD9bGSMzWIhFX8QpjtqC2c27dhdnn/9iKzEjZ3qL62o3gHpKbIuo6qn4WxTKRoaokgNdReBig==}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@pnpm/exe.android-x64@12.10.1':
|
||||
resolution: {integrity: sha512-NFiYLIaodY7da0XIebwcU3JoP7IK/lzF7zgKSSP+9H+QcFnQ1GDfBkuBKCYJzZ+Xu0WH54neIPLBBCaOjaaauA==}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@pnpm/exe.darwin-arm64@12.10.1':
|
||||
resolution: {integrity: sha512-0O8tdQ8ijlPJudXspLMTcitMkdBEz6uUflazL5uQE0WMs85oQIp0U+5hfEktMFB64QqEhjqlvvujTwbzdr9oMg==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@pnpm/exe.darwin-x64@12.10.1':
|
||||
resolution: {integrity: sha512-ud+1/uZOKEo8P/AsPynOJc/LNwfI0j6WwCElibj71rU9l4nIBnsSn+wWTIPZPkZ4BBSH5+82azSnAARMyJ1lNA==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@pnpm/exe.freebsd-x64@12.10.1':
|
||||
resolution: {integrity: sha512-mj9PE5rf8uAfQQ2K66HSATMSut+lNiB4eSqFd5+azFVCMnxNSy/32xpZQJT21PhnN3Y4qbK+iKHe5nWAxSuRhw==}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@pnpm/exe.linux-arm64-musl@12.10.1':
|
||||
resolution: {integrity: sha512-xba+00wZJ8Um6rBJJjWl88uJj0tydB8dtomvjlCLvi0uL32vpa9CRP8Vv26FIXvbiZ1bo2RCzYt/eChxJVV52w==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@pnpm/exe.linux-arm64@12.10.1':
|
||||
resolution: {integrity: sha512-oBdMp69ScC9UnniG4qZAQVr2fBDSXjm4yk/72vkEVyrlwS23yMxcRQl7Qdo3sgXPAygY6TWnPRKm4rXzjoyW7w==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@pnpm/exe.linux-ppc64@12.10.1':
|
||||
resolution: {integrity: sha512-Yyb3wu09h3c7mLme270uHFiVaAIUHC411Ru9LIU2BKUIstrGlXa9OqqVWoXaRgYRdFdOFknkwG7GNn6ygwLbnw==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@pnpm/exe.linux-riscv64@12.10.1':
|
||||
resolution: {integrity: sha512-PdkeUFFkybknnGkWP0H22eMpIiS4OZXL5BcQzF4lh+vmrW3IaC2HOkbANKgot/PKO0Kl77pzA/6PKFxgbgfXIQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@pnpm/exe.linux-s390x@12.10.1':
|
||||
resolution: {integrity: sha512-1okKuHKJwJPAbofDxfhZWAUxEWtee4ZSf7Pah8kcvocaJkf2WxFJ5KVTiNIvDml5GLidDQsgf7Pcf/WHbCak7Q==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@pnpm/exe.linux-x64-musl@12.10.1':
|
||||
resolution: {integrity: sha512-e6IqzVNkPa7srFKJKnY23uWJ+qz0LaRWiTUo/sHfFXRRMRzB5uNOr6qWWip/jqhdGwU91n9J7LrEYZ6e0tRczw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@pnpm/exe.linux-x64@12.10.1':
|
||||
resolution: {integrity: sha512-ZByV0DRgWzGaDSjT5ddRBASdwz+r/qPMYh7o6RGmPQjtRGNFbSl94xY2DcAM8mqCOZJUUDtAXQ+wpBXcdpKlkg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@pnpm/exe.win32-arm64@12.10.1':
|
||||
resolution: {integrity: sha512-I7QZHcjioG7orFeCRUFUHByJ1G0zFPnAlaGGyrUvb6MGkv5oeNwNPl7t+Gp+7zHfnMcgLGJ1a+Wfim0IDw3lOw==}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@pnpm/exe.win32-x64@12.10.1':
|
||||
resolution: {integrity: sha512-4WJFdp58EKlCj0zLobrXU0vpszSZIr4X/3y/uNbAQqGW0zE+VdruZB0gFc3L2kzsUzVwTYbaCn+XEhnCYSAlTg==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
pnpm@12.10.1:
|
||||
resolution: {integrity: sha512-ukCjfrGjcNYP6oxM8dNkwTvMzvUY6YrSvO6VT1OdONo3RY5YJho/pV4GUjRQ6g//+fSXsTW+QfLBXBZCusrObA==}
|
||||
engines: {node: '>=18.*'}
|
||||
hasBin: true
|
||||
|
||||
snapshots:
|
||||
|
||||
'@pnpm/exe.android-arm64@12.10.1':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.android-x64@12.10.1':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.darwin-arm64@12.10.1':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.darwin-x64@12.10.1':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.freebsd-x64@12.10.1':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-arm64-musl@12.10.1':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-arm64@12.10.1':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-ppc64@12.10.1':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-riscv64@12.10.1':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-s390x@12.10.1':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-x64-musl@12.10.1':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.linux-x64@12.10.1':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.win32-arm64@12.10.1':
|
||||
optional: true
|
||||
|
||||
'@pnpm/exe.win32-x64@12.10.1':
|
||||
optional: true
|
||||
|
||||
pnpm@12.10.1:
|
||||
optionalDependencies:
|
||||
'@pnpm/exe.android-arm64': 12.10.1
|
||||
'@pnpm/exe.android-x64': 12.10.1
|
||||
'@pnpm/exe.darwin-arm64': 12.10.1
|
||||
'@pnpm/exe.darwin-x64': 12.10.1
|
||||
'@pnpm/exe.freebsd-x64': 12.10.1
|
||||
'@pnpm/exe.linux-arm64': 12.10.1
|
||||
'@pnpm/exe.linux-arm64-musl': 12.10.1
|
||||
'@pnpm/exe.linux-ppc64': 12.10.1
|
||||
'@pnpm/exe.linux-riscv64': 12.10.1
|
||||
'@pnpm/exe.linux-s390x': 12.10.1
|
||||
'@pnpm/exe.linux-x64': 12.10.1
|
||||
'@pnpm/exe.linux-x64-musl': 12.10.1
|
||||
'@pnpm/exe.win32-arm64': 12.10.1
|
||||
'@pnpm/exe.win32-x64': 12.10.1
|
||||
|
||||
---
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
|
||||
@ -1,77 +0,0 @@
|
||||
import { clearTokens, getAccessToken, getRefreshToken, setTokens } from '@/utils/token';
|
||||
|
||||
const BASE_URL = import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:3000/api/v1';
|
||||
|
||||
interface ApiEnvelope<T = unknown> {
|
||||
code: number;
|
||||
message: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(
|
||||
public code: number,
|
||||
message: string,
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
async function rawRequest<T>(path: string, options: RequestInit = {}, token?: string): Promise<ApiEnvelope<T>> {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
...(options.headers as Record<string, string> | undefined),
|
||||
};
|
||||
if (token) headers.Authorization = `Bearer ${token}`;
|
||||
|
||||
const res = await fetch(`${BASE_URL}${path}`, { ...options, headers });
|
||||
return (await res.json()) as ApiEnvelope<T>;
|
||||
}
|
||||
|
||||
let refreshing: Promise<boolean> | null = null;
|
||||
|
||||
async function tryRefresh(): Promise<boolean> {
|
||||
const refreshToken = getRefreshToken();
|
||||
if (!refreshToken) return false;
|
||||
try {
|
||||
const body = await rawRequest<{ accessToken: string; refreshToken: string }>('/auth/refresh', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ refreshToken }),
|
||||
});
|
||||
if (body.code === 0) {
|
||||
setTokens(body.data.accessToken, body.data.refreshToken);
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// 刷新失败,走清空逻辑
|
||||
}
|
||||
clearTokens();
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 统一请求:解析 {code,message,data},成功返回 data;401 自动刷新重试一次。 */
|
||||
export async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
|
||||
const attempt = async (retry: boolean): Promise<T> => {
|
||||
const body = await rawRequest<T>(path, options, getAccessToken() ?? undefined);
|
||||
if (body.code === 0) return body.data;
|
||||
|
||||
if (body.code >= 40100 && body.code < 40200 && retry) {
|
||||
if (refreshing === null) {
|
||||
refreshing = tryRefresh().finally(() => {
|
||||
refreshing = null;
|
||||
});
|
||||
}
|
||||
const ok = await refreshing;
|
||||
if (ok) return attempt(false);
|
||||
}
|
||||
throw new ApiError(body.code, body.message);
|
||||
};
|
||||
return attempt(true);
|
||||
}
|
||||
|
||||
export const http = {
|
||||
get: <T>(path: string) => request<T>(path, { method: 'GET' }),
|
||||
post: <T>(path: string, data?: unknown) => request<T>(path, { method: 'POST', body: JSON.stringify(data ?? {}) }),
|
||||
patch: <T>(path: string, data?: unknown) => request<T>(path, { method: 'PATCH', body: JSON.stringify(data ?? {}) }),
|
||||
delete: <T>(path: string) => request<T>(path, { method: 'DELETE' }),
|
||||
};
|
||||
@ -1,61 +0,0 @@
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: false
|
||||
requiresAuth: false
|
||||
</route>
|
||||
|
||||
<template>
|
||||
<div class="login">
|
||||
<form @submit.prevent="onSubmit">
|
||||
<h1>登录</h1>
|
||||
<input v-model="tenantCode" placeholder="租户编码" />
|
||||
<input v-model="username" placeholder="用户名" />
|
||||
<input v-model="password" type="password" placeholder="密码" />
|
||||
<button type="submit">登录</button>
|
||||
<p v-if="error">{{ error }}</p>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useAuthStore } from '@/stores/modules/auth';
|
||||
|
||||
const router = useRouter();
|
||||
const auth = useAuthStore();
|
||||
|
||||
const tenantCode = ref('');
|
||||
const username = ref('');
|
||||
const password = ref('');
|
||||
const error = ref('');
|
||||
|
||||
async function onSubmit(): Promise<void> {
|
||||
error.value = '';
|
||||
try {
|
||||
await auth.login(tenantCode.value, username.value, password.value);
|
||||
router.push('/');
|
||||
} catch (e) {
|
||||
error.value = (e as Error).message;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.login {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
}
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
width: 280px;
|
||||
}
|
||||
input,
|
||||
button {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
@ -8,7 +8,6 @@ import type { App } from 'vue'
|
||||
import { routes } from 'vue-router/auto-routes'
|
||||
import { setupLayouts } from 'virtual:generated-layouts'
|
||||
import i18n, { loadLanguage } from '@/i18n'
|
||||
import { getAccessToken } from '@/utils/token'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
@ -42,13 +41,7 @@ async function loadRouteLocales(to: RouteLocationNormalized) {
|
||||
// 路由前置守卫
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
await loadRouteLocales(to)
|
||||
// 未标记 requiresAuth: false 的路由默认需要登录
|
||||
const requiresAuth = to.meta.requiresAuth !== false
|
||||
if (requiresAuth && !getAccessToken()) {
|
||||
next('/login')
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
next()
|
||||
})
|
||||
|
||||
/**
|
||||
|
||||
@ -1,38 +0,0 @@
|
||||
import { computed, ref } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
import { http } from '@/api/client';
|
||||
import { clearTokens, getAccessToken, getRefreshToken, setTokens } from '@/utils/token';
|
||||
|
||||
export const useAuthStore = defineStore('authStore', () => {
|
||||
const accessToken = ref<string | null>(getAccessToken());
|
||||
const refreshToken = ref<string | null>(getRefreshToken());
|
||||
|
||||
const isAuthenticated = computed(() => !!accessToken.value);
|
||||
|
||||
async function login(tenantCode: string, username: string, password: string): Promise<void> {
|
||||
const data = await http.post<{ accessToken: string; refreshToken: string }>('/auth/login', {
|
||||
tenantCode,
|
||||
username,
|
||||
password,
|
||||
});
|
||||
setTokens(data.accessToken, data.refreshToken);
|
||||
accessToken.value = data.accessToken;
|
||||
refreshToken.value = data.refreshToken;
|
||||
}
|
||||
|
||||
async function logout(): Promise<void> {
|
||||
const rt = refreshToken.value;
|
||||
if (rt) {
|
||||
try {
|
||||
await http.post('/auth/logout', { refreshToken: rt });
|
||||
} catch {
|
||||
// 忽略登出接口错误
|
||||
}
|
||||
}
|
||||
clearTokens();
|
||||
accessToken.value = null;
|
||||
refreshToken.value = null;
|
||||
}
|
||||
|
||||
return { accessToken, refreshToken, isAuthenticated, login, logout };
|
||||
});
|
||||
@ -1,20 +0,0 @@
|
||||
const ACCESS_KEY = 'sysmgr_access_token';
|
||||
const REFRESH_KEY = 'sysmgr_refresh_token';
|
||||
|
||||
export function getAccessToken(): string | null {
|
||||
return localStorage.getItem(ACCESS_KEY);
|
||||
}
|
||||
|
||||
export function getRefreshToken(): string | null {
|
||||
return localStorage.getItem(REFRESH_KEY);
|
||||
}
|
||||
|
||||
export function setTokens(access: string, refresh: string): void {
|
||||
localStorage.setItem(ACCESS_KEY, access);
|
||||
localStorage.setItem(REFRESH_KEY, refresh);
|
||||
}
|
||||
|
||||
export function clearTokens(): void {
|
||||
localStorage.removeItem(ACCESS_KEY);
|
||||
localStorage.removeItem(REFRESH_KEY);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user