create-rue 0.0.14 → 0.0.16
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/bundle.js +4 -19
- package/package.json +2 -2
- package/template/base/app/pages/TodoApp.tsx +563 -198
- package/template/base/app/pages/components/Layout.tsx +131 -27
- package/template/base/package-lock.json +617 -432
- package/template/base/package.json +19 -17
- package/template/base/pnpm-lock.yaml +550 -480
- package/template/base/tsconfig.json +1 -2
|
@@ -2,7 +2,7 @@ import { computed, type FC, ref, useEffect, watch } from '@rue-js/rue'
|
|
|
2
2
|
import { RouterLink, useRoute } from '@rue-js/router'
|
|
3
3
|
|
|
4
4
|
const themeStorageKey = 'rue.base.theme'
|
|
5
|
-
const
|
|
5
|
+
const headerToggleRoutes = ['/', '/report', '/todo']
|
|
6
6
|
|
|
7
7
|
const navItems = [
|
|
8
8
|
{ to: '/', label: '首页' },
|
|
@@ -80,9 +80,20 @@ const themeLabels: Record<string, string> = {
|
|
|
80
80
|
sunset: '日落',
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
const getStoredTheme = () =>
|
|
83
|
+
const getStoredTheme = () => {
|
|
84
|
+
const storedTheme = localStorage.getItem(themeStorageKey)
|
|
85
|
+
return storedTheme && themes.includes(storedTheme) ? storedTheme : 'light'
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const getHeaderStorageKey = (path: string) => {
|
|
89
|
+
if (path === '/') {
|
|
90
|
+
return 'rue.base.header.hide.home'
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return `rue.base.header.hide.${path.replace(/^\//, '')}`
|
|
94
|
+
}
|
|
84
95
|
|
|
85
|
-
const getStoredHeaderHidden = () => localStorage.getItem(
|
|
96
|
+
const getStoredHeaderHidden = (path: string) => localStorage.getItem(getHeaderStorageKey(path)) === '1'
|
|
86
97
|
|
|
87
98
|
const applyTheme = (theme: string) => {
|
|
88
99
|
document.documentElement.setAttribute('data-theme', theme)
|
|
@@ -91,20 +102,48 @@ const applyTheme = (theme: string) => {
|
|
|
91
102
|
const ThemePicker: FC = () => {
|
|
92
103
|
const theme = ref(getStoredTheme())
|
|
93
104
|
|
|
105
|
+
const applyThemeState = () => {
|
|
106
|
+
const currentTheme = theme.value
|
|
107
|
+
const themePicker = document.querySelector<HTMLSelectElement>('[data-base-theme-picker="true"]')
|
|
108
|
+
|
|
109
|
+
applyTheme(currentTheme)
|
|
110
|
+
|
|
111
|
+
if (themePicker && themePicker.value !== currentTheme) {
|
|
112
|
+
themePicker.value = currentTheme
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const syncStoredTheme = () => {
|
|
117
|
+
theme.value = getStoredTheme()
|
|
118
|
+
requestAnimationFrame(applyThemeState)
|
|
119
|
+
}
|
|
120
|
+
|
|
94
121
|
useEffect(() => {
|
|
95
|
-
|
|
96
|
-
|
|
122
|
+
localStorage.setItem(themeStorageKey, theme.value)
|
|
123
|
+
requestAnimationFrame(applyThemeState)
|
|
124
|
+
|
|
125
|
+
window.addEventListener('storage', syncStoredTheme)
|
|
126
|
+
window.addEventListener('focus', syncStoredTheme)
|
|
127
|
+
document.addEventListener('visibilitychange', syncStoredTheme)
|
|
128
|
+
|
|
129
|
+
return () => {
|
|
130
|
+
window.removeEventListener('storage', syncStoredTheme)
|
|
131
|
+
window.removeEventListener('focus', syncStoredTheme)
|
|
132
|
+
document.removeEventListener('visibilitychange', syncStoredTheme)
|
|
133
|
+
}
|
|
134
|
+
}, [])
|
|
97
135
|
|
|
98
136
|
watch(
|
|
99
137
|
() => theme.value,
|
|
100
138
|
() => {
|
|
101
|
-
applyTheme(theme.value)
|
|
102
139
|
localStorage.setItem(themeStorageKey, theme.value)
|
|
140
|
+
requestAnimationFrame(applyThemeState)
|
|
103
141
|
},
|
|
104
142
|
)
|
|
105
143
|
|
|
106
144
|
return (
|
|
107
145
|
<select
|
|
146
|
+
data-base-theme-picker="true"
|
|
108
147
|
aria-label="切换主题"
|
|
109
148
|
className="select select-bordered select-sm w-40"
|
|
110
149
|
onChange={(event: Event) => {
|
|
@@ -112,7 +151,7 @@ const ThemePicker: FC = () => {
|
|
|
112
151
|
}}
|
|
113
152
|
>
|
|
114
153
|
{themes.map((name) => (
|
|
115
|
-
<option key={name} value={name}
|
|
154
|
+
<option key={name} value={name}>
|
|
116
155
|
{themeLabels[name] || name}
|
|
117
156
|
</option>
|
|
118
157
|
))}
|
|
@@ -123,11 +162,34 @@ const ThemePicker: FC = () => {
|
|
|
123
162
|
const Header: FC = () => {
|
|
124
163
|
const route = useRoute()
|
|
125
164
|
const currentPath = computed(() => route.get()?.path || '/')
|
|
165
|
+
const navClass = (path: string) =>
|
|
166
|
+
`btn btn-sm ${currentPath.get() === path ? 'btn-primary' : 'btn-ghost'}`.trim()
|
|
167
|
+
|
|
168
|
+
const applyHeaderNavState = () => {
|
|
169
|
+
const path = currentPath.get()
|
|
170
|
+
const navLinks = document.querySelectorAll<HTMLElement>('[data-base-nav-path]')
|
|
171
|
+
|
|
172
|
+
navLinks.forEach((link) => {
|
|
173
|
+
const targetPath = link.dataset.baseNavPath || ''
|
|
174
|
+
link.className = `btn btn-sm ${path === targetPath ? 'btn-primary' : 'btn-ghost'}`.trim()
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
watch(
|
|
179
|
+
() => currentPath.get(),
|
|
180
|
+
() => {
|
|
181
|
+
requestAnimationFrame(applyHeaderNavState)
|
|
182
|
+
},
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
useEffect(() => {
|
|
186
|
+
requestAnimationFrame(applyHeaderNavState)
|
|
187
|
+
}, [])
|
|
126
188
|
|
|
127
189
|
return (
|
|
128
|
-
<header className="sticky top-0 z-40 border-b border-base-300/70 bg-base-100/
|
|
129
|
-
<div className="content-width
|
|
130
|
-
<div className="
|
|
190
|
+
<header className="sticky top-0 z-40 border-b border-base-300/70 bg-base-100/90 backdrop-blur">
|
|
191
|
+
<div className="content-width flex flex-wrap items-center gap-3 py-3">
|
|
192
|
+
<div className="min-w-0 flex-1 md:flex-none">
|
|
131
193
|
<RouterLink to="/" className="flex min-w-0 items-center gap-3">
|
|
132
194
|
<div className="flex h-10 w-10 items-center justify-center rounded-2xl bg-primary text-primary-content shadow-lg shadow-primary/20">
|
|
133
195
|
R
|
|
@@ -137,18 +199,19 @@ const Header: FC = () => {
|
|
|
137
199
|
</div>
|
|
138
200
|
</RouterLink>
|
|
139
201
|
</div>
|
|
140
|
-
<nav className="
|
|
202
|
+
<nav className="order-3 flex w-full flex-wrap gap-2 md:order-none md:w-auto md:flex-1 md:justify-center">
|
|
141
203
|
{navItems.map((item) => (
|
|
142
204
|
<RouterLink
|
|
143
205
|
key={item.to}
|
|
144
206
|
to={item.to}
|
|
145
|
-
|
|
207
|
+
data-base-nav-path={item.to}
|
|
208
|
+
className={navClass(item.to)}
|
|
146
209
|
>
|
|
147
|
-
{item.label}
|
|
210
|
+
<span>{item.label}</span>
|
|
148
211
|
</RouterLink>
|
|
149
212
|
))}
|
|
150
213
|
</nav>
|
|
151
|
-
<div className="
|
|
214
|
+
<div className="flex items-center gap-2 md:ml-auto">
|
|
152
215
|
<ThemePicker />
|
|
153
216
|
</div>
|
|
154
217
|
</div>
|
|
@@ -174,26 +237,69 @@ const Footer: FC = () => (
|
|
|
174
237
|
const HeaderToggleButton: FC<{ hidden: boolean; onToggle: () => void }> = (props) => (
|
|
175
238
|
<button
|
|
176
239
|
type="button"
|
|
177
|
-
className="btn btn-circle btn-sm fixed left-3 top-3 z-50 border-base-300 bg-base-100/
|
|
178
|
-
aria-label={props.hidden ? '
|
|
179
|
-
title={props.hidden ? '
|
|
240
|
+
className="btn btn-circle btn-sm fixed left-3 top-3 z-50 border-base-300 bg-base-100/95 text-base-content shadow-lg backdrop-blur"
|
|
241
|
+
aria-label={props.hidden ? '显示头部和底部' : '隐藏头部和底部'}
|
|
242
|
+
title={props.hidden ? '显示头部和底部' : '隐藏头部和底部'}
|
|
180
243
|
onClick={props.onToggle}
|
|
181
244
|
>
|
|
182
|
-
{props.hidden ?
|
|
245
|
+
{props.hidden ? (
|
|
246
|
+
<svg
|
|
247
|
+
viewBox="0 0 24 24"
|
|
248
|
+
width="16"
|
|
249
|
+
height="16"
|
|
250
|
+
aria-hidden="true"
|
|
251
|
+
fill="none"
|
|
252
|
+
stroke="currentColor"
|
|
253
|
+
stroke-width="2"
|
|
254
|
+
stroke-linecap="round"
|
|
255
|
+
stroke-linejoin="round"
|
|
256
|
+
>
|
|
257
|
+
<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z" />
|
|
258
|
+
<circle cx="12" cy="12" r="3" />
|
|
259
|
+
</svg>
|
|
260
|
+
) : (
|
|
261
|
+
<svg
|
|
262
|
+
viewBox="0 0 24 24"
|
|
263
|
+
width="16"
|
|
264
|
+
height="16"
|
|
265
|
+
aria-hidden="true"
|
|
266
|
+
fill="none"
|
|
267
|
+
stroke="currentColor"
|
|
268
|
+
stroke-width="2"
|
|
269
|
+
stroke-linecap="round"
|
|
270
|
+
stroke-linejoin="round"
|
|
271
|
+
>
|
|
272
|
+
<path d="M3 3l18 18" />
|
|
273
|
+
<path d="M10.6 10.7a3 3 0 0 0 4.2 4.2" />
|
|
274
|
+
<path d="M9.9 5.1A10.9 10.9 0 0 1 12 5c6.5 0 10 7 10 7a21.7 21.7 0 0 1-4 5.2" />
|
|
275
|
+
<path d="M6.6 6.7C4.2 8.3 2.7 10.9 2 12c0 0 3.5 7 10 7 1.8 0 3.4-.5 4.8-1.3" />
|
|
276
|
+
</svg>
|
|
277
|
+
)}
|
|
183
278
|
</button>
|
|
184
279
|
)
|
|
185
280
|
|
|
186
281
|
const SiteLayout: FC = (props) => {
|
|
187
282
|
const route = useRoute()
|
|
188
|
-
const reportHeaderHidden = ref(getStoredHeaderHidden())
|
|
189
283
|
const currentPath = computed(() => route.get()?.path || '/')
|
|
190
|
-
const
|
|
191
|
-
const
|
|
284
|
+
const allowHeaderToggle = computed(() => headerToggleRoutes.includes(currentPath.get()))
|
|
285
|
+
const reportHeaderHidden = ref(getStoredHeaderHidden(currentPath.get()))
|
|
286
|
+
const hideHeader = computed(() => allowHeaderToggle.get() && reportHeaderHidden.value)
|
|
287
|
+
|
|
288
|
+
watch(
|
|
289
|
+
() => currentPath.get(),
|
|
290
|
+
(path) => {
|
|
291
|
+
reportHeaderHidden.value = allowHeaderToggle.get() ? getStoredHeaderHidden(path) : false
|
|
292
|
+
},
|
|
293
|
+
)
|
|
192
294
|
|
|
193
295
|
watch(
|
|
194
296
|
() => reportHeaderHidden.value,
|
|
195
297
|
() => {
|
|
196
|
-
|
|
298
|
+
if (!allowHeaderToggle.get()) {
|
|
299
|
+
return
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
localStorage.setItem(getHeaderStorageKey(currentPath.get()), reportHeaderHidden.value ? '1' : '0')
|
|
197
303
|
},
|
|
198
304
|
)
|
|
199
305
|
|
|
@@ -201,7 +307,7 @@ const SiteLayout: FC = (props) => {
|
|
|
201
307
|
<div className="app-shell">
|
|
202
308
|
<div className="app-surface">
|
|
203
309
|
{!hideHeader.get() && <Header />}
|
|
204
|
-
{
|
|
310
|
+
{allowHeaderToggle.get() && (
|
|
205
311
|
<HeaderToggleButton
|
|
206
312
|
hidden={hideHeader.get()}
|
|
207
313
|
onToggle={() => {
|
|
@@ -209,12 +315,10 @@ const SiteLayout: FC = (props) => {
|
|
|
209
315
|
}}
|
|
210
316
|
/>
|
|
211
317
|
)}
|
|
212
|
-
<main
|
|
213
|
-
className={`content-width pb-10 ${hideHeader.get() ? 'pt-6 sm:pt-8' : 'pt-6 sm:pt-8'}`}
|
|
214
|
-
>
|
|
318
|
+
<main className="content-width pb-10 pt-6 sm:pt-8">
|
|
215
319
|
{props.children}
|
|
216
320
|
</main>
|
|
217
|
-
<Footer />
|
|
321
|
+
{!hideHeader.get() && <Footer />}
|
|
218
322
|
</div>
|
|
219
323
|
</div>
|
|
220
324
|
)
|