-
+
+
diff --git a/code/fms/fms-vue/src/components/ui/button/button.vue b/code/fms/fms-vue/src/components/ui/button/button.vue
index 7a73cf8d..54ad1b6c 100644
--- a/code/fms/fms-vue/src/components/ui/button/button.vue
+++ b/code/fms/fms-vue/src/components/ui/button/button.vue
@@ -5,7 +5,7 @@ import { Loader } from '@lucide/vue'
/* ---------- props / emits / slots ---------- */
/**
* 按钮视觉变体
- * @typedef {'default' | 'secondary' | 'outline' | 'ghost' | 'danger' | 'link'} ButtonType
+ * @typedef {'primary' | 'secondary' | 'outline' | 'ghost' | 'danger' | 'link'} ButtonType
*/
const props = defineProps({
@@ -13,9 +13,9 @@ const props = defineProps({
type: {
/** @type {ButtonType} */
type: String,
- default: 'default',
+ default: 'primary',
validator: (value) =>
- ['default', 'secondary', 'outline', 'ghost', 'danger', 'link'].includes(value),
+ ['primary', 'secondary', 'outline', 'ghost', 'danger', 'link'].includes(value),
},
/** 禁用态 */
disabled: {
diff --git a/code/fms/fms-vue/src/components/ui/button/index.scss b/code/fms/fms-vue/src/components/ui/button/index.scss
index e1cbd2e7..11e0a568 100644
--- a/code/fms/fms-vue/src/components/ui/button/index.scss
+++ b/code/fms/fms-vue/src/components/ui/button/index.scss
@@ -41,16 +41,16 @@
}
/* 主要操作 —— 跟随主色 */
-.button-default {
+.button-primary {
background: var(--fms-primary);
color: var(--fms-primary-text);
}
-.button-default:hover:not(:disabled) {
+.button-primary:hover:not(:disabled) {
background: var(--fms-primary-hover);
}
-.button-default:active {
+.button-primary:active {
background: var(--fms-primary-active);
}
diff --git a/code/fms/fms-vue/src/components/ui/demo/button.vue b/code/fms/fms-vue/src/components/ui/demo/button.vue
index 3ea0c3f2..0b14d036 100644
--- a/code/fms/fms-vue/src/components/ui/demo/button.vue
+++ b/code/fms/fms-vue/src/components/ui/demo/button.vue
@@ -13,7 +13,7 @@ import Button from '../button/button.vue'
-
+
diff --git a/code/fms/fms-vue/src/components/ui/demo/drawer.vue b/code/fms/fms-vue/src/components/ui/demo/drawer.vue
index 493be487..001c1ed4 100644
--- a/code/fms/fms-vue/src/components/ui/demo/drawer.vue
+++ b/code/fms/fms-vue/src/components/ui/demo/drawer.vue
@@ -63,7 +63,7 @@ function openDrawer(placement) {
-
+
diff --git a/code/fms/fms-vue/src/components/ui/demo/form.vue b/code/fms/fms-vue/src/components/ui/demo/form.vue
index f00d405a..c6cd29a1 100644
--- a/code/fms/fms-vue/src/components/ui/demo/form.vue
+++ b/code/fms/fms-vue/src/components/ui/demo/form.vue
@@ -116,7 +116,7 @@ function onPickerReset() {
-
+
@@ -139,7 +139,7 @@ function onPickerReset() {
-
+
@@ -166,7 +166,7 @@ function onPickerReset() {
-
+
@@ -195,7 +195,7 @@ function onPickerReset() {
-
+
diff --git a/code/fms/fms-vue/src/components/ui/demo/modal.vue b/code/fms/fms-vue/src/components/ui/demo/modal.vue
index 9c3ca17a..09a7c0f1 100644
--- a/code/fms/fms-vue/src/components/ui/demo/modal.vue
+++ b/code/fms/fms-vue/src/components/ui/demo/modal.vue
@@ -38,7 +38,7 @@ function saveWithFeedback() {
基础用法
-
+
垂直居中 / 无遮罩
diff --git a/code/fms/fms-vue/src/layouts/DefaultLayout.vue b/code/fms/fms-vue/src/layouts/DefaultLayout.vue
index 7a5b207f..e1a61200 100644
--- a/code/fms/fms-vue/src/layouts/DefaultLayout.vue
+++ b/code/fms/fms-vue/src/layouts/DefaultLayout.vue
@@ -111,20 +111,22 @@ function onSplitterResize(sizes) {
@@ -228,6 +230,12 @@ function onSplitterResize(sizes) {
padding: 12px 16px 16px;
}
+/* 页面过渡外层包裹:稳定的 DOM 根,供 transition 的 out-in 模式直接管理,
+ * 避免 keep-alive 直接作 Transition 子节点时在 patch 阶段访问 null.parentNode 报错 */
+.fms-page-wrap {
+ min-height: 100%;
+}
+
/* ---- 页面过渡 ---- */
.page-enter-active {
transition:
diff --git a/code/fms/fms-vue/src/layouts/components/AppTabs.vue b/code/fms/fms-vue/src/layouts/components/AppTabs.vue
index f7900659..886ec7ca 100644
--- a/code/fms/fms-vue/src/layouts/components/AppTabs.vue
+++ b/code/fms/fms-vue/src/layouts/components/AppTabs.vue
@@ -151,11 +151,18 @@ function scrollTabs(direction) {
function revealActiveTab(behavior = 'smooth') {
const track = trackRef.value
- if (!track) return
+ if (!track || !track.isConnected) return
const activeElement = Array.from(track.querySelectorAll('[data-tab-key]')).find(
(element) => element.dataset.tabKey === activeKey.value,
)
- activeElement?.scrollIntoView({ behavior, block: 'nearest', inline: 'center' })
+ // 元素未挂载(路由过渡/布局重建时可能短暂脱离 DOM,parentNode 为 null)则跳过,
+ // 避免 scrollIntoView 抛出未捕获异常向上冒泡成 Promise 拒绝。
+ if (!activeElement || !activeElement.isConnected) return
+ try {
+ activeElement.scrollIntoView({ behavior, block: 'nearest', inline: 'center' })
+ } catch {
+ /* 元素正处于卸载/过渡中,忽略 */
+ }
}
function handleWheel(e) {
diff --git a/code/fms/fms-vue/src/layouts/components/AppTopbar.vue b/code/fms/fms-vue/src/layouts/components/AppTopbar.vue
index 075fc494..66b5ef3f 100644
--- a/code/fms/fms-vue/src/layouts/components/AppTopbar.vue
+++ b/code/fms/fms-vue/src/layouts/components/AppTopbar.vue
@@ -45,10 +45,10 @@ const user = computed(() => ({
account: authStore.user?.account ?? '',
}))
-function logout() {
+async function logout() {
authStore.clearSession()
Message.info('已退出登录')
- router.replace('/login')
+ await router.replace('/login')
}
/* 用户下拉菜单:使用组件库标准 items 渲染(图标/危险色/分隔线/键盘导航均由库负责) */
diff --git a/code/fms/fms-vue/src/main.js b/code/fms/fms-vue/src/main.js
index 51dee0ad..ecebc29c 100644
--- a/code/fms/fms-vue/src/main.js
+++ b/code/fms/fms-vue/src/main.js
@@ -6,6 +6,8 @@ import './styles/demo.css'
import App from './App.vue'
import router from './router'
import { initThemeSync } from './stores/app'
+import { useAuthStore } from '@/stores/auth'
+import { usePermissionStore } from '@/stores/permissions'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
@@ -37,7 +39,35 @@ function showBootstrapError(error) {
async function bootstrap() {
try {
await router.isReady()
+
+ // 登录态下,首屏需要先准备好菜单/权限数据再挂载布局,
+ // 否则挂载后菜单异步到达会“跳出”(对齐 cool-admin:菜单加载放在 mount 之前)
+ const authStore = useAuthStore()
+ if (authStore.isAuthenticated) {
+ const permissionStore = usePermissionStore()
+ await permissionStore.load().catch(() => {})
+ }
+
+ // 先把 boot 屏移出 #app,避免被 mount 整体替换而瞬间消失(那样没有过渡且会卡顿)
+ const boot = document.querySelector('#app-boot')
+ if (boot) document.body.appendChild(boot)
+
+ // 数据已就绪,挂载路由首屏(布局含完整菜单将一次性渲染出来)
app.mount('#app')
+
+ // 下一帧启动 boot 屏的 opacity 淡出(合成属性,由 compositor 驱动,不被下方渲染阻塞)。
+ // 此时菜单已在布局中就位,淡出过程中直接露出完整页面,不会“跳出”。
+ requestAnimationFrame(() => {
+ if (boot) boot.classList.add('app-boot--hide')
+ })
+
+ if (boot) {
+ boot.addEventListener('transitionend', () => boot.remove(), { once: true })
+ // 兜底:防止 transitionend 未触发导致节点残留
+ setTimeout(() => {
+ if (boot.isConnected) boot.remove()
+ }, 600)
+ }
} catch (error) {
showBootstrapError(error)
}
diff --git a/code/fms/fms-vue/src/stores/auth.js b/code/fms/fms-vue/src/stores/auth.js
index e716d9e1..0209fac7 100644
--- a/code/fms/fms-vue/src/stores/auth.js
+++ b/code/fms/fms-vue/src/stores/auth.js
@@ -2,6 +2,7 @@ import { computed, ref } from 'vue'
import { defineStore } from 'pinia'
const AUTH_STORAGE_KEY = 'fms-auth'
+const LAST_LOGIN_KEY = 'fms-last-login'
// 兼容旧版 sessionStorage 会话:将历史会话迁移到 localStorage
try {
@@ -25,10 +26,20 @@ export const useAuthStore = defineStore(
token.value = session.token ?? session.access_token ?? ''
orgId.value = session.orgid ?? organizationId ?? ''
user.value = session.user ?? session.user_info ?? null
+
+ // 单独保存最近登录信息,供登录页回填(与认证状态分离)
+ try {
+ const last = { orgId: orgId.value, account: user.value?.account ?? '' }
+ localStorage.setItem(LAST_LOGIN_KEY, JSON.stringify(last))
+ } catch {
+ // Storage 不可用时静默忽略
+ }
}
function clearSession() {
token.value = ''
+ orgId.value = ''
+ user.value = null
}
return {
diff --git a/code/fms/fms-vue/src/views/login/index.vue b/code/fms/fms-vue/src/views/login/index.vue
index 3e7d7e49..ab146e9b 100644
--- a/code/fms/fms-vue/src/views/login/index.vue
+++ b/code/fms/fms-vue/src/views/login/index.vue
@@ -9,6 +9,7 @@ import Button from '@/components/ui/button/button.vue'
import Message from '@/components/ui/message/message-manager.js'
import { loginApi } from '@/services/api'
import { useAuthStore } from '@/stores/auth'
+import { usePermissionStore } from '@/stores/permissions'
const router = useRouter()
const route = useRoute()
@@ -36,9 +37,24 @@ const formRules = {
const submitting = ref(false)
+const LAST_LOGIN_KEY = 'fms-last-login'
+
onMounted(() => {
- formModel.orgId = authStore.orgId || ''
- formModel.userId = authStore.user?.account || ''
+ // 从独立存储读取上次登录信息回填,与认证状态分离
+ let lastOrgId = ''
+ let lastAccount = ''
+ try {
+ const raw = localStorage.getItem(LAST_LOGIN_KEY)
+ if (raw) {
+ const last = JSON.parse(raw)
+ lastOrgId = last.orgId || ''
+ lastAccount = last.account || ''
+ }
+ } catch {
+ // 解析失败不影响登录
+ }
+ formModel.orgId = lastOrgId
+ formModel.userId = lastAccount
formModel.password = ''
// 聚焦机构码输入框(id 落在 Input 根元素上,需定位内部 input)
document.querySelector('#orgId input')?.focus()
@@ -53,6 +69,8 @@ async function onSubmit(values) {
{ token: res.data.token, user: { ...res.data.user, account: values.userId } },
res.data.orgid ?? values.orgId,
)
+ // 登录成功后、进入布局前先把菜单/权限数据拉齐,避免 /dashboard 挂载时菜单异步“跳出”
+ await usePermissionStore().load().catch(() => {})
Message.success('登录成功')
router.replace(String(route.query.redirect || '/dashboard'))
} catch (e) {
@@ -127,7 +145,7 @@ function onFailed() {
-