@mindbase/vue3-app-shell 1.0.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/store/pwa.ts ADDED
@@ -0,0 +1,141 @@
1
+ import { defineStore } from 'pinia'
2
+ import { ref, computed } from 'vue'
3
+ import { getStorage, setStorage } from '@mindbase/vue3-kit/utils/storage'
4
+ import type { PWAInstallState, PWAInstallConfig } from '../types'
5
+
6
+ const STORAGE_KEY = 'pwa_install_dont_show_again'
7
+ const DEFAULT_CONFIG: PWAInstallConfig = {
8
+ title: '安装应用',
9
+ description: '添加到主屏幕,获得更好的体验',
10
+ installButtonText: '安装',
11
+ cancelButtonText: '暂不安装',
12
+ dontShowAgainText: '不再提示',
13
+ autoShow: true
14
+ }
15
+
16
+ /**
17
+ * App Shell PWA 状态管理
18
+ */
19
+ export const useAppShellPWAStore = defineStore('appShellPWA', () => {
20
+ // 配置
21
+ const config = ref<PWAInstallConfig>(DEFAULT_CONFIG)
22
+
23
+ // 状态
24
+ const canInstall = ref(false)
25
+ const isInstalled = ref(false)
26
+ const promptVisible = ref(false)
27
+ const deferredPrompt = ref<Event | null>(null)
28
+
29
+ // "不再提示"状态(从 storage 读取)
30
+ const dontShowAgain = ref(getStorage<boolean>(STORAGE_KEY) ?? false)
31
+
32
+ /**
33
+ * 设置 PWA 配置
34
+ */
35
+ function setConfig(newConfig: Partial<PWAInstallConfig>) {
36
+ config.value = { ...DEFAULT_CONFIG, ...newConfig }
37
+ }
38
+
39
+ /**
40
+ * 设置可安装状态
41
+ */
42
+ function setCanInstall(value: boolean) {
43
+ canInstall.value = value
44
+ }
45
+
46
+ /**
47
+ * 设置已安装状态
48
+ */
49
+ function setIsInstalled(value: boolean) {
50
+ isInstalled.value = value
51
+ }
52
+
53
+ /**
54
+ * 保存延迟的安装事件
55
+ */
56
+ function setDeferredPrompt(event: Event | null) {
57
+ deferredPrompt.value = event
58
+ }
59
+
60
+ /**
61
+ * 显示安装提示
62
+ */
63
+ function showPrompt() {
64
+ if (canInstall.value && !isInstalled.value && !dontShowAgain.value) {
65
+ promptVisible.value = true
66
+ }
67
+ }
68
+
69
+ /**
70
+ * 隐藏安装提示
71
+ */
72
+ function hidePrompt() {
73
+ promptVisible.value = false
74
+ }
75
+
76
+ /**
77
+ * 设置"不再提示"
78
+ */
79
+ function setDontShowAgain(value: boolean) {
80
+ dontShowAgain.value = value
81
+ setStorage(STORAGE_KEY, value)
82
+ if (value) {
83
+ hidePrompt()
84
+ }
85
+ }
86
+
87
+ /**
88
+ * 触发安装
89
+ */
90
+ async function promptInstall() {
91
+ const promptEvent = deferredPrompt.value as any
92
+ if (!promptEvent) {
93
+ console.warn('No deferred prompt event available')
94
+ return
95
+ }
96
+
97
+ // 显示原生安装提示
98
+ promptEvent.prompt()
99
+
100
+ // 等待用户响应
101
+ const { outcome } = await promptEvent.userChoice
102
+
103
+ if (outcome === 'accepted') {
104
+ setIsInstalled(true)
105
+ }
106
+
107
+ // 清除延迟的事件
108
+ setDeferredPrompt(null)
109
+ hidePrompt()
110
+ }
111
+
112
+ // 计算属性:是否应该显示提示
113
+ const shouldShowPrompt = computed(() => {
114
+ return (
115
+ config.value.autoShow &&
116
+ canInstall.value &&
117
+ !isInstalled.value &&
118
+ !dontShowAgain.value
119
+ )
120
+ })
121
+
122
+ return {
123
+ config,
124
+ canInstall,
125
+ isInstalled,
126
+ dontShowAgain,
127
+ promptVisible,
128
+ deferredPrompt,
129
+ shouldShowPrompt,
130
+ setConfig,
131
+ setCanInstall,
132
+ setIsInstalled,
133
+ setDeferredPrompt,
134
+ showPrompt,
135
+ hidePrompt,
136
+ setDontShowAgain,
137
+ promptInstall
138
+ }
139
+ })
140
+
141
+ export type AppShellPWAStore = ReturnType<typeof useAppShellPWAStore>
package/store/slots.ts ADDED
@@ -0,0 +1,68 @@
1
+ import { defineStore } from 'pinia'
2
+ import { reactive, computed } from 'vue'
3
+ import type { Component } from 'vue'
4
+ import { AppShellSlotLocation, type AppShellSlotRegistration } from '../types'
5
+
6
+ /**
7
+ * App Shell 插槽状态管理
8
+ */
9
+ export const useAppShellSlotsStore = defineStore('appShellSlots', () => {
10
+ const slots = reactive<Record<string, AppShellSlotRegistration[]>>({})
11
+
12
+ /**
13
+ * 注册插槽
14
+ */
15
+ function registerSlot(registration: AppShellSlotRegistration) {
16
+ const { location, component } = registration
17
+
18
+ if (!slots[location]) {
19
+ slots[location] = []
20
+ }
21
+
22
+ // 检查是否已存在相同的组件
23
+ const isDuplicate = slots[location].some((slot) => slot.component === component)
24
+ if (isDuplicate) {
25
+ console.warn(`Component already registered for location: ${location}`)
26
+ return
27
+ }
28
+
29
+ slots[location].push(registration)
30
+ }
31
+
32
+ /**
33
+ * 清空插槽
34
+ */
35
+ function clearSlots(location?: string) {
36
+ if (location) {
37
+ slots[location] = []
38
+ } else {
39
+ Object.keys(slots).forEach((key) => {
40
+ slots[key] = []
41
+ })
42
+ }
43
+ }
44
+
45
+ // 为每个位置创建计算属性
46
+ const headerLeft = computed(() => slots[AppShellSlotLocation.HEADER_LEFT] || [])
47
+ const headerCenter = computed(() => slots[AppShellSlotLocation.HEADER_CENTER] || [])
48
+ const headerRight = computed(() => slots[AppShellSlotLocation.HEADER_RIGHT] || [])
49
+ const sidebar = computed(() => slots[AppShellSlotLocation.SIDEBAR] || [])
50
+ const contentTop = computed(() => slots[AppShellSlotLocation.CONTENT_TOP] || [])
51
+ const contentBottom = computed(() => slots[AppShellSlotLocation.CONTENT_BOTTOM] || [])
52
+
53
+ return {
54
+ slots,
55
+ // 计算属性
56
+ headerLeft,
57
+ headerCenter,
58
+ headerRight,
59
+ sidebar,
60
+ contentTop,
61
+ contentBottom,
62
+ // 方法
63
+ registerSlot,
64
+ clearSlots
65
+ }
66
+ })
67
+
68
+ export type AppShellSlotsStore = ReturnType<typeof useAppShellSlotsStore>
@@ -0,0 +1,62 @@
1
+ // 抽屉样式
2
+ @import './variables.scss';
3
+
4
+ // PC 端抽屉(固定侧边栏)
5
+ .app-drawer--pc {
6
+ transition: width var(--mb-app-shell-transition-duration, 0.3s) var(--mb-app-shell-transition-function, ease);
7
+
8
+ &.is-collapsed {
9
+ // 折叠状态下的样式
10
+ &:deep(*) {
11
+ // 隐藏文本内容
12
+ .text,
13
+ span:not(.icon) {
14
+ display: none;
15
+ }
16
+ }
17
+ }
18
+ }
19
+
20
+ // 移动端抽屉(全屏覆盖)
21
+ .app-drawer--mobile {
22
+ // 添加阴影
23
+ box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
24
+
25
+ &.visible {
26
+ box-shadow: 2px 0 16px rgba(0, 0, 0, 0.15);
27
+ }
28
+ }
29
+
30
+ // 抽屉内的滚动条样式
31
+ .app-drawer {
32
+ &::-webkit-scrollbar {
33
+ width: 6px;
34
+ height: 6px;
35
+ }
36
+
37
+ &::-webkit-scrollbar-thumb {
38
+ background-color: rgba(0, 0, 0, 0.2);
39
+ border-radius: 3px;
40
+
41
+ &:hover {
42
+ background-color: rgba(0, 0, 0, 0.3);
43
+ }
44
+ }
45
+
46
+ &::-webkit-scrollbar-track {
47
+ background-color: transparent;
48
+ }
49
+ }
50
+
51
+ // 暗色主题下的滚动条
52
+ html.dark {
53
+ .app-drawer {
54
+ &::-webkit-scrollbar-thumb {
55
+ background-color: rgba(255, 255, 255, 0.2);
56
+
57
+ &:hover {
58
+ background-color: rgba(255, 255, 255, 0.3);
59
+ }
60
+ }
61
+ }
62
+ }
@@ -0,0 +1,5 @@
1
+ // App Shell 样式入口
2
+ @import './variables.scss';
3
+ @import './responsive.scss';
4
+ @import './drawer.scss';
5
+ @import './mobile.scss';
@@ -0,0 +1,58 @@
1
+ // 移动端样式
2
+ @import './variables.scss';
3
+
4
+ // 移动端优化
5
+ @media (max-width: #{($breakpoint-small - 1px)}) {
6
+ // 触摸优化
7
+ * {
8
+ // 增大触摸目标
9
+ -webkit-tap-highlight-color: transparent;
10
+ }
11
+
12
+ // 按钮、链接等触摸元素的最小尺寸
13
+ button,
14
+ a,
15
+ [role="button"] {
16
+ min-height: 44px;
17
+ min-width: 44px;
18
+ }
19
+
20
+ // 优化文本选择
21
+ .app-main__content {
22
+ -webkit-user-select: text;
23
+ user-select: text;
24
+ }
25
+
26
+ // 移动端抽屉全屏
27
+ .app-drawer--mobile {
28
+ // 确保全屏显示
29
+ top: 0;
30
+ left: 0;
31
+ right: 0;
32
+ bottom: 0;
33
+
34
+ // 添加状态栏安全区域
35
+ padding-top: env(safe-area-inset-top);
36
+ padding-left: env(safe-area-inset-left);
37
+ padding-right: env(safe-area-inset-right);
38
+ }
39
+
40
+ // 移动端顶栏添加安全区域
41
+ .app-header {
42
+ padding-top: calc(env(safe-area-inset-top) + 16px);
43
+ height: calc(var(--mb-app-shell-header-height, 60px) + env(safe-area-inset-top));
44
+ }
45
+
46
+ // 移动端主内容区添加安全区域
47
+ .app-main__content {
48
+ padding-bottom: calc(var(--mb-app-shell-main-padding, 16px) + env(safe-area-inset-bottom));
49
+ }
50
+ }
51
+
52
+ // 横屏模式优化
53
+ @media (max-width: #{($breakpoint-small - 1px)}) and (orientation: landscape) {
54
+ .app-drawer--mobile {
55
+ // 横屏时抽屉高度减小
56
+ height: 100vh;
57
+ }
58
+ }
@@ -0,0 +1,33 @@
1
+ // 响应式样式
2
+ @import './variables.scss';
3
+
4
+ // 小屏(< 768px)
5
+ @media (max-width: #{($breakpoint-small - 1px)}) {
6
+ :root {
7
+ --mb-app-shell-drawer-width: 100%;
8
+ }
9
+
10
+ .app-header {
11
+ padding: 0 12px;
12
+ }
13
+
14
+ .app-main__content {
15
+ padding: 12px;
16
+ }
17
+ }
18
+
19
+ // 中屏(768px - 1199px)
20
+ @media (min-width: $breakpoint-small) and (max-width: #{($breakpoint-medium - 1px)}) {
21
+ .app-drawer--pc {
22
+ width: 200px;
23
+
24
+ &.is-collapsed {
25
+ width: var(--mb-app-shell-drawer-collapsed-width);
26
+ }
27
+ }
28
+ }
29
+
30
+ // 大屏(≥ 1200px)
31
+ @media (min-width: $breakpoint-medium) {
32
+ // 大屏可以添加额外的样式
33
+ }
@@ -0,0 +1,44 @@
1
+ // App Shell CSS 变量
2
+
3
+ // 断点
4
+ $breakpoint-small: 768px;
5
+ $breakpoint-medium: 1200px;
6
+
7
+ // 尺寸
8
+ $drawer-width: 240px;
9
+ $drawer-collapsed-width: 64px;
10
+ $header-height: 60px;
11
+
12
+ // 过渡
13
+ $transition-duration: 0.3s;
14
+ $transition-function: ease;
15
+
16
+ // CSS 变量定义
17
+ :root {
18
+ // 布局尺寸
19
+ --mb-app-shell-drawer-width: #{$drawer-width};
20
+ --mb-app-shell-drawer-collapsed-width: #{$drawer-collapsed-width};
21
+ --mb-app-shell-header-height: #{$header-height};
22
+
23
+ // 颜色
24
+ --mb-app-shell-header-bg: #ffffff;
25
+ --mb-app-shell-header-text-color: #606266;
26
+ --mb-app-shell-header-border-bottom: 1px solid #e4e7ed;
27
+ --mb-app-shell-drawer-bg: #ffffff;
28
+ --mb-app-shell-drawer-border-right: 1px solid #e4e7ed;
29
+ --mb-app-shell-main-bg: #f5f5f5;
30
+
31
+ // 间距
32
+ --mb-app-shell-main-padding: 16px;
33
+ --mb-app-shell-header-gap: 12px;
34
+ }
35
+
36
+ // 暗色主题
37
+ html.dark {
38
+ --mb-app-shell-header-bg: #1f1f1f;
39
+ --mb-app-shell-header-text-color: #e5e5e5;
40
+ --mb-app-shell-header-border-bottom: 1px solid #333333;
41
+ --mb-app-shell-drawer-bg: #1f1f1f;
42
+ --mb-app-shell-drawer-border-right: 1px solid #333333;
43
+ --mb-app-shell-main-bg: #141414;
44
+ }
package/types/app.ts ADDED
@@ -0,0 +1,41 @@
1
+ import type { Router } from 'vue-router'
2
+ import type { App } from 'vue'
3
+ import type { PWAConfig } from './pwa'
4
+ import type { LayoutConfig } from './layout'
5
+
6
+ /**
7
+ * 应用创建选项
8
+ */
9
+ export interface AppShellOptions {
10
+ /** 挂载元素选择器 */
11
+ el?: string
12
+ /** 路由配置 */
13
+ router?: {
14
+ /** 基础路由 */
15
+ baseRoutes?: any[]
16
+ /** 路由模式 */
17
+ historyMode?: 'hash' | 'html'
18
+ /** 基础路径 */
19
+ base?: string
20
+ }
21
+ /** PWA 配置 */
22
+ pwa?: PWAConfig
23
+ /** 布局配置 */
24
+ layout?: LayoutConfig
25
+ /** 是否设置 Pinia */
26
+ setupPinia?: boolean
27
+ /** 是否设置 Router */
28
+ setupRouter?: boolean
29
+ }
30
+
31
+ /**
32
+ * 应用创建结果
33
+ */
34
+ export interface AppShellResult {
35
+ /** Vue 应用实例 */
36
+ app: App
37
+ /** 路由实例 */
38
+ router?: Router
39
+ /** Pinia 实例 */
40
+ pinia?: any
41
+ }
package/types/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from './slots'
2
+ export * from './layout'
3
+ export * from './pwa'
4
+ export * from './app'
@@ -0,0 +1,28 @@
1
+ /**
2
+ * 断点类型
3
+ */
4
+ export type Breakpoint = 'small' | 'medium' | 'large'
5
+
6
+ /**
7
+ * 布局配置
8
+ */
9
+ export interface LayoutConfig {
10
+ /** 侧边栏宽度(PC端) */
11
+ siderWidth?: string
12
+ /** 侧边栏折叠宽度 */
13
+ siderCollapsedWidth?: string
14
+ /** 顶栏高度 */
15
+ headerHeight?: string
16
+ /** 是否默认折叠 */
17
+ defaultCollapsed?: boolean
18
+ }
19
+
20
+ /**
21
+ * 抽屉状态
22
+ */
23
+ export interface DrawerState {
24
+ /** 是否可见(移动端) */
25
+ visible: boolean
26
+ /** 是否折叠(PC端) */
27
+ collapsed: boolean
28
+ }
package/types/pwa.ts ADDED
@@ -0,0 +1,53 @@
1
+ /**
2
+ * PWA 安装引导配置
3
+ */
4
+ export interface PWAInstallConfig {
5
+ /** 安装提示标题 */
6
+ title?: string
7
+ /** 安装提示描述 */
8
+ description?: string
9
+ /** 安装按钮文案 */
10
+ installButtonText?: string
11
+ /** 取消按钮文案 */
12
+ cancelButtonText?: string
13
+ /** "不再提示"选项文案 */
14
+ dontShowAgainText?: string
15
+ /** 是否自动显示(默认 true) */
16
+ autoShow?: boolean
17
+ }
18
+
19
+ /**
20
+ * PWA 配置
21
+ */
22
+ export interface PWAConfig {
23
+ /** 应用名称 */
24
+ name?: string
25
+ /** 应用简称 */
26
+ shortName?: string
27
+ /** 应用描述 */
28
+ description?: string
29
+ /** 主题色 */
30
+ themeColor?: string
31
+ /** 背景色 */
32
+ backgroundColor?: string
33
+ /** 应用图标(路径或对象) */
34
+ icon?: string | Record<string, string>
35
+ /** 安装引导配置 */
36
+ installPrompt?: PWAInstallConfig
37
+ }
38
+
39
+ /**
40
+ * PWA 安装事件状态
41
+ */
42
+ export interface PWAInstallState {
43
+ /** 是否可安装 */
44
+ canInstall: boolean
45
+ /** 是否已安装 */
46
+ isInstalled: boolean
47
+ /** 是否不再提示 */
48
+ dontShowAgain: boolean
49
+ /** 安装提示是否可见 */
50
+ promptVisible: boolean
51
+ /** 延迟安装的安装事件 */
52
+ deferredPrompt: Event | null
53
+ }
package/types/slots.ts ADDED
@@ -0,0 +1,31 @@
1
+ import type { Component } from 'vue'
2
+
3
+ /**
4
+ * App Shell 插槽位置
5
+ */
6
+ export enum AppShellSlotLocation {
7
+ /** 顶栏左侧 */
8
+ HEADER_LEFT = 'HEADER_LEFT',
9
+ /** 顶栏中间 */
10
+ HEADER_CENTER = 'HEADER_CENTER',
11
+ /** 顶栏右侧 */
12
+ HEADER_RIGHT = 'HEADER_RIGHT',
13
+ /** 侧边栏(抽屉式) */
14
+ SIDEBAR = 'SIDEBAR',
15
+ /** 内容区域顶部 */
16
+ CONTENT_TOP = 'CONTENT_TOP',
17
+ /** 内容区域底部 */
18
+ CONTENT_BOTTOM = 'CONTENT_BOTTOM'
19
+ }
20
+
21
+ /**
22
+ * 插槽注册信息
23
+ */
24
+ export interface AppShellSlotRegistration {
25
+ /** 插槽位置 */
26
+ location: AppShellSlotLocation
27
+ /** 组件 */
28
+ component: Component
29
+ /** 组件属性 */
30
+ props?: Record<string, any>
31
+ }