smart-admin-web 10.21.0 → 10.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/icons/ph-subset.json.js +1 -1
- package/dist/components/FormContainer/index.vue.js +67 -67
- package/dist/components/FormContainer/index.vue.js.map +1 -1
- package/dist/components/SidePanelDrawer/index.vue.d.ts +25 -0
- package/dist/components/SidePanelDrawer/index.vue.js +40 -0
- package/dist/components/SidePanelDrawer/index.vue.js.map +1 -0
- package/dist/components/SidePanelDrawer/index3.vue.js +6 -0
- package/dist/components/SidePanelDrawer/index3.vue.js.map +1 -0
- package/dist/composables/authMenuRoute.d.ts +10 -0
- package/dist/composables/authMenuRoute.js +39 -23
- package/dist/composables/authMenuRoute.js.map +1 -1
- package/dist/composables/useModule.d.ts +3 -1
- package/dist/composables/useModule.js +44 -32
- package/dist/composables/useModule.js.map +1 -1
- package/dist/composables/useShellBreakpoint.js +14 -13
- package/dist/composables/useShellBreakpoint.js.map +1 -1
- package/dist/composables/useSidePanel.d.ts +9 -0
- package/dist/composables/useSidePanel.js +12 -0
- package/dist/composables/useSidePanel.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +297 -290
- package/dist/index.js.map +1 -1
- package/dist/layouts/components/SideNav.vue.js +2 -2
- package/dist/layouts/components/SideNav2.vue.js.map +1 -1
- package/dist/layouts/default.vue.js +2 -2
- package/dist/layouts/default2.vue.js.map +1 -1
- package/dist/lib/runtime.js +1 -1
- package/dist/locales/en-US.d.ts +6 -0
- package/dist/locales/en-US.js +9 -2
- package/dist/locales/en-US.js.map +1 -1
- package/dist/locales/zh-CN.d.ts +6 -0
- package/dist/locales/zh-CN.js +9 -2
- package/dist/locales/zh-CN.js.map +1 -1
- package/dist/style.css +1 -1
- package/dist/utils/tree.d.ts +15 -0
- package/dist/utils/tree.js +43 -29
- package/dist/utils/tree.js.map +1 -1
- package/dist/utils/treeCard.d.ts +6 -0
- package/dist/utils/treeCard.js +9 -0
- package/dist/utils/treeCard.js.map +1 -0
- package/dist/views/error/404.vue.js +2 -2
- package/dist/views/error/4042.vue.js +81 -24
- package/dist/views/error/4042.vue.js.map +1 -1
- package/dist/views/login/components/LoginDesk.vue.js +2 -2
- package/dist/views/login/components/LoginDesk2.vue.js.map +1 -1
- package/dist/views/login/components/LoginHeroPanel.vue.js +2 -2
- package/dist/views/login/components/LoginHeroPanel2.vue.js.map +1 -1
- package/dist/views/system/job-monitor/index.vue.js +2 -2
- package/dist/views/system/job-monitor/index2.vue.js +46 -46
- package/dist/views/system/job-monitor/index2.vue.js.map +1 -1
- package/dist/views/system/menu/index.vue.js +2 -2
- package/dist/views/system/menu/index2.vue.js +161 -150
- package/dist/views/system/menu/index2.vue.js.map +1 -1
- package/dist/views/system/org/index.vue.js +2 -2
- package/dist/views/system/org/index2.vue.js +182 -176
- package/dist/views/system/org/index2.vue.js.map +1 -1
- package/dist/views/system/user/index.vue.js +1 -1
- package/dist/views/system/user/index2.vue.js +296 -277
- package/dist/views/system/user/index2.vue.js.map +1 -1
- package/package.json +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.vue.js","sources":["../../../src/components/FormContainer/index.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// 弹窗/抽屉二合一表单容器:消化 CRUD 页「saving ref + 手写 footer + n-modal 包装」样板。\n// 形态默认跟随全局偏好(app.formStyle,外观设置里切,默认弹窗),variant 按实例覆盖。\n// 抽屉形态按壳层档位走:宽 / 中档 = 右侧抽屉,窄档(< 600)= 底部抽屉,高 92%,\n// 底栏两个按钮等分整行。弹窗形态不分档,仍是居中卡片。\n// onConfirm 协议:返回 Promise → 确认钮自动 loading;reject 或 resolve(false) → 不关闭\n// (n-form 校验放 onConfirm 首行,失败 reject 即挡住关闭);其余情况自动关。\n// 弹窗形态下,没提交成功(校验不过 / 业务拒绝)时卡片左右晃一下,像 macOS 拒绝输入的提示框,\n// 补足内联提示之外的就地反馈;抽屉与全屏形态不晃(整块贴边,位移会露出缝)。\nimport { computed, onDeactivated, ref } from 'vue'\nimport { NModal, NDrawer, NDrawerContent, NButton, NSpace } from 'naive-ui'\nimport { useI18n } from 'vue-i18n'\nimport { useWindowSize } from '@vueuse/core'\nimport { useCompactScreen } from '#/composables/useCompactScreen'\nimport { useShellBreakpoint } from '#/composables/useShellBreakpoint'\nimport { useAppStore, type FormStyle } from '#/stores/app'\nimport { shake } from '#/utils/shake'\n\nconst show = defineModel<boolean>('show', { default: false })\n\nconst props = defineProps<{\n title: string\n /** 不传 → 跟随 app.formStyle 全局偏好 */\n variant?: FormStyle\n /** modal=卡片宽 / drawer=抽屉宽,实际生效 min(width, 90vw) */\n width?: number\n /** 见顶部协议注释;不传则确认钮直接关闭 */\n onConfirm?: () => unknown | Promise<unknown>\n confirmText?: string\n cancelText?: string\n /** 默认 false:表单防误触丢输入 */\n maskClosable?: boolean\n /**\n * 铺满视口。默认 `false` —— 不给 `'auto'` 当默认值是刻意的:没传这个 prop 的既有页面\n * 不该因为换了台平板就换一种形态。字段多的表单显式传 `true`,想跟着屏幕走再传 `'auto'`\n * (紧凑屏判定见 composables/useCompactScreen)。\n */\n fullscreen?: boolean | 'auto'\n}>()\n\nconst { t } = useI18n()\nconst app = useAppStore()\n\nconst mode = computed(() => props.variant ?? app.formStyle)\nconst loading = ref(false)\n// 窄屏不溢出:同 SettingsDrawer 惯例,宽度取 min(width, 90vw)。\nconst { width: winW } = useWindowSize()\nconst compact = useCompactScreen()\nconst fullscreen = computed(() =>\n props.fullscreen === 'auto' ? compact.value : props.fullscreen === true,\n)\n// 窄档抽屉改底部抽屉。全屏表单(fullscreen)本就铺满视口,不叠底部抽屉形态。\nconst { bp } = useShellBreakpoint()\nconst sheet = computed(() => mode.value === 'drawer' && bp.value === 'narrow' && !fullscreen.value)\nconst btnSize = computed(() => (sheet.value ? 'large' : 'medium'))\nconst w = computed(() =>\n fullscreen.value ? winW.value : Math.min(props.width ?? 560, Math.round(winW.value * 0.9)),\n)\n// 全屏时不再钉宽度,交给 .smart-form--fullscreen 那条全局样式铺满。\nconst modalStyle = computed(() => (fullscreen.value ? undefined : { width: `${w.value}px` }))\n// 提交中禁止任何途径关闭(esc/遮罩/关闭钮/取消钮),防提交中途关闭导致状态错乱。\nconst canClose = computed(() => !loading.value)\nconst maskClose = computed(() => (props.maskClosable ?? false) && !loading.value)\n\n// 卡片 teleport 到 body,拿不到它的 ref;在正文里放一个不可见的锚点,从锚点向上找到所在的卡片。\nconst anchor = ref<HTMLElement>()\nfunction shakeCard() {\n if (mode.value !== 'modal' || fullscreen.value) return\n shake(anchor.value?.closest('.n-card.n-modal'))\n}\n\nasync function handleConfirm() {\n if (!props.onConfirm) {\n show.value = false\n return\n }\n loading.value = true\n try {\n if ((await props.onConfirm()) !== false) show.value = false\n else shakeCard()\n } catch {\n // 静默:n-form 校验失败已有内联提示;API 错误 toast 由业务 onConfirm 内负责。这里只补一下晃动。\n shakeCard()\n } finally {\n loading.value = false\n }\n}\n\n// keep-alive 页签切走时,teleport 到 body 的弹层会残留可见 → 强制收起。\nonDeactivated(() => {\n show.value = false\n})\n</script>\n\n<template>\n <n-modal\n v-if=\"mode === 'modal'\"\n v-model:show=\"show\"\n preset=\"card\"\n :title=\"title\"\n :closable=\"canClose\"\n :close-on-esc=\"canClose\"\n :mask-closable=\"maskClose\"\n :class=\"{ 'smart-form--fullscreen': fullscreen }\"\n :style=\"modalStyle\"\n >\n <template v-if=\"$slots['header-extra']\" #header-extra>\n <slot name=\"header-extra\" />\n </template>\n <i ref=\"anchor\" hidden aria-hidden=\"true\" />\n <slot />\n <template #footer>\n <slot name=\"footer\">\n <n-space justify=\"end\">\n <n-button secondary :disabled=\"loading\" @click=\"show = false\">\n {{ cancelText ?? t('common.cancel') }}\n </n-button>\n <n-button type=\"primary\" :loading=\"loading\" @click=\"handleConfirm\">\n {{ confirmText ?? t('common.confirm') }}\n </n-button>\n </n-space>\n </slot>\n </template>\n </n-modal>\n\n <n-drawer\n v-else\n v-model:show=\"show\"\n :placement=\"sheet ? 'bottom' : 'right'\"\n :width=\"sheet ? undefined : w\"\n :height=\"sheet ? '92%' : undefined\"\n :class=\"{ 'smart-form-sheet': sheet }\"\n :close-on-esc=\"canClose\"\n :mask-closable=\"maskClose\"\n >\n <n-drawer-content :title=\"title\" :closable=\"canClose\" :native-scrollbar=\"false\">\n <!-- n-drawer-content 无 header-extra 原生槽:业务传了才自拼 #header(标题 + 附加区并排) -->\n <template v-if=\"$slots['header-extra']\" #header>\n <span>{{ title }}</span>\n <slot name=\"header-extra\" />\n </template>\n <slot />\n <template #footer>\n <slot name=\"footer\">\n <!-- 窄档底部抽屉:两个按钮等分整行、大号 -->\n <div v-if=\"sheet\" class=\"smart-form-foot\">\n <n-button secondary :size=\"btnSize\" :disabled=\"loading\" @click=\"show = false\">\n {{ cancelText ?? t('common.cancel') }}\n </n-button>\n <n-button type=\"primary\" :size=\"btnSize\" :loading=\"loading\" @click=\"handleConfirm\">\n {{ confirmText ?? t('common.confirm') }}\n </n-button>\n </div>\n <n-space v-else justify=\"end\">\n <n-button secondary :disabled=\"loading\" @click=\"show = false\">\n {{ cancelText ?? t('common.cancel') }}\n </n-button>\n <n-button type=\"primary\" :loading=\"loading\" @click=\"handleConfirm\">\n {{ confirmText ?? t('common.confirm') }}\n </n-button>\n </n-space>\n </slot>\n </template>\n </n-drawer-content>\n </n-drawer>\n</template>\n\n<!-- 不能加 scoped:n-modal / n-drawer 的内容 teleport 到 body,scoped 编译出的 data-v 属性到不了那棵子树,\n 写了等于没写(这正是\"全屏样式不生效\"最常见的原因)。故用带前缀的类名自行隔离。 -->\n<style>\n.smart-form--fullscreen {\n width: 100vw;\n max-width: 100vw;\n height: 100dvh;\n max-height: 100dvh;\n display: flex;\n flex-direction: column;\n border-radius: 0;\n}\n/* 头/脚按内容高,正文吃满剩余并自己滚 —— 否则长表单会把整张卡撑出视口,底部按钮点不到。 */\n.smart-form--fullscreen > .n-card__content {\n flex: 1;\n min-height: 0;\n overflow: auto;\n}\n/* 窄档底部抽屉:圆角只在上沿;按钮等分整行。\n 高度用 92% 而不是随内容:表单长短不一,固定高度让不同表单的抽屉一致,正文自己滚。 */\n.n-drawer.smart-form-sheet {\n border-radius: var(--radius-xl) var(--radius-xl) 0 0;\n}\n.smart-form-foot {\n display: flex;\n gap: 10px;\n width: 100%;\n}\n.smart-form-foot > .n-button {\n flex: 1;\n}\n</style>\n"],"names":["show","_useModel","__props","props","t","useI18n","app","useAppStore","mode","computed","loading","ref","winW","useWindowSize","compact","useCompactScreen","fullscreen","bp","useShellBreakpoint","sheet","btnSize","w","modalStyle","canClose","maskClose","anchor","shakeCard","shake","_a","handleConfirm","onDeactivated","_createBlock","_unref","NModal","$event","_renderSlot","_ctx","_createVNode","NSpace","NButton","_createTextVNode","_toDisplayString","_createElementVNode","$slots","NDrawer","NDrawerContent","_openBlock","_createElementBlock","_hoisted_1"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.vue.js","sources":["../../../src/components/FormContainer/index.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// 弹窗/抽屉二合一表单容器:消化 CRUD 页「saving ref + 手写 footer + n-modal 包装」样板。\n// 形态默认跟随全局偏好(app.formStyle,外观设置里切,默认弹窗),variant 按实例覆盖。\n// 抽屉形态按壳层档位走:宽 / 中档 = 右侧抽屉,窄档(< 600)= 底部抽屉,高 92%,\n// 底栏两个按钮等分整行。弹窗形态不分档,仍是居中卡片。\n// onConfirm 协议:返回 Promise → 确认钮自动 loading;reject 或 resolve(false) → 不关闭\n// (n-form 校验放 onConfirm 首行,失败 reject 即挡住关闭);其余情况自动关。\n// 弹窗形态下,没提交成功(校验不过 / 业务拒绝)时卡片左右晃一下,像 macOS 拒绝输入的提示框,\n// 补足内联提示之外的就地反馈;抽屉与全屏形态不晃(整块贴边,位移会露出缝)。\nimport { computed, onDeactivated, ref } from 'vue'\nimport { NModal, NDrawer, NDrawerContent, NButton, NSpace } from 'naive-ui'\nimport { useI18n } from 'vue-i18n'\nimport { useWindowSize } from '@vueuse/core'\nimport { useCompactScreen } from '#/composables/useCompactScreen'\nimport { useShellBreakpoint } from '#/composables/useShellBreakpoint'\nimport { useAppStore, type FormStyle } from '#/stores/app'\nimport { shake } from '#/utils/shake'\n\nconst show = defineModel<boolean>('show', { default: false })\n\nconst props = defineProps<{\n title: string\n /** 不传 → 跟随 app.formStyle 全局偏好 */\n variant?: FormStyle\n /** modal=卡片宽 / drawer=抽屉宽,实际生效 min(width, 90vw) */\n width?: number\n /** 见顶部协议注释;不传则确认钮直接关闭 */\n onConfirm?: () => unknown | Promise<unknown>\n confirmText?: string\n cancelText?: string\n /** 默认 false:表单防误触丢输入 */\n maskClosable?: boolean\n /**\n * 铺满视口。默认 `false` —— 不给 `'auto'` 当默认值是刻意的:没传这个 prop 的既有页面\n * 不该因为换了台平板就换一种形态。字段多的表单显式传 `true`,想跟着屏幕走再传 `'auto'`\n * (紧凑屏判定见 composables/useCompactScreen)。\n */\n fullscreen?: boolean | 'auto'\n}>()\n\nconst { t } = useI18n()\nconst app = useAppStore()\n\nconst mode = computed(() => props.variant ?? app.formStyle)\nconst loading = ref(false)\n// 窄屏不溢出:同 SettingsDrawer 惯例,宽度取 min(width, 90vw)。\nconst { width: winW } = useWindowSize()\nconst compact = useCompactScreen()\nconst fullscreen = computed(() =>\n props.fullscreen === 'auto' ? compact.value : props.fullscreen === true,\n)\n// 窄档抽屉改底部抽屉。全屏表单(fullscreen)本就铺满视口,不叠底部抽屉形态。\nconst { bp } = useShellBreakpoint()\nconst sheet = computed(() => mode.value === 'drawer' && bp.value === 'narrow' && !fullscreen.value)\n// 窄档的弹窗卡片限高在视口内、正文自己滚:内容比视口高时底栏(保存 / 取消)不随正文滚出屏外。\n// 全屏态自带这套(.smart-form--fullscreen),所以只给非全屏的窄档弹窗加。宽 / 中档保持 naive 默认(整张卡随遮罩层滚)。\nconst fixedFoot = computed(() => bp.value === 'narrow' && !fullscreen.value)\nconst btnSize = computed(() => (sheet.value ? 'large' : 'medium'))\nconst w = computed(() =>\n fullscreen.value ? winW.value : Math.min(props.width ?? 560, Math.round(winW.value * 0.9)),\n)\n// 全屏时不再钉宽度,交给 .smart-form--fullscreen 那条全局样式铺满。\nconst modalStyle = computed(() => (fullscreen.value ? undefined : { width: `${w.value}px` }))\n// 提交中禁止任何途径关闭(esc/遮罩/关闭钮/取消钮),防提交中途关闭导致状态错乱。\nconst canClose = computed(() => !loading.value)\nconst maskClose = computed(() => (props.maskClosable ?? false) && !loading.value)\n\n// 卡片 teleport 到 body,拿不到它的 ref;在正文里放一个不可见的锚点,从锚点向上找到所在的卡片。\nconst anchor = ref<HTMLElement>()\nfunction shakeCard() {\n if (mode.value !== 'modal' || fullscreen.value) return\n shake(anchor.value?.closest('.n-card.n-modal'))\n}\n\nasync function handleConfirm() {\n if (!props.onConfirm) {\n show.value = false\n return\n }\n loading.value = true\n try {\n if ((await props.onConfirm()) !== false) show.value = false\n else shakeCard()\n } catch {\n // 静默:n-form 校验失败已有内联提示;API 错误 toast 由业务 onConfirm 内负责。这里只补一下晃动。\n shakeCard()\n } finally {\n loading.value = false\n }\n}\n\n// keep-alive 页签切走时,teleport 到 body 的弹层会残留可见 → 强制收起。\nonDeactivated(() => {\n show.value = false\n})\n</script>\n\n<template>\n <n-modal\n v-if=\"mode === 'modal'\"\n v-model:show=\"show\"\n preset=\"card\"\n :title=\"title\"\n :closable=\"canClose\"\n :close-on-esc=\"canClose\"\n :mask-closable=\"maskClose\"\n :class=\"{ 'smart-form--fullscreen': fullscreen, 'smart-form--fixed-foot': fixedFoot }\"\n :style=\"modalStyle\"\n >\n <template v-if=\"$slots['header-extra']\" #header-extra>\n <slot name=\"header-extra\" />\n </template>\n <i ref=\"anchor\" hidden aria-hidden=\"true\" />\n <slot />\n <template #footer>\n <slot name=\"footer\">\n <n-space justify=\"end\">\n <n-button secondary :disabled=\"loading\" @click=\"show = false\">\n {{ cancelText ?? t('common.cancel') }}\n </n-button>\n <n-button type=\"primary\" :loading=\"loading\" @click=\"handleConfirm\">\n {{ confirmText ?? t('common.confirm') }}\n </n-button>\n </n-space>\n </slot>\n </template>\n </n-modal>\n\n <n-drawer\n v-else\n v-model:show=\"show\"\n :placement=\"sheet ? 'bottom' : 'right'\"\n :width=\"sheet ? undefined : w\"\n :height=\"sheet ? '92%' : undefined\"\n :class=\"{ 'smart-form-sheet': sheet }\"\n :close-on-esc=\"canClose\"\n :mask-closable=\"maskClose\"\n >\n <n-drawer-content :title=\"title\" :closable=\"canClose\" :native-scrollbar=\"false\">\n <!-- n-drawer-content 无 header-extra 原生槽:业务传了才自拼 #header(标题 + 附加区并排) -->\n <template v-if=\"$slots['header-extra']\" #header>\n <span>{{ title }}</span>\n <slot name=\"header-extra\" />\n </template>\n <slot />\n <template #footer>\n <slot name=\"footer\">\n <!-- 窄档底部抽屉:两个按钮等分整行、大号 -->\n <div v-if=\"sheet\" class=\"smart-form-foot\">\n <n-button secondary :size=\"btnSize\" :disabled=\"loading\" @click=\"show = false\">\n {{ cancelText ?? t('common.cancel') }}\n </n-button>\n <n-button type=\"primary\" :size=\"btnSize\" :loading=\"loading\" @click=\"handleConfirm\">\n {{ confirmText ?? t('common.confirm') }}\n </n-button>\n </div>\n <n-space v-else justify=\"end\">\n <n-button secondary :disabled=\"loading\" @click=\"show = false\">\n {{ cancelText ?? t('common.cancel') }}\n </n-button>\n <n-button type=\"primary\" :loading=\"loading\" @click=\"handleConfirm\">\n {{ confirmText ?? t('common.confirm') }}\n </n-button>\n </n-space>\n </slot>\n </template>\n </n-drawer-content>\n </n-drawer>\n</template>\n\n<!-- 不能加 scoped:n-modal / n-drawer 的内容 teleport 到 body,scoped 编译出的 data-v 属性到不了那棵子树,\n 写了等于没写(这正是\"全屏样式不生效\"最常见的原因)。故用带前缀的类名自行隔离。 -->\n<style>\n.smart-form--fullscreen {\n width: 100vw;\n max-width: 100vw;\n height: 100dvh;\n max-height: 100dvh;\n display: flex;\n flex-direction: column;\n border-radius: 0;\n}\n/* 窄档非全屏弹窗:卡片限高在视口内(上下各留 12px 的遮罩),头/脚按内容高,正文吃满剩余并自己滚。 */\n.smart-form--fixed-foot {\n display: flex;\n flex-direction: column;\n max-height: calc(100dvh - 24px);\n}\n/* 头/脚按内容高,正文吃满剩余并自己滚 —— 否则长表单会把整张卡撑出视口,底部按钮点不到。\n 内容层的类名两种都写:naive-ui 2.45 输出 .n-card-content,只有 content-scrollable 时的滚动外层才带 __content-scrollbar,\n 光写 .n-card__content 在 2.45 上一条都命不中。 */\n.smart-form--fullscreen > :is(.n-card-content, .n-card__content),\n.smart-form--fixed-foot > :is(.n-card-content, .n-card__content) {\n flex: 1;\n min-height: 0;\n overflow: auto;\n}\n/* 窄档底部抽屉:圆角只在上沿;按钮等分整行。\n 高度用 92% 而不是随内容:表单长短不一,固定高度让不同表单的抽屉一致,正文自己滚。 */\n.n-drawer.smart-form-sheet {\n border-radius: var(--radius-xl) var(--radius-xl) 0 0;\n}\n.smart-form-foot {\n display: flex;\n gap: 10px;\n width: 100%;\n}\n.smart-form-foot > .n-button {\n flex: 1;\n}\n</style>\n"],"names":["show","_useModel","__props","props","t","useI18n","app","useAppStore","mode","computed","loading","ref","winW","useWindowSize","compact","useCompactScreen","fullscreen","bp","useShellBreakpoint","sheet","fixedFoot","btnSize","w","modalStyle","canClose","maskClose","anchor","shakeCard","shake","_a","handleConfirm","onDeactivated","_createBlock","_unref","NModal","$event","_normalizeClass","_renderSlot","_ctx","_createVNode","NSpace","NButton","_createTextVNode","_toDisplayString","_createElementVNode","$slots","NDrawer","NDrawerContent","_openBlock","_createElementBlock","_hoisted_1"],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
/** 抽屉标题,通常是分组名(「机构」「分类」) */
|
|
3
|
+
title: string;
|
|
4
|
+
};
|
|
5
|
+
type __VLS_ModelProps = {
|
|
6
|
+
'show'?: boolean;
|
|
7
|
+
};
|
|
8
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
9
|
+
declare var __VLS_14: {};
|
|
10
|
+
type __VLS_Slots = {} & {
|
|
11
|
+
default?: (props: typeof __VLS_14) => any;
|
|
12
|
+
};
|
|
13
|
+
declare const __VLS_base: import('vue').DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
|
|
14
|
+
"update:show": (value: boolean) => any;
|
|
15
|
+
}, string, import('vue').PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
16
|
+
"onUpdate:show"?: ((value: boolean) => any) | undefined;
|
|
17
|
+
}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
|
|
18
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
19
|
+
declare const _default: typeof __VLS_export;
|
|
20
|
+
export default _default;
|
|
21
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
22
|
+
new (): {
|
|
23
|
+
$slots: S;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { defineComponent as r, useModel as d, openBlock as i, createBlock as f, unref as l, withCtx as s, createVNode as u, createElementVNode as w, renderSlot as m, mergeModels as p } from "vue";
|
|
2
|
+
import { NDrawer as c, NDrawerContent as h } from "naive-ui";
|
|
3
|
+
const _ = { class: "side-panel-body" }, N = /* @__PURE__ */ r({
|
|
4
|
+
__name: "index",
|
|
5
|
+
props: /* @__PURE__ */ p({
|
|
6
|
+
title: {}
|
|
7
|
+
}, {
|
|
8
|
+
show: { type: Boolean, default: !1 },
|
|
9
|
+
showModifiers: {}
|
|
10
|
+
}),
|
|
11
|
+
emits: ["update:show"],
|
|
12
|
+
setup(e) {
|
|
13
|
+
const t = d(e, "show");
|
|
14
|
+
return (a, o) => (i(), f(l(c), {
|
|
15
|
+
show: t.value,
|
|
16
|
+
"onUpdate:show": o[0] || (o[0] = (n) => t.value = n),
|
|
17
|
+
placement: "left",
|
|
18
|
+
width: "min(300px, 88vw)"
|
|
19
|
+
}, {
|
|
20
|
+
default: s(() => [
|
|
21
|
+
u(l(h), {
|
|
22
|
+
title: e.title,
|
|
23
|
+
closable: ""
|
|
24
|
+
}, {
|
|
25
|
+
default: s(() => [
|
|
26
|
+
w("div", _, [
|
|
27
|
+
m(a.$slots, "default")
|
|
28
|
+
])
|
|
29
|
+
]),
|
|
30
|
+
_: 3
|
|
31
|
+
}, 8, ["title"])
|
|
32
|
+
]),
|
|
33
|
+
_: 3
|
|
34
|
+
}, 8, ["show"]));
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
export {
|
|
38
|
+
N as default
|
|
39
|
+
};
|
|
40
|
+
//# sourceMappingURL=index.vue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.vue.js","sources":["../../../src/components/SidePanelDrawer/index.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// 带左侧面板的页面在窄内容区里的面板抽屉:从左侧滑出,标题栏写分组名,插槽放面板内容。\n// 与 useSidePanel 配对使用:`compact` 时不画 aside,点工具栏按钮把 `drawerOpen` 置 true 打开这里。\n// 面板里的 `.side-filter__btn` 等沿用 layout.css 的侧栏约定,抽屉里是触屏,图标按钮放大到可点的尺寸。\nimport { NDrawer, NDrawerContent } from 'naive-ui'\n\ndefineProps<{\n /** 抽屉标题,通常是分组名(「机构」「分类」) */\n title: string\n}>()\n\nconst show = defineModel<boolean>('show', { default: false })\n</script>\n\n<template>\n <n-drawer v-model:show=\"show\" placement=\"left\" width=\"min(300px, 88vw)\">\n <n-drawer-content :title=\"title\" closable>\n <div class=\"side-panel-body\">\n <slot />\n </div>\n </n-drawer-content>\n </n-drawer>\n</template>\n\n<!-- 不加 scoped:面板内容是调用方模板里的节点,带的是调用方的 data-v,组件自己的 scoped 规则够不着里面的 .side-filter__btn。 -->\n<style>\n/* 与 aside 里同样的纵向堆叠与间距 */\n.side-panel-body {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n/* 抽屉在触屏上用:头部图标按钮放大到可点的尺寸 */\n.side-panel-body .side-filter__btn {\n width: 32px;\n height: 32px;\n}\n</style>\n"],"names":["show","_useModel","__props","_createBlock","_unref","NDrawer","$event","_createVNode","NDrawerContent","_createElementVNode","_hoisted_1","_renderSlot","_ctx"],"mappings":";;;;;;;;;;;;AAWA,UAAMA,IAAOC,EAAoBC,GAAC,MAA0B;2BAI1DC,EAMWC,EAAAC,CAAA,GAAA;AAAA,MANO,MAAML,EAAA;AAAA,8CAAAA,EAAI,QAAAM;AAAA,MAAE,WAAU;AAAA,MAAO,OAAM;AAAA,IAAA;iBACnD,MAImB;AAAA,QAJnBC,EAImBH,EAAAI,CAAA,GAAA;AAAA,UAJA,OAAON,EAAA;AAAA,UAAO,UAAA;AAAA,QAAA;qBAC/B,MAEM;AAAA,YAFNO,EAEM,OAFNC,GAEM;AAAA,cADJC,EAAQC,EAAA,QAAA,SAAA;AAAA,YAAA;;;;;;;;;"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index3.vue.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;"}
|
|
@@ -30,3 +30,13 @@ export type MenuRouteDescriptor = {
|
|
|
30
30
|
* `useAuthMenu` 负责。`viewKeys` 必须来自同一张页面 glob,避免存在性判断与 loader 来源漂移。
|
|
31
31
|
*/
|
|
32
32
|
export declare function describeMenuRoute(node: MenuNode, viewKeys: ReadonlySet<string>): MenuRouteDescriptor | null;
|
|
33
|
+
/**
|
|
34
|
+
* 比对路由路径的口径:补前导斜杠、去末尾斜杠(vue-router 非严格模式下 /a/ 与 /a 是同一条路由)。
|
|
35
|
+
* 末尾斜杠用循环从后往前数:路径来自地址栏,是用户可控的输入,`/\/+$/` 这类正则遇到一长串斜杠会二次方回溯。
|
|
36
|
+
*/
|
|
37
|
+
export declare function normalizeRoutePath(path: string): string;
|
|
38
|
+
/**
|
|
39
|
+
* 这棵菜单树会不会注册出 `path` 这条路由。判据就是 describeMenuRoute 的产出,所以与真正注册时同口径:
|
|
40
|
+
* 目录、外链、没配组件的节点不算。只认整条路径相等 —— 前缀相同不代表是同一个页面。
|
|
41
|
+
*/
|
|
42
|
+
export declare function menuHasPath(tree: MenuNode[], path: string, viewKeys: ReadonlySet<string>): boolean;
|
|
@@ -1,37 +1,53 @@
|
|
|
1
|
-
import { isHttpUrl as
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
import { isHttpUrl as u } from "../utils/url.js";
|
|
2
|
+
import { flattenTree as p } from "../utils/tree.js";
|
|
3
|
+
import { menuTitleKey as c } from "../locales/menuTitle.js";
|
|
4
|
+
import { MenuType as s } from "../types/menu.js";
|
|
5
|
+
function m(t, r) {
|
|
6
|
+
if (t.type !== s.Menu || !t.path || u(t.path)) return null;
|
|
7
|
+
const i = t.path.startsWith("/") ? t.path : `/${t.path}`, n = `menu-${t.id}`, e = t.component;
|
|
8
|
+
if (e && u(e))
|
|
8
9
|
return {
|
|
9
10
|
kind: "iframe",
|
|
10
|
-
path:
|
|
11
|
-
name:
|
|
12
|
-
title:
|
|
11
|
+
path: i,
|
|
12
|
+
name: n,
|
|
13
|
+
title: c(t.title, t.path),
|
|
13
14
|
icon: t.icon,
|
|
14
|
-
iframeSrc:
|
|
15
|
+
iframeSrc: e
|
|
15
16
|
};
|
|
16
|
-
if (!
|
|
17
|
-
const
|
|
18
|
-
return
|
|
17
|
+
if (!e) return null;
|
|
18
|
+
const a = e.replace(/^\/+/, "").replace(/\.vue$/, "");
|
|
19
|
+
return r.has(a) ? {
|
|
19
20
|
kind: "view",
|
|
20
|
-
path:
|
|
21
|
-
name:
|
|
22
|
-
title:
|
|
21
|
+
path: i,
|
|
22
|
+
name: n,
|
|
23
|
+
title: c(t.title, t.path),
|
|
23
24
|
icon: t.icon,
|
|
24
|
-
viewKey:
|
|
25
|
+
viewKey: a
|
|
25
26
|
} : {
|
|
26
27
|
kind: "missing",
|
|
27
|
-
path:
|
|
28
|
-
name:
|
|
29
|
-
title:
|
|
28
|
+
path: i,
|
|
29
|
+
name: n,
|
|
30
|
+
title: c(t.title, t.path),
|
|
30
31
|
icon: t.icon,
|
|
31
|
-
component:
|
|
32
|
+
component: e
|
|
32
33
|
};
|
|
33
34
|
}
|
|
35
|
+
function l(t) {
|
|
36
|
+
const r = t.startsWith("/") ? t : `/${t}`;
|
|
37
|
+
let i = r.length;
|
|
38
|
+
for (; i > 1 && r.charCodeAt(i - 1) === 47; ) i--;
|
|
39
|
+
return r.slice(0, i);
|
|
40
|
+
}
|
|
41
|
+
function y(t, r, i) {
|
|
42
|
+
const n = l(r);
|
|
43
|
+
return p(t).some((e) => {
|
|
44
|
+
const a = m(e, i);
|
|
45
|
+
return a !== null && l(a.path) === n;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
34
48
|
export {
|
|
35
|
-
|
|
49
|
+
m as describeMenuRoute,
|
|
50
|
+
y as menuHasPath,
|
|
51
|
+
l as normalizeRoutePath
|
|
36
52
|
};
|
|
37
53
|
//# sourceMappingURL=authMenuRoute.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"authMenuRoute.js","sources":["../../src/composables/authMenuRoute.ts"],"sourcesContent":["import { isHttpUrl } from '#/utils/url'\nimport { menuTitleKey } from '#/locales/menuTitle'\nimport { MenuType, type MenuNode } from '#/types/menu'\n\n/**\n * 菜单节点的可物化结果。三种变体把 iframe/view/missing 各自所需的数据收窄,调用方无需再次\n * 解释后端的 `component` 字符串。\n */\nexport type MenuRouteDescriptor =\n | { kind: 'iframe'; path: string; name: string; title: string; icon?: string; iframeSrc: string }\n | { kind: 'view'; path: string; name: string; title: string; icon?: string; viewKey: string }\n | { kind: 'missing'; path: string; name: string; title: string; icon?: string; component: string }\n\n/**\n * 只负责菜单路由决策,让分支逻辑能脱离 vue-router 做单测;路由注册与组件物化仍由\n * `useAuthMenu` 负责。`viewKeys` 必须来自同一张页面 glob,避免存在性判断与 loader 来源漂移。\n */\nexport function describeMenuRoute(\n node: MenuNode,\n viewKeys: ReadonlySet<string>,\n): MenuRouteDescriptor | null {\n if (node.type !== MenuType.Menu || !node.path || isHttpUrl(node.path)) return null\n\n const path = node.path.startsWith('/') ? node.path : `/${node.path}`\n const name = `menu-${node.id}`\n const component = node.component\n if (component && isHttpUrl(component)) {\n return {\n kind: 'iframe',\n path,\n name,\n title: menuTitleKey(node.title, node.path),\n icon: node.icon,\n iframeSrc: component,\n }\n }\n if (!component) return null\n\n // 页面表的 key 形如 system/user/index;容忍存量配置带前导斜杠或 .vue 后缀\n const viewKey = component.replace(/^\\/+/, '').replace(/\\.vue$/, '')\n if (viewKeys.has(viewKey))\n return {\n kind: 'view',\n path,\n name,\n title: menuTitleKey(node.title, node.path),\n icon: node.icon,\n viewKey,\n }\n return {\n kind: 'missing',\n path,\n name,\n title: menuTitleKey(node.title, node.path),\n icon: node.icon,\n component,\n }\n}\n"],"names":["describeMenuRoute","node","viewKeys","MenuType","isHttpUrl","path","name","component","menuTitleKey","viewKey"],"mappings":"
|
|
1
|
+
{"version":3,"file":"authMenuRoute.js","sources":["../../src/composables/authMenuRoute.ts"],"sourcesContent":["import { isHttpUrl } from '#/utils/url'\nimport { flattenTree } from '#/utils/tree'\nimport { menuTitleKey } from '#/locales/menuTitle'\nimport { MenuType, type MenuNode } from '#/types/menu'\n\n/**\n * 菜单节点的可物化结果。三种变体把 iframe/view/missing 各自所需的数据收窄,调用方无需再次\n * 解释后端的 `component` 字符串。\n */\nexport type MenuRouteDescriptor =\n | { kind: 'iframe'; path: string; name: string; title: string; icon?: string; iframeSrc: string }\n | { kind: 'view'; path: string; name: string; title: string; icon?: string; viewKey: string }\n | { kind: 'missing'; path: string; name: string; title: string; icon?: string; component: string }\n\n/**\n * 只负责菜单路由决策,让分支逻辑能脱离 vue-router 做单测;路由注册与组件物化仍由\n * `useAuthMenu` 负责。`viewKeys` 必须来自同一张页面 glob,避免存在性判断与 loader 来源漂移。\n */\nexport function describeMenuRoute(\n node: MenuNode,\n viewKeys: ReadonlySet<string>,\n): MenuRouteDescriptor | null {\n if (node.type !== MenuType.Menu || !node.path || isHttpUrl(node.path)) return null\n\n const path = node.path.startsWith('/') ? node.path : `/${node.path}`\n const name = `menu-${node.id}`\n const component = node.component\n if (component && isHttpUrl(component)) {\n return {\n kind: 'iframe',\n path,\n name,\n title: menuTitleKey(node.title, node.path),\n icon: node.icon,\n iframeSrc: component,\n }\n }\n if (!component) return null\n\n // 页面表的 key 形如 system/user/index;容忍存量配置带前导斜杠或 .vue 后缀\n const viewKey = component.replace(/^\\/+/, '').replace(/\\.vue$/, '')\n if (viewKeys.has(viewKey))\n return {\n kind: 'view',\n path,\n name,\n title: menuTitleKey(node.title, node.path),\n icon: node.icon,\n viewKey,\n }\n return {\n kind: 'missing',\n path,\n name,\n title: menuTitleKey(node.title, node.path),\n icon: node.icon,\n component,\n }\n}\n\n/**\n * 比对路由路径的口径:补前导斜杠、去末尾斜杠(vue-router 非严格模式下 /a/ 与 /a 是同一条路由)。\n * 末尾斜杠用循环从后往前数:路径来自地址栏,是用户可控的输入,`/\\/+$/` 这类正则遇到一长串斜杠会二次方回溯。\n */\nexport function normalizeRoutePath(path: string): string {\n const withSlash = path.startsWith('/') ? path : `/${path}`\n let end = withSlash.length\n while (end > 1 && withSlash.charCodeAt(end - 1) === 47 /* '/' */) end--\n return withSlash.slice(0, end)\n}\n\n/**\n * 这棵菜单树会不会注册出 `path` 这条路由。判据就是 describeMenuRoute 的产出,所以与真正注册时同口径:\n * 目录、外链、没配组件的节点不算。只认整条路径相等 —— 前缀相同不代表是同一个页面。\n */\nexport function menuHasPath(\n tree: MenuNode[],\n path: string,\n viewKeys: ReadonlySet<string>,\n): boolean {\n const target = normalizeRoutePath(path)\n return flattenTree(tree).some(node => {\n const route = describeMenuRoute(node, viewKeys)\n return route !== null && normalizeRoutePath(route.path) === target\n })\n}\n"],"names":["describeMenuRoute","node","viewKeys","MenuType","isHttpUrl","path","name","component","menuTitleKey","viewKey","normalizeRoutePath","withSlash","end","menuHasPath","tree","target","flattenTree","route"],"mappings":";;;;AAkBO,SAASA,EACdC,GACAC,GAC4B;AAC5B,MAAID,EAAK,SAASE,EAAS,QAAQ,CAACF,EAAK,QAAQG,EAAUH,EAAK,IAAI,EAAG,QAAO;AAE9E,QAAMI,IAAOJ,EAAK,KAAK,WAAW,GAAG,IAAIA,EAAK,OAAO,IAAIA,EAAK,IAAI,IAC5DK,IAAO,QAAQL,EAAK,EAAE,IACtBM,IAAYN,EAAK;AACvB,MAAIM,KAAaH,EAAUG,CAAS;AAClC,WAAO;AAAA,MACL,MAAM;AAAA,MACN,MAAAF;AAAA,MACA,MAAAC;AAAA,MACA,OAAOE,EAAaP,EAAK,OAAOA,EAAK,IAAI;AAAA,MACzC,MAAMA,EAAK;AAAA,MACX,WAAWM;AAAA,IAAA;AAGf,MAAI,CAACA,EAAW,QAAO;AAGvB,QAAME,IAAUF,EAAU,QAAQ,QAAQ,EAAE,EAAE,QAAQ,UAAU,EAAE;AAClE,SAAIL,EAAS,IAAIO,CAAO,IACf;AAAA,IACL,MAAM;AAAA,IACN,MAAAJ;AAAA,IACA,MAAAC;AAAA,IACA,OAAOE,EAAaP,EAAK,OAAOA,EAAK,IAAI;AAAA,IACzC,MAAMA,EAAK;AAAA,IACX,SAAAQ;AAAA,EAAA,IAEG;AAAA,IACL,MAAM;AAAA,IACN,MAAAJ;AAAA,IACA,MAAAC;AAAA,IACA,OAAOE,EAAaP,EAAK,OAAOA,EAAK,IAAI;AAAA,IACzC,MAAMA,EAAK;AAAA,IACX,WAAAM;AAAA,EAAA;AAEJ;AAMO,SAASG,EAAmBL,GAAsB;AACvD,QAAMM,IAAYN,EAAK,WAAW,GAAG,IAAIA,IAAO,IAAIA,CAAI;AACxD,MAAIO,IAAMD,EAAU;AACpB,SAAOC,IAAM,KAAKD,EAAU,WAAWC,IAAM,CAAC,MAAM,KAAc,CAAAA;AAClE,SAAOD,EAAU,MAAM,GAAGC,CAAG;AAC/B;AAMO,SAASC,EACdC,GACAT,GACAH,GACS;AACT,QAAMa,IAASL,EAAmBL,CAAI;AACtC,SAAOW,EAAYF,CAAI,EAAE,KAAK,CAAAb,MAAQ;AACpC,UAAMgB,IAAQjB,EAAkBC,GAAMC,CAAQ;AAC9C,WAAOe,MAAU,QAAQP,EAAmBO,EAAM,IAAI,MAAMF;AAAA,EAC9D,CAAC;AACH;"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AppModule } from '../types/menu';
|
|
1
2
|
type EnterResult = {
|
|
2
3
|
chooser: true;
|
|
3
4
|
} | {
|
|
@@ -10,7 +11,8 @@ declare function enter(moduleId: number): Promise<EnterResult>;
|
|
|
10
11
|
export declare function useModule(): {
|
|
11
12
|
enter: typeof enter;
|
|
12
13
|
enterInitial: () => Promise<EnterResult>;
|
|
13
|
-
switchModule: (moduleId: number) => Promise<void>;
|
|
14
|
+
switchModule: (moduleId: number, target?: string) => Promise<void>;
|
|
15
|
+
findOwnerModule: (path: string) => Promise<AppModule | null>;
|
|
14
16
|
setDefault: (moduleId: number) => Promise<void>;
|
|
15
17
|
};
|
|
16
18
|
export {};
|
|
@@ -1,25 +1,27 @@
|
|
|
1
|
-
import { useAuthStore as
|
|
2
|
-
import { useTabsStore as
|
|
3
|
-
import { useUserStore as
|
|
4
|
-
import { personalApi as
|
|
5
|
-
import { buildRoutesForModule as
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
import { useAuthStore as g } from "../stores/auth.js";
|
|
2
|
+
import { useTabsStore as y } from "../stores/tabs.js";
|
|
3
|
+
import { useUserStore as d } from "../stores/user.js";
|
|
4
|
+
import { personalApi as i } from "../api/index.js";
|
|
5
|
+
import { buildRoutesForModule as v } from "./useAuthMenu.js";
|
|
6
|
+
import { normalizeRoutePath as c, menuHasPath as S } from "./authMenuRoute.js";
|
|
7
|
+
import { viewComponentPaths as P } from "../router/viewRegistry.js";
|
|
8
|
+
import { router as T } from "../router/index.js";
|
|
9
|
+
let m = null;
|
|
10
|
+
async function l(o) {
|
|
11
|
+
return await v(o), { chooser: !1, moduleId: o };
|
|
10
12
|
}
|
|
11
|
-
function
|
|
12
|
-
const
|
|
13
|
-
function
|
|
14
|
-
return
|
|
15
|
-
|
|
16
|
-
})),
|
|
13
|
+
function z() {
|
|
14
|
+
const o = g();
|
|
15
|
+
function h() {
|
|
16
|
+
return m ?? (m = p().finally(() => {
|
|
17
|
+
m = null;
|
|
18
|
+
})), m;
|
|
17
19
|
}
|
|
18
|
-
async function
|
|
19
|
-
const [{ modules:
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
async function p() {
|
|
21
|
+
const [{ modules: t, defaultModuleId: r }, u, a] = await Promise.all([
|
|
22
|
+
i.modules(),
|
|
23
|
+
i.permissions().then((e) => ({ ok: !0, codes: e })).catch(() => ({ ok: !1, codes: [] })),
|
|
24
|
+
i.profile().then((e) => ({
|
|
23
25
|
sadm: e.isSuperAdmin,
|
|
24
26
|
avatar: e.avatar ?? null,
|
|
25
27
|
orgName: e.orgName ?? null,
|
|
@@ -27,28 +29,38 @@ function b() {
|
|
|
27
29
|
})).catch(() => {
|
|
28
30
|
var e;
|
|
29
31
|
return {
|
|
30
|
-
sadm: ((e =
|
|
32
|
+
sadm: ((e = d().userInfo) == null ? void 0 : e.isSuperAdmin) ?? !1,
|
|
31
33
|
avatar: null,
|
|
32
34
|
orgName: null,
|
|
33
35
|
phoneTail: null
|
|
34
36
|
};
|
|
35
37
|
})
|
|
36
38
|
]);
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
if (
|
|
40
|
-
const
|
|
41
|
-
return
|
|
39
|
+
o.modules = t, o.defaultModuleId = r ?? null, o.permissionCodes = u.codes, o.permissionsLoaded = u.ok, o.isSuperAdmin = a.sadm;
|
|
40
|
+
const s = d();
|
|
41
|
+
if (s.userInfo && (s.userInfo.avatar = a.avatar, s.userInfo.orgName = a.orgName, s.userInfo.phoneTail = a.phoneTail), t.length === 0) return { chooser: !0 };
|
|
42
|
+
const f = o.currentModuleId;
|
|
43
|
+
return f && t.some((e) => e.id === f) ? l(f) : t.length === 1 ? l(t[0].id) : r && t.some((e) => e.id === r) ? l(r) : { chooser: !0 };
|
|
42
44
|
}
|
|
43
|
-
async function
|
|
44
|
-
await
|
|
45
|
+
async function I(t, r) {
|
|
46
|
+
await l(t), y().clearTabs(), T.replace(r ?? o.homePath);
|
|
45
47
|
}
|
|
46
|
-
async function
|
|
47
|
-
|
|
48
|
+
async function w(t) {
|
|
49
|
+
const r = c(t), u = o.modules.filter((n) => n.id !== o.currentModuleId), a = u.find(
|
|
50
|
+
(n) => n.defaultRoute && c(n.defaultRoute) === r
|
|
51
|
+
);
|
|
52
|
+
if (a) return a;
|
|
53
|
+
const s = new Set(P()), e = (await Promise.all(
|
|
54
|
+
u.map((n) => i.menu(n.id).catch(() => []))
|
|
55
|
+
)).findIndex((n) => S(n, r, s));
|
|
56
|
+
return e < 0 ? null : u[e];
|
|
48
57
|
}
|
|
49
|
-
|
|
58
|
+
async function M(t) {
|
|
59
|
+
await i.setDefaultModule(t), o.defaultModuleId = t;
|
|
60
|
+
}
|
|
61
|
+
return { enter: l, enterInitial: h, switchModule: I, findOwnerModule: w, setDefault: M };
|
|
50
62
|
}
|
|
51
63
|
export {
|
|
52
|
-
|
|
64
|
+
z as useModule
|
|
53
65
|
};
|
|
54
66
|
//# sourceMappingURL=useModule.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useModule.js","sources":["../../src/composables/useModule.ts"],"sourcesContent":["// 多应用门户的进门/切换流程:登录后该直接进哪个应用、还是弹应用选择器,以及切换时清理旧应用的路由与标签。\n// enterInitial() 也是 F5/深链的恢复入口(动态路由只在内存里),路由守卫会调它。\n// 坑:权限码拉取失败时 permissionsLoaded 保持 false,v-auth 走 fail-closed 藏按钮 —— 宁可少显示,不谎报有权限。\nimport { useAuthStore } from '#/stores/auth'\nimport { useTabsStore } from '#/stores/tabs'\nimport { useUserStore } from '#/stores/user'\nimport { personalApi } from '#/api'\nimport { buildRoutesForModule } from './useAuthMenu'\nimport { router } from '#/router'\n\ntype EnterResult = { chooser: true } | { chooser: false; moduleId: number }\n\n// 进门单飞:F5/深链时守卫可能被并发导航各调一次 enterInitial,而 buildRoutesForModule 起手就 resetRouter()\n// —— 后一次会把前一次刚注册的动态路由整片摘掉,表现是偶发 404/白屏。模块级 promise 让并发合流成一次。\nlet entering: Promise<EnterResult> | null = null\n\n/** 进指定应用:重建它的动态路由。 */\nasync function enter(moduleId: number): Promise<EnterResult> {\n await buildRoutesForModule(moduleId)\n return { chooser: false, moduleId }\n}\n\n/** 门户:登录后决定直接进 / 进默认 / 弹选择器,以及切换应用。逻辑与 Naive 无关。 */\nexport function useModule() {\n const auth = useAuthStore()\n\n function enterInitial(): Promise<EnterResult> {\n entering ??= doEnterInitial().finally(() => {\n entering = null\n })\n return entering\n }\n\n async function doEnterInitial(): Promise<EnterResult> {\n // 并行拉模块 + 当前用户权限码。权限码喂 v-auth:成功(哪怕空集=超管)才标 loaded;\n // 失败不阻断进门户,但 permissionsLoaded 保持 false → v-auth fail-closed(藏按钮),不谎报\"有权限\"。\n // profile 拿超管标记喂 v-auth(只对超管 fail-open,普通用户空集则隐藏)+ 顺手回填顶栏头像;失败按普通用户处理(安全侧,不误放行)。\n const [{ modules, defaultModuleId }, perm, profile] = await Promise.all([\n personalApi.modules(),\n personalApi\n .permissions()\n .then(codes => ({ ok: true, codes }))\n .catch(() => ({ ok: false, codes: [] as string[] })),\n personalApi\n .profile()\n .then(p => ({\n sadm: p.isSuperAdmin,\n avatar: p.avatar ?? null,\n orgName: p.orgName ?? null,\n phoneTail: p.phone ? p.phone.slice(-4) : null,\n }))\n .catch(() => ({\n sadm: useUserStore().userInfo?.isSuperAdmin ?? false,\n avatar: null,\n orgName: null,\n phoneTail: null,\n })),\n ])\n auth.modules = modules\n auth.defaultModuleId = defaultModuleId ?? null\n auth.permissionCodes = perm.codes\n auth.permissionsLoaded = perm.ok\n auth.isSuperAdmin = profile.sadm\n // 顶栏头像:登录出参不含 avatar,这里回填(取不到按无头像处理,顶栏回落图标)\n const user = useUserStore()\n if (user.userInfo) {\n user.userInfo.avatar = profile.avatar\n user.userInfo.orgName = profile.orgName // 水印要印机构和手机尾号,同样只有 profile 里有\n user.userInfo.phoneTail = profile.phoneTail\n }\n if (modules.length === 0) return { chooser: true } // 空态:选择器里提示未分配应用\n // F5/深链优先重建\"上次所在应用\"(持久化的 currentModuleId),让其动态路由复活,跨应用深链不落 404。\n const remembered = auth.currentModuleId\n if (remembered && modules.some(m => m.id === remembered)) return enter(remembered)\n if (modules.length === 1) return enter(modules[0]!.id)\n if (defaultModuleId && modules.some(m => m.id === defaultModuleId))\n return enter(defaultModuleId)\n return { chooser: true }\n }\n\n async function switchModule(moduleId: number): Promise<void> {\n await enter(moduleId)\n useTabsStore().clearTabs() // 切应用 → 标签归零(新应用路由已重建)\n router.replace(auth.homePath) //
|
|
1
|
+
{"version":3,"file":"useModule.js","sources":["../../src/composables/useModule.ts"],"sourcesContent":["// 多应用门户的进门/切换流程:登录后该直接进哪个应用、还是弹应用选择器,以及切换时清理旧应用的路由与标签。\n// enterInitial() 也是 F5/深链的恢复入口(动态路由只在内存里),路由守卫会调它。\n// 坑:权限码拉取失败时 permissionsLoaded 保持 false,v-auth 走 fail-closed 藏按钮 —— 宁可少显示,不谎报有权限。\nimport { useAuthStore } from '#/stores/auth'\nimport { useTabsStore } from '#/stores/tabs'\nimport { useUserStore } from '#/stores/user'\nimport { personalApi } from '#/api'\nimport { buildRoutesForModule } from './useAuthMenu'\nimport { menuHasPath, normalizeRoutePath } from './authMenuRoute'\nimport { viewComponentPaths } from '#/router/viewRegistry'\nimport { router } from '#/router'\nimport type { AppModule, MenuNode } from '#/types/menu'\n\ntype EnterResult = { chooser: true } | { chooser: false; moduleId: number }\n\n// 进门单飞:F5/深链时守卫可能被并发导航各调一次 enterInitial,而 buildRoutesForModule 起手就 resetRouter()\n// —— 后一次会把前一次刚注册的动态路由整片摘掉,表现是偶发 404/白屏。模块级 promise 让并发合流成一次。\nlet entering: Promise<EnterResult> | null = null\n\n/** 进指定应用:重建它的动态路由。 */\nasync function enter(moduleId: number): Promise<EnterResult> {\n await buildRoutesForModule(moduleId)\n return { chooser: false, moduleId }\n}\n\n/** 门户:登录后决定直接进 / 进默认 / 弹选择器,以及切换应用。逻辑与 Naive 无关。 */\nexport function useModule() {\n const auth = useAuthStore()\n\n function enterInitial(): Promise<EnterResult> {\n entering ??= doEnterInitial().finally(() => {\n entering = null\n })\n return entering\n }\n\n async function doEnterInitial(): Promise<EnterResult> {\n // 并行拉模块 + 当前用户权限码。权限码喂 v-auth:成功(哪怕空集=超管)才标 loaded;\n // 失败不阻断进门户,但 permissionsLoaded 保持 false → v-auth fail-closed(藏按钮),不谎报\"有权限\"。\n // profile 拿超管标记喂 v-auth(只对超管 fail-open,普通用户空集则隐藏)+ 顺手回填顶栏头像;失败按普通用户处理(安全侧,不误放行)。\n const [{ modules, defaultModuleId }, perm, profile] = await Promise.all([\n personalApi.modules(),\n personalApi\n .permissions()\n .then(codes => ({ ok: true, codes }))\n .catch(() => ({ ok: false, codes: [] as string[] })),\n personalApi\n .profile()\n .then(p => ({\n sadm: p.isSuperAdmin,\n avatar: p.avatar ?? null,\n orgName: p.orgName ?? null,\n phoneTail: p.phone ? p.phone.slice(-4) : null,\n }))\n .catch(() => ({\n sadm: useUserStore().userInfo?.isSuperAdmin ?? false,\n avatar: null,\n orgName: null,\n phoneTail: null,\n })),\n ])\n auth.modules = modules\n auth.defaultModuleId = defaultModuleId ?? null\n auth.permissionCodes = perm.codes\n auth.permissionsLoaded = perm.ok\n auth.isSuperAdmin = profile.sadm\n // 顶栏头像:登录出参不含 avatar,这里回填(取不到按无头像处理,顶栏回落图标)\n const user = useUserStore()\n if (user.userInfo) {\n user.userInfo.avatar = profile.avatar\n user.userInfo.orgName = profile.orgName // 水印要印机构和手机尾号,同样只有 profile 里有\n user.userInfo.phoneTail = profile.phoneTail\n }\n if (modules.length === 0) return { chooser: true } // 空态:选择器里提示未分配应用\n // F5/深链优先重建\"上次所在应用\"(持久化的 currentModuleId),让其动态路由复活,跨应用深链不落 404。\n const remembered = auth.currentModuleId\n if (remembered && modules.some(m => m.id === remembered)) return enter(remembered)\n if (modules.length === 1) return enter(modules[0]!.id)\n if (defaultModuleId && modules.some(m => m.id === defaultModuleId))\n return enter(defaultModuleId)\n return { chooser: true }\n }\n\n /** `target` 缺省落新应用自己的首页;给了就落到那个地址(跨应用深链:先切过去再打开它)。 */\n async function switchModule(moduleId: number, target?: string): Promise<void> {\n await enter(moduleId)\n useTabsStore().clearTabs() // 切应用 → 标签归零(新应用路由已重建)\n router.replace(target ?? auth.homePath)\n }\n\n /**\n * 用户有权进入的其它应用里,哪个注册了 `path` 这个页面;没有则 null。\n * 动态路由只按当前应用的菜单注册,别的应用的页面地址会落到通配 404 —— 404 页靠它把\n * 「地址不存在」和「页面属于别的应用」分开。只在命中 404 时才调,所以不预拉各应用的菜单。\n * 某个应用的菜单拉取失败按「没有」处理:这是个提示,不该因它再报一层错。\n */\n async function findOwnerModule(path: string): Promise<AppModule | null> {\n const target = normalizeRoutePath(path)\n const others = auth.modules.filter(m => m.id !== auth.currentModuleId)\n // defaultRoute 是现成的索引,命中就不必拉菜单\n const byDefault = others.find(\n m => m.defaultRoute && normalizeRoutePath(m.defaultRoute) === target,\n )\n if (byDefault) return byDefault\n\n const viewKeys = new Set(viewComponentPaths())\n const trees = await Promise.all(\n others.map(m => personalApi.menu(m.id).catch(() => [] as MenuNode[])),\n )\n const hit = trees.findIndex(tree => menuHasPath(tree, target, viewKeys))\n return hit < 0 ? null : others[hit]!\n }\n\n async function setDefault(moduleId: number): Promise<void> {\n await personalApi.setDefaultModule(moduleId)\n auth.defaultModuleId = moduleId // 本地同步,选择页角标立刻转移,不必重拉 /personal/modules\n }\n\n return { enter, enterInitial, switchModule, findOwnerModule, setDefault }\n}\n"],"names":["entering","enter","moduleId","buildRoutesForModule","useModule","auth","useAuthStore","enterInitial","doEnterInitial","modules","defaultModuleId","perm","profile","personalApi","codes","p","_a","useUserStore","user","remembered","m","switchModule","target","useTabsStore","router","findOwnerModule","path","normalizeRoutePath","others","byDefault","viewKeys","viewComponentPaths","hit","tree","menuHasPath","setDefault"],"mappings":"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"}
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import { inject as v, computed as c, ref as h, provide as w, watch as
|
|
2
|
-
import { useWindowSize as
|
|
3
|
-
const
|
|
1
|
+
import { inject as v, computed as c, ref as h, provide as w, watch as S, onScopeDispose as _ } from "vue";
|
|
2
|
+
import { useWindowSize as m } from "@vueuse/core";
|
|
3
|
+
const E = 600, l = 1400, A = 1e3;
|
|
4
4
|
function u(t) {
|
|
5
|
-
return t >= l ? "wide" : t >=
|
|
5
|
+
return t >= l ? "wide" : t >= E ? "mid" : "narrow";
|
|
6
6
|
}
|
|
7
7
|
const p = Symbol("shellBreakpoint");
|
|
8
|
-
function
|
|
8
|
+
function k(t) {
|
|
9
9
|
const n = h(l), d = c(() => u(n.value)), o = { width: n, bp: d };
|
|
10
10
|
w(p, o);
|
|
11
11
|
let e;
|
|
12
|
-
return
|
|
12
|
+
return S(
|
|
13
13
|
t,
|
|
14
|
-
(i,
|
|
14
|
+
(i, L, f) => {
|
|
15
15
|
!i || typeof ResizeObserver > "u" || (e = new ResizeObserver((a) => {
|
|
16
16
|
var s;
|
|
17
17
|
const r = (s = a[0]) == null ? void 0 : s.contentRect.width;
|
|
@@ -19,19 +19,20 @@ function E(t) {
|
|
|
19
19
|
}), e.observe(i), f(() => e == null ? void 0 : e.disconnect()));
|
|
20
20
|
},
|
|
21
21
|
{ flush: "post" }
|
|
22
|
-
),
|
|
22
|
+
), _(() => e == null ? void 0 : e.disconnect()), o;
|
|
23
23
|
}
|
|
24
|
-
function
|
|
24
|
+
function B() {
|
|
25
25
|
const t = v(p, null);
|
|
26
26
|
if (t) return t;
|
|
27
|
-
const { width: n } =
|
|
27
|
+
const { width: n } = m();
|
|
28
28
|
return { width: n, bp: c(() => u(n.value)) };
|
|
29
29
|
}
|
|
30
30
|
export {
|
|
31
|
-
|
|
31
|
+
E as SHELL_NARROW_MAX,
|
|
32
|
+
A as SHELL_PANEL_DRAWER_MAX,
|
|
32
33
|
l as SHELL_WIDE_MIN,
|
|
33
|
-
|
|
34
|
+
k as provideShellBreakpoint,
|
|
34
35
|
u as shellBreakpointOf,
|
|
35
|
-
|
|
36
|
+
B as useShellBreakpoint
|
|
36
37
|
};
|
|
37
38
|
//# sourceMappingURL=useShellBreakpoint.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useShellBreakpoint.js","sources":["../../src/composables/useShellBreakpoint.ts"],"sourcesContent":["// 壳层档位:按「内容区」宽度判(不是视口),侧栏展开 / 收起、抽屉开合都会改变档位。\n//\n// wide ≥ 1400 统计卡等页面栅格铺开\n// mid 600–1399 栅格收窄;带左侧面板的页面在内容区 < 1000
|
|
1
|
+
{"version":3,"file":"useShellBreakpoint.js","sources":["../../src/composables/useShellBreakpoint.ts"],"sourcesContent":["// 壳层档位:按「内容区」宽度判(不是视口),侧栏展开 / 收起、抽屉开合都会改变档位。\n//\n// wide ≥ 1400 统计卡等页面栅格铺开\n// mid 600–1399 栅格收窄;带左侧面板的页面在内容区 < 1000 时面板收进抽屉(useSidePanel + SidePanelDrawer)\n// narrow < 600 整页自然滚动,不锁一屏高度;统计卡两列;设置行上下排\n//\n// 阈值 600 = smart-naive-table 的窄档(表格根节点宽 < 600 换卡片)。壳层 narrow 必须和它对齐:\n// 壳层进入「整页自然滚动」时表格一定已是卡片列表;壳层不是 narrow 时表格一定仍是表格。\n// 两层阈值错开会出现一段宽度:页面不定高、表格却还在 fill-height,表体塌成最小高度。\n// 这条对齐只对开了 card-on-narrow 的表成立;没开的表(表格形态的 fill-height 表)由 layout.css ①′ 让那一页保持定高兜底。\n// 阈值直接取 600:量的是 `.page` 的 content box(ResizeObserver 的 contentRect 本身已扣掉内边距),\n// 留白不进数字,日后改 `.page` 的内边距不用回来改阈值。\n//\n// 用法:default.vue 调 provideShellBreakpoint(el) 绑定被量的元素;页面里调 useShellBreakpoint()\n// 取 { bp, width }(没有壳层时,如单测或独立挂载,退回按视口宽度估算)。\nimport {\n computed,\n inject,\n onScopeDispose,\n provide,\n ref,\n watch,\n type InjectionKey,\n type Ref,\n} from 'vue'\nimport { useWindowSize } from '@vueuse/core'\n\nexport type ShellBreakpoint = 'wide' | 'mid' | 'narrow'\n\n/** 壳层 narrow 的上界(不含)。与表格窄档 600 对齐,见文件头。 */\nexport const SHELL_NARROW_MAX = 600\n/** 壳层 wide 的下界(含)。 */\nexport const SHELL_WIDE_MIN = 1400\n/** 带左侧面板的页面:内容区窄于此值时面板收进抽屉。 */\nexport const SHELL_PANEL_DRAWER_MAX = 1000\n\n/** 纯判定,便于单测。 */\nexport function shellBreakpointOf(width: number): ShellBreakpoint {\n return width >= SHELL_WIDE_MIN ? 'wide' : width >= SHELL_NARROW_MAX ? 'mid' : 'narrow'\n}\n\nexport interface ShellBreakpointState {\n /** 内容区(`.page` content box)宽度,px */\n width: Ref<number>\n bp: Readonly<Ref<ShellBreakpoint>>\n}\n\nconst KEY: InjectionKey<ShellBreakpointState> = Symbol('shellBreakpoint')\n\n/** 壳层调用:盯住 el 的 content box,把 { width, bp } 提供给后代。 */\nexport function provideShellBreakpoint(el: Ref<HTMLElement | undefined>): ShellBreakpointState {\n const width = ref(SHELL_WIDE_MIN) // 首帧按宽档,避免挂载瞬间闪一下 narrow 的自然滚动布局\n const bp = computed(() => shellBreakpointOf(width.value))\n const state: ShellBreakpointState = { width, bp }\n provide(KEY, state)\n\n let ro: ResizeObserver | undefined\n watch(\n el,\n (node, _old, onCleanup) => {\n if (!node || typeof ResizeObserver === 'undefined') return\n ro = new ResizeObserver(entries => {\n const w = entries[0]?.contentRect.width\n if (w !== undefined) width.value = w\n })\n ro.observe(node)\n onCleanup(() => ro?.disconnect())\n },\n { flush: 'post' },\n )\n onScopeDispose(() => ro?.disconnect())\n return state\n}\n\n/** 页面 / 组件调用:取壳层档位。壳层之外退回视口宽度(近似,够单测与独立挂载用)。 */\nexport function useShellBreakpoint(): ShellBreakpointState {\n const injected = inject(KEY, null)\n if (injected) return injected\n const { width } = useWindowSize()\n return { width, bp: computed(() => shellBreakpointOf(width.value)) }\n}\n"],"names":["SHELL_NARROW_MAX","SHELL_WIDE_MIN","SHELL_PANEL_DRAWER_MAX","shellBreakpointOf","width","KEY","provideShellBreakpoint","el","ref","bp","computed","state","provide","ro","watch","node","_old","onCleanup","entries","w","_a","onScopeDispose","useShellBreakpoint","injected","inject","useWindowSize"],"mappings":";;AA8BO,MAAMA,IAAmB,KAEnBC,IAAiB,MAEjBC,IAAyB;AAG/B,SAASC,EAAkBC,GAAgC;AAChE,SAAOA,KAASH,IAAiB,SAASG,KAASJ,IAAmB,QAAQ;AAChF;AAQA,MAAMK,IAA0C,OAAO,iBAAiB;AAGjE,SAASC,EAAuBC,GAAwD;AAC7F,QAAMH,IAAQI,EAAIP,CAAc,GAC1BQ,IAAKC,EAAS,MAAMP,EAAkBC,EAAM,KAAK,CAAC,GAClDO,IAA8B,EAAE,OAAAP,GAAO,IAAAK,EAAA;AAC7C,EAAAG,EAAQP,GAAKM,CAAK;AAElB,MAAIE;AACJ,SAAAC;AAAA,IACEP;AAAA,IACA,CAACQ,GAAMC,GAAMC,MAAc;AACzB,MAAI,CAACF,KAAQ,OAAO,iBAAmB,QACvCF,IAAK,IAAI,eAAe,CAAAK,MAAW;;AACjC,cAAMC,KAAIC,IAAAF,EAAQ,CAAC,MAAT,gBAAAE,EAAY,YAAY;AAClC,QAAID,MAAM,WAAWf,EAAM,QAAQe;AAAA,MACrC,CAAC,GACDN,EAAG,QAAQE,CAAI,GACfE,EAAU,MAAMJ,KAAA,gBAAAA,EAAI,YAAY;AAAA,IAClC;AAAA,IACA,EAAE,OAAO,OAAA;AAAA,EAAO,GAElBQ,EAAe,MAAMR,KAAA,gBAAAA,EAAI,YAAY,GAC9BF;AACT;AAGO,SAASW,IAA2C;AACzD,QAAMC,IAAWC,EAAOnB,GAAK,IAAI;AACjC,MAAIkB,EAAU,QAAOA;AACrB,QAAM,EAAE,OAAAnB,EAAA,IAAUqB,EAAA;AAClB,SAAO,EAAE,OAAArB,GAAO,IAAIM,EAAS,MAAMP,EAAkBC,EAAM,KAAK,CAAC,EAAA;AACnE;"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ComputedRef, Ref } from 'vue';
|
|
2
|
+
export interface SidePanelState {
|
|
3
|
+
/** 内容区宽度 < 阈值:面板该收进抽屉。 */
|
|
4
|
+
compact: ComputedRef<boolean>;
|
|
5
|
+
/** 抽屉开合;宽度回到不紧凑时自动关,别留一个对不上布局的遮罩。 */
|
|
6
|
+
drawerOpen: Ref<boolean>;
|
|
7
|
+
}
|
|
8
|
+
/** @param max 内容区窄于它时面板收进抽屉;默认 `SHELL_PANEL_DRAWER_MAX`(1000)。 */
|
|
9
|
+
export declare function useSidePanel(max?: number): SidePanelState;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { computed as n, ref as c, watch as f } from "vue";
|
|
2
|
+
import { SHELL_PANEL_DRAWER_MAX as i, useShellBreakpoint as l } from "./useShellBreakpoint.js";
|
|
3
|
+
function u(o = i) {
|
|
4
|
+
const { width: r } = l(), e = n(() => r.value < o), t = c(!1);
|
|
5
|
+
return f(e, (a) => {
|
|
6
|
+
a || (t.value = !1);
|
|
7
|
+
}), { compact: e, drawerOpen: t };
|
|
8
|
+
}
|
|
9
|
+
export {
|
|
10
|
+
u as useSidePanel
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=useSidePanel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useSidePanel.js","sources":["../../src/composables/useSidePanel.ts"],"sourcesContent":["// 带左侧面板(分组 / 机构树…)的页面:内容区窄于阈值时,面板不再占一列,收进左侧抽屉。\n//\n// 判据是壳层内容区宽度(`useShellBreakpoint().width`,不是视口):侧栏展开 / 收起、抽屉开合都会改变它。\n// 面板占掉的宽度本来就会让右侧表格变窄,收进抽屉后表格得到整行宽度;表格自己的窄档(卡片列表)由库按表格宽度另行判定。\n//\n// 用法(范例 views/system/user):\n// const { compact, drawerOpen } = useSidePanel()\n// <aside v-if=\"!compact\" class=\"side-filter\">…面板…</aside>\n// <SmartTable> … #toolbar 里放一个按钮:`@click=\"drawerOpen = true\"` …</SmartTable>\n// <SidePanelDrawer v-model:show=\"drawerOpen\" :title=\"…\">…同一份面板内容…</SidePanelDrawer>\n// 面板内容只写一份用 VueUse 的 createReusableTemplate,宽屏放进 aside、窄屏放进抽屉。\nimport { computed, ref, watch, type ComputedRef, type Ref } from 'vue'\nimport { SHELL_PANEL_DRAWER_MAX, useShellBreakpoint } from './useShellBreakpoint'\n\nexport interface SidePanelState {\n /** 内容区宽度 < 阈值:面板该收进抽屉。 */\n compact: ComputedRef<boolean>\n /** 抽屉开合;宽度回到不紧凑时自动关,别留一个对不上布局的遮罩。 */\n drawerOpen: Ref<boolean>\n}\n\n/** @param max 内容区窄于它时面板收进抽屉;默认 `SHELL_PANEL_DRAWER_MAX`(1000)。 */\nexport function useSidePanel(max: number = SHELL_PANEL_DRAWER_MAX): SidePanelState {\n const { width } = useShellBreakpoint()\n const compact = computed(() => width.value < max)\n const drawerOpen = ref(false)\n watch(compact, isCompact => {\n if (!isCompact) drawerOpen.value = false\n })\n return { compact, drawerOpen }\n}\n"],"names":["useSidePanel","max","SHELL_PANEL_DRAWER_MAX","width","useShellBreakpoint","compact","computed","drawerOpen","ref","watch","isCompact"],"mappings":";;AAsBO,SAASA,EAAaC,IAAcC,GAAwC;AACjF,QAAM,EAAE,OAAAC,EAAA,IAAUC,EAAA,GACZC,IAAUC,EAAS,MAAMH,EAAM,QAAQF,CAAG,GAC1CM,IAAaC,EAAI,EAAK;AAC5B,SAAAC,EAAMJ,GAAS,CAAAK,MAAa;AAC1B,IAAKA,MAAWH,EAAW,QAAQ;AAAA,EACrC,CAAC,GACM,EAAE,SAAAF,GAAS,YAAAE,EAAA;AACpB;"}
|
package/dist/index.d.ts
CHANGED
|
@@ -38,6 +38,7 @@ export { useTableZoom } from './composables/useTableZoom';
|
|
|
38
38
|
export { useTabTitle } from './composables/useTabTitle';
|
|
39
39
|
export { useTableTitle } from './composables/useTableTitle';
|
|
40
40
|
export { useShellBreakpoint } from './composables/useShellBreakpoint';
|
|
41
|
+
export { useSidePanel, type SidePanelState } from './composables/useSidePanel';
|
|
41
42
|
export { useTheme } from './composables/useTheme';
|
|
42
43
|
export { useVisualViewportHeight } from './composables/useVisualViewportHeight';
|
|
43
44
|
export { default as ApiSelect } from './components/ApiSelect/index.vue';
|
|
@@ -64,6 +65,7 @@ export { default as MarkdownView } from './components/MarkdownEditor/MarkdownVie
|
|
|
64
65
|
export { default as OrgTreeSelect } from './components/OrgTreeSelect/index.vue';
|
|
65
66
|
export { default as PasswordStrength } from './components/PasswordStrength/index.vue';
|
|
66
67
|
export { default as RoleSelect } from './components/RoleSelect/index.vue';
|
|
68
|
+
export { default as SidePanelDrawer } from './components/SidePanelDrawer/index.vue';
|
|
67
69
|
export { default as StatusSwitch } from './components/StatusSwitch/index.vue';
|
|
68
70
|
export { default as TableTotal } from './components/TableTotal/index.vue';
|
|
69
71
|
export { default as TableZoomButton } from './components/TableZoomButton/index.vue';
|