feat(web): add login, auth store and router guard
This commit is contained in:
@@ -27,11 +27,27 @@
|
|||||||
<el-menu-item index="/inbound-flow">
|
<el-menu-item index="/inbound-flow">
|
||||||
<span>入站流程</span>
|
<span>入站流程</span>
|
||||||
</el-menu-item>
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/flow-trace">
|
||||||
|
<span>流程追踪</span>
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/message-test">
|
||||||
|
<span>消息测试</span>
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item v-if="authStore.roleCodes.includes('platform_admin')" index="/tenant-management">
|
||||||
|
<span>租户管理</span>
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/user-management">
|
||||||
|
<span>用户管理</span>
|
||||||
|
</el-menu-item>
|
||||||
</el-menu>
|
</el-menu>
|
||||||
</el-aside>
|
</el-aside>
|
||||||
<el-container>
|
<el-container>
|
||||||
<el-header class="header">
|
<el-header class="header">
|
||||||
<span>{{ $route.meta.title || '消息中台' }}</span>
|
<span>{{ $route.meta.title || '消息中台' }}</span>
|
||||||
|
<div class="header-right">
|
||||||
|
<span class="user-info">{{ authStore.tenantCode }} / {{ authStore.username }}</span>
|
||||||
|
<el-button type="primary" text @click="handleLogout">退出登录</el-button>
|
||||||
|
</div>
|
||||||
</el-header>
|
</el-header>
|
||||||
<el-main>
|
<el-main>
|
||||||
<router-view />
|
<router-view />
|
||||||
@@ -41,8 +57,17 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { useAuthStore } from '../stores/auth'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
function handleLogout() {
|
||||||
|
authStore.clearAuth()
|
||||||
|
router.push('/login')
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -67,6 +92,16 @@ const route = useRoute()
|
|||||||
border-bottom: 1px solid #e4e7ed;
|
border-bottom: 1px solid #e4e7ed;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
.header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.user-info {
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
import ElementPlus from 'element-plus'
|
import ElementPlus from 'element-plus'
|
||||||
import 'element-plus/dist/index.css'
|
import 'element-plus/dist/index.css'
|
||||||
import './style.css'
|
import './style.css'
|
||||||
@@ -6,6 +7,7 @@ import App from './App.vue'
|
|||||||
import router from './router'
|
import router from './router'
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
app.use(createPinia())
|
||||||
app.use(ElementPlus)
|
app.use(ElementPlus)
|
||||||
app.use(router)
|
app.use(router)
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
|||||||
@@ -8,8 +8,19 @@ import Person from '../views/Person.vue'
|
|||||||
import Tag from '../views/Tag.vue'
|
import Tag from '../views/Tag.vue'
|
||||||
import SendRecord from '../views/SendRecord.vue'
|
import SendRecord from '../views/SendRecord.vue'
|
||||||
import InboundFlow from '../views/InboundFlow.vue'
|
import InboundFlow from '../views/InboundFlow.vue'
|
||||||
|
import FlowTrace from '../views/FlowTrace.vue'
|
||||||
|
import MessageTest from '../views/MessageTest.vue'
|
||||||
|
import TenantManagement from '../views/TenantManagement.vue'
|
||||||
|
import UserManagement from '../views/UserManagement.vue'
|
||||||
|
import Login from '../views/Login.vue'
|
||||||
|
import { useAuthStore } from '../stores/auth'
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
component: Login,
|
||||||
|
meta: { public: true },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
component: Layout,
|
component: Layout,
|
||||||
@@ -23,6 +34,10 @@ const routes = [
|
|||||||
{ path: 'tag', component: Tag, meta: { title: '标签' } },
|
{ path: 'tag', component: Tag, meta: { title: '标签' } },
|
||||||
{ path: 'send-record', component: SendRecord, meta: { title: '发送记录' } },
|
{ path: 'send-record', component: SendRecord, meta: { title: '发送记录' } },
|
||||||
{ path: 'inbound-flow', component: InboundFlow, meta: { title: '入站流程' } },
|
{ path: 'inbound-flow', component: InboundFlow, meta: { title: '入站流程' } },
|
||||||
|
{ path: 'flow-trace', component: FlowTrace, meta: { title: '流程追踪' } },
|
||||||
|
{ path: 'message-test', component: MessageTest, meta: { title: '消息测试' } },
|
||||||
|
{ path: 'tenant-management', component: TenantManagement, meta: { title: '租户管理' } },
|
||||||
|
{ path: 'user-management', component: UserManagement, meta: { title: '用户管理' } },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -32,4 +47,17 @@ const router = createRouter({
|
|||||||
routes,
|
routes,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
router.beforeEach((to, from, next) => {
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const isPublic = to.meta?.public === true
|
||||||
|
|
||||||
|
if (!isPublic && !authStore.isLoggedIn) {
|
||||||
|
next('/login')
|
||||||
|
} else if (to.path === '/login' && authStore.isLoggedIn) {
|
||||||
|
next('/dashboard')
|
||||||
|
} else {
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|||||||
31
admin-web/src/stores/auth.ts
Normal file
31
admin-web/src/stores/auth.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
|
export const useAuthStore = defineStore('auth', () => {
|
||||||
|
const token = ref(localStorage.getItem('mci-token') || '')
|
||||||
|
const tenantCode = ref(localStorage.getItem('mci-tenant-code') || '')
|
||||||
|
const username = ref('')
|
||||||
|
const roleCodes = ref<string[]>([])
|
||||||
|
|
||||||
|
const isLoggedIn = computed(() => !!token.value)
|
||||||
|
|
||||||
|
function setAuth(t: string, tc: string, user: string, roles: string[]) {
|
||||||
|
token.value = t
|
||||||
|
tenantCode.value = tc
|
||||||
|
username.value = user
|
||||||
|
roleCodes.value = roles
|
||||||
|
localStorage.setItem('mci-token', t)
|
||||||
|
localStorage.setItem('mci-tenant-code', tc)
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAuth() {
|
||||||
|
token.value = ''
|
||||||
|
tenantCode.value = ''
|
||||||
|
username.value = ''
|
||||||
|
roleCodes.value = []
|
||||||
|
localStorage.removeItem('mci-token')
|
||||||
|
localStorage.removeItem('mci-tenant-code')
|
||||||
|
}
|
||||||
|
|
||||||
|
return { token, tenantCode, username, roleCodes, isLoggedIn, setAuth, clearAuth }
|
||||||
|
})
|
||||||
5
admin-web/src/views/FlowTrace.vue
Normal file
5
admin-web/src/views/FlowTrace.vue
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h2>流程追踪</h2>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
110
admin-web/src/views/Login.vue
Normal file
110
admin-web/src/views/Login.vue
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
<template>
|
||||||
|
<div class="login-page">
|
||||||
|
<el-card class="login-card" shadow="always">
|
||||||
|
<template #header>
|
||||||
|
<div class="login-title">Sino MCI 管理后台</div>
|
||||||
|
</template>
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
:model="form"
|
||||||
|
:rules="rules"
|
||||||
|
label-position="top"
|
||||||
|
@keyup.enter="handleSubmit"
|
||||||
|
>
|
||||||
|
<el-form-item label="租户编码" prop="tenantCode">
|
||||||
|
<el-input v-model="form.tenantCode" placeholder="请输入租户编码" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="用户名" prop="username">
|
||||||
|
<el-input v-model="form.username" placeholder="请输入用户名" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="密码" prop="password">
|
||||||
|
<el-input
|
||||||
|
v-model="form.password"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
placeholder="请输入密码"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
class="login-btn"
|
||||||
|
:loading="loading"
|
||||||
|
@click="handleSubmit"
|
||||||
|
>
|
||||||
|
登录
|
||||||
|
</el-button>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import type { FormInstance, FormRules } from 'element-plus'
|
||||||
|
import { login } from '@/api/account'
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const formRef = ref<FormInstance>()
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
tenantCode: '',
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules: FormRules = {
|
||||||
|
tenantCode: [{ required: true, message: '请输入租户编码', trigger: 'blur' }],
|
||||||
|
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
||||||
|
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
const valid = await formRef.value?.validate().catch(() => false)
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res: any = await login(form)
|
||||||
|
const userInfo = res.data?.userInfo || res.data
|
||||||
|
authStore.setAuth(
|
||||||
|
res.data?.token || '',
|
||||||
|
userInfo.tenantCode,
|
||||||
|
userInfo.username,
|
||||||
|
userInfo.roleCodes || []
|
||||||
|
)
|
||||||
|
ElMessage.success('登录成功')
|
||||||
|
router.push('/dashboard')
|
||||||
|
} catch (err: any) {
|
||||||
|
ElMessage.error(err?.message || '登录失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.login-page {
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #f5f7fa;
|
||||||
|
}
|
||||||
|
.login-card {
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
.login-title {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.login-btn {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
5
admin-web/src/views/MessageTest.vue
Normal file
5
admin-web/src/views/MessageTest.vue
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h2>消息测试</h2>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
5
admin-web/src/views/TenantManagement.vue
Normal file
5
admin-web/src/views/TenantManagement.vue
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h2>租户管理</h2>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
5
admin-web/src/views/UserManagement.vue
Normal file
5
admin-web/src/views/UserManagement.vue
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h2>用户管理</h2>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
Reference in New Issue
Block a user