create-weapp-vite 2.0.57 → 2.0.59

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/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- import { n as TemplateName, t as createProject } from "./src-hBW5b5RM.js";
1
+ import { n as TemplateName, t as createProject } from "./src-D1-gOdfL.js";
2
2
  import logger from "@weapp-core/logger";
3
3
  import fs from "fs-extra";
4
4
  import path from "node:path";
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import { n as TemplateName, t as createProject } from "./src-hBW5b5RM.js";
1
+ import { n as TemplateName, t as createProject } from "./src-D1-gOdfL.js";
2
2
  export { TemplateName, createProject };
@@ -4,10 +4,10 @@ import fs from "fs-extra";
4
4
  import path from "pathe";
5
5
  import https from "node:https";
6
6
  //#region ../weapp-vite/package.json
7
- var version$1 = "6.11.3";
7
+ var version$1 = "6.11.4";
8
8
  //#endregion
9
9
  //#region ../wevu/package.json
10
- var version = "6.11.3";
10
+ var version = "6.11.4";
11
11
  //#endregion
12
12
  //#region src/enums.ts
13
13
  let TemplateName = /* @__PURE__ */ function(TemplateName) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "create-weapp-vite",
3
3
  "type": "module",
4
- "version": "2.0.57",
4
+ "version": "2.0.59",
5
5
  "description": "create-weapp-vite",
6
6
  "author": "ice breaker <1324318532@qq.com>",
7
7
  "license": "MIT",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "weapp-vite-lib-template",
3
3
  "type": "module",
4
- "version": "2.0.3",
4
+ "version": "2.0.4",
5
5
  "private": true,
6
6
  "description": "weapp-vite 组件库(lib 模式)模板",
7
7
  "author": "ice breaker <1324318532@qq.com>",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "weapp-vite-wevu-tailwindcss-tdesign-template",
3
3
  "type": "module",
4
- "version": "1.0.3",
4
+ "version": "1.0.5",
5
5
  "private": true,
6
6
  "description": "weapp-vite + wevu (Vue SFC) 模板",
7
7
  "author": "ice breaker <1324318532@qq.com>",
@@ -13,6 +13,24 @@
13
13
  "query": "",
14
14
  "scene": null
15
15
  },
