diff --git a/clinical-web/README.md b/clinical-web/README.md
index 4d02ee2..8ea799e 100644
--- a/clinical-web/README.md
+++ b/clinical-web/README.md
@@ -84,11 +84,14 @@ TanStack Vue Query、PDF 预览、报表图表、自动化测试和签字板适
## 当前状态
-已完成 Vite 基础初始化、路由、Pinia、原型主题 CSS 和医护端整体布局。当前所有业务 View 暂时保留文字占位,尚未接入真实后端。
+已完成 Vite 基础初始化、路由、Pinia、原型主题 CSS、登录页和医护端整体布局。当前所有业务 View 暂时保留文字占位,登录使用演示认证逻辑,尚未接入真实后端。
## 当前路由结构
```text
+公开页
+└─ /login 登录页
+
ClinicalLayout
├─ 工作台
│ ├─ /workbench/home 首页
diff --git a/clinical-web/src/components/layout/Menu.vue b/clinical-web/src/components/layout/Menu.vue
index 9124a78..c4cc8bd 100644
--- a/clinical-web/src/components/layout/Menu.vue
+++ b/clinical-web/src/components/layout/Menu.vue
@@ -1,5 +1,5 @@
@@ -84,7 +90,7 @@ const menuGroups: MenuGroup[] = [
{{ appStore.userName }}
{{ appStore.department }}
-
+
diff --git a/clinical-web/src/main.ts b/clinical-web/src/main.ts
index c1df6cc..47c5ee6 100644
--- a/clinical-web/src/main.ts
+++ b/clinical-web/src/main.ts
@@ -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')
diff --git a/clinical-web/src/router/index.ts b/clinical-web/src/router/index.ts
index 6612d90..b8402f8 100644
--- a/clinical-web/src/router/index.ts
+++ b/clinical-web/src/router/index.ts
@@ -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
diff --git a/clinical-web/src/stores/app.ts b/clinical-web/src/stores/app.ts
index 525440f..c4e5c86 100644
--- a/clinical-web/src/stores/app.ts
+++ b/clinical-web/src/stores/app.ts
@@ -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
+ },
+ },
})
diff --git a/clinical-web/src/styles/index.css b/clinical-web/src/styles/index.css
index 1873243..27ebd9c 100644
--- a/clinical-web/src/styles/index.css
+++ b/clinical-web/src/styles/index.css
@@ -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;
+ }
+}
diff --git a/clinical-web/src/styles/theme.css b/clinical-web/src/styles/theme.css
index 520a2e7..2dd1283 100644
--- a/clinical-web/src/styles/theme.css
+++ b/clinical-web/src/styles/theme.css
@@ -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;
diff --git a/clinical-web/src/views/auth/LoginView.vue b/clinical-web/src/views/auth/LoginView.vue
new file mode 100644
index 0000000..dc75414
--- /dev/null
+++ b/clinical-web/src/views/auth/LoginView.vue
@@ -0,0 +1,113 @@
+
+
+
+
+
+
+
+
+