create-art-app-pino 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +213 -0
- package/dist/index.cjs +418 -0
- package/package.json +44 -0
- package/template/auth-art/_env.development +2 -0
- package/template/auth-art/src/api/auth.ts +37 -0
- package/template/auth-art/src/router/guards/beforeEach.ts +64 -0
- package/template/auth-art/src/router/index.ts.ejs +52 -0
- package/template/auth-art/src/store/modules/user.ts +64 -0
- package/template/auth-art/src/utils/http/index.ts +60 -0
- package/template/auth-art/src/views/auth/login/index.vue +172 -0
- package/template/auth-zhihuishu/_env.development +5 -0
- package/template/auth-zhihuishu/_env.local.example +5 -0
- package/template/auth-zhihuishu/src/constants/auth.ts +10 -0
- package/template/auth-zhihuishu/src/router/guards/beforeEach.ts +47 -0
- package/template/auth-zhihuishu/src/router/index.ts.ejs +46 -0
- package/template/auth-zhihuishu/src/services/authService.ts +55 -0
- package/template/auth-zhihuishu/src/store/modules/auth.ts +227 -0
- package/template/auth-zhihuishu/src/types/auth.ts +43 -0
- package/template/auth-zhihuishu/src/utils/auth.ts +161 -0
- package/template/auth-zhihuishu/src/utils/http/index.ts +61 -0
- package/template/base/AGENTS.md.ejs +65 -0
- package/template/base/_gitignore.ejs +6 -0
- package/template/base/_husky/commit-msg +1 -0
- package/template/base/_husky/pre-commit.ejs +3 -0
- package/template/base/_husky/pre-push +1 -0
- package/template/base/_vscode/extensions.json.ejs +6 -0
- package/template/base/_vscode/settings.json.ejs +26 -0
- package/template/base/commitlint.config.mjs +6 -0
- package/template/base/eslint.config.mjs +11 -0
- package/template/base/index.html +13 -0
- package/template/base/lint-staged.config.mjs.ejs +4 -0
- package/template/base/package.json.ejs +58 -0
- package/template/base/src/App.vue +3 -0
- package/template/base/src/assets/styles/index.scss +10 -0
- package/template/base/src/env.d.ts +7 -0
- package/template/base/src/main.ts +19 -0
- package/template/base/src/router/index.ts.ejs +42 -0
- package/template/base/src/store/index.ts +7 -0
- package/template/base/src/store/modules/setting.ts +17 -0
- package/template/base/src/utils/http/index.ts +34 -0
- package/template/base/src/views/dashboard/index.vue +42 -0
- package/template/base/src/views/exception/403.vue +25 -0
- package/template/base/src/views/exception/404.vue +25 -0
- package/template/base/src/views/index/index.vue +122 -0
- package/template/base/tsconfig.app.json +12 -0
- package/template/base/tsconfig.json +19 -0
- package/template/base/tsconfig.node.json +7 -0
- package/template/base/vite.config.ts +34 -0
- package/template/feature-echarts/package.json +5 -0
- package/template/feature-echarts/src/plugins/echarts.ts +22 -0
- package/template/feature-markdown/package.json +5 -0
- package/template/feature-markdown/src/components/MdPreviewWrapper.vue +26 -0
- package/template/feature-sse/package.json +5 -0
- package/template/feature-sse/src/utils/sse.ts +100 -0
- package/template/scaffold-doc-governance/CHANGELOG.md +19 -0
- package/template/scaffold-doc-governance/_github/PULL_REQUEST_TEMPLATE.md +28 -0
- package/template/scaffold-doc-governance/docs/README.md +35 -0
- package/template/scaffold-doc-governance/docs/adr/0001-doc-governance.md +195 -0
- package/template/scaffold-doc-governance/docs/architecture/.gitkeep +0 -0
- package/template/scaffold-doc-governance/docs/archive/.gitkeep +0 -0
- package/template/scaffold-doc-governance/docs/changes/requirements/.gitkeep +0 -0
- package/template/scaffold-doc-governance/docs/getting-started/quick-start.md.ejs +62 -0
- package/template/scaffold-doc-governance/docs/guides/.gitkeep +0 -0
- package/template/scaffold-doc-governance/docs/maintenance/incidents/.gitkeep +0 -0
- package/template/scaffold-doc-governance/docs/maintenance/troubleshooting.md +25 -0
- package/template/scaffold-doc-governance/docs/reference/.gitkeep +0 -0
- package/template/scaffold-doc-governance/docs/reference/cli.md +46 -0
- package/template/scaffold-doc-governance/docs/templates/adr.md +21 -0
- package/template/scaffold-doc-governance/docs/templates/incident.md +25 -0
- package/template/scaffold-doc-governance/docs/templates/requirement.md +29 -0
- package/template/scaffold-doc-governance/package.json +5 -0
- package/template/scaffold-doc-governance/scripts/docs/lint-frontmatter.mjs +85 -0
- package/template/scaffold-doc-governance/scripts/docs/lint-links.sh +13 -0
- package/template/scaffold-doc-governance/scripts/docs/lint-naming.mjs +89 -0
- package/template/scaffold-doc-governance/scripts/docs/lint-no-private-path.sh +19 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import request from '@/utils/http'
|
|
2
|
+
|
|
3
|
+
export interface LoginParams {
|
|
4
|
+
userName: string
|
|
5
|
+
password: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface LoginResponse {
|
|
9
|
+
token: string
|
|
10
|
+
refreshToken?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface UserInfo {
|
|
14
|
+
userId: number | string
|
|
15
|
+
userName: string
|
|
16
|
+
avatar?: string
|
|
17
|
+
roles?: string[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 登录
|
|
22
|
+
*/
|
|
23
|
+
export function fetchLogin(params: LoginParams) {
|
|
24
|
+
return request.post<LoginResponse>({
|
|
25
|
+
url: '/api/auth/login',
|
|
26
|
+
params,
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 获取用户信息
|
|
32
|
+
*/
|
|
33
|
+
export function fetchGetUserInfo() {
|
|
34
|
+
return request.get<UserInfo>({
|
|
35
|
+
url: '/api/user/info',
|
|
36
|
+
})
|
|
37
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { Router } from 'vue-router'
|
|
2
|
+
import NProgress from 'nprogress'
|
|
3
|
+
import { useUserStore } from '@/store/modules/user'
|
|
4
|
+
import { fetchGetUserInfo } from '@/api/auth'
|
|
5
|
+
|
|
6
|
+
// 是否已获取过用户信息
|
|
7
|
+
let hasLoadedUserInfo = false
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 重置路由守卫状态(登出时调用)
|
|
11
|
+
*/
|
|
12
|
+
export function resetRouterState(): void {
|
|
13
|
+
hasLoadedUserInfo = false
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 设置路由前置守卫
|
|
18
|
+
*/
|
|
19
|
+
export function setupBeforeEachGuard(router: Router): void {
|
|
20
|
+
router.beforeEach(async (to, _from, next) => {
|
|
21
|
+
NProgress.start()
|
|
22
|
+
|
|
23
|
+
const userStore = useUserStore()
|
|
24
|
+
|
|
25
|
+
// 访问登录页直接放行
|
|
26
|
+
if (to.path === '/login') {
|
|
27
|
+
if (userStore.isLogin) {
|
|
28
|
+
next('/')
|
|
29
|
+
} else {
|
|
30
|
+
next()
|
|
31
|
+
}
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 未登录跳转到登录页
|
|
36
|
+
if (!userStore.isLogin) {
|
|
37
|
+
next({
|
|
38
|
+
path: '/login',
|
|
39
|
+
query: { redirect: to.fullPath },
|
|
40
|
+
})
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 首次进入已登录页面时获取用户信息
|
|
45
|
+
if (!hasLoadedUserInfo) {
|
|
46
|
+
try {
|
|
47
|
+
const userInfo = await fetchGetUserInfo()
|
|
48
|
+
userStore.setUserInfo(userInfo as any)
|
|
49
|
+
hasLoadedUserInfo = true
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error('[Router] 获取用户信息失败:', error)
|
|
52
|
+
userStore.logOut()
|
|
53
|
+
next({ path: '/login' })
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
next()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
router.afterEach(() => {
|
|
62
|
+
NProgress.done()
|
|
63
|
+
})
|
|
64
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
|
|
2
|
+
import type { RouteRecordRaw } from 'vue-router'
|
|
3
|
+
import { setupBeforeEachGuard } from './guards/beforeEach'
|
|
4
|
+
|
|
5
|
+
const routes: RouteRecordRaw[] = [
|
|
6
|
+
{
|
|
7
|
+
path: '/login',
|
|
8
|
+
name: 'Login',
|
|
9
|
+
component: () => import('@/views/auth/login/index.vue'),
|
|
10
|
+
meta: { title: '登录' },
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
path: '/',
|
|
14
|
+
name: 'Layout',
|
|
15
|
+
component: () => import('@/views/index/index.vue'),
|
|
16
|
+
redirect: '/dashboard',
|
|
17
|
+
children: [
|
|
18
|
+
{
|
|
19
|
+
path: 'dashboard',
|
|
20
|
+
name: 'Dashboard',
|
|
21
|
+
component: () => import('@/views/dashboard/index.vue'),
|
|
22
|
+
meta: { title: '仪表盘', icon: 'ep:home-filled' },
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
path: '/403',
|
|
28
|
+
name: 'Forbidden',
|
|
29
|
+
component: () => import('@/views/exception/403.vue'),
|
|
30
|
+
meta: { title: '403' },
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
path: '/404',
|
|
34
|
+
name: 'NotFound',
|
|
35
|
+
component: () => import('@/views/exception/404.vue'),
|
|
36
|
+
meta: { title: '404' },
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
path: '/:pathMatch(.*)*',
|
|
40
|
+
redirect: '/404',
|
|
41
|
+
},
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
const router = createRouter({
|
|
45
|
+
history: <%- options.routerMode === 'hash' ? 'createWebHashHistory()' : "createWebHistory(import.meta.env.BASE_URL)" %>,
|
|
46
|
+
routes,
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// 设置路由守卫
|
|
50
|
+
setupBeforeEachGuard(router)
|
|
51
|
+
|
|
52
|
+
export default router
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { ref, computed } from 'vue'
|
|
3
|
+
import { useRouter } from 'vue-router'
|
|
4
|
+
|
|
5
|
+
export const useUserStore = defineStore(
|
|
6
|
+
'userStore',
|
|
7
|
+
() => {
|
|
8
|
+
const router = useRouter()
|
|
9
|
+
|
|
10
|
+
const isLogin = ref(false)
|
|
11
|
+
const accessToken = ref('')
|
|
12
|
+
const refreshToken = ref('')
|
|
13
|
+
const info = ref<Record<string, any>>({})
|
|
14
|
+
|
|
15
|
+
const getUserInfo = computed(() => info.value)
|
|
16
|
+
|
|
17
|
+
function setUserInfo(newInfo: Record<string, any>) {
|
|
18
|
+
info.value = newInfo
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function setLoginStatus(status: boolean) {
|
|
22
|
+
isLogin.value = status
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function setToken(newAccessToken: string, newRefreshToken?: string) {
|
|
26
|
+
accessToken.value = newAccessToken
|
|
27
|
+
if (newRefreshToken) {
|
|
28
|
+
refreshToken.value = newRefreshToken
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function logOut() {
|
|
33
|
+
info.value = {}
|
|
34
|
+
isLogin.value = false
|
|
35
|
+
accessToken.value = ''
|
|
36
|
+
refreshToken.value = ''
|
|
37
|
+
|
|
38
|
+
const currentRoute = router.currentRoute.value
|
|
39
|
+
const redirect = currentRoute.path !== '/login' ? currentRoute.fullPath : undefined
|
|
40
|
+
router.push({
|
|
41
|
+
path: '/login',
|
|
42
|
+
query: redirect ? { redirect } : undefined,
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
isLogin,
|
|
48
|
+
accessToken,
|
|
49
|
+
refreshToken,
|
|
50
|
+
info,
|
|
51
|
+
getUserInfo,
|
|
52
|
+
setUserInfo,
|
|
53
|
+
setLoginStatus,
|
|
54
|
+
setToken,
|
|
55
|
+
logOut,
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
persist: {
|
|
60
|
+
key: 'user',
|
|
61
|
+
storage: localStorage,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { AxiosRequestConfig } from 'axios'
|
|
2
|
+
import axios from 'axios'
|
|
3
|
+
import { ElMessage } from 'element-plus'
|
|
4
|
+
import { useUserStore } from '@/store/modules/user'
|
|
5
|
+
|
|
6
|
+
const http = axios.create({
|
|
7
|
+
baseURL: import.meta.env.VITE_API_BASE_URL || '',
|
|
8
|
+
timeout: 30000,
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
// 请求拦截器:注入 token
|
|
12
|
+
http.interceptors.request.use(
|
|
13
|
+
(config) => {
|
|
14
|
+
const userStore = useUserStore()
|
|
15
|
+
if (userStore.accessToken) {
|
|
16
|
+
config.headers.Authorization = `Bearer ${userStore.accessToken}`
|
|
17
|
+
}
|
|
18
|
+
return config
|
|
19
|
+
},
|
|
20
|
+
error => Promise.reject(error),
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
// 响应拦截器:处理 401 和业务错误
|
|
24
|
+
http.interceptors.response.use(
|
|
25
|
+
(response) => {
|
|
26
|
+
return response.data
|
|
27
|
+
},
|
|
28
|
+
(error) => {
|
|
29
|
+
if (error.response?.status === 401) {
|
|
30
|
+
const userStore = useUserStore()
|
|
31
|
+
userStore.logOut()
|
|
32
|
+
return Promise.reject(error)
|
|
33
|
+
}
|
|
34
|
+
const message = error.response?.data?.message || error.message || '请求失败'
|
|
35
|
+
ElMessage.error(message)
|
|
36
|
+
return Promise.reject(error)
|
|
37
|
+
},
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
interface RequestConfig extends AxiosRequestConfig {
|
|
41
|
+
url: string
|
|
42
|
+
params?: any
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const request = {
|
|
46
|
+
get<T>(config: RequestConfig): Promise<T> {
|
|
47
|
+
return http.get(config.url, { params: config.params, ...config }) as unknown as Promise<T>
|
|
48
|
+
},
|
|
49
|
+
post<T>(config: RequestConfig): Promise<T> {
|
|
50
|
+
return http.post(config.url, config.params, config) as unknown as Promise<T>
|
|
51
|
+
},
|
|
52
|
+
put<T>(config: RequestConfig): Promise<T> {
|
|
53
|
+
return http.put(config.url, config.params, config) as unknown as Promise<T>
|
|
54
|
+
},
|
|
55
|
+
delete<T>(config: RequestConfig): Promise<T> {
|
|
56
|
+
return http.delete(config.url, { params: config.params, ...config }) as unknown as Promise<T>
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export default request
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="login-container">
|
|
3
|
+
<div class="login-card">
|
|
4
|
+
<div class="login-header">
|
|
5
|
+
<h2 class="login-title">欢迎登录</h2>
|
|
6
|
+
<p class="login-subtitle">请输入您的账号和密码</p>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<el-form
|
|
10
|
+
ref="formRef"
|
|
11
|
+
:model="formData"
|
|
12
|
+
:rules="rules"
|
|
13
|
+
class="login-form"
|
|
14
|
+
@keyup.enter="handleSubmit"
|
|
15
|
+
>
|
|
16
|
+
<el-form-item prop="username">
|
|
17
|
+
<el-input
|
|
18
|
+
v-model.trim="formData.username"
|
|
19
|
+
placeholder="请输入用户名"
|
|
20
|
+
size="large"
|
|
21
|
+
:prefix-icon="User"
|
|
22
|
+
/>
|
|
23
|
+
</el-form-item>
|
|
24
|
+
|
|
25
|
+
<el-form-item prop="password">
|
|
26
|
+
<el-input
|
|
27
|
+
v-model.trim="formData.password"
|
|
28
|
+
placeholder="请输入密码"
|
|
29
|
+
type="password"
|
|
30
|
+
size="large"
|
|
31
|
+
show-password
|
|
32
|
+
:prefix-icon="Lock"
|
|
33
|
+
/>
|
|
34
|
+
</el-form-item>
|
|
35
|
+
|
|
36
|
+
<el-form-item>
|
|
37
|
+
<div class="login-options">
|
|
38
|
+
<el-checkbox v-model="formData.rememberPassword">记住密码</el-checkbox>
|
|
39
|
+
</div>
|
|
40
|
+
</el-form-item>
|
|
41
|
+
|
|
42
|
+
<el-form-item>
|
|
43
|
+
<el-button
|
|
44
|
+
type="primary"
|
|
45
|
+
size="large"
|
|
46
|
+
class="login-button"
|
|
47
|
+
:loading="loading"
|
|
48
|
+
@click="handleSubmit"
|
|
49
|
+
>
|
|
50
|
+
登录
|
|
51
|
+
</el-button>
|
|
52
|
+
</el-form-item>
|
|
53
|
+
</el-form>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</template>
|
|
57
|
+
|
|
58
|
+
<script setup lang="ts">
|
|
59
|
+
import { ref, reactive } from 'vue'
|
|
60
|
+
import { useRouter, useRoute } from 'vue-router'
|
|
61
|
+
import { User, Lock } from '@element-plus/icons-vue'
|
|
62
|
+
import { ElNotification, type FormInstance, type FormRules } from 'element-plus'
|
|
63
|
+
import { useUserStore } from '@/store/modules/user'
|
|
64
|
+
import { fetchLogin } from '@/api/auth'
|
|
65
|
+
|
|
66
|
+
defineOptions({ name: 'Login' })
|
|
67
|
+
|
|
68
|
+
const router = useRouter()
|
|
69
|
+
const route = useRoute()
|
|
70
|
+
const userStore = useUserStore()
|
|
71
|
+
const formRef = ref<FormInstance>()
|
|
72
|
+
const loading = ref(false)
|
|
73
|
+
|
|
74
|
+
const formData = reactive({
|
|
75
|
+
username: '',
|
|
76
|
+
password: '',
|
|
77
|
+
rememberPassword: true,
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
const rules: FormRules = {
|
|
81
|
+
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
|
82
|
+
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function handleSubmit() {
|
|
86
|
+
if (!formRef.value) return
|
|
87
|
+
|
|
88
|
+
const valid = await formRef.value.validate().catch(() => false)
|
|
89
|
+
if (!valid) return
|
|
90
|
+
|
|
91
|
+
loading.value = true
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const { token, refreshToken } = await fetchLogin({
|
|
95
|
+
userName: formData.username,
|
|
96
|
+
password: formData.password,
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
if (!token) {
|
|
100
|
+
throw new Error('登录失败:未获取到 token')
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
userStore.setToken(token, refreshToken)
|
|
104
|
+
userStore.setLoginStatus(true)
|
|
105
|
+
|
|
106
|
+
ElNotification({
|
|
107
|
+
title: '登录成功',
|
|
108
|
+
type: 'success',
|
|
109
|
+
duration: 2500,
|
|
110
|
+
message: '欢迎回来!',
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
const redirect = route.query.redirect as string
|
|
114
|
+
router.push(redirect || '/')
|
|
115
|
+
} catch (error: any) {
|
|
116
|
+
ElNotification({
|
|
117
|
+
title: '登录失败',
|
|
118
|
+
type: 'error',
|
|
119
|
+
duration: 3000,
|
|
120
|
+
message: error.message || '请检查用户名和密码',
|
|
121
|
+
})
|
|
122
|
+
} finally {
|
|
123
|
+
loading.value = false
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
</script>
|
|
127
|
+
|
|
128
|
+
<style scoped>
|
|
129
|
+
.login-container {
|
|
130
|
+
display: flex;
|
|
131
|
+
align-items: center;
|
|
132
|
+
justify-content: center;
|
|
133
|
+
min-height: 100vh;
|
|
134
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.login-card {
|
|
138
|
+
width: 400px;
|
|
139
|
+
padding: 40px;
|
|
140
|
+
background: #fff;
|
|
141
|
+
border-radius: 12px;
|
|
142
|
+
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.login-header {
|
|
146
|
+
text-align: center;
|
|
147
|
+
margin-bottom: 30px;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.login-title {
|
|
151
|
+
font-size: 24px;
|
|
152
|
+
font-weight: 600;
|
|
153
|
+
color: #1a1a1a;
|
|
154
|
+
margin-bottom: 8px;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.login-subtitle {
|
|
158
|
+
font-size: 14px;
|
|
159
|
+
color: #999;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.login-options {
|
|
163
|
+
display: flex;
|
|
164
|
+
justify-content: space-between;
|
|
165
|
+
align-items: center;
|
|
166
|
+
width: 100%;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.login-button {
|
|
170
|
+
width: 100%;
|
|
171
|
+
}
|
|
172
|
+
</style>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const casCookieName = 'CASLOGC'
|
|
2
|
+
export const casTicketCookieName = 'CASTGC'
|
|
3
|
+
export const zhihuishuCASLoginUrl = 'https://passport.zhihuishu.com/login?source=20&service='
|
|
4
|
+
|
|
5
|
+
export const authStorageKeys = {
|
|
6
|
+
token: 'auth_token',
|
|
7
|
+
userInfo: 'user_info',
|
|
8
|
+
expiresAt: 'auth_token_expires_at',
|
|
9
|
+
mode: 'auth_mode',
|
|
10
|
+
} as const
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Router } from 'vue-router'
|
|
2
|
+
import NProgress from 'nprogress'
|
|
3
|
+
import { useAuthStore } from '@/store/modules/auth'
|
|
4
|
+
import { isDevTokenAuthMode, isZhihuishuDomain } from '@/utils/auth'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 设置路由前置守卫(智慧树 CAS + dev-token 双模式)
|
|
8
|
+
*/
|
|
9
|
+
export function setupBeforeEachGuard(router: Router): void {
|
|
10
|
+
router.beforeEach(async (to) => {
|
|
11
|
+
NProgress.start()
|
|
12
|
+
|
|
13
|
+
const authStore = useAuthStore()
|
|
14
|
+
|
|
15
|
+
// 错误页直接放行
|
|
16
|
+
if (to.name === 'Forbidden' || to.name === 'NotFound') {
|
|
17
|
+
return true
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// dev-token 模式
|
|
21
|
+
if (isDevTokenAuthMode()) {
|
|
22
|
+
const success = await authStore.autoLogin()
|
|
23
|
+
if (!success) {
|
|
24
|
+
return { path: '/403', replace: true }
|
|
25
|
+
}
|
|
26
|
+
return true
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// CAS 模式:需要在智慧树域名下运行
|
|
30
|
+
if (!isZhihuishuDomain()) {
|
|
31
|
+
authStore.logout(to.fullPath)
|
|
32
|
+
return false
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const success = await authStore.autoLogin()
|
|
36
|
+
if (!success) {
|
|
37
|
+
authStore.logout(to.fullPath)
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return true
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
router.afterEach(() => {
|
|
45
|
+
NProgress.done()
|
|
46
|
+
})
|
|
47
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
|
|
2
|
+
import type { RouteRecordRaw } from 'vue-router'
|
|
3
|
+
import { setupBeforeEachGuard } from './guards/beforeEach'
|
|
4
|
+
|
|
5
|
+
const routes: RouteRecordRaw[] = [
|
|
6
|
+
{
|
|
7
|
+
path: '/',
|
|
8
|
+
name: 'Layout',
|
|
9
|
+
component: () => import('@/views/index/index.vue'),
|
|
10
|
+
redirect: '/dashboard',
|
|
11
|
+
children: [
|
|
12
|
+
{
|
|
13
|
+
path: 'dashboard',
|
|
14
|
+
name: 'Dashboard',
|
|
15
|
+
component: () => import('@/views/dashboard/index.vue'),
|
|
16
|
+
meta: { title: '仪表盘', icon: 'ep:home-filled' },
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
path: '/403',
|
|
22
|
+
name: 'Forbidden',
|
|
23
|
+
component: () => import('@/views/exception/403.vue'),
|
|
24
|
+
meta: { title: '403' },
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
path: '/404',
|
|
28
|
+
name: 'NotFound',
|
|
29
|
+
component: () => import('@/views/exception/404.vue'),
|
|
30
|
+
meta: { title: '404' },
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
path: '/:pathMatch(.*)*',
|
|
34
|
+
redirect: '/404',
|
|
35
|
+
},
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
const router = createRouter({
|
|
39
|
+
history: <%- options.routerMode === 'hash' ? 'createWebHashHistory()' : "createWebHistory(import.meta.env.BASE_URL)" %>,
|
|
40
|
+
routes,
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
// 设置路由守卫(CAS + dev-token 双模式)
|
|
44
|
+
setupBeforeEachGuard(router)
|
|
45
|
+
|
|
46
|
+
export default router
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AuthLoginData,
|
|
3
|
+
AuthLoginParams,
|
|
4
|
+
AuthLoginResponseData,
|
|
5
|
+
AuthLoginResponsePayload,
|
|
6
|
+
} from '@/types/auth'
|
|
7
|
+
import axios from 'axios'
|
|
8
|
+
|
|
9
|
+
const authService = axios.create({
|
|
10
|
+
baseURL: import.meta.env.VITE_API_BASE_URL || '',
|
|
11
|
+
timeout: 30000,
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
export class AuthServiceError extends Error {
|
|
15
|
+
code?: number
|
|
16
|
+
|
|
17
|
+
constructor(message: string, code?: number) {
|
|
18
|
+
super(message)
|
|
19
|
+
this.name = 'AuthServiceError'
|
|
20
|
+
this.code = code
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 从登录响应中提取 token 与用户信息
|
|
26
|
+
*/
|
|
27
|
+
function resolveLoginData(payload: AuthLoginResponsePayload, fallbackUserUid: string): AuthLoginData {
|
|
28
|
+
const responseData: AuthLoginResponseData = payload.data ?? payload
|
|
29
|
+
|
|
30
|
+
if (!responseData.token) {
|
|
31
|
+
throw new AuthServiceError(payload.message || '登录响应缺少 token', payload.code)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
token: responseData.token,
|
|
36
|
+
userUid: responseData.user_uid === undefined ? fallbackUserUid : String(responseData.user_uid),
|
|
37
|
+
name: responseData.name?.trim() || fallbackUserUid,
|
|
38
|
+
expiresIn: responseData.expires_in,
|
|
39
|
+
isSuperAdmin: responseData.is_super_admin ?? false,
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 使用 CASLOGC 中的用户 ID 换取业务 token
|
|
45
|
+
*/
|
|
46
|
+
export async function loginWithCasUser(params: AuthLoginParams): Promise<AuthLoginData> {
|
|
47
|
+
const response = await authService.post<AuthLoginResponsePayload>('/auth/login', params)
|
|
48
|
+
const payload = response.data
|
|
49
|
+
|
|
50
|
+
if (typeof payload.code === 'number' && payload.code !== 200) {
|
|
51
|
+
throw new AuthServiceError(payload.message || '登录失败', payload.code)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return resolveLoginData(payload, params.user_uid)
|
|
55
|
+
}
|