feat(auth): 添加登录页与演示认证逻辑
This commit is contained in:
@@ -84,11 +84,14 @@ TanStack Vue Query、PDF 预览、报表图表、自动化测试和签字板适
|
||||
|
||||
## 当前状态
|
||||
|
||||
已完成 Vite 基础初始化、路由、Pinia、原型主题 CSS 和医护端整体布局。当前所有业务 View 暂时保留文字占位,尚未接入真实后端。
|
||||
已完成 Vite 基础初始化、路由、Pinia、原型主题 CSS、登录页和医护端整体布局。当前所有业务 View 暂时保留文字占位,登录使用演示认证逻辑,尚未接入真实后端。
|
||||
|
||||
## 当前路由结构
|
||||
|
||||
```text
|
||||
公开页
|
||||
└─ /login 登录页
|
||||
|
||||
ClinicalLayout
|
||||
├─ 工作台
|
||||
│ ├─ /workbench/home 首页
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { RouterLink, useRouter } from 'vue-router'
|
||||
|
||||
import { useAppStore } from '@/stores/app'
|
||||
|
||||
@@ -14,6 +14,7 @@ interface MenuGroup {
|
||||
}
|
||||
|
||||
const appStore = useAppStore()
|
||||
const router = useRouter()
|
||||
|
||||
const menuGroups: MenuGroup[] = [
|
||||
{
|
||||
@@ -34,6 +35,11 @@ const menuGroups: MenuGroup[] = [
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
function handleLogout() {
|
||||
appStore.logout()
|
||||
void router.push('/login')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -84,7 +90,7 @@ const menuGroups: MenuGroup[] = [
|
||||
<strong>{{ appStore.userName }}</strong>
|
||||
<span>{{ appStore.department }}</span>
|
||||
</div>
|
||||
<button class="menu-logout" type="button">退出</button>
|
||||
<button class="menu-logout" type="button" @click="handleLogout">退出</button>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
@@ -7,4 +7,7 @@ import App from './App.vue'
|
||||
import router from './router'
|
||||
import './styles/index.css'
|
||||
|
||||
createApp(App).use(router).use(createPinia()).use(ElementPlus).mount('#app')
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
|
||||
app.use(pinia).use(router).use(ElementPlus).mount('#app')
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import ClinicalLayout from '@/layouts/ClinicalLayout.vue'
|
||||
import ConsentTasksView from '@/views/consent-tasks/ConsentTasksView.vue'
|
||||
import DashboardView from '@/views/dashboard/DashboardView.vue'
|
||||
import DocumentPermissionsView from '@/views/document-permissions/DocumentPermissionsView.vue'
|
||||
import DocumentsView from '@/views/documents/DocumentsView.vue'
|
||||
import LoginView from '@/views/auth/LoginView.vue'
|
||||
import ReportsView from '@/views/reports/ReportsView.vue'
|
||||
import SettingsView from '@/views/settings/SettingsView.vue'
|
||||
import UserPermissionsView from '@/views/users/UserPermissionsView.vue'
|
||||
@@ -12,10 +14,18 @@ import UserPermissionsView from '@/views/users/UserPermissionsView.vue'
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
component: LoginView,
|
||||
meta: { title: '登录' },
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
name: 'clinical',
|
||||
component: ClinicalLayout,
|
||||
redirect: '/workbench/home',
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{
|
||||
path: 'workbench',
|
||||
@@ -23,11 +33,13 @@ const router = createRouter({
|
||||
children: [
|
||||
{
|
||||
path: 'home',
|
||||
name: 'workbench-home',
|
||||
component: DashboardView,
|
||||
meta: { title: '首页', group: '工作台' },
|
||||
},
|
||||
{
|
||||
path: 'signing',
|
||||
name: 'workbench-signing',
|
||||
component: ConsentTasksView,
|
||||
meta: { title: '签署工作台', group: '工作台' },
|
||||
},
|
||||
@@ -39,26 +51,31 @@ const router = createRouter({
|
||||
children: [
|
||||
{
|
||||
path: 'documents',
|
||||
name: 'management-documents',
|
||||
component: DocumentsView,
|
||||
meta: { title: '文档库管理', group: '管理' },
|
||||
},
|
||||
{
|
||||
path: 'reports',
|
||||
name: 'management-reports',
|
||||
component: ReportsView,
|
||||
meta: { title: '报表分析', group: '管理' },
|
||||
},
|
||||
{
|
||||
path: 'users',
|
||||
name: 'management-users',
|
||||
component: UserPermissionsView,
|
||||
meta: { title: '用户与权限', group: '管理' },
|
||||
},
|
||||
{
|
||||
path: 'document-permissions',
|
||||
name: 'management-document-permissions',
|
||||
component: DocumentPermissionsView,
|
||||
meta: { title: '文档权限', group: '管理' },
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
name: 'management-settings',
|
||||
component: SettingsView,
|
||||
meta: { title: '系统设置', group: '管理' },
|
||||
},
|
||||
@@ -75,4 +92,21 @@ const router = createRouter({
|
||||
],
|
||||
})
|
||||
|
||||
router.beforeEach((to) => {
|
||||
const appStore = useAppStore()
|
||||
|
||||
if (to.name === 'login' && appStore.isLoggedIn) {
|
||||
return { name: 'workbench-home' }
|
||||
}
|
||||
|
||||
if (to.meta.requiresAuth && !appStore.isLoggedIn) {
|
||||
return {
|
||||
name: 'login',
|
||||
query: { redirect: to.fullPath },
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -2,8 +2,18 @@ import { defineStore } from 'pinia'
|
||||
|
||||
export const useAppStore = defineStore('app', {
|
||||
state: () => ({
|
||||
isLoggedIn: false,
|
||||
userName: '张文静',
|
||||
department: '医务管理 · 主任医师',
|
||||
environment: '原型演示环境',
|
||||
}),
|
||||
actions: {
|
||||
login(userName: string) {
|
||||
this.userName = userName || '张文静'
|
||||
this.isLoggedIn = true
|
||||
},
|
||||
logout() {
|
||||
this.isLoggedIn = false
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -365,3 +365,160 @@ a {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 原型登录页 */
|
||||
.login-page {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--login-gradient);
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: 380px;
|
||||
padding: 38px 36px;
|
||||
background: #fff;
|
||||
border-radius: 14px;
|
||||
box-shadow: var(--sh-login);
|
||||
}
|
||||
|
||||
.login-brand {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.login-brand-icon {
|
||||
display: grid;
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
place-items: center;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, var(--brand), #18a0c0);
|
||||
}
|
||||
|
||||
.login-brand h1 {
|
||||
margin: 0;
|
||||
color: var(--ink);
|
||||
font-size: 22px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.login-brand p {
|
||||
margin: 2px 0 0;
|
||||
color: #8aa7b3;
|
||||
font-size: 11px;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
margin: 0 0 26px;
|
||||
color: var(--mut);
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-field {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.login-field label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
color: var(--mut);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.login-field input {
|
||||
width: 100%;
|
||||
padding: 7px 10px;
|
||||
color: var(--ink);
|
||||
font-size: 13px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.login-field input:focus {
|
||||
border-color: var(--brand);
|
||||
}
|
||||
|
||||
.login-captcha-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.login-captcha-row input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.login-captcha-image {
|
||||
flex-shrink: 0;
|
||||
padding: 8px 14px;
|
||||
color: #5c7d8a;
|
||||
font-size: 13px;
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
letter-spacing: 6px;
|
||||
user-select: none;
|
||||
background: linear-gradient(120deg, #dfeef3, #c8dfe8);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.login-error {
|
||||
margin: -4px 0 10px;
|
||||
color: var(--err);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.login-submit {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
background: var(--brand);
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
.login-submit:hover {
|
||||
background: var(--brand-d);
|
||||
}
|
||||
|
||||
.login-tip {
|
||||
margin: 16px 0 0;
|
||||
padding: 8px;
|
||||
color: var(--mut);
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
background: var(--brand-l);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
position: absolute;
|
||||
bottom: 18px;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
color: rgb(255 255 255 / 55%);
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
body {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
width: calc(100% - 32px);
|
||||
padding: 32px 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
--brand: #0e6e8c;
|
||||
--brand-d: #0a4f66;
|
||||
--brand-l: #e3f0f4;
|
||||
--brand-m: #1487a5;
|
||||
--side: #0a3d51;
|
||||
--bg: #f1f5f7;
|
||||
--card: #ffffff;
|
||||
@@ -20,6 +21,8 @@
|
||||
--sh: 0 2px 10px rgb(13 62 81 / 7%);
|
||||
--sh-hover: 0 6px 20px rgb(14 110 140 / 18%);
|
||||
--sh-modal: 0 20px 60px rgb(0 0 0 / 30%);
|
||||
--sh-login: 0 20px 60px rgb(0 0 0 / 30%);
|
||||
--login-gradient: linear-gradient(135deg, #0a3d51 0%, var(--brand) 55%, var(--brand-m) 100%);
|
||||
--font-sans:
|
||||
'Microsoft YaHei', 'PingFang SC', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI',
|
||||
sans-serif;
|
||||
|
||||
113
clinical-web/src/views/auth/LoginView.vue
Normal file
113
clinical-web/src/views/auth/LoginView.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import { useAppStore } from '@/stores/app'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const appStore = useAppStore()
|
||||
|
||||
const username = ref('zhangwj')
|
||||
const password = ref('******')
|
||||
const captcha = ref('8362')
|
||||
const errorMessage = ref('')
|
||||
const captchaCode = '8362'
|
||||
|
||||
function getRedirectPath() {
|
||||
const redirect = route.query.redirect
|
||||
return typeof redirect === 'string' && redirect.startsWith('/') ? redirect : '/workbench/home'
|
||||
}
|
||||
|
||||
function handleLogin() {
|
||||
if (!username.value.trim() || !password.value.trim() || !captcha.value.trim()) {
|
||||
errorMessage.value = '请输入完整的账号、密码和验证码'
|
||||
return
|
||||
}
|
||||
|
||||
if (captcha.value.trim() !== captchaCode) {
|
||||
errorMessage.value = '验证码错误,请重新输入'
|
||||
return
|
||||
}
|
||||
|
||||
errorMessage.value = ''
|
||||
appStore.login(username.value.trim())
|
||||
void router.replace(getRedirectPath())
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="login-page">
|
||||
<form class="login-card" novalidate @submit.prevent="handleLogin">
|
||||
<div class="login-brand">
|
||||
<div class="login-brand-icon" aria-hidden="true">
|
||||
<svg width="26" height="26" viewBox="0 0 24 24" fill="none">
|
||||
<path
|
||||
d="M14 3v5h5M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8l-5-5z"
|
||||
stroke="#fff"
|
||||
stroke-width="1.7"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="m9 13.5 2.5 2.5 4-4.5"
|
||||
stroke="#fff"
|
||||
stroke-width="1.7"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1>医签通</h1>
|
||||
<p>MEDISIGN · 患者电子签系统</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="login-subtitle">安全留痕 · 高效签署 · 医患无忧</p>
|
||||
|
||||
<div class="login-field">
|
||||
<label for="login-username">账号</label>
|
||||
<input
|
||||
id="login-username"
|
||||
v-model="username"
|
||||
type="text"
|
||||
autocomplete="username"
|
||||
placeholder="请输入工号 / 账号"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="login-field">
|
||||
<label for="login-password">密码</label>
|
||||
<input
|
||||
id="login-password"
|
||||
v-model="password"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
placeholder="请输入密码"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="login-field">
|
||||
<label for="login-captcha">验证码</label>
|
||||
<div class="login-captcha-row">
|
||||
<input
|
||||
id="login-captcha"
|
||||
v-model="captcha"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
autocomplete="one-time-code"
|
||||
placeholder="验证码"
|
||||
/>
|
||||
<span class="login-captcha-image" aria-label="验证码">{{ captchaCode }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="errorMessage" class="login-error" role="alert">{{ errorMessage }}</p>
|
||||
|
||||
<button class="login-submit" type="submit">登 录</button>
|
||||
<p class="login-tip">原型演示:任意账号密码均可登录</p>
|
||||
</form>
|
||||
|
||||
<p class="login-footer">© 2026 医签通 MEDISIGN · 患者电子签署系统 V1.1 原型</p>
|
||||
</main>
|
||||
</template>
|
||||
Reference in New Issue
Block a user