feat: 登录联调
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -178,38 +178,12 @@ p {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-options {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-submit {
|
.login-submit {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-divider {
|
.login-role-select {
|
||||||
display: flex;
|
width: 100%;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-shell {
|
.admin-shell {
|
||||||
|
|||||||
+29
-6
@@ -1,5 +1,6 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
import type { LoginRole } from '@/api/auth'
|
||||||
import type { RoleKey, UserProfile } from '@/types'
|
import type { RoleKey, UserProfile } from '@/types'
|
||||||
import { roleOptions } from '@/mock/dashboard'
|
import { roleOptions } from '@/mock/dashboard'
|
||||||
|
|
||||||
@@ -10,34 +11,55 @@ const roleProfiles: Record<RoleKey, Pick<UserProfile, 'name' | 'avatarText'>> =
|
|||||||
teacher: { 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', () => {
|
export const useAppStore = defineStore('app', () => {
|
||||||
const token = ref(localStorage.getItem('mediai-token') || '')
|
const token = ref(localStorage.getItem('mediai-token') || '')
|
||||||
const collapsed = ref(false)
|
const collapsed = ref(false)
|
||||||
const darkMode = ref(false)
|
const darkMode = ref(false)
|
||||||
|
const storedRole = getStoredRole()
|
||||||
const user = ref<UserProfile>({
|
const user = ref<UserProfile>({
|
||||||
name: '张管理员',
|
name: roleProfiles[storedRole].name,
|
||||||
avatarText: '张',
|
avatarText: roleProfiles[storedRole].avatarText,
|
||||||
role: 'super-admin'
|
role: storedRole
|
||||||
})
|
})
|
||||||
|
|
||||||
const roleLabel = computed(() => roleOptions.find(item => item.value === user.value.role)?.label || '管理员')
|
const roleLabel = computed(() => roleOptions.find(item => item.value === user.value.role)?.label || '管理员')
|
||||||
const isLoggedIn = computed(() => Boolean(token.value))
|
const isLoggedIn = computed(() => Boolean(token.value))
|
||||||
|
|
||||||
function login(username: string) {
|
function login(username: string, role: LoginRole, authToken: string) {
|
||||||
token.value = `mock-token-${username}`
|
const appRole = loginRoleMap[role]
|
||||||
|
token.value = authToken || `session-${username}`
|
||||||
localStorage.setItem('mediai-token', token.value)
|
localStorage.setItem('mediai-token', token.value)
|
||||||
|
localStorage.setItem('mediai-role', appRole)
|
||||||
|
user.value.role = appRole
|
||||||
user.value.name = username === 'admin' ? '张管理员' : username
|
user.value.name = username === 'admin' ? '张管理员' : username
|
||||||
|
user.value.avatarText = user.value.name.slice(0, 1).toUpperCase()
|
||||||
}
|
}
|
||||||
|
|
||||||
function logout() {
|
function logout() {
|
||||||
token.value = ''
|
token.value = ''
|
||||||
localStorage.removeItem('mediai-token')
|
localStorage.removeItem('mediai-token')
|
||||||
|
localStorage.removeItem('mediai-role')
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchRole(role: RoleKey) {
|
function switchRole(role: RoleKey) {
|
||||||
user.value.role = role
|
user.value.role = role
|
||||||
user.value.name = roleProfiles[role].name
|
user.value.name = roleProfiles[role].name
|
||||||
user.value.avatarText = roleProfiles[role].avatarText
|
user.value.avatarText = roleProfiles[role].avatarText
|
||||||
|
localStorage.setItem('mediai-role', role)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRoleByLoginRole(role: LoginRole) {
|
||||||
|
return loginRoleMap[role]
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -49,6 +71,7 @@ export const useAppStore = defineStore('app', () => {
|
|||||||
isLoggedIn,
|
isLoggedIn,
|
||||||
login,
|
login,
|
||||||
logout,
|
logout,
|
||||||
switchRole
|
switchRole,
|
||||||
|
getRoleByLoginRole
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
+37
-25
@@ -26,24 +26,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-form ref="formRef" :model="form" :rules="rules" size="large" @keyup.enter="handleLogin">
|
<el-form ref="formRef" :model="form" :rules="rules" size="large" @keyup.enter="handleLogin">
|
||||||
<el-form-item prop="username">
|
<el-form-item prop="account">
|
||||||
<el-input v-model="form.username" :prefix-icon="User" placeholder="请输入账号" />
|
<el-input v-model="form.account" :prefix-icon="User" placeholder="请输入账号" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item prop="password">
|
<el-form-item prop="password">
|
||||||
<el-input v-model="form.password" :prefix-icon="Lock" placeholder="请输入密码" show-password type="password" />
|
<el-input v-model="form.password" :prefix-icon="Lock" placeholder="请输入密码" show-password type="password" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<div class="login-options">
|
<el-form-item prop="role">
|
||||||
<el-checkbox v-model="form.remember">记住我</el-checkbox>
|
<el-select v-model="form.role" class="login-role-select" :prefix-icon="UserFilled" placeholder="请选择角色">
|
||||||
<el-button link type="primary">忘记密码?</el-button>
|
<el-option v-for="item in loginRoleOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||||
</div>
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
<el-button :loading="loading" class="login-submit" type="primary" @click="handleLogin">登录</el-button>
|
<el-button :loading="loading" class="login-submit" type="primary" @click="handleLogin">登录</el-button>
|
||||||
</el-form>
|
</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>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -51,8 +46,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { reactive, ref } from 'vue'
|
import { reactive, ref } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
import type { FormInstance, FormRules } 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'
|
import { useAppStore } from '@/stores/app'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -62,16 +60,22 @@ const formRef = ref<FormInstance>()
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
username: 'admin',
|
account: 'admin',
|
||||||
password: 'admin123',
|
password: '',
|
||||||
remember: true
|
role: 'super_admin' as LoginRole
|
||||||
})
|
})
|
||||||
|
|
||||||
const rules: FormRules = {
|
const rules: FormRules = {
|
||||||
username: [{ required: true, message: '请输入账号', trigger: 'blur' }],
|
account: [{ required: true, message: '请输入账号', trigger: 'blur' }],
|
||||||
password: [{ 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 = [
|
const features = [
|
||||||
{ title: '智能病例训练', desc: 'AI驱动的沉浸式病例问诊训练', icon: FirstAidKit },
|
{ title: '智能病例训练', desc: 'AI驱动的沉浸式病例问诊训练', icon: FirstAidKit },
|
||||||
{ title: '数据驱动决策', desc: '全方位数据分析与能力评估', icon: DataAnalysis },
|
{ title: '数据驱动决策', desc: '全方位数据分析与能力评估', icon: DataAnalysis },
|
||||||
@@ -80,13 +84,21 @@ const features = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
async function handleLogin() {
|
async function handleLogin() {
|
||||||
await formRef.value?.validate()
|
try {
|
||||||
loading.value = true
|
await formRef.value?.validate()
|
||||||
|
loading.value = true
|
||||||
window.setTimeout(() => {
|
const result = await login({
|
||||||
appStore.login(form.username)
|
account: form.account,
|
||||||
|
password: form.password,
|
||||||
|
role: form.role
|
||||||
|
})
|
||||||
|
appStore.login(form.account, form.role, result.token)
|
||||||
loading.value = false
|
loading.value = false
|
||||||
router.push((route.query.redirect as string) || '/')
|
const defaultPath = getPagePath(getFirstPage(appStore.getRoleByLoginRole(form.role)))
|
||||||
}, 420)
|
router.push((route.query.redirect as string) || defaultPath)
|
||||||
|
} catch (error) {
|
||||||
|
loading.value = false
|
||||||
|
ElMessage.error(error instanceof Error ? error.message : '登录失败,请稍后重试')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+7
-1
@@ -10,6 +10,12 @@ export default defineConfig({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
port: 5173
|
port: 5173,
|
||||||
|
proxy: {
|
||||||
|
'/server': {
|
||||||
|
target: 'http://8.160.178.88/',
|
||||||
|
changeOrigin: true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user