feat: 登录联调

This commit is contained in:
王天骄
2026-06-05 16:20:01 +08:00
parent 875bf1f098
commit d97be506ce
5 changed files with 155 additions and 60 deletions
+80
View File
@@ -0,0 +1,80 @@
export type LoginRole = 'super_admin' | 'doctor'
export interface LoginPayload {
account: string
password: string
role: LoginRole
}
export interface LoginResult {
token: string
raw: unknown
}
function getTokenFromResponse(data: unknown): string {
if (!data || typeof data !== 'object') {
return ''
}
const record = data as Record<string, unknown>
const directToken = record.token || record.access_token || record.accessToken
if (typeof directToken === 'string') {
return directToken
}
const nested = record.data
if (nested && typeof nested === 'object') {
return getTokenFromResponse(nested)
}
return ''
}
function getMessageFromResponse(data: unknown): string {
if (!data || typeof data !== 'object') {
return ''
}
const record = data as Record<string, unknown>
const message = record.message || record.msg || record.detail
if (typeof message === 'string') {
return message
}
return getMessageFromResponse(record.data)
}
function parseResponseText(text: string): unknown {
if (!text) {
return null
}
try {
return JSON.parse(text)
} catch {
return null
}
}
export async function login(payload: LoginPayload): Promise<LoginResult> {
const response = await fetch('/server/api/user/auth/login/', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
})
const text = await response.text()
const data = parseResponseText(text)
if (!response.ok) {
const message = getMessageFromResponse(data) || '登录失败,请检查账号、密码和角色'
throw new Error(message)
}
return {
token: getTokenFromResponse(data),
raw: data
}
}
+2 -28
View File
@@ -178,38 +178,12 @@ p {
}
}
.login-options {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 22px;
}
.login-submit {
width: 100%;
}
.login-divider {
display: flex;
align-items: center;
gap: 12px;
margin: 28px 0 18px;
color: var(--muted);
font-size: 13px;
&::before,
&::after {
flex: 1;
height: 1px;
content: "";
background: var(--border);
}
}
.login-methods {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
.login-role-select {
width: 100%;
}
.admin-shell {
+29 -6
View File
@@ -1,5 +1,6 @@
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import type { LoginRole } from '@/api/auth'
import type { RoleKey, UserProfile } from '@/types'
import { roleOptions } from '@/mock/dashboard'
@@ -10,34 +11,55 @@ const roleProfiles: Record<RoleKey, Pick<UserProfile, 'name' | 'avatarText'>> =
teacher: { name: '陈医生', avatarText: '陈' }
}
const loginRoleMap: Record<LoginRole, RoleKey> = {
super_admin: 'super-admin',
doctor: 'teacher'
}
function getStoredRole(): RoleKey {
const storedRole = localStorage.getItem('mediai-role')
return storedRole && storedRole in roleProfiles ? (storedRole as RoleKey) : 'super-admin'
}
export const useAppStore = defineStore('app', () => {
const token = ref(localStorage.getItem('mediai-token') || '')
const collapsed = ref(false)
const darkMode = ref(false)
const storedRole = getStoredRole()
const user = ref<UserProfile>({
name: '张管理员',
avatarText: '张',
role: 'super-admin'
name: roleProfiles[storedRole].name,
avatarText: roleProfiles[storedRole].avatarText,
role: storedRole
})
const roleLabel = computed(() => roleOptions.find(item => item.value === user.value.role)?.label || '管理员')
const isLoggedIn = computed(() => Boolean(token.value))
function login(username: string) {
token.value = `mock-token-${username}`
function login(username: string, role: LoginRole, authToken: string) {
const appRole = loginRoleMap[role]
token.value = authToken || `session-${username}`
localStorage.setItem('mediai-token', token.value)
localStorage.setItem('mediai-role', appRole)
user.value.role = appRole
user.value.name = username === 'admin' ? '张管理员' : username
user.value.avatarText = user.value.name.slice(0, 1).toUpperCase()
}
function logout() {
token.value = ''
localStorage.removeItem('mediai-token')
localStorage.removeItem('mediai-role')
}
function switchRole(role: RoleKey) {
user.value.role = role
user.value.name = roleProfiles[role].name
user.value.avatarText = roleProfiles[role].avatarText
localStorage.setItem('mediai-role', role)
}
function getRoleByLoginRole(role: LoginRole) {
return loginRoleMap[role]
}
return {
@@ -49,6 +71,7 @@ export const useAppStore = defineStore('app', () => {
isLoggedIn,
login,
logout,
switchRole
switchRole,
getRoleByLoginRole
}
})
+35 -23
View File
@@ -26,24 +26,19 @@
</div>
<el-form ref="formRef" :model="form" :rules="rules" size="large" @keyup.enter="handleLogin">
<el-form-item prop="username">
<el-input v-model="form.username" :prefix-icon="User" placeholder="请输入账号" />
<el-form-item prop="account">
<el-input v-model="form.account" :prefix-icon="User" placeholder="请输入账号" />
</el-form-item>
<el-form-item prop="password">
<el-input v-model="form.password" :prefix-icon="Lock" placeholder="请输入密码" show-password type="password" />
</el-form-item>
<div class="login-options">
<el-checkbox v-model="form.remember">记住我</el-checkbox>
<el-button link type="primary">忘记密码</el-button>
</div>
<el-form-item prop="role">
<el-select v-model="form.role" class="login-role-select" :prefix-icon="UserFilled" placeholder="请选择角色">
<el-option v-for="item in loginRoleOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-button :loading="loading" class="login-submit" type="primary" @click="handleLogin">登录</el-button>
</el-form>
<div class="login-divider"><span>其他登录方式</span></div>
<div class="login-methods">
<el-button :icon="Iphone">扫码登录</el-button>
<el-button :icon="Key">SSO登录</el-button>
</div>
</section>
</div>
</template>
@@ -51,8 +46,11 @@
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { Collection, DataAnalysis, FirstAidKit, Iphone, Key, Lock, OfficeBuilding, User } from '@element-plus/icons-vue'
import { Collection, DataAnalysis, FirstAidKit, Lock, OfficeBuilding, User, UserFilled } from '@element-plus/icons-vue'
import { login, type LoginRole } from '@/api/auth'
import { getFirstPage, getPagePath } from '@/mock/navigation'
import { useAppStore } from '@/stores/app'
const route = useRoute()
@@ -62,16 +60,22 @@ const formRef = ref<FormInstance>()
const loading = ref(false)
const form = reactive({
username: 'admin',
password: 'admin123',
remember: true
account: 'admin',
password: '',
role: 'super_admin' as LoginRole
})
const rules: FormRules = {
username: [{ required: true, message: '请输入账号', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
account: [{ required: true, message: '请输入账号', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
role: [{ required: true, message: '请选择角色', trigger: 'change' }]
}
const loginRoleOptions: Array<{ label: string; value: LoginRole }> = [
{ label: '超级管理员', value: 'super_admin' },
{ label: '医生', value: 'doctor' }
]
const features = [
{ title: '智能病例训练', desc: 'AI驱动的沉浸式病例问诊训练', icon: FirstAidKit },
{ title: '数据驱动决策', desc: '全方位数据分析与能力评估', icon: DataAnalysis },
@@ -80,13 +84,21 @@ const features = [
]
async function handleLogin() {
try {
await formRef.value?.validate()
loading.value = true
window.setTimeout(() => {
appStore.login(form.username)
const result = await login({
account: form.account,
password: form.password,
role: form.role
})
appStore.login(form.account, form.role, result.token)
loading.value = false
router.push((route.query.redirect as string) || '/')
}, 420)
const defaultPath = getPagePath(getFirstPage(appStore.getRoleByLoginRole(form.role)))
router.push((route.query.redirect as string) || defaultPath)
} catch (error) {
loading.value = false
ElMessage.error(error instanceof Error ? error.message : '登录失败,请稍后重试')
}
}
</script>
+7 -1
View File
@@ -10,6 +10,12 @@ export default defineConfig({
}
},
server: {
port: 5173
port: 5173,
proxy: {
'/server': {
target: 'http://8.160.178.88/',
changeOrigin: true
}
}
}
})