3
This commit is contained in:
@@ -4,13 +4,32 @@ const storageBase = (() => { try { return localStorage.getItem('API_BASE_URL') |
|
||||
const API_BASE_URL = (storageBase || import.meta.env.VITE_APP_API_BASE_URL || 'http://127.0.0.1:8080').replace(/\/$/, '')
|
||||
export const http = axios.create({ baseURL: API_BASE_URL, timeout: 15000 })
|
||||
|
||||
const SHOP_ID = (() => { try { const v = localStorage.getItem('SHOP_ID'); if (v) return Number(v) } catch {} return Number(import.meta.env.VITE_APP_SHOP_ID || '1') })()
|
||||
const USER_ID = (() => { try { const v = localStorage.getItem('USER_ID'); if (v) return Number(v) } catch {} return Number(import.meta.env.VITE_APP_USER_ID || '') })()
|
||||
function readNumber(key: string): number { try { const v = localStorage.getItem(key); if (v) return Number(v) } catch {} return NaN }
|
||||
const SHOP_ID = Number.isFinite(readNumber('SHOP_ID')) ? readNumber('SHOP_ID') : Number(import.meta.env.VITE_APP_SHOP_ID || '1')
|
||||
const USER_ID = Number.isFinite(readNumber('USER_ID')) ? readNumber('USER_ID') : Number(import.meta.env.VITE_APP_USER_ID || '')
|
||||
|
||||
function parseJwtClaims(token: string): any {
|
||||
try {
|
||||
const parts = String(token || '').split('.')
|
||||
if (parts.length < 2) return {}
|
||||
const payload = JSON.parse(decodeURIComponent(escape(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/')))))
|
||||
return payload || {}
|
||||
} catch { return {} }
|
||||
}
|
||||
|
||||
http.interceptors.request.use(cfg => {
|
||||
cfg.headers = cfg.headers || {}
|
||||
if (!cfg.headers['X-Shop-Id']) cfg.headers['X-Shop-Id'] = String(SHOP_ID)
|
||||
if (USER_ID) cfg.headers['X-User-Id'] = String(USER_ID)
|
||||
const token = ((): string => { try { return localStorage.getItem('TOKEN') || '' } catch { return '' } })()
|
||||
if (token && !cfg.headers['Authorization']) cfg.headers['Authorization'] = `Bearer ${token}`
|
||||
// 从 Token 自动解析 userId/shopId 作为兜底
|
||||
if (token) {
|
||||
const claims = parseJwtClaims(token)
|
||||
if (claims && claims.userId && !cfg.headers['X-User-Id']) cfg.headers['X-User-Id'] = String(claims.userId)
|
||||
if (claims && claims.shopId && (!cfg.headers['X-Shop-Id'] || cfg.headers['X-Shop-Id'] === 'NaN')) cfg.headers['X-Shop-Id'] = String(claims.shopId)
|
||||
}
|
||||
// 最后再使用本地 USER_ID 覆盖(如已明确设置)
|
||||
if (USER_ID && !cfg.headers['X-User-Id']) cfg.headers['X-User-Id'] = String(USER_ID)
|
||||
cfg.headers['Accept'] = cfg.headers['Accept'] || 'application/json'
|
||||
return cfg
|
||||
})
|
||||
|
||||
@@ -2,11 +2,18 @@ import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{ path: '/', redirect: '/parts/submissions' },
|
||||
{ path: '/login', component: () => import('../views/Login.vue') },
|
||||
{ path: '/parts/submissions', component: () => import('../views/parts/Submissions.vue') },
|
||||
{ path: '/my', component: () => import('../views/My.vue') }
|
||||
]
|
||||
|
||||
const router = createRouter({ history: createWebHistory(), routes })
|
||||
|
||||
router.beforeEach((to, _from, next) => {
|
||||
const token = ((): string => { try { return localStorage.getItem('TOKEN') || '' } catch { return '' } })()
|
||||
if (!token && to.path !== '/login') return next('/login')
|
||||
next()
|
||||
})
|
||||
export default router
|
||||
|
||||
|
||||
|
||||
58
normal-admin/src/views/Login.vue
Normal file
58
normal-admin/src/views/Login.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div class="login-wrap">
|
||||
<el-card class="login-card">
|
||||
<div class="title">普通管理端登录</div>
|
||||
<el-form :model="form" @keyup.enter.native="onLogin">
|
||||
<el-form-item label="邮箱/手机号">
|
||||
<el-input v-model="form.account" placeholder="邮箱或手机号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="密码">
|
||||
<el-input v-model="form.password" type="password" placeholder="密码" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="loading" @click="onLogin">登录</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="tips">说明:登录成功后将把 USER_ID/SHOP_ID/ROLE/TOKEN 写入本地。</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { post } from '../api/http'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
const form = reactive({ account: '', password: '' })
|
||||
const loading = ref(false)
|
||||
|
||||
async function onLogin(){
|
||||
if (!form.account.trim() || !form.password) return ElMessage.warning('请输入账号与密码')
|
||||
loading.value = true
|
||||
try {
|
||||
const resp:any = await post('/api/auth/password/login', { account: form.account, password: form.password })
|
||||
const u = resp?.user || {}
|
||||
try {
|
||||
localStorage.setItem('TOKEN', String(resp?.token||''))
|
||||
if (u?.userId) localStorage.setItem('USER_ID', String(u.userId))
|
||||
if (u?.shopId) localStorage.setItem('SHOP_ID', String(u.shopId))
|
||||
localStorage.setItem('ROLE', 'normal_admin')
|
||||
} catch {}
|
||||
ElMessage.success('登录成功')
|
||||
router.replace('/parts/submissions')
|
||||
} catch (e:any) {
|
||||
ElMessage.error(e.message || '登录失败')
|
||||
} finally { loading.value=false }
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-wrap { height: 100vh; display:flex; align-items:center; justify-content:center; padding: 16px; }
|
||||
.login-card { width: 380px; }
|
||||
.title { font-weight: 800; font-size: 18px; margin-bottom: 12px; }
|
||||
.tips { color:#888; font-size:12px; margin-top:8px; }
|
||||
</style>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user