16
+ {
17
+ "name": "布局页",
18
+ "pathName": "pages/layouts/index",
19
+ "query": "",
20
+ "scene": null
21
+ },
22
+ {
23
+ "name": "Layout 通信演示",
24
+ "pathName": "pages/layout-feedback/index",
25
+ "query": "",
26
+ "scene": null
27
+ },
28
+ {
29
+ "name": "Store 调用 Layout",
30
+ "pathName": "pages/layout-store/index",
31
+ "query": "",
32
+ "scene": null
33
+ },
16
34
  {
17
35
  "name": "数据页",
18
36
  "pathName": "pages/data/index",
@@ -5,6 +5,8 @@ defineAppJson({
5
5
  pages: [
6
6
  'pages/index/index',
7
7
  'pages/layouts/index',
8
+ 'pages/layout-store/index',
9
+ 'pages/layout-feedback/index',
8
10
  'pages/data/index',
9
11
  'pages/form/index',
10
12
  'pages/list/index',
@@ -1,12 +1,15 @@
1
1
  import Dialog from 'tdesign-miniprogram/dialog/index'
2
- import { getCurrentInstance } from 'wevu'
2
+ import { getCurrentInstance, resolveLayoutBridge, resolveLayoutHost } from 'wevu'
3
+ import { LAYOUT_DIALOG_BRIDGE_KEY } from '@/hooks/useLayoutFeedbackBridge'
3
4
 
4
5
  export interface DialogOptions {
6
+ bridgeKey?: string
5
7
  context?: any
6
8
  selector?: string
7
9
  }
8
10
 
9
- export interface AlertOptions {
11
+ interface BaseDialogPayload {
12
+ bridgeKey?: string
10
13
  confirmBtn?: string
11
14
  content: string
12
15
  context?: any
@@ -14,61 +17,189 @@ export interface AlertOptions {
14
17
  title: string
15
18
  }
16
19
 
17
- export interface ConfirmOptions {
20
+ export interface AlertOptions extends BaseDialogPayload {}
21
+
22
+ export interface ConfirmOptions extends BaseDialogPayload {
18
23
  cancelBtn?: string
19
- confirmBtn?: string
20
- content: string
21
- context?: any
22
- selector?: string
23
- title: string
24
24
  }
25
25
 
26
- function resolveDialogContext(context?: any) {
27
- return context ?? getCurrentInstance()
26
+ interface HostDialogInstance {
27
+ _onCancel?: (reason?: unknown) => void
28
+ _onConfirm?: (value?: unknown) => void
29
+ close?: () => void
30
+ setData?: (payload: Record<string, unknown>) => void
28
31
  }
29
32
 
30
- export function alertDialog(payload: AlertOptions) {
31
- const context = resolveDialogContext(payload.context)
32
- if (!context) {
33
+ type DialogMode = 'alert' | 'confirm'
34
+
35
+ type ResolvedDialogPayload<T extends BaseDialogPayload> = Omit<T, 'bridgeKey' | 'context' | 'selector'>
36
+
37
+ function resolveDialogContext(options: DialogOptions) {
38
+ return options.bridgeKey
39
+ ? resolveLayoutBridge(options.bridgeKey, options.context ?? getCurrentInstance())
40
+ : options.context ?? getCurrentInstance()
41
+ }
42
+
43
+ function resolveDialogHost(options: DialogOptions) {
44
+ const bridgeKey = options.bridgeKey ?? LAYOUT_DIALOG_BRIDGE_KEY
45
+ const context = resolveDialogContext({
46
+ bridgeKey,
47
+ context: options.context,
48
+ })
49
+ const host = bridgeKey
50
+ ? resolveLayoutHost<HostDialogInstance>(bridgeKey, { context })
51
+ : options.selector
52
+ ? context?.selectComponent?.(options.selector) ?? null
53
+ : null
54
+
55
+ return {
56
+ context,
57
+ host,
58
+ selector: options.selector,
59
+ }
60
+ }
61
+
62
+ function closeDialogHost(host: HostDialogInstance) {
63
+ if (typeof host.close === 'function') {
64
+ host.close()
33
65
  return
34
66
  }
35
- const { selector = '#t-dialog', ...rest } = payload
36
- return Dialog.alert({
37
- selector,
38
- context: context as any,
39
- ...rest,
67
+ if (typeof host.setData === 'function') {
68
+ host.setData({ visible: false })
69
+ }
70
+ }
71
+
72
+ function attachHostDialogHandlers(
73
+ host: HostDialogInstance,
74
+ handlers: {
75
+ onCancel?: (reason?: unknown) => void
76
+ onConfirm?: (value?: unknown) => void
77
+ },
78
+ ) {
79
+ const originalConfirm = typeof host._onConfirm === 'function' ? host._onConfirm : undefined
80
+ const originalCancel = typeof host._onCancel === 'function' ? host._onCancel : undefined
81
+
82
+ host._onConfirm = (value?: unknown) => {
83
+ host._onConfirm = originalConfirm
84
+ host._onCancel = originalCancel
85
+
86
+ if (originalConfirm) {
87
+ originalConfirm.call(host, value)
88
+ }
89
+ else {
90
+ closeDialogHost(host)
91
+ }
92
+
93
+ handlers.onConfirm?.(value)
94
+ }
95
+
96
+ host._onCancel = (reason?: unknown) => {
97
+ host._onConfirm = originalConfirm
98
+ host._onCancel = originalCancel
99
+
100
+ if (originalCancel) {
101
+ originalCancel.call(host, reason)
102
+ }
103
+ else {
104
+ closeDialogHost(host)
105
+ }
106
+
107
+ handlers.onCancel?.(reason)
108
+ }
109
+ }
110
+
111
+ function normalizeDialogPayload<T extends BaseDialogPayload>(
112
+ mode: DialogMode,
113
+ payload: T,
114
+ ): ResolvedDialogPayload<T> {
115
+ const { bridgeKey: _bridgeKey, context: _context, selector: _selector, ...rest } = payload
116
+
117
+ if (mode === 'alert') {
118
+ return {
119
+ ...rest,
120
+ cancelBtn: null,
121
+ } as ResolvedDialogPayload<T>
122
+ }
123
+
124
+ return rest as ResolvedDialogPayload<T>
125
+ }
126
+
127
+ function openDialogWithHost<T extends BaseDialogPayload>(
128
+ mode: DialogMode,
129
+ host: HostDialogInstance,
130
+ payload: ResolvedDialogPayload<T>,
131
+ ) {
132
+ return new Promise((resolve, reject) => {
133
+ host.setData?.({
134
+ ...payload,
135
+ visible: true,
136
+ })
137
+
138
+ attachHostDialogHandlers(host, {
139
+ onConfirm: resolve,
140
+ ...(mode === 'confirm' ? { onCancel: reject } : {}),
141
+ })
40
142
  })
41
143
  }
42
144
 
43
- export function confirmDialog(payload: ConfirmOptions) {
44
- const context = resolveDialogContext(payload.context)
145
+ function openDialog<T extends BaseDialogPayload>(mode: DialogMode, payload: T) {
146
+ const bridgeKey = payload.bridgeKey ?? LAYOUT_DIALOG_BRIDGE_KEY
147
+ const { context, host, selector } = resolveDialogHost({
148
+ bridgeKey,
149
+ context: payload.context,
150
+ selector: payload.selector,
151
+ })
152
+
45
153
  if (!context) {
46
- return
154
+ return Promise.resolve()
47
155
  }
48
- const { selector = '#t-dialog', ...rest } = payload
49
- return Dialog.confirm({
156
+
157
+ const normalizedPayload = normalizeDialogPayload(mode, payload)
158
+
159
+ if (host && typeof host.setData === 'function') {
160
+ return openDialogWithHost(mode, host, normalizedPayload)
161
+ }
162
+
163
+ if (!selector) {
164
+ return Promise.resolve()
165
+ }
166
+
167
+ const open = mode === 'alert' ? Dialog.alert : Dialog.confirm
168
+
169
+ return open({
50
170
  selector,
51
171
  context: context as any,
52
- ...rest,
172
+ ...normalizedPayload,
53
173
  })
54
174
  }
55
175
 
176
+ export function alertDialog(payload: AlertOptions) {
177
+ return openDialog('alert', payload)
178
+ }
179
+
180
+ export function confirmDialog(payload: ConfirmOptions) {
181
+ return openDialog('confirm', payload)
182
+ }
183
+
56
184
  export function useDialog(options: DialogOptions = {}) {
185
+ const bridgeKey = options.bridgeKey ?? LAYOUT_DIALOG_BRIDGE_KEY
57
186
  const context = options.context ?? getCurrentInstance()
187
+
188
+ function withDefaults<T extends BaseDialogPayload>(payload: T): T {
189
+ return {
190
+ ...payload,
191
+ bridgeKey,
192
+ context: payload.context ?? context,
193
+ selector: payload.selector ?? options.selector,
194
+ }
195
+ }
196
+
58
197
  return {
59
198
  alert(payload: AlertOptions) {
60
- return alertDialog({
61
- ...payload,
62
- context: payload.context ?? context,
63
- selector: payload.selector ?? options.selector,
64
- })
199
+ return alertDialog(withDefaults(payload))
65
200
  },
66
201
  confirm(payload: ConfirmOptions) {
67
- return confirmDialog({
68
- ...payload,
69
- context: payload.context ?? context,
70
- selector: payload.selector ?? options.selector,
71
- })
202
+ return confirmDialog(withDefaults(payload))
72
203
  },
73
204
  }
74
205
  }
@@ -0,0 +1,17 @@
1
+ export const LAYOUT_TOAST_BRIDGE_KEY = 'layout-toast'
2
+ export const LAYOUT_DIALOG_BRIDGE_KEY = 'layout-dialog'
3
+
4
+ export interface ToastHostInstance {
5
+ hide?: () => void
6
+ show: (options: Record<string, unknown>) => void
7
+ }
8
+
9
+ export interface DialogHostInstance {
10
+ _onCancel?: (reason?: unknown) => void
11
+ _onConfirm?: (value?: unknown) => void
12
+ close?: () => void
13
+ onCancel?: (reason?: unknown) => void
14
+ onConfirm?: (value?: unknown) => void
15
+ properties?: Record<string, unknown>
16
+ setData: (payload: Record<string, unknown>) => void
17
+ }
@@ -1,9 +1,11 @@
1
1
  import Toast from 'tdesign-miniprogram/toast/index'
2
- import { getCurrentInstance } from 'wevu'
2
+ import { getCurrentInstance, resolveLayoutBridge, resolveLayoutHost } from 'wevu'
3
+ import { LAYOUT_TOAST_BRIDGE_KEY } from '@/hooks/useLayoutFeedbackBridge'
3
4
 
4
5
  export type ToastTheme = 'success' | 'warning' | 'error' | 'default' | 'loading'
5
6
 
6
7
  export interface ShowToastPayload {
8
+ bridgeKey?: string
7
9
  context?: any
8
10
  duration?: number
9
11
  icon?: string
@@ -15,41 +17,78 @@ export interface ShowToastPayload {
15
17
  }
16
18
 
17
19
  export interface ToastOptions {
20
+ bridgeKey?: string
18
21
  selector?: string
19
22
  duration?: number
20
23
  theme?: ToastTheme
21
24
  }
22
25
 
26
+ function resolveToastContext(options: { bridgeKey?: string, context?: any }) {
27
+ return options.bridgeKey
28
+ ? resolveLayoutBridge(options.bridgeKey, options.context ?? getCurrentInstance())
29
+ : options.context ?? getCurrentInstance()
30
+ }
31
+
23
32
  export function showToast(payload: string | ShowToastPayload, theme?: ToastTheme) {
24
33
  const mpContext = getCurrentInstance()
25
34
  const normalized = typeof payload === 'string'
26
35
  ? { message: payload, theme }
27
36
  : payload
28
-
29
- const context = normalized.context ?? mpContext
37
+ const bridgeKey = normalized.bridgeKey ?? LAYOUT_TOAST_BRIDGE_KEY
38
+ const selector = normalized.selector
39
+ const {
40
+ bridgeKey: _bridgeKey,
41
+ context: _context,
42
+ selector: _selector,
43
+ theme: nextTheme,
44
+ title,
45
+ message,
46
+ ...rest
47
+ } = normalized
48
+ const options = {
49
+ message: message ?? title ?? '',
50
+ ...rest,
51
+ ...(nextTheme && nextTheme !== 'default' ? { theme: nextTheme } : {}),
52
+ }
53
+ const context = resolveToastContext({
54
+ bridgeKey,
55
+ context: normalized.context ?? mpContext,
56
+ })
57
+ const host = bridgeKey
58
+ ? resolveLayoutHost<{
59
+ show?: (payload: typeof options) => void
60
+ }>(bridgeKey, { context })
61
+ : selector
62
+ ? context?.selectComponent?.(selector) ?? null
63
+ : null
30
64
  if (!context) {
31
65
  return
32
66
  }
33
-
34
- const { selector = '#t-toast', theme: nextTheme, title, message, ...rest } = normalized
67
+ if (host && typeof host.show === 'function') {
68
+ host.show(options)
69
+ return
70
+ }
71
+ if (!selector) {
72
+ return
73
+ }
35
74
  Toast({
36
75
  selector,
37
76
  context: context as any,
38
- message: message ?? title ?? '',
39
- ...rest,
40
- ...(nextTheme && nextTheme !== 'default' ? { theme: nextTheme } : {}),
77
+ ...options,
41
78
  } as any)
42
79
  }
43
80
 
44
81
  export function useToast(options: ToastOptions = {}) {
45
82
  const context = getCurrentInstance()
46
- const selector = options.selector ?? '#t-toast'
83
+ const bridgeKey = options.bridgeKey ?? LAYOUT_TOAST_BRIDGE_KEY
84
+ const selector = options.selector
47
85
  const duration = options.duration ?? 1200
48
86
  const defaultTheme = options.theme ?? 'success'
49
87
 
50
88
  return {
51
89
  showToast(message: string, theme: ToastTheme = defaultTheme) {
52
90
  return showToast({
91
+ bridgeKey,
53
92
  context,
54
93
  selector,
55
94
  message,
@@ -29,8 +29,8 @@ defineComponentJson({
29
29
  <view class="pb-[32rpx]">
30
30
  <slot />
31
31
  </view>
32
- <t-toast id="t-toast" />
33
- <t-dialog id="t-dialog" />
32
+ <t-toast layout-host="layout-toast" />
33
+ <t-dialog layout-host="layout-dialog" />
34
34
  </view>
35
35
  </template>
36
36
 
@@ -11,8 +11,8 @@ defineComponentJson({
11
11
  <template>
12
12
  <view class="layout-default">
13
13
  <slot />
14
- <t-toast id="t-toast" />
15
- <t-dialog id="t-dialog" />
14
+ <t-toast layout-host="layout-toast" />
15
+ <t-dialog layout-host="layout-dialog" />
16
16
  </view>
17
17
  </template>
18
18
 
@@ -1,9 +1,10 @@
1
1
  <script setup lang="ts">
2
2
  import type { QuickActionItem } from '@/types/action'
3
3
 
4
- import { computed, ref, watch } from 'wevu'
4
+ import { computed, getCurrentInstance, ref, resolveLayoutBridge, resolveLayoutHost, watch } from 'wevu'
5
5
  import KpiBoard from '@/components/KpiBoard/index.vue'
6
6
  import QuickActionGrid from '@/components/QuickActionGrid/index.vue'
7
+ import { LAYOUT_TOAST_BRIDGE_KEY } from '@/hooks/useLayoutFeedbackBridge'
7
8
  import { usePullDownRefresh } from '@/hooks/usePullDownRefresh'
8
9
  import { useToast } from '@/hooks/useToast'
9
10
 
@@ -14,6 +15,7 @@ definePageJson({
14
15
  })
15
16
 
16
17
  const { showToast } = useToast()
18
+ const pageInstance = getCurrentInstance<any>()
17
19
 
18
20
  const noticeText = ref('欢迎体验 wevu + weapp-vite + TDesign 模板,已启用分包与多页面导航。')
19
21
  const lastUpdated = ref('刚刚')
@@ -108,6 +110,26 @@ const quickActions = ref<QuickActionItem[]>([
108
110
  path: '/pages/layouts/index',
109
111
  type: 'sub',
110
112
  },
113
+ {
114
+ key: 'layout-feedback',
115
+ title: 'Layout 通信',
116
+ description: '页面/组件调用 layout',
117
+ icon: 'chat-bubble',
118
+ tag: 'Bridge',
119
+ tone: 'brand',
120
+ path: '/pages/layout-feedback/index',
121
+ type: 'sub',
122
+ },
123
+ {
124
+ key: 'layout-store',
125
+ title: 'Store 调用 Layout',
126
+ description: 'store 驱动 layout 宿主',
127
+ icon: 'layers',
128
+ tag: 'Store',
129
+ tone: 'brand',
130
+ path: '/pages/layout-store/index',
131
+ type: 'sub',
132
+ },
111
133
  {
112
134
  key: 'lab',
113
135
  title: '组件实验室',
@@ -151,11 +173,49 @@ watch(refreshSeed, () => {
151
173
  lastUpdated.value = `更新于 ${new Date().toLocaleTimeString()}`
152
174
  })
153
175
 
176
+ function nextRefreshSeedValue() {
177
+ return refreshSeed.value >= 9 ? 1 : refreshSeed.value + 1
178
+ }
179
+
154
180
  function refreshDashboard() {
155
- refreshSeed.value = Math.max(1, Math.floor(Math.random() * 9))
181
+ refreshSeed.value = nextRefreshSeedValue()
156
182
  showToast('指标已刷新')
157
183
  }
158
184
 
185
+ function inspectLayoutToastBridge() {
186
+ const bridge = resolveLayoutBridge(LAYOUT_TOAST_BRIDGE_KEY, pageInstance)
187
+ const layoutByPage = pageInstance?.selectComponent?.('weapp-layout-default')
188
+ ?? pageInstance?.selectComponent?.('.weapp-layout-default')
189
+ ?? null
190
+ const toastFromBridge = resolveLayoutHost(LAYOUT_TOAST_BRIDGE_KEY, { context: pageInstance })
191
+
192
+ return {
193
+ bridgeResolved: Boolean(bridge),
194
+ bridgeIsPage: bridge === pageInstance,
195
+ bridgeHasSelectComponent: typeof bridge?.selectComponent === 'function',
196
+ layoutFoundByPage: Boolean(layoutByPage),
197
+ toastFoundByBridge: Boolean(toastFromBridge),
198
+ bridgeKeys: bridge ? Object.keys(bridge).slice(0, 20) : [],
199
+ layoutKeys: layoutByPage ? Object.keys(layoutByPage).slice(0, 20) : [],
200
+ }
201
+ }
202
+
203
+ void inspectLayoutToastBridge
204
+
205
+ function runLayoutToastE2E() {
206
+ refreshSeed.value = nextRefreshSeedValue()
207
+ const bridgeState = inspectLayoutToastBridge()
208
+ setTimeout(() => {
209
+ showToast('指标已刷新')
210
+ }, 0)
211
+ return {
212
+ ...bridgeState,
213
+ refreshSeed: refreshSeed.value,
214
+ }
215
+ }
216
+
217
+ void runLayoutToastE2E
218
+
159
219
  usePullDownRefresh(refreshDashboard)
160
220
 
161
221
  function onQuickAction(action: QuickActionItem) {
@@ -193,9 +253,11 @@ function onQuickAction(action: QuickActionItem) {
193
253
  <text class="text-[20rpx] text-white/70">
194
254
  {{ lastUpdated }}
195
255
  </text>
196
- <t-button size="small" theme="default" variant="outline" @tap="refreshDashboard">
197
- 刷新指标
198
- </t-button>
256
+ <view id="refresh-dashboard-trigger" @tap="refreshDashboard">
257
+ <t-button size="small" theme="default" variant="outline">
258
+ 刷新指标
259
+ </t-button>
260
+ </view>
199
261
  </view>
200
262
  </view>
201
263
 
@@ -0,0 +1,79 @@
1
+ <script setup lang="ts">
2
+ import { ref } from 'wevu'
3
+ import { useDialog } from '@/hooks/useDialog'
4
+ import { useToast } from '@/hooks/useToast'
5
+
6
+ const props = defineProps<{
7
+ onReport?: (payload: string) => void
8
+ }>()
9
+
10
+ const actionSeed = ref(0)
11
+ const { showToast } = useToast({ duration: 1400 })
12
+ const { alert, confirm } = useDialog()
13
+
14
+ function nextLabel(prefix: string) {
15
+ actionSeed.value += 1
16
+ return `${prefix} #${actionSeed.value}`
17
+ }
18
+
19
+ function triggerChildToast() {
20
+ const label = nextLabel('子组件 Toast')
21
+ showToast(`${label} 已通过 layout 宿主触发`)
22
+ props.onReport?.(`${label} 已触发`)
23
+ }
24
+
25
+ function triggerChildAlert() {
26
+ const label = nextLabel('子组件 Alert')
27
+ void alert({
28
+ title: label,
29
+ content: '当前弹窗由子组件直接调用 useDialog(),但实际宿主仍在 layout 内。',
30
+ confirmBtn: '知道了',
31
+ }).then(() => {
32
+ props.onReport?.(`${label} 已确认`)
33
+ })
34
+ }
35
+
36
+ function triggerChildConfirm() {
37
+ const label = nextLabel('子组件 Confirm')
38
+ void confirm({
39
+ title: label,
40
+ content: '点击确认后会回传一条日志,证明子组件与页面都能经由同一 layout 宿主通信。',
41
+ confirmBtn: '确认',
42
+ cancelBtn: '取消',
43
+ }).then(() => {
44
+ props.onReport?.(`${label} 点击确认`)
45
+ }).catch(() => {
46
+ props.onReport?.(`${label} 点击取消`)
47
+ })
48
+ }
49
+ </script>
50
+
51
+ <template>
52
+ <view class="rounded-[24rpx] bg-white p-[20rpx] shadow-[0_18rpx_40rpx_rgba(17,24,39,0.08)]">
53
+ <view class="flex items-center justify-between">
54
+ <view>
55
+ <text class="text-[28rpx] font-semibold text-[#1f1a3f]">
56
+ 子组件直连 layout 宿主
57
+ </text>
58
+ <text class="mt-[6rpx] block text-[22rpx] leading-[1.7] text-[#6f6b8a]">
59
+ 这个组件不接收 toast/dialog 实例,也不手动 selectComponent,只直接调用 useToast() / useDialog()。
60
+ </text>
61
+ </view>
62
+ <t-tag size="small" theme="primary" variant="light">
63
+ Child
64
+ </t-tag>
65
+ </view>
66
+
67
+ <view class="mt-[18rpx] flex flex-col gap-[12rpx]">
68
+ <t-button block theme="primary" variant="outline" @tap="triggerChildToast">
69
+ 子组件触发 Toast
70
+ </t-button>
71
+ <t-button block theme="primary" variant="outline" @tap="triggerChildAlert">
72
+ 子组件触发 Alert
73
+ </t-button>
74
+ <t-button block theme="danger" variant="outline" @tap="triggerChildConfirm">
75
+ 子组件触发 Confirm
76
+ </t-button>
77
+ </view>
78
+ </view>
79
+ </template>
@@ -0,0 +1,211 @@
1
+ <script setup lang="ts">
2
+ import { computed, getCurrentInstance, ref, resolveLayoutHost } from 'wevu'
3
+ import SectionTitle from '@/components/SectionTitle/index.vue'
4
+ import { useDialog } from '@/hooks/useDialog'
5
+ import { LAYOUT_DIALOG_BRIDGE_KEY, LAYOUT_TOAST_BRIDGE_KEY } from '@/hooks/useLayoutFeedbackBridge'
6
+ import { useToast } from '@/hooks/useToast'
7
+ import FeedbackCallerCard from './components/FeedbackCallerCard.vue'
8
+
9
+ definePageJson({
10
+ navigationBarTitleText: 'Layout 通信演示',
11
+ backgroundColor: '#f6f7fb',
12
+ })
13
+
14
+ const pageInstance = getCurrentInstance<any>()
15
+ const { showToast } = useToast({ duration: 1400 })
16
+ const { alert, confirm } = useDialog()
17
+ const actionSeed = ref(0)
18
+ const actionLogs = ref<string[]>([
19
+ '页面与子组件都会直接调用 useToast() / useDialog(),由 layout 统一承载宿主。',
20
+ ])
21
+
22
+ const bridgeStatus = computed(() => {
23
+ const toastHost = resolveLayoutHost(LAYOUT_TOAST_BRIDGE_KEY, { context: pageInstance })
24
+ const dialogHost = resolveLayoutHost(LAYOUT_DIALOG_BRIDGE_KEY, { context: pageInstance })
25
+
26
+ return [
27
+ {
28
+ key: 'toast',
29
+ title: 'Toast Host',
30
+ description: toastHost ? '已解析到 layout 内的 t-toast 实例。' : '尚未解析到 toast 宿主。',
31
+ ready: Boolean(toastHost),
32
+ },
33
+ {
34
+ key: 'dialog',
35
+ title: 'Dialog Host',
36
+ description: dialogHost ? '已解析到 layout 内的 t-dialog 实例。' : '尚未解析到 dialog 宿主。',
37
+ ready: Boolean(dialogHost),
38
+ },
39
+ ]
40
+ })
41
+
42
+ function pushLog(message: string) {
43
+ actionLogs.value = [`${new Date().toLocaleTimeString()} ${message}`, ...actionLogs.value].slice(0, 8)
44
+ }
45
+
46
+ function onChildReport(message: string) {
47
+ pushLog(message)
48
+ }
49
+
50
+ function nextLabel(prefix: string) {
51
+ actionSeed.value += 1
52
+ return `${prefix} #${actionSeed.value}`
53
+ }
54
+
55
+ function triggerPageToast() {
56
+ const label = nextLabel('页面 Toast')
57
+ showToast(`${label} 已通过 layout 宿主触发`)
58
+ pushLog(`${label} 已触发`)
59
+ }
60
+
61
+ function triggerPageAlert() {
62
+ const label = nextLabel('页面 Alert')
63
+ void alert({
64
+ title: label,
65
+ content: '这是页面直接调用 useDialog() 后,由 layout 内 t-dialog 承载的弹窗。',
66
+ confirmBtn: '知道了',
67
+ }).then(() => {
68
+ pushLog(`${label} 已确认`)
69
+ })
70
+ }
71
+
72
+ function triggerPageConfirm() {
73
+ const label = nextLabel('页面 Confirm')
74
+ void confirm({
75
+ title: label,
76
+ content: '确认后会写入日志,方便观察页面与 layout 宿主之间的通信。',
77
+ confirmBtn: '确认',
78
+ cancelBtn: '取消',
79
+ }).then(() => {
80
+ pushLog(`${label} 点击确认`)
81
+ }).catch(() => {
82
+ pushLog(`${label} 点击取消`)
83
+ })
84
+ }
85
+
86
+ function inspectDialogHostE2E() {
87
+ const dialogHost = resolveLayoutHost<any>(LAYOUT_DIALOG_BRIDGE_KEY, { context: pageInstance })
88
+ return {
89
+ hasHost: Boolean(dialogHost),
90
+ visible: dialogHost?.data?.visible ?? dialogHost?.properties?.visible ?? null,
91
+ hasOnConfirm: typeof dialogHost?._onConfirm === 'function',
92
+ hasOnCancel: typeof dialogHost?._onCancel === 'function',
93
+ hasNativeConfirm: typeof dialogHost?.onConfirm === 'function',
94
+ hasNativeCancel: typeof dialogHost?.onCancel === 'function',
95
+ title: dialogHost?.data?.title ?? dialogHost?.properties?.title ?? '',
96
+ confirmBtn: dialogHost?.data?._confirm?.content ?? dialogHost?.properties?.confirmBtn ?? '',
97
+ cancelBtn: dialogHost?.data?._cancel?.content ?? dialogHost?.properties?.cancelBtn ?? '',
98
+ }
99
+ }
100
+
101
+ async function runPageAlertCloseE2E() {
102
+ triggerPageAlert()
103
+ await new Promise(resolve => setTimeout(resolve, 120))
104
+ return inspectDialogHostE2E()
105
+ }
106
+
107
+ async function runPageConfirmOpenE2E() {
108
+ triggerPageConfirm()
109
+ await new Promise(resolve => setTimeout(resolve, 120))
110
+ return inspectDialogHostE2E()
111
+ }
112
+
113
+ function getLayoutFeedbackLogsE2E() {
114
+ return actionLogs.value.slice()
115
+ }
116
+
117
+ async function runDialogHostConfirmE2E() {
118
+ const dialogHost = resolveLayoutHost<any>(LAYOUT_DIALOG_BRIDGE_KEY, { context: pageInstance })
119
+ dialogHost?.onConfirm?.()
120
+ await new Promise(resolve => setTimeout(resolve, 60))
121
+ return inspectDialogHostE2E()
122
+ }
123
+
124
+ async function runDialogHostCancelE2E() {
125
+ const dialogHost = resolveLayoutHost<any>(LAYOUT_DIALOG_BRIDGE_KEY, { context: pageInstance })
126
+ dialogHost?.onCancel?.()
127
+ await new Promise(resolve => setTimeout(resolve, 60))
128
+ return inspectDialogHostE2E()
129
+ }
130
+
131
+ void inspectDialogHostE2E
132
+ void runPageAlertCloseE2E
133
+ void runPageConfirmOpenE2E
134
+ void getLayoutFeedbackLogsE2E
135
+ void runDialogHostConfirmE2E
136
+ void runDialogHostCancelE2E
137
+ </script>
138
+
139
+ <template>
140
+ <view class="min-h-screen bg-[#f6f7fb] px-[28rpx] pb-[88rpx] pt-[24rpx] text-[#1c1c3c]">
141
+ <view class="rounded-[28rpx] bg-gradient-to-br from-[#eef2ff] via-[#ffffff] to-[#ede9fe] p-[20rpx] shadow-[0_18rpx_40rpx_rgba(17,24,39,0.06)]">
142
+ <SectionTitle title="Layout 反馈宿主通信" subtitle="页面与子组件都直接调用 hooks,由 layout 持有 toast / dialog 组件实例" />
143
+ <text class="mt-[12rpx] block text-[22rpx] leading-[1.7] text-[#5b5b7b]">
144
+ 推荐用法是业务侧只关心 useToast() / useDialog(),layout 负责注册宿主;页面和组件都不需要关心 id、selector,也不需要直接拿 layout 实例。
145
+ </text>
146
+ </view>
147
+
148
+ <view class="mt-[18rpx] rounded-[24rpx] bg-white p-[20rpx] shadow-[0_18rpx_40rpx_rgba(17,24,39,0.08)]">
149
+ <SectionTitle title="Bridge 状态" subtitle="当前页面是否成功解析到 layout 宿主" />
150
+ <view class="mt-[16rpx] flex flex-col gap-[12rpx]">
151
+ <view
152
+ v-for="item in bridgeStatus"
153
+ :key="item.key"
154
+ class="rounded-[18rpx] bg-[#f7f7fb] p-[16rpx]"
155
+ >
156
+ <view class="flex items-center justify-between">
157
+ <text class="text-[24rpx] font-semibold text-[#1f1a3f]">
158
+ {{ item.title }}
159
+ </text>
160
+ <t-tag :theme="item.ready ? 'success' : 'warning'" size="small" variant="light">
161
+ {{ item.ready ? 'Ready' : 'Pending' }}
162
+ </t-tag>
163
+ </view>
164
+ <text class="mt-[8rpx] block text-[20rpx] leading-[1.7] text-[#6f6b8a]">
165
+ {{ item.description }}
166
+ </text>
167
+ </view>
168
+ </view>
169
+ </view>
170
+
171
+ <view class="mt-[18rpx] rounded-[24rpx] bg-white p-[20rpx] shadow-[0_18rpx_40rpx_rgba(17,24,39,0.08)]">
172
+ <SectionTitle title="页面直接调用" subtitle="页面本身直接触发 layout 内的 toast / dialog 方法" />
173
+ <view class="mt-[16rpx] flex flex-col gap-[12rpx]">
174
+ <view id="layout-feedback-page-toast-trigger">
175
+ <t-button block theme="primary" @tap="triggerPageToast">
176
+ 页面触发 Toast
177
+ </t-button>
178
+ </view>
179
+ <view id="layout-feedback-page-alert-trigger">
180
+ <t-button block theme="primary" variant="outline" @tap="triggerPageAlert">
181
+ 页面触发 Alert
182
+ </t-button>
183
+ </view>
184
+ <view id="layout-feedback-page-confirm-trigger">
185
+ <t-button block theme="danger" variant="outline" @tap="triggerPageConfirm">
186
+ 页面触发 Confirm
187
+ </t-button>
188
+ </view>
189
+ </view>
190
+ </view>
191
+
192
+ <view class="mt-[18rpx]">
193
+ <FeedbackCallerCard :on-report="onChildReport" />
194
+ </view>
195
+
196
+ <view class="mt-[18rpx] rounded-[24rpx] bg-white p-[20rpx] shadow-[0_18rpx_40rpx_rgba(17,24,39,0.08)]">
197
+ <SectionTitle title="通信日志" subtitle="观察页面与子组件调用 layout 宿主后的反馈结果" />
198
+ <view class="mt-[16rpx] flex flex-col gap-[10rpx]">
199
+ <view
200
+ v-for="(item, index) in actionLogs"
201
+ :key="`${item}-${index}`"
202
+ class="rounded-[16rpx] bg-[#f7f7fb] px-[16rpx] py-[14rpx]"
203
+ >
204
+ <text class="block text-[20rpx] leading-[1.7] text-[#4c4b6c]">
205
+ {{ item }}
206
+ </text>
207
+ </view>
208
+ </view>
209
+ </view>
210
+ </view>
211
+ </template>
@@ -0,0 +1,127 @@
1
+ <script setup lang="ts">
2
+ import { onUnload, setPageLayout, storeToRefs, watch } from 'wevu'
3
+ import SectionTitle from '@/components/SectionTitle/index.vue'
4
+ import { useLayoutInteractionDemoStore } from '@/stores/layoutInteractionDemo'
5
+
6
+ definePageJson({
7
+ navigationBarTitleText: 'Store 调用 Layout',
8
+ backgroundColor: '#f6f7fb',
9
+ })
10
+
11
+ const store = useLayoutInteractionDemoStore()
12
+ const { activeLayout, adminLayoutProps, commandStatus, lastResult, logs } = storeToRefs(store)
13
+
14
+ watch([activeLayout, adminLayoutProps], ([layout, props]) => {
15
+ if (layout === 'admin') {
16
+ setPageLayout('admin', props)
17
+ return
18
+ }
19
+ setPageLayout('default')
20
+ }, { immediate: true })
21
+
22
+ function useDefaultLayout() {
23
+ store.setLayout('default')
24
+ }
25
+
26
+ function useAdminLayout() {
27
+ store.setLayout('admin')
28
+ }
29
+
30
+ function openToastByStore() {
31
+ store.triggerToast()
32
+ }
33
+
34
+ function openAlertByStore() {
35
+ store.triggerAlert()
36
+ }
37
+
38
+ function openConfirmByStore() {
39
+ store.triggerConfirm()
40
+ }
41
+
42
+ function clearLogs() {
43
+ store.resetLogs()
44
+ }
45
+
46
+ onUnload(() => {
47
+ setPageLayout('default')
48
+ })
49
+ </script>
50
+
51
+ <template>
52
+ <view class="min-h-screen bg-[#f6f7fb] px-[28rpx] pb-[88rpx] pt-[24rpx] text-[#1c1c3c]">
53
+ <view class="rounded-[28rpx] bg-gradient-to-br from-[#e8f1ff] via-[#ffffff] to-[#eef2ff] p-[20rpx] shadow-[0_18rpx_40rpx_rgba(17,24,39,0.06)]">
54
+ <SectionTitle title="Store 驱动 Layout 交互" subtitle="wevu/store 发起意图,页面上下文消费后命中 layout 内的 toast / dialog 宿主" />
55
+ <text class="mt-[12rpx] block text-[22rpx] leading-[1.7] text-[#5b5b7b]">
56
+ 推荐边界是 store 只保存布局状态和交互意图,真正调用 useToast() / useDialog() / setPageLayout() 仍由页面执行,这样不会把 page runtime hook 直接塞进 store。
57
+ </text>
58
+ </view>
59
+
60
+ <view class="mt-[18rpx] rounded-[24rpx] bg-white p-[20rpx] shadow-[0_18rpx_40rpx_rgba(17,24,39,0.08)]">
61
+ <SectionTitle title="当前状态" subtitle="观察 store 对布局和宿主交互的描述" />
62
+ <view class="mt-[16rpx] flex flex-col gap-[12rpx]">
63
+ <view class="rounded-[18rpx] bg-[#f7f7fb] p-[16rpx]">
64
+ <text class="text-[24rpx] font-semibold text-[#1f1a3f]">
65
+ 当前布局:{{ activeLayout }}
66
+ </text>
67
+ <text class="mt-[8rpx] block text-[20rpx] leading-[1.7] text-[#6f6b8a]">
68
+ command 状态:{{ commandStatus }}
69
+ </text>
70
+ </view>
71
+ <view class="rounded-[18rpx] bg-[#f7f7fb] p-[16rpx]">
72
+ <text class="text-[24rpx] font-semibold text-[#1f1a3f]">
73
+ 最近结果
74
+ </text>
75
+ <text class="mt-[8rpx] block text-[20rpx] leading-[1.7] text-[#6f6b8a]">
76
+ {{ lastResult }}
77
+ </text>
78
+ </view>
79
+ </view>
80
+ </view>
81
+
82
+ <view class="mt-[18rpx] rounded-[24rpx] bg-white p-[20rpx] shadow-[0_18rpx_40rpx_rgba(17,24,39,0.08)]">
83
+ <SectionTitle title="切换 Layout" subtitle="store 修改布局状态,页面 watch 后调用 setPageLayout()" />
84
+ <view class="mt-[16rpx] flex flex-col gap-[12rpx]">
85
+ <t-button block theme="primary" @tap="useDefaultLayout">
86
+ Store 切到 default 布局
87
+ </t-button>
88
+ <t-button block theme="primary" variant="outline" @tap="useAdminLayout">
89
+ Store 切到 admin 布局
90
+ </t-button>
91
+ </view>
92
+ </view>
93
+
94
+ <view class="mt-[18rpx] rounded-[24rpx] bg-white p-[20rpx] shadow-[0_18rpx_40rpx_rgba(17,24,39,0.08)]">
95
+ <SectionTitle title="触发 Layout 宿主" subtitle="store 直接调用 toast / dialog,命中当前 layout 内的反馈宿主" />
96
+ <view class="mt-[16rpx] flex flex-col gap-[12rpx]">
97
+ <t-button block theme="primary" @tap="openToastByStore">
98
+ Store 触发 Toast
99
+ </t-button>
100
+ <t-button block theme="primary" variant="outline" @tap="openAlertByStore">
101
+ Store 触发 Alert
102
+ </t-button>
103
+ <t-button block theme="danger" variant="outline" @tap="openConfirmByStore">
104
+ Store 触发 Confirm
105
+ </t-button>
106
+ </view>
107
+ </view>
108
+
109
+ <view class="mt-[18rpx] rounded-[24rpx] bg-white p-[20rpx] shadow-[0_18rpx_40rpx_rgba(17,24,39,0.08)]">
110
+ <SectionTitle title="通信日志" subtitle="记录 store 发出的布局切换与交互命令" />
111
+ <t-button class="mt-[16rpx]" size="small" theme="default" variant="outline" @tap="clearLogs">
112
+ 清空日志
113
+ </t-button>
114
+ <view class="mt-[16rpx] flex flex-col gap-[10rpx]">
115
+ <view
116
+ v-for="(item, index) in logs"
117
+ :key="`${item}-${index}`"
118
+ class="rounded-[16rpx] bg-[#f7f7fb] px-[16rpx] py-[14rpx]"
119
+ >
120
+ <text class="block text-[20rpx] leading-[1.7] text-[#4c4b6c]">
121
+ {{ item }}
122
+ </text>
123
+ </view>
124
+ </view>
125
+ </view>
126
+ </view>
127
+ </template>
@@ -50,6 +50,18 @@ function clearLayout() {
50
50
  setPageLayout(false)
51
51
  showToast('已关闭布局')
52
52
  }
53
+
54
+ function openLayoutFeedbackDemo() {
55
+ wx.navigateTo({
56
+ url: '/pages/layout-feedback/index',
57
+ })
58
+ }
59
+
60
+ function openLayoutStoreDemo() {
61
+ wx.navigateTo({
62
+ url: '/pages/layout-store/index',
63
+ })
64
+ }
53
65
  </script>
54
66
 
55
67
  <template>
@@ -90,5 +102,18 @@ function clearLayout() {
90
102
  </t-button>
91
103
  </view>
92
104
  </view>
105
+
106
+ <view class="mt-[18rpx] rounded-[24rpx] bg-white p-[20rpx] shadow-[0_18rpx_40rpx_rgba(17,24,39,0.08)]">
107
+ <SectionTitle title="通信演示" subtitle="查看页面/组件如何直接使用 layout 里的反馈宿主" />
108
+ <text class="mt-[12rpx] block text-[22rpx] leading-[1.7] text-[#6f6b8a]">
109
+ 演示页会同时展示页面调用和子组件调用,它们都只依赖 useToast() / useDialog(),不会直接操作 layout 实例。
110
+ </text>
111
+ <t-button class="mt-[16rpx]" block theme="primary" variant="outline" @tap="openLayoutFeedbackDemo">
112
+ 打开 Layout 通信演示
113
+ </t-button>
114
+ <t-button class="mt-[12rpx]" block theme="primary" variant="outline" @tap="openLayoutStoreDemo">
115
+ 打开 Store 驱动 Layout 演示
116
+ </t-button>
117
+ </view>
93
118
  </view>
94
119
  </template>
@@ -0,0 +1,129 @@
1
+ import { computed, defineStore, ref } from 'wevu'
2
+ import { alertDialog, confirmDialog } from '@/hooks/useDialog'
3
+ import { showToast } from '@/hooks/useToast'
4
+
5
+ export type LayoutKind = 'default' | 'admin'
6
+ export type LayoutCommandType = 'toast' | 'alert' | 'confirm'
7
+ export type LayoutCommandStatus = 'idle' | 'running'
8
+
9
+ export interface LayoutCommand {
10
+ id: number
11
+ title: string
12
+ content: string
13
+ type: LayoutCommandType
14
+ }
15
+
16
+ function createLog(message: string) {
17
+ return `${new Date().toLocaleTimeString()} ${message}`
18
+ }
19
+
20
+ export const useLayoutInteractionDemoStore = defineStore('layout-interaction-demo', () => {
21
+ const activeLayout = ref<LayoutKind>('default')
22
+ const commandSeed = ref(0)
23
+ const commandStatus = ref<LayoutCommandStatus>('idle')
24
+ const lastResult = ref('尚未触发交互')
25
+ const logs = ref<string[]>([
26
+ createLog('Store 已就绪,当前由 store 直接调用 layout 宿主。'),
27
+ ])
28
+
29
+ const adminLayoutProps = computed(() => ({
30
+ title: 'Store Admin Layout',
31
+ subtitle: `当前通过 wevu/store 驱动,最近动作:${lastResult.value}`,
32
+ }))
33
+
34
+ function appendLog(message: string) {
35
+ logs.value = [createLog(message), ...logs.value].slice(0, 8)
36
+ }
37
+
38
+ function setLayout(layout: LayoutKind) {
39
+ activeLayout.value = layout
40
+ appendLog(`切换到 ${layout} 布局`)
41
+ }
42
+
43
+ function createCommand(type: LayoutCommandType, title: string, content: string): LayoutCommand {
44
+ commandSeed.value += 1
45
+ return {
46
+ id: commandSeed.value,
47
+ type,
48
+ title,
49
+ content,
50
+ }
51
+ }
52
+
53
+ async function runCommand(command: LayoutCommand) {
54
+ commandStatus.value = 'running'
55
+ appendLog(`Store 请求 ${command.title}`)
56
+
57
+ if (command.type === 'toast') {
58
+ showToast(command.content)
59
+ finishCommand(`${command.title} 已由 ${activeLayout.value} layout 宿主展示`)
60
+ return
61
+ }
62
+
63
+ if (command.type === 'alert') {
64
+ await alertDialog({
65
+ title: command.title,
66
+ content: command.content,
67
+ confirmBtn: '知道了',
68
+ })
69
+ finishCommand(`${command.title} 已确认`)
70
+ return
71
+ }
72
+
73
+ try {
74
+ await confirmDialog({
75
+ title: command.title,
76
+ content: command.content,
77
+ confirmBtn: '确认',
78
+ cancelBtn: '取消',
79
+ })
80
+ finishCommand(`${command.title} 点击确认`)
81
+ }
82
+ catch {
83
+ finishCommand(`${command.title} 点击取消`)
84
+ }
85
+ }
86
+
87
+ function triggerToast() {
88
+ return runCommand(
89
+ createCommand('toast', `Store Toast #${commandSeed.value + 1}`, `当前由 ${activeLayout.value} layout 宿主展示 toast。`),
90
+ )
91
+ }
92
+
93
+ function triggerAlert() {
94
+ return runCommand(
95
+ createCommand('alert', `Store Alert #${commandSeed.value + 1}`, `当前由 ${activeLayout.value} layout 宿主展示 alert。`),
96
+ )
97
+ }
98
+
99
+ function triggerConfirm() {
100
+ return runCommand(
101
+ createCommand('confirm', `Store Confirm #${commandSeed.value + 1}`, `请确认由 ${activeLayout.value} layout 宿主承载的确认弹窗。`),
102
+ )
103
+ }
104
+
105
+ function finishCommand(message: string) {
106
+ lastResult.value = message
107
+ commandStatus.value = 'idle'
108
+ appendLog(message)
109
+ }
110
+
111
+ function resetLogs() {
112
+ logs.value = [createLog('已清空日志,继续观察 store 与 layout 的通信。')]
113
+ lastResult.value = '日志已清空'
114
+ }
115
+
116
+ return {
117
+ activeLayout,
118
+ adminLayoutProps,
119
+ commandStatus,
120
+ lastResult,
121
+ logs,
122
+ finishCommand,
123
+ resetLogs,
124
+ setLayout,
125
+ triggerAlert,
126
+ triggerConfirm,
127
+ triggerToast,
128
+ }
129
+ })