新增空闲锁屏、修复401等问题,新增文件版本管理、共享库等占位符

This commit is contained in:
Cheng Zhou
2026-01-13 13:59:07 +08:00
parent b3cd8e03f2
commit 0c7c03069a
21 changed files with 829 additions and 40 deletions
+11 -2
View File
@@ -54,6 +54,10 @@
</template>
<el-menu-item index="/drug/shipments">{{ TEXT.menu.drugShipments }}</el-menu-item>
</el-sub-menu>
<el-menu-item index="/file-versions">
<el-icon><Files /></el-icon>
<span>{{ TEXT.menu.fileVersionManagement }}</span>
</el-menu-item>
<el-menu-item index="/startup/feasibility-ethics">
<el-icon><Calendar /></el-icon>
<span>{{ TEXT.menu.startupFeasibilityEthics }}</span>
@@ -77,10 +81,12 @@
<el-sub-menu index="knowledge">
<template #title>
<el-icon><Notebook /></el-icon>
<span>{{ TEXT.menu.knowledge }}</span>
<span>{{ TEXT.menu.sharedLibrary }}</span>
</template>
<el-menu-item index="/knowledge/medical-consult">{{ TEXT.menu.knowledgeMedicalConsult }}</el-menu-item>
<el-menu-item index="/knowledge/notes">{{ TEXT.menu.knowledgeNotes }}</el-menu-item>
<el-menu-item index="/knowledge/support-files">{{ TEXT.menu.knowledgeSupportFiles }}</el-menu-item>
<el-menu-item index="/knowledge/instruction-files">{{ TEXT.menu.knowledgeInstructionFiles }}</el-menu-item>
</el-sub-menu>
</template>
</el-menu>
@@ -151,7 +157,7 @@ import StudySelector from "./StudySelector.vue";
import { TEXT } from "../locales";
import {
Monitor, User, Suitcase, House, Calendar, Flag, ChatDotRound,
CircleCheck, Box, Coin, Notebook, Document, ArrowDown, SwitchButton
CircleCheck, Box, Coin, Notebook, Document, ArrowDown, SwitchButton, Files
} from "@element-plus/icons-vue";
const auth = useAuthStore();
@@ -167,6 +173,7 @@ const activeMenu = computed(() => {
if (path.startsWith("/finance/contracts")) return "/finance/contracts";
if (path.startsWith("/finance/special")) return "/finance/special";
if (path.startsWith("/drug/shipments")) return "/drug/shipments";
if (path.startsWith("/file-versions")) return "/file-versions";
if (path.startsWith("/startup/feasibility") || path.startsWith("/startup/ethics")) return "/startup/feasibility-ethics";
if (path.startsWith("/startup/meeting-auth") || path.startsWith("/startup/kickoff") || path.startsWith("/startup/training")) {
return "/startup/meeting-auth";
@@ -176,6 +183,8 @@ const activeMenu = computed(() => {
if (path.startsWith("/audit")) return "/audit";
if (path.startsWith("/knowledge/medical-consult")) return "/knowledge/medical-consult";
if (path.startsWith("/knowledge/notes")) return "/knowledge/notes";
if (path.startsWith("/knowledge/support-files")) return "/knowledge/support-files";
if (path.startsWith("/knowledge/instruction-files")) return "/knowledge/instruction-files";
if (path.startsWith("/projects/")) return "/admin/projects";
if (path.startsWith("/admin/projects/")) return "/admin/projects";
return path;
+149
View File
@@ -0,0 +1,149 @@
<template>
<teleport to="body">
<div class="lock-screen" role="dialog" aria-modal="true">
<div class="lock-backdrop" />
<div class="lock-panel">
<h2 class="lock-title">已锁定</h2>
<p class="lock-desc">30 分钟无操作请输入密码解锁</p>
<el-input
ref="passwordInput"
v-model="password"
type="password"
placeholder="请输入登录密码"
show-password
@keyup.enter="onUnlock"
/>
<div class="lock-actions">
<el-button type="primary" :loading="loading" @click="onUnlock">解锁</el-button>
<el-button @click="onLogout">退出登录</el-button>
</div>
<p v-if="errorMessage" class="lock-error">{{ errorMessage }}</p>
</div>
</div>
</teleport>
</template>
<script setup lang="ts">
import { nextTick, onMounted, ref, watch } from "vue";
import { ElMessage } from "element-plus";
import { useAuthStore } from "../store/auth";
import { useSessionStore } from "../store/session";
import { UNLOCK_MAX_ATTEMPTS, unlockWithPassword, forceLogout } from "../session/sessionManager";
const auth = useAuthStore();
const session = useSessionStore();
const password = ref("");
const loading = ref(false);
const errorMessage = ref("");
const passwordInput = ref<{ focus?: () => void } | null>(null);
const focusPassword = () => {
nextTick(() => {
passwordInput.value?.focus?.();
});
};
const onUnlock = async () => {
if (loading.value) return;
errorMessage.value = "";
const email = auth.user?.email;
if (!email) {
ElMessage.error("无法获取用户信息,请重新登录");
forceLogout();
return;
}
if (!password.value) {
errorMessage.value = "请输入密码";
focusPassword();
return;
}
loading.value = true;
try {
await unlockWithPassword(email, password.value);
password.value = "";
} catch (err: any) {
session.incrementUnlockAttempt();
const message = err?.response?.data?.detail || err?.response?.data?.message || "密码错误";
errorMessage.value = message;
if (session.unlockAttempts >= UNLOCK_MAX_ATTEMPTS) {
ElMessage.error("错误次数过多,请重新登录");
forceLogout();
}
} finally {
loading.value = false;
focusPassword();
}
};
const onLogout = () => {
forceLogout();
};
onMounted(() => {
focusPassword();
});
watch(
() => session.locked,
(value) => {
if (value) {
focusPassword();
}
}
);
</script>
<style scoped>
.lock-screen {
position: fixed;
inset: 0;
z-index: 2000;
display: grid;
place-items: center;
}
.lock-backdrop {
position: absolute;
inset: 0;
background: rgba(7, 10, 20, 0.35);
backdrop-filter: blur(10px);
}
.lock-panel {
position: relative;
z-index: 1;
width: min(420px, 92vw);
background: #ffffff;
border-radius: 16px;
padding: 28px;
box-shadow: 0 18px 48px rgba(15, 20, 35, 0.2);
display: flex;
flex-direction: column;
gap: 16px;
}
.lock-title {
margin: 0;
font-size: 22px;
font-weight: 600;
color: #0f172a;
}
.lock-desc {
margin: 0;
color: #64748b;
font-size: 14px;
}
.lock-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
}
.lock-error {
margin: 0;
color: #d8342c;
font-size: 13px;
}
</style>