20260819101952
This commit is contained in:
1 parent
e91fd1d8de
commit
1ffc8e1ac9
14 files changed
+258
-886
No files matched your search
+149
-852
File diff suppressed because it is too large.
Load diff
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import Button from '../button/button.vue'
|
||||
|
||||
<p class="demo-label">基础用法</p>
|
||||
<div class="demo-panel">
|
||||
<Button type="default">主要操作</Button>
|
||||
<Button type="primary">主要操作</Button>
|
||||
<Button type="secondary">次要操作</Button>
|
||||
<Button type="outline">描边按钮</Button>
|
||||
<Button type="ghost">低强调</Button>
|
||||
|
||||
@@ -63,7 +63,7 @@ function openDrawer(placement) {
|
||||
</p>
|
||||
<template #footer>
|
||||
<Button type="outline" @click="drawerCustomOpen = false">取消</Button>
|
||||
<Button type="default" @click="drawerCustomOpen = false">确定</Button>
|
||||
<Button type="primary" @click="drawerCustomOpen = false">确定</Button>
|
||||
</template>
|
||||
</Drawer>
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ function onPickerReset() {
|
||||
<Input v-model="basicModel.email" placeholder="you@example.com" allow-clear />
|
||||
</FormItem>
|
||||
<div class="form-demo-actions">
|
||||
<Button type="default" html-type="submit">提交</Button>
|
||||
<Button type="primary" html-type="submit">提交</Button>
|
||||
<Button type="outline" html-type="reset">重置</Button>
|
||||
</div>
|
||||
</Form>
|
||||
@@ -139,7 +139,7 @@ function onPickerReset() {
|
||||
<Input v-model="verticalModel.bio" type="textarea" :rows="3" :maxlength="20" show-count />
|
||||
</FormItem>
|
||||
<div class="form-demo-actions">
|
||||
<Button type="default" html-type="submit">提交</Button>
|
||||
<Button type="primary" html-type="submit">提交</Button>
|
||||
<Button type="outline" html-type="reset">重置</Button>
|
||||
</div>
|
||||
</Form>
|
||||
@@ -166,7 +166,7 @@ function onPickerReset() {
|
||||
<Input v-model="customModel.code" placeholder="6 位数字" maxlength="6" allow-clear />
|
||||
</FormItem>
|
||||
<div class="form-demo-actions">
|
||||
<Button type="default" html-type="submit">提交</Button>
|
||||
<Button type="primary" html-type="submit">提交</Button>
|
||||
<Button type="outline" html-type="reset">重置</Button>
|
||||
</div>
|
||||
</Form>
|
||||
@@ -195,7 +195,7 @@ function onPickerReset() {
|
||||
<RangePicker v-model="pickerModel.period" />
|
||||
</FormItem>
|
||||
<div class="form-demo-actions">
|
||||
<Button type="default" html-type="submit">提交</Button>
|
||||
<Button type="primary" html-type="submit">提交</Button>
|
||||
<Button type="outline" html-type="reset">重置</Button>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
@@ -38,7 +38,7 @@ function saveWithFeedback() {
|
||||
|
||||
<p class="demo-label">基础用法</p>
|
||||
<div class="demo-panel">
|
||||
<Button type="default" @click="modalBasicOpen = true">打开模态框</Button>
|
||||
<Button type="primary" @click="modalBasicOpen = true">打开模态框</Button>
|
||||
</div>
|
||||
|
||||
<p class="demo-label">垂直居中 / 无遮罩</p>
|
||||
|
||||
@@ -111,20 +111,22 @@ function onSplitterResize(sizes) {
|
||||
<div class="fms-content">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="page" mode="out-in">
|
||||
<keep-alive :include="appStore.cachedViews" :max="8">
|
||||
<component
|
||||
:is="
|
||||
getPageWrapper(
|
||||
route.meta?.cacheByKey
|
||||
? cacheKeyOf(route.fullPath)
|
||||
: route.meta?.componentName,
|
||||
route.fullPath,
|
||||
Component,
|
||||
)
|
||||
"
|
||||
:key="route.fullPath"
|
||||
/>
|
||||
</keep-alive>
|
||||
<div :key="route.fullPath" class="fms-page-wrap">
|
||||
<keep-alive :include="appStore.cachedViews" :max="8">
|
||||
<component
|
||||
:is="
|
||||
getPageWrapper(
|
||||
route.meta?.cacheByKey
|
||||
? cacheKeyOf(route.fullPath)
|
||||
: route.meta?.componentName,
|
||||
route.fullPath,
|
||||
Component,
|
||||
)
|
||||
"
|
||||
:key="route.fullPath"
|
||||
/>
|
||||
</keep-alive>
|
||||
</div>
|
||||
</transition>
|
||||
</router-view>
|
||||
</div>
|
||||
@@ -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:
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 渲染(图标/危险色/分隔线/键盘导航均由库负责) */
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() {
|
||||
</Input>
|
||||
</FormItem>
|
||||
|
||||
<Button type="default" block :loading="submitting" html-type="submit" class="login-submit">
|
||||
<Button type="primary" block :loading="submitting" html-type="submit" class="login-submit">
|
||||
登录
|
||||
</Button>
|
||||
</Form>
|
||||
|
||||
Submodule
+1
Submodule code/test/cool-admin-vue added at a2d4ee9bbf.
Reference in new issue
Block a user