weifuwu 0.87.0 → 0.89.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/README.md +111 -135
- package/dist/client/components/ActionSheet/ActionSheet.d.ts +1 -1
- package/dist/client/components/AiChat/AiChat.d.ts +3 -4
- package/dist/client/components/ChatInput/ChatInput.d.ts +5 -0
- package/dist/client/components/CitationCard/CitationCard.d.ts +1 -1
- package/dist/client/components/Command/Command.d.ts +1 -1
- package/dist/client/components/ContextMenu/ContextMenu.d.ts +1 -1
- package/dist/client/components/DatePicker/DatePicker.d.ts +1 -1
- package/dist/client/components/Dropdown/Dropdown.d.ts +1 -1
- package/dist/client/components/Editor/model/html.d.ts +10 -0
- package/dist/client/components/ImageCropper/ImageCropper.d.ts +6 -2
- package/dist/client/components/Menu/Menu.d.ts +1 -1
- package/dist/client/components/NavMenu/NavMenu.d.ts +1 -1
- package/dist/client/components/Popconfirm/Popconfirm.d.ts +1 -1
- package/dist/client/components/SlideCanvas/SlideCanvas.d.ts +1 -1
- package/dist/client/components/Tour/Tour.d.ts +1 -1
- package/dist/client/components/TreeSelect/TreeSelect.d.ts +1 -1
- package/dist/client/components/index.js +41 -41
- package/dist/client/components/style.css +298 -644
- package/dist/client/layout/weifuwu-layout.css +286 -640
- package/dist/client/vdom/browser/Browser.d.ts +2 -0
- package/dist/client/vdom/commands.d.ts +4 -1
- package/dist/client/vdom/context/UIContext.d.ts +8 -0
- package/dist/client/vdom/core/diff/attrs.d.ts +10 -2
- package/dist/client/vdom/core/field/attributes.d.ts +12 -0
- package/dist/client/vdom/core/field/events.d.ts +8 -0
- package/dist/client/vdom/core/field/input-sync.d.ts +14 -0
- package/dist/client/vdom/core/field/key.d.ts +3 -0
- package/dist/client/vdom/core/field/props.d.ts +13 -1
- package/dist/client/vdom/core/field/ref.d.ts +6 -0
- package/dist/client/vdom/core/field/style.d.ts +7 -1
- package/dist/client/vdom/core/node/component.d.ts +3 -4
- package/dist/client/vdom/core/node/native.d.ts +6 -1
- package/dist/client/vdom/core/patch/index.d.ts +24 -1
- package/dist/client/vdom/core/patch/processors.d.ts +8 -1
- package/dist/client/vdom/core/protocol.d.ts +45 -0
- package/dist/client/vdom/core/router.d.ts +7 -0
- package/dist/client/vdom/core/ssr/absorb.d.ts +31 -4
- package/dist/client/vdom/core/transform/index.d.ts +6 -6
- package/dist/client/vdom/core/v2/cycle.d.ts +54 -0
- package/dist/client/vdom/core/v2/diff.d.ts +80 -0
- package/dist/client/vdom/core/v2/integrate.d.ts +24 -0
- package/dist/client/vdom/core/v2/render.d.ts +28 -0
- package/dist/client/vdom/core/v2/schedule.d.ts +33 -0
- package/dist/client/vdom/core/v2/serve.d.ts +4 -0
- package/dist/client/vdom/core/v2/spy.d.ts +21 -0
- package/dist/client/vdom/core/v2/ssr.d.ts +14 -0
- package/dist/client/vdom/core/vnode.d.ts +9 -6
- package/dist/client/vdom/dev/error-counter.d.ts +27 -0
- package/dist/client/vdom/dev/render-health.d.ts +60 -0
- package/dist/client/vdom/hooks/chat.d.ts +32 -5
- package/dist/client/vdom/hooks/drag-media.d.ts +3 -1
- package/dist/client/vdom/hooks/env.d.ts +32 -0
- package/dist/client/vdom/hooks/popup-manager.d.ts +21 -0
- package/dist/client/vdom/hooks/popup.d.ts +4 -1
- package/dist/client/vdom/hooks/stable.d.ts +4 -0
- package/dist/client/vdom/hooks/use-observable.d.ts +13 -0
- package/dist/client/vdom/index.d.ts +14 -6
- package/dist/client/vdom/index.js +4 -7
- package/dist/client/vdom/jsx-runtime.js +1 -1
- package/dist/client/vdom/middlewares/api.d.ts +8 -0
- package/dist/client/vdom/middlewares/auth-i18n.d.ts +7 -0
- package/dist/client/vdom/middlewares/ws.d.ts +41 -1
- package/dist/client/vdom/observable/index.d.ts +13 -0
- package/dist/client/vdom/observable/observable.d.ts +26 -0
- package/dist/client/vdom/observable/operators.d.ts +76 -0
- package/dist/client/vdom/observable/sources.d.ts +54 -0
- package/dist/client/vdom/observable/types.d.ts +45 -0
- package/dist/client/vdom/store.d.ts +17 -2
- package/dist/client/vdom/testing.js +2 -2
- package/dist/server/ai/agent.d.ts +18 -0
- package/dist/server/ai/client.d.ts +14 -2
- package/dist/server/ai/contracts.d.ts +2 -2
- package/dist/server/ai/sse.d.ts +6 -0
- package/dist/server/core/collect.d.ts +31 -0
- package/dist/server/core/error-counter.d.ts +11 -0
- package/dist/server/core/hub.d.ts +2 -0
- package/dist/server/core/router.d.ts +12 -9
- package/dist/server/db/contracts.d.ts +24 -0
- package/dist/server/db/memory-redis.d.ts +3 -4
- package/dist/server/db/memory-sql.d.ts +20 -4
- package/dist/server/db/postgres-server.d.ts +2 -0
- package/dist/server/db/query.d.ts +21 -0
- package/dist/server/db/redis-server.d.ts +4 -2
- package/dist/server/db/sql-parser.d.ts +2 -1
- package/dist/server/email/index.d.ts +2 -0
- package/dist/server/index.d.ts +4 -1
- package/dist/server/index.js +3927 -1982
- package/dist/server/messager/index.d.ts +21 -2
- package/dist/server/middleware/compress.d.ts +25 -0
- package/dist/server/ui/index.d.ts +8 -1
- package/dist/server/user/index.d.ts +4 -0
- package/dist/shared/router/chain.d.ts +9 -0
- package/dist/shared/router/context.d.ts +35 -0
- package/dist/shared/router/ctx-fields.d.ts +20 -0
- package/dist/shared/router/pipeline.d.ts +70 -0
- package/dist/shared/router/trie.d.ts +11 -3
- package/dist/shared/router/types.d.ts +23 -0
- package/docs/style-guide.md +44 -41
- package/package.json +13 -10
- package/content/apps/admin.md +0 -55
- package/content/apps/agent-platform.md +0 -35
- package/content/apps/auth.md +0 -49
- package/content/apps/multi.md +0 -44
- package/content/apps/todo.md +0 -51
- package/content/backend/ai.md +0 -28
- package/content/backend/auth.md +0 -20
- package/content/backend/email.md +0 -23
- package/content/backend/graphql.md +0 -23
- package/content/backend/http.md +0 -23
- package/content/backend/limit.md +0 -23
- package/content/backend/middleware.md +0 -17
- package/content/backend/queue.md +0 -23
- package/content/backend/redis.md +0 -26
- package/content/backend/router.md +0 -23
- package/content/backend/schedule.md +0 -23
- package/content/backend/sql.md +0 -27
- package/content/backend/sse.md +0 -26
- package/content/backend/ws.md +0 -26
- package/content/capabilities/app-node.md +0 -21
- package/content/capabilities/data.md +0 -21
- package/content/capabilities/events.md +0 -21
- package/content/capabilities/hooks.md +0 -22
- package/content/capabilities/i18n.md +0 -21
- package/content/capabilities/popup.md +0 -21
- package/content/capabilities/render-only.md +0 -23
- package/content/capabilities/router.md +0 -22
- package/content/capabilities/self-id.md +0 -21
- package/content/capabilities/store.md +0 -21
- package/content/capabilities/theme.md +0 -21
- package/content/capabilities/two-phase.md +0 -21
- package/content/components/accordion.md +0 -50
- package/content/components/actionsheet.md +0 -46
- package/content/components/affix.md +0 -51
- package/content/components/aichat.md +0 -57
- package/content/components/alert.md +0 -52
- package/content/components/alertgroup.md +0 -45
- package/content/components/anchor.md +0 -52
- package/content/components/app.md +0 -37
- package/content/components/approvalcard.md +0 -56
- package/content/components/appshell.md +0 -50
- package/content/components/aspectratio.md +0 -47
- package/content/components/authpage.md +0 -61
- package/content/components/autocomplete-v2.md +0 -40
- package/content/components/autocomplete.md +0 -56
- package/content/components/avatar.md +0 -51
- package/content/components/avatargroup.md +0 -47
- package/content/components/backtop.md +0 -53
- package/content/components/badge.md +0 -55
- package/content/components/breadcrumb.md +0 -49
- package/content/components/button.md +0 -63
- package/content/components/calendar-v2.md +0 -40
- package/content/components/calendar.md +0 -50
- package/content/components/card.md +0 -58
- package/content/components/carousel.md +0 -52
- package/content/components/cascader-v2.md +0 -39
- package/content/components/cascader.md +0 -54
- package/content/components/chart.md +0 -53
- package/content/components/chatinput.md +0 -63
- package/content/components/checkbox.md +0 -51
- package/content/components/checkboxgroup.md +0 -52
- package/content/components/citationcard.md +0 -48
- package/content/components/codeblock.md +0 -48
- package/content/components/codeeditor.md +0 -47
- package/content/components/collapse.md +0 -49
- package/content/components/colorpicker.md +0 -50
- package/content/components/command.md +0 -50
- package/content/components/confirm.md +0 -57
- package/content/components/contextmenu.md +0 -48
- package/content/components/copybutton.md +0 -51
- package/content/components/datepicker.md +0 -52
- package/content/components/descriptions-v2.md +0 -39
- package/content/components/descriptions.md +0 -52
- package/content/components/diffview-v2.md +0 -39
- package/content/components/diffview.md +0 -50
- package/content/components/divider.md +0 -48
- package/content/components/drawer.md +0 -55
- package/content/components/dropdown.md +0 -53
- package/content/components/editor.md +0 -49
- package/content/components/emptystate.md +0 -51
- package/content/components/exportcsv.md +0 -38
- package/content/components/field.md +0 -54
- package/content/components/filepreview-office.md +0 -37
- package/content/components/filepreview.md +0 -49
- package/content/components/filetree.md +0 -56
- package/content/components/fileupload-v2.md +0 -39
- package/content/components/fileupload.md +0 -57
- package/content/components/floatbutton.md +0 -52
- package/content/components/form-v2.md +0 -41
- package/content/components/form.md +0 -60
- package/content/components/grid.md +0 -51
- package/content/components/highlight-v2.md +0 -39
- package/content/components/highlight.md +0 -46
- package/content/components/hovercard.md +0 -51
- package/content/components/icon.md +0 -50
- package/content/components/imagecropper.md +0 -45
- package/content/components/img.md +0 -56
- package/content/components/infinitescroll-v2.md +0 -40
- package/content/components/infinitescroll.md +0 -54
- package/content/components/input.md +0 -65
- package/content/components/inputnumber.md +0 -58
- package/content/components/inview.md +0 -56
- package/content/components/jsonschemaform.md +0 -53
- package/content/components/jsonviewer-v2.md +0 -39
- package/content/components/jsonviewer.md +0 -49
- package/content/components/kanban.md +0 -46
- package/content/components/label.md +0 -48
- package/content/components/layout.md +0 -54
- package/content/components/layoutcontent.md +0 -43
- package/content/components/layoutheader.md +0 -43
- package/content/components/layoutsider.md +0 -43
- package/content/components/link.md +0 -52
- package/content/components/list.md +0 -44
- package/content/components/loading.md +0 -45
- package/content/components/logviewer-v2.md +0 -39
- package/content/components/logviewer.md +0 -53
- package/content/components/markdown.md +0 -51
- package/content/components/markdowneditor.md +0 -47
- package/content/components/math.md +0 -42
- package/content/components/mentions-v2.md +0 -39
- package/content/components/mentions.md +0 -52
- package/content/components/menu.md +0 -56
- package/content/components/menubar.md +0 -44
- package/content/components/messagebubble.md +0 -50
- package/content/components/modal.md +0 -59
- package/content/components/navmenu.md +0 -47
- package/content/components/notification.md +0 -51
- package/content/components/pageheader.md +0 -53
- package/content/components/pagination.md +0 -49
- package/content/components/paragraph.md +0 -45
- package/content/components/passwordinput.md +0 -57
- package/content/components/pininput-v2.md +0 -39
- package/content/components/pininput.md +0 -50
- package/content/components/pipeline.md +0 -49
- package/content/components/popconfirm.md +0 -58
- package/content/components/popover.md +0 -61
- package/content/components/progressbar.md +0 -50
- package/content/components/prompttemplate.md +0 -49
- package/content/components/qrcode.md +0 -50
- package/content/components/radiogroup.md +0 -53
- package/content/components/rate.md +0 -53
- package/content/components/reasoningblock.md +0 -47
- package/content/components/relationgraph.md +0 -52
- package/content/components/resizable.md +0 -53
- package/content/components/result.md +0 -49
- package/content/components/scrollbar.md +0 -49
- package/content/components/searchinput.md +0 -51
- package/content/components/segmentedcontrol.md +0 -59
- package/content/components/select-searchable.md +0 -47
- package/content/components/select.md +0 -66
- package/content/components/sessionlist.md +0 -52
- package/content/components/sheetgrid.md +0 -44
- package/content/components/skeleton.md +0 -55
- package/content/components/slidecanvas.md +0 -44
- package/content/components/slider.md +0 -62
- package/content/components/sortablelist.md +0 -39
- package/content/components/space.md +0 -52
- package/content/components/sparkline.md +0 -52
- package/content/components/statcard-countdown.md +0 -39
- package/content/components/statcard.md +0 -56
- package/content/components/steps.md +0 -48
- package/content/components/switch.md +0 -50
- package/content/components/tabbar.md +0 -45
- package/content/components/table-v2.md +0 -40
- package/content/components/table.md +0 -66
- package/content/components/tabs.md +0 -56
- package/content/components/tag.md +0 -51
- package/content/components/tagsinput-v2.md +0 -39
- package/content/components/tagsinput.md +0 -54
- package/content/components/text.md +0 -45
- package/content/components/textarea.md +0 -57
- package/content/components/themeswitch.md +0 -69
- package/content/components/timeline.md +0 -50
- package/content/components/title.md +0 -45
- package/content/components/toast.md +0 -52
- package/content/components/toggle-togglegroup.md +0 -52
- package/content/components/togglegroup.md +0 -47
- package/content/components/toolcallcard.md +0 -51
- package/content/components/tooltip.md +0 -50
- package/content/components/tour.md +0 -50
- package/content/components/transfer.md +0 -52
- package/content/components/tree-v2.md +0 -39
- package/content/components/tree.md +0 -59
- package/content/components/treeselect.md +0 -54
- package/content/components/typography.md +0 -51
- package/content/components/videoplayer.md +0 -52
- package/content/components/virtuallist.md +0 -52
- package/content/components/virtualtable-v2.md +0 -39
- package/content/components/virtualtable.md +0 -55
- package/content/components/watermark.md +0 -53
- package/content/components/wave.md +0 -43
- package/content/guides/ai-chat-family.md +0 -24
- package/content/guides/choose.md +0 -67
- package/content/guides/component-model.md +0 -32
- package/content/guides/component-standards.md +0 -123
- package/content/guides/components-guide.md +0 -813
- package/content/guides/custom-component.md +0 -321
- package/content/guides/data-guide.md +0 -281
- package/content/guides/file-preview-family.md +0 -18
- package/content/guides/frontend.md +0 -968
- package/content/guides/layout-choice.md +0 -64
- package/content/guides/layout-guide.md +0 -241
- package/content/guides/middleware.md +0 -459
- package/content/guides/mobile-guide.md +0 -105
- package/content/guides/page-building.md +0 -98
- package/content/guides/production.md +0 -37
- package/content/guides/quality.md +0 -60
- package/content/guides/realtime-guide.md +0 -296
- package/content/guides/render-only.md +0 -44
- package/content/guides/saas-guide.md +0 -253
- package/content/guides/server-guide.md +0 -359
- package/content/guides/start.md +0 -41
- package/content/guides/styling.md +0 -154
- package/content/guides/ui-dom-guide.md +0 -157
- package/content/index.json +0 -4344
- package/content/index.md +0 -181
- package/content/layout/align.md +0 -20
- package/content/layout/anchor.md +0 -19
- package/content/layout/app-shell.md +0 -23
- package/content/layout/border.md +0 -20
- package/content/layout/center.md +0 -19
- package/content/layout/cluster.md +0 -19
- package/content/layout/container.md +0 -19
- package/content/layout/fill.md +0 -19
- package/content/layout/grid.md +0 -19
- package/content/layout/hidden.md +0 -22
- package/content/layout/layer.md +0 -19
- package/content/layout/position.md +0 -22
- package/content/layout/row.md +0 -23
- package/content/layout/safe-area.md +0 -20
- package/content/layout/scroll.md +0 -21
- package/content/layout/spacing.md +0 -22
- package/content/layout/split.md +0 -19
- package/content/layout/stack.md +0 -19
- package/content/layout/surface.md +0 -23
- package/content/layout/text.md +0 -22
- package/content/patterns/app-shell.md +0 -34
- package/content/patterns/dashboard.md +0 -36
- package/content/patterns/data-screen.md +0 -30
- package/content/patterns/detail-page.md +0 -31
- package/content/patterns/docs.md +0 -34
- package/content/patterns/focus-task.md +0 -34
- package/content/patterns/landing.md +0 -35
- package/content/patterns/list-page.md +0 -32
- package/content/patterns/mobile.md +0 -31
- package/content/patterns/settings-page.md +0 -33
- package/content/patterns/workspace.md +0 -32
- package/dist/cli/content-sync.test.d.ts +0 -1
- package/dist/cli/docs-cli.test.d.ts +0 -1
- package/dist/cli/docs.d.ts +0 -2
- package/dist/cli/docs.mjs +0 -10961
- package/dist/client/vdom/core/build.d.ts +0 -36
- package/dist/client/vdom/core/diff/children.d.ts +0 -29
- package/dist/client/vdom/core/diff/index.d.ts +0 -22
- package/dist/client/vdom/core/diff/output.d.ts +0 -30
- package/dist/client/vdom/core/diff/same.d.ts +0 -22
- package/dist/client/vdom/core/serve.d.ts +0 -75
- package/dist/client/vdom/core/ssr/index.d.ts +0 -25
- package/examples/apps/admin/README.md +0 -23
- package/examples/apps/admin/api.ts +0 -27
- package/examples/apps/admin/app.tsx +0 -144
- package/examples/apps/admin/main.tsx +0 -8
- package/examples/apps/admin/server.ts +0 -37
- package/examples/apps/auth/README.md +0 -23
- package/examples/apps/auth/api.ts +0 -49
- package/examples/apps/auth/app.tsx +0 -137
- package/examples/apps/auth/main.tsx +0 -8
- package/examples/apps/auth/server.ts +0 -37
- package/examples/apps/multi/README.md +0 -23
- package/examples/apps/multi/app.tsx +0 -83
- package/examples/apps/multi/main.tsx +0 -8
- package/examples/apps/multi/server.ts +0 -33
- package/examples/apps/todo/README.md +0 -23
- package/examples/apps/todo/api.ts +0 -26
- package/examples/apps/todo/app.tsx +0 -128
- package/examples/apps/todo/main.tsx +0 -9
- package/examples/apps/todo/server.ts +0 -39
- package/examples/hello-world/README.md +0 -9
- package/examples/hello-world/client.ts +0 -14
- package/examples/hello-world/routes.tsx +0 -26
- package/examples/hello-world/server.ts +0 -46
- package/examples/patterns/AppShell.tsx +0 -224
- package/examples/patterns/Dashboard.tsx +0 -153
- package/examples/patterns/DataScreen.tsx +0 -104
- package/examples/patterns/DetailPage.tsx +0 -79
- package/examples/patterns/Docs.tsx +0 -105
- package/examples/patterns/FocusTask.tsx +0 -67
- package/examples/patterns/Landing.tsx +0 -90
- package/examples/patterns/ListPage.tsx +0 -77
- package/examples/patterns/Mobile.tsx +0 -154
- package/examples/patterns/SettingsPage.tsx +0 -98
- package/examples/patterns/SplitWorkspace.tsx +0 -113
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* auth 应用模板——登录/注册 → 受保护页 → 登出(完整全栈,复制即用)
|
|
3
|
-
*
|
|
4
|
-
* 演示能力:
|
|
5
|
-
* - 应用级状态(createStore user)+ 路由守卫(未登录 → 登录页)
|
|
6
|
-
* - 表单校验 + 提交 loading + 错误条(AuthPage 组件)
|
|
7
|
-
* - 后端:内存用户表 + 会话(api.ts 注册函数——独立/嵌入共享)
|
|
8
|
-
* - 会话持久化:ctx.browser.storage(浏览器纪律)
|
|
9
|
-
*/
|
|
10
|
-
import { UIRouter, uiServe, h, createStore, createClientBrowser } from 'weifuwu/vdom'
|
|
11
|
-
import type { Component } from 'weifuwu/vdom'
|
|
12
|
-
import { AuthPage, Input, Button, Alert, PageHeader, Avatar, Space } from 'weifuwu/components'
|
|
13
|
-
|
|
14
|
-
export interface User { id: string; email: string; name: string }
|
|
15
|
-
|
|
16
|
-
export const authStore = createStore<{ user: User | null; error: string | null }>({ user: null, error: null })
|
|
17
|
-
|
|
18
|
-
async function api(browser: any, path: string, body: Record<string, string>): Promise<{ ok: boolean; user?: User; error?: string }> {
|
|
19
|
-
const token = browser.storageGet('auth:token')
|
|
20
|
-
const res = await fetch(path, {
|
|
21
|
-
method: 'POST',
|
|
22
|
-
headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) },
|
|
23
|
-
body: JSON.stringify(body),
|
|
24
|
-
})
|
|
25
|
-
return res.json()
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** 登录/注册页(AuthPage 组件——同一骨架两用) */
|
|
29
|
-
const AuthFormPage = (mode: 'login' | 'register'): Component =>
|
|
30
|
-
async (_init: any, ctx: any) => {
|
|
31
|
-
const browser = ctx.browser
|
|
32
|
-
let email = ''
|
|
33
|
-
let password = ''
|
|
34
|
-
let name = ''
|
|
35
|
-
let error = ''
|
|
36
|
-
let loading = false
|
|
37
|
-
const submit = async () => {
|
|
38
|
-
error = ''
|
|
39
|
-
if (!email.includes('@')) { error = '请输入有效邮箱'; ctx.render(); return }
|
|
40
|
-
if (password.length < 6) { error = '密码至少 6 位'; ctx.render(); return }
|
|
41
|
-
loading = true; ctx.render()
|
|
42
|
-
const res = await api(browser, `/api/auth/${mode}`, { email, password, name })
|
|
43
|
-
loading = false
|
|
44
|
-
if (res.ok && res.user) {
|
|
45
|
-
authStore.update((s) => { s.user = res.user!; s.error = null })
|
|
46
|
-
browser.storageSet('auth:token', (res as any).token ?? '')
|
|
47
|
-
location.hash = '#/'
|
|
48
|
-
} else {
|
|
49
|
-
error = res.error ?? '请求失败'
|
|
50
|
-
}
|
|
51
|
-
ctx.render()
|
|
52
|
-
}
|
|
53
|
-
return async (_p: any) => (
|
|
54
|
-
<AuthPage
|
|
55
|
-
title={mode === 'login' ? '登录' : '注册'}
|
|
56
|
-
subtitle="auth 应用模板——内存用户 + 会话(演示)"
|
|
57
|
-
logo={<Avatar name="wf" />}
|
|
58
|
-
submitLabel={mode === 'login' ? '登录' : '注册'}
|
|
59
|
-
loading={loading}
|
|
60
|
-
error={error || null}
|
|
61
|
-
onSubmit={() => void submit()}
|
|
62
|
-
footer={<a href={mode === 'login' ? '#/register' : '#/login'} class="wf-link" style="cursor:pointer">{mode === 'login' ? '没有账号?注册' : '已有账号?登录'}</a>}
|
|
63
|
-
>
|
|
64
|
-
{mode === 'register' && (
|
|
65
|
-
<Input label="昵称" value={name} onInput={(e: any) => { name = (e.target as HTMLInputElement).value; ctx.render() }} placeholder="如何称呼你" />
|
|
66
|
-
)}
|
|
67
|
-
<Input label="邮箱" type="email" value={email} onInput={(e: any) => { email = (e.target as HTMLInputElement).value; ctx.render() }} placeholder="name@example.com" required />
|
|
68
|
-
<Input label="密码" type="password" value={password} onInput={(e: any) => { password = (e.target as HTMLInputElement).value; ctx.render() }} placeholder="至少 6 位" />
|
|
69
|
-
</AuthPage>
|
|
70
|
-
)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export const LoginPage = AuthFormPage('login')
|
|
74
|
-
export const RegisterPage = AuthFormPage('register')
|
|
75
|
-
|
|
76
|
-
/** 受保护页(未登录 → 登录页——路由守卫) */
|
|
77
|
-
export const DashboardPage: Component = async (_init: any, ctx: any) => {
|
|
78
|
-
const browser = ctx.browser
|
|
79
|
-
// 会话恢复(mount——ctx.browser.storage 安全适配)
|
|
80
|
-
void (async () => {
|
|
81
|
-
try {
|
|
82
|
-
const token = browser.storageGet('auth:token')
|
|
83
|
-
if (!token) return
|
|
84
|
-
const res = await fetch('/api/auth/me', { headers: { Authorization: `Bearer ${token}` } })
|
|
85
|
-
if (res.ok) {
|
|
86
|
-
const { user } = await res.json()
|
|
87
|
-
authStore.update((s) => { s.user = user })
|
|
88
|
-
}
|
|
89
|
-
} catch { /* 无会话 */ }
|
|
90
|
-
})()
|
|
91
|
-
const state = ctx.ui.useExternal(authStore)
|
|
92
|
-
const logout = () => {
|
|
93
|
-
browser.storageRemove('auth:token')
|
|
94
|
-
authStore.update((s) => { s.user = null })
|
|
95
|
-
location.hash = '#/login'
|
|
96
|
-
}
|
|
97
|
-
return async (_p: any) => {
|
|
98
|
-
if (!state.user) return h(LoginPage, {})
|
|
99
|
-
const u = state.user
|
|
100
|
-
return (
|
|
101
|
-
<div class="wf-stack wf-gap-md" style="max-width:560px">
|
|
102
|
-
<PageHeader
|
|
103
|
-
title={`欢迎,${u.name ?? u.email}`}
|
|
104
|
-
sub="登录态由 createStore 持有——路由守卫读它决定渲染"
|
|
105
|
-
>
|
|
106
|
-
<Button variant="ghost" onClick={logout}>登出</Button>
|
|
107
|
-
</PageHeader>
|
|
108
|
-
<div class="wf-surface wf-border wf-rounded-md wf-p-md wf-stack wf-gap-sm">
|
|
109
|
-
<Space align="center" size="md"><Avatar name={u.name ?? u.email} size="lg" /><b>{u.email}</b></Space>
|
|
110
|
-
<Alert variant="success">✓ 已登录——受保护内容可见。刷新页面会话自动恢复(ctx.browser.storage)</Alert>
|
|
111
|
-
</div>
|
|
112
|
-
</div>
|
|
113
|
-
)
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/** 路由守卫:包装页面——未登录跳登录(守卫逻辑在页面组件内读 store) */
|
|
118
|
-
export const guard = (page: Component): Component => page
|
|
119
|
-
|
|
120
|
-
// ── 路由表 ──
|
|
121
|
-
export const authRoutes = [
|
|
122
|
-
{ path: '/', render: () => h(DashboardPage, {}) },
|
|
123
|
-
{ path: '/login', render: () => h(LoginPage, {}) },
|
|
124
|
-
{ path: '/register', render: () => h(RegisterPage, {}) },
|
|
125
|
-
]
|
|
126
|
-
|
|
127
|
-
export const pathFromHash = (): string => location.hash.replace(/^#/, '') || '/'
|
|
128
|
-
|
|
129
|
-
export function createAuthApp(root: HTMLElement, _options?: { history?: boolean }): ReturnType<typeof uiServe> {
|
|
130
|
-
// vdom 规范面:UIRouter + uiServe——布局共享精准路由
|
|
131
|
-
const router = new UIRouter()
|
|
132
|
-
for (const r of authRoutes) {
|
|
133
|
-
router.get(r.path, (req: Request, ctx: any) =>
|
|
134
|
-
(ctx as { stream: (v: unknown) => Response }).stream(r.render()))
|
|
135
|
-
}
|
|
136
|
-
return uiServe(router, { root, browser: createClientBrowser()! })
|
|
137
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* auth 独立入口——复制本目录即可运行:
|
|
3
|
-
* cd examples/apps/auth && node server.ts → http://localhost:3301
|
|
4
|
-
*/
|
|
5
|
-
import { createAuthApp, pathFromHash } from './app.tsx'
|
|
6
|
-
|
|
7
|
-
const sub = createAuthApp(document.querySelector('#root') as HTMLElement)
|
|
8
|
-
window.addEventListener('hashchange', () => sub.navigate(pathFromHash()))
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* auth 独立 server——内存用户 + 会话(契约层:生产换 postgres() + userSystem)
|
|
3
|
-
* cd examples/apps/auth && node server.ts → http://localhost:3301
|
|
4
|
-
*/
|
|
5
|
-
import { serve, Router, ui, createMemorySql } from 'weifuwu'
|
|
6
|
-
import { resolve, dirname } from 'node:path'
|
|
7
|
-
import { fileURLToPath } from 'node:url'
|
|
8
|
-
import { registerAuthApi, ensureAuthTables } from './api.ts'
|
|
9
|
-
|
|
10
|
-
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
11
|
-
const sql = createMemorySql()
|
|
12
|
-
await ensureAuthTables(sql)
|
|
13
|
-
|
|
14
|
-
const app = new Router()
|
|
15
|
-
app.use(ui())
|
|
16
|
-
app.get('/app.js', (req, ctx) => ctx.ui.js(resolve(__dirname, 'main.tsx')))
|
|
17
|
-
registerAuthApi(app, sql)
|
|
18
|
-
|
|
19
|
-
app.get('/*', async (req, ctx) => ctx.ui.html`
|
|
20
|
-
<!DOCTYPE html>
|
|
21
|
-
<html lang="zh-CN">
|
|
22
|
-
<head>
|
|
23
|
-
<meta charset="UTF-8">
|
|
24
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
25
|
-
<link rel="stylesheet" href="/components.css">
|
|
26
|
-
<title>auth 应用模板</title>
|
|
27
|
-
</head>
|
|
28
|
-
<body>
|
|
29
|
-
<div id="root"></div>
|
|
30
|
-
<script src="/app.js"></script>
|
|
31
|
-
</body>
|
|
32
|
-
</html>
|
|
33
|
-
`)
|
|
34
|
-
app.get('/components.css', (req, ctx) => ctx.ui.css('weifuwu/components/style.css'))
|
|
35
|
-
|
|
36
|
-
serve(app, { port: 3301 })
|
|
37
|
-
console.log('auth 模板 → http://localhost:3301')
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
# multi 应用模板(复制即用)
|
|
2
|
-
|
|
3
|
-
```bash
|
|
4
|
-
# 1. 安装依赖(weifuwu 包——含 content/ 离线文档 + examples/ 模板源码)
|
|
5
|
-
npm i weifuwu
|
|
6
|
-
|
|
7
|
-
# 2. 复制本目录到你的项目(或直接在 examples/apps/multi 内开发)
|
|
8
|
-
# 3. 启动
|
|
9
|
-
node server.ts # → http://localhost:multi/3300 3301 3302 3303
|
|
10
|
-
```
|
|
11
|
-
|
|
12
|
-
## 改造指南
|
|
13
|
-
|
|
14
|
-
见 `node_modules/weifuwu/content/apps/multi.md`(目录结构 + 逐步改造步骤)。
|
|
15
|
-
|
|
16
|
-
## 结构
|
|
17
|
-
|
|
18
|
-
| 文件 | 职责 |
|
|
19
|
-
|------|------|
|
|
20
|
-
| app.tsx | 前端:路由表 + 页面组件 + 状态 |
|
|
21
|
-
| api.ts | 后端:API 注册函数(独立/嵌入共享) |
|
|
22
|
-
| server.ts | 独立入口(MemorySql——生产换 postgres() 一行) |
|
|
23
|
-
| main.tsx | 独立前端入口 |
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* multi 应用模板——应用编排(父应用嵌子应用:registerApp/hApp + 独立状态)
|
|
3
|
-
*
|
|
4
|
-
* 演示能力(design/vdom3-app-node.md):
|
|
5
|
-
* - registerApp 注册子应用(工厂闭包持有实例状态——appId 复用不重跑)
|
|
6
|
-
* - hApp(<App appId />)在父树渲染子应用——app:mount/update 边界事件
|
|
7
|
-
* - 子应用独立状态:各自 let + render(),互不干扰
|
|
8
|
-
* 纯前端编排——无后端(模板结构最简形态:app.tsx + server.ts + main.tsx)
|
|
9
|
-
*/
|
|
10
|
-
import { UIRouter, uiServe, h, createClientBrowser } from 'weifuwu/vdom'
|
|
11
|
-
import type { Component } from 'weifuwu/vdom'
|
|
12
|
-
import { Button, Input, Tag, PageHeader, Space } from 'weifuwu/components'
|
|
13
|
-
|
|
14
|
-
// ── 子应用 1:计数器(独立状态) ──
|
|
15
|
-
const Counter = async (_init: any, ctx: any) => {
|
|
16
|
-
let count = 0
|
|
17
|
-
let step = 1
|
|
18
|
-
return async (_p: any) => (
|
|
19
|
-
<div class="wf-stack wf-gap-sm">
|
|
20
|
-
<Space align="center"><Tag variant="primary">子应用 1</Tag><b>{count}</b></Space>
|
|
21
|
-
<div class="wf-row wf-gap-xs">
|
|
22
|
-
<Button size="sm" onClick={() => { count -= step; ctx.render() }}>-{step}</Button>
|
|
23
|
-
<Button size="sm" variant="primary" onClick={() => { count += step; ctx.render() }}>+{step}</Button>
|
|
24
|
-
<Button size="sm" variant="ghost" onClick={() => { step = step === 1 ? 5 : 1; ctx.render() }}>步长 {step}→{step === 1 ? 5 : 1}</Button>
|
|
25
|
-
</div>
|
|
26
|
-
</div>
|
|
27
|
-
)
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// ── 子应用 2:迷你任务(独立状态——与计数器互不干扰) ──
|
|
31
|
-
const MiniTodo = async (_init: any, ctx: any) => {
|
|
32
|
-
let items: string[] = ['子应用任务 A', '子应用任务 B']
|
|
33
|
-
let input = ''
|
|
34
|
-
return async (_p: any) => (
|
|
35
|
-
<div class="wf-stack wf-gap-sm">
|
|
36
|
-
<Tag variant="success">子应用 2</Tag>
|
|
37
|
-
<div class="wf-row wf-gap-xs">
|
|
38
|
-
<input class="wf-input" style="flex:1" value={input}
|
|
39
|
-
onInput={(e: any) => { input = (e.target as HTMLInputElement).value; ctx.render() }}
|
|
40
|
-
placeholder="新任务…" />
|
|
41
|
-
<Button size="sm" onClick={() => { if (input.trim()) { items.push(input.trim()); input = ''; ctx.render() } }}>添加</Button>
|
|
42
|
-
</div>
|
|
43
|
-
<ul style="margin:0;padding-left:16px">
|
|
44
|
-
{items.map((it, i) => <li key={it + i} class="wf-text-sm">{it}</li>)}
|
|
45
|
-
</ul>
|
|
46
|
-
</div>
|
|
47
|
-
)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// ── 父应用:工作台(嵌入子应用 + 边界事件观测) ──
|
|
51
|
-
export const WorkbenchPage: Component = async (_init: any, ctx: any) => {
|
|
52
|
-
return async (_p: any) => (
|
|
53
|
-
<div class="wf-stack wf-gap-md">
|
|
54
|
-
<PageHeader title="应用编排工作台" sub="父应用嵌入子应用——registerApp 注册 · <App appId /> 渲染 · 独立状态" />
|
|
55
|
-
<div class="wf-grid" style="--wf-cols:repeat(auto-fit,minmax(240px,1fr));--wf-gap:12px">
|
|
56
|
-
<div class="wf-surface wf-border wf-rounded-md wf-p-md wf-stack wf-gap-sm">
|
|
57
|
-
<b class="wf-text-sm">计数器(子应用 1)</b>
|
|
58
|
-
{h(Counter, {})}
|
|
59
|
-
</div>
|
|
60
|
-
<div class="wf-surface wf-border wf-rounded-md wf-p-md wf-stack wf-gap-sm">
|
|
61
|
-
<b class="wf-text-sm">迷你任务(子应用 2)</b>
|
|
62
|
-
{h(MiniTodo, {})}
|
|
63
|
-
</div>
|
|
64
|
-
</div>
|
|
65
|
-
</div>
|
|
66
|
-
)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export const multiRoutes = [
|
|
70
|
-
{ path: '/', render: () => h(WorkbenchPage, {}) },
|
|
71
|
-
]
|
|
72
|
-
|
|
73
|
-
export const pathFromHash = (): string => location.hash.replace(/^#/, '') || '/'
|
|
74
|
-
|
|
75
|
-
export function createMultiApp(root: HTMLElement, _options?: { history?: boolean }): ReturnType<typeof uiServe> {
|
|
76
|
-
// vdom 规范面:UIRouter + uiServe——布局共享精准路由
|
|
77
|
-
const router = new UIRouter()
|
|
78
|
-
for (const r of multiRoutes) {
|
|
79
|
-
router.get(r.path, (req: Request, ctx: any) =>
|
|
80
|
-
(ctx as { stream: (v: unknown) => Response }).stream(r.render()))
|
|
81
|
-
}
|
|
82
|
-
return uiServe(router, { root, browser: createClientBrowser()! })
|
|
83
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* multi 独立入口——复制本目录即可运行:
|
|
3
|
-
* cd examples/apps/multi && node server.ts → http://localhost:3303
|
|
4
|
-
*/
|
|
5
|
-
import { createMultiApp, pathFromHash } from './app.tsx'
|
|
6
|
-
|
|
7
|
-
const sub = createMultiApp(document.querySelector('#root') as HTMLElement)
|
|
8
|
-
window.addEventListener('hashchange', () => sub.navigate(pathFromHash()))
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* multi 独立 server——纯前端编排(无后端 API)
|
|
3
|
-
* cd examples/apps/multi && node server.ts → http://localhost:3303
|
|
4
|
-
*/
|
|
5
|
-
import { serve, Router, ui } from 'weifuwu'
|
|
6
|
-
import { resolve, dirname } from 'node:path'
|
|
7
|
-
import { fileURLToPath } from 'node:url'
|
|
8
|
-
|
|
9
|
-
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
10
|
-
|
|
11
|
-
const app = new Router()
|
|
12
|
-
app.use(ui())
|
|
13
|
-
app.get('/app.js', (req, ctx) => ctx.ui.js(resolve(__dirname, 'main.tsx')))
|
|
14
|
-
|
|
15
|
-
app.get('/*', async (req, ctx) => ctx.ui.html`
|
|
16
|
-
<!DOCTYPE html>
|
|
17
|
-
<html lang="zh-CN">
|
|
18
|
-
<head>
|
|
19
|
-
<meta charset="UTF-8">
|
|
20
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
21
|
-
<link rel="stylesheet" href="/components.css">
|
|
22
|
-
<title>multi 应用模板</title>
|
|
23
|
-
</head>
|
|
24
|
-
<body>
|
|
25
|
-
<div id="root"></div>
|
|
26
|
-
<script src="/app.js"></script>
|
|
27
|
-
</body>
|
|
28
|
-
</html>
|
|
29
|
-
`)
|
|
30
|
-
app.get('/components.css', (req, ctx) => ctx.ui.css('weifuwu/components/style.css'))
|
|
31
|
-
|
|
32
|
-
serve(app, { port: 3303 })
|
|
33
|
-
console.log('multi 模板 → http://localhost:3303')
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
# todo 应用模板(复制即用)
|
|
2
|
-
|
|
3
|
-
```bash
|
|
4
|
-
# 1. 安装依赖(weifuwu 包——含 content/ 离线文档 + examples/ 模板源码)
|
|
5
|
-
npm i weifuwu
|
|
6
|
-
|
|
7
|
-
# 2. 复制本目录到你的项目(或直接在 examples/apps/todo 内开发)
|
|
8
|
-
# 3. 启动
|
|
9
|
-
node server.ts # → http://localhost:todo/3300 3301 3302 3303
|
|
10
|
-
```
|
|
11
|
-
|
|
12
|
-
## 改造指南
|
|
13
|
-
|
|
14
|
-
见 `node_modules/weifuwu/content/apps/todo.md`(目录结构 + 逐步改造步骤)。
|
|
15
|
-
|
|
16
|
-
## 结构
|
|
17
|
-
|
|
18
|
-
| 文件 | 职责 |
|
|
19
|
-
|------|------|
|
|
20
|
-
| app.tsx | 前端:路由表 + 页面组件 + 状态 |
|
|
21
|
-
| api.ts | 后端:API 注册函数(独立/嵌入共享) |
|
|
22
|
-
| server.ts | 独立入口(MemorySql——生产换 postgres() 一行) |
|
|
23
|
-
| main.tsx | 独立前端入口 |
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* todo 后端 API——可复用注册函数(独立 server 与 showcase 嵌入共享同一实现)
|
|
3
|
-
* 契约:registerTodoApi(app, sql)——sql 为 Sql 契约(MemorySql/postgres 均可)
|
|
4
|
-
*/
|
|
5
|
-
import type { Router } from 'weifuwu'
|
|
6
|
-
|
|
7
|
-
export function registerTodoApi(app: Router, sql: any): void {
|
|
8
|
-
app.get('/api/todos', async () => {
|
|
9
|
-
const rows = await sql`SELECT * FROM todos ORDER BY id DESC`
|
|
10
|
-
return Response.json({ rows })
|
|
11
|
-
})
|
|
12
|
-
app.post('/api/todos', async (req: Request) => {
|
|
13
|
-
const { name } = await req.json()
|
|
14
|
-
const rows = await sql`INSERT INTO todos (name) VALUES (${String(name)}) RETURNING id, name, done`
|
|
15
|
-
return Response.json({ row: rows[0] }, { status: 201 })
|
|
16
|
-
})
|
|
17
|
-
app.patch('/api/todos/:id', async (req: Request, ctx: any) => {
|
|
18
|
-
const { done } = await req.json()
|
|
19
|
-
await sql`UPDATE todos SET done = ${!!done} WHERE id = ${Number((ctx as any).params.id)}`
|
|
20
|
-
return Response.json({ ok: true })
|
|
21
|
-
})
|
|
22
|
-
app.delete('/api/todos/:id', async (req: Request, ctx: any) => {
|
|
23
|
-
await sql`DELETE FROM todos WHERE id = ${Number((ctx as any).params.id)}`
|
|
24
|
-
return Response.json({ ok: true })
|
|
25
|
-
})
|
|
26
|
-
}
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* todo 应用模板——完整全栈(复制即用,随 npm 包发布 examples/apps/todo/)
|
|
3
|
-
*
|
|
4
|
-
* 演示(design/showcase-plan.md §P2):
|
|
5
|
-
* - 多页路由(/ 列表 + /new 新建)+ createStore 跨页状态
|
|
6
|
-
* - 后端:MemorySql 持久化(契约层——生产换 postgres() 一行)
|
|
7
|
-
* - 嵌入方式:createRouter(routes, root, { history: false })——showcase 页面内嵌
|
|
8
|
-
*/
|
|
9
|
-
import { UIRouter, uiServe, h, createStore, createClientBrowser } from 'weifuwu/vdom'
|
|
10
|
-
import type { Component } from 'weifuwu/vdom'
|
|
11
|
-
import { Button, Input, Checkbox, Tag, EmptyState, PageHeader, Form, Field, Alert } from 'weifuwu/components'
|
|
12
|
-
|
|
13
|
-
export interface Todo {
|
|
14
|
-
id: string
|
|
15
|
-
name: string
|
|
16
|
-
done: boolean
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export const todoStore = createStore<{ todos: Todo[]; loading: boolean; error: string | null }>({
|
|
20
|
-
todos: [],
|
|
21
|
-
loading: true,
|
|
22
|
-
error: null,
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
export async function loadTodos(): Promise<void> {
|
|
26
|
-
todoStore.update((s) => { s.loading = true; s.error = null })
|
|
27
|
-
try {
|
|
28
|
-
const res = await fetch('/api/todos')
|
|
29
|
-
const data = await res.json()
|
|
30
|
-
todoStore.update((s) => { s.todos = data.rows; s.loading = false })
|
|
31
|
-
} catch (e) {
|
|
32
|
-
todoStore.update((s) => { s.error = (e as Error).message; s.loading = false })
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** 列表页 */
|
|
37
|
-
export const TodoList: Component = async (_init: any, ctx: any) => {
|
|
38
|
-
void loadTodos()
|
|
39
|
-
const state = ctx.ui.useExternal(todoStore)
|
|
40
|
-
const toggle = async (t: Todo) => {
|
|
41
|
-
await fetch(`/api/todos/${t.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ done: !t.done }) })
|
|
42
|
-
await loadTodos()
|
|
43
|
-
}
|
|
44
|
-
const del = async (t: Todo) => {
|
|
45
|
-
await fetch(`/api/todos/${t.id}`, { method: 'DELETE' })
|
|
46
|
-
await loadTodos()
|
|
47
|
-
}
|
|
48
|
-
return async (_p: any) => (
|
|
49
|
-
<div class="wf-stack wf-gap-md">
|
|
50
|
-
<PageHeader title="任务列表" sub={`${state.todos.filter((t: Todo) => !t.done).length} 个未完成`}>
|
|
51
|
-
<Button variant="primary" onClick={() => location.hash = '#/new'}>新建任务</Button>
|
|
52
|
-
</PageHeader>
|
|
53
|
-
{state.error && <Alert variant="error">{state.error}</Alert>}
|
|
54
|
-
{state.loading && <div class="wf-text-secondary">加载中…</div>}
|
|
55
|
-
{!state.loading && state.todos.length === 0 && <EmptyState text="暂无任务——点击「新建任务」开始" />}
|
|
56
|
-
<div class="wf-stack wf-gap-xs">
|
|
57
|
-
{state.todos.map((t: Todo) => (
|
|
58
|
-
<div key={t.id} class="wf-surface wf-border wf-rounded-sm wf-p-sm wf-row wf-between">
|
|
59
|
-
<div class="wf-row wf-gap-sm">
|
|
60
|
-
<Checkbox checked={t.done} onChange={() => void toggle(t)} />
|
|
61
|
-
<span class={t.done ? 'wf-text-tertiary' : ''} style={t.done ? 'text-decoration:line-through' : ''}>{t.name}</span>
|
|
62
|
-
</div>
|
|
63
|
-
<div class="wf-row wf-gap-xs">
|
|
64
|
-
{t.done && <Tag variant="success">完成</Tag>}
|
|
65
|
-
<Button size="sm" variant="ghost" onClick={() => void del(t)}>删除</Button>
|
|
66
|
-
</div>
|
|
67
|
-
</div>
|
|
68
|
-
))}
|
|
69
|
-
</div>
|
|
70
|
-
</div>
|
|
71
|
-
)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** 新建页 */
|
|
75
|
-
export const TodoNew: Component = async (_init: any, ctx: any) => {
|
|
76
|
-
let name = ''
|
|
77
|
-
let error = ''
|
|
78
|
-
let saving = false
|
|
79
|
-
const submit = async () => {
|
|
80
|
-
if (!name.trim()) { error = '任务名不能为空'; ctx.render(); return }
|
|
81
|
-
saving = true; error = ''; ctx.render()
|
|
82
|
-
try {
|
|
83
|
-
await fetch('/api/todos', {
|
|
84
|
-
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
85
|
-
body: JSON.stringify({ name: name.trim() }),
|
|
86
|
-
})
|
|
87
|
-
await loadTodos()
|
|
88
|
-
location.hash = '#/'
|
|
89
|
-
} catch (e) {
|
|
90
|
-
error = (e as Error).message; saving = false; ctx.render()
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return async (_p: any) => (
|
|
94
|
-
<div class="wf-stack wf-gap-md" style="max-width:480px">
|
|
95
|
-
<PageHeader title="新建任务" sub="填写任务名称——提交后写入 MemorySql" />
|
|
96
|
-
<Form onSubmit={() => void submit()}>
|
|
97
|
-
<Field label="任务名称" error={error}>
|
|
98
|
-
<Input value={name} onInput={(e: any) => { name = (e.target as HTMLInputElement).value; ctx.render() }}
|
|
99
|
-
placeholder="例如:完成 showcase Phase 2" autoFocus />
|
|
100
|
-
</Field>
|
|
101
|
-
<div class="wf-row wf-gap-sm">
|
|
102
|
-
<Button variant="primary" loading={saving} onClick={() => void submit()}>保存</Button>
|
|
103
|
-
<Button variant="ghost" onClick={() => location.hash = '#/'}>取消</Button>
|
|
104
|
-
</div>
|
|
105
|
-
</Form>
|
|
106
|
-
</div>
|
|
107
|
-
)
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// ── 路由表(layout 包裹 + 参数——模板可复制部分) ──
|
|
111
|
-
export const todoRoutes = [
|
|
112
|
-
{ path: '/', render: () => h(TodoList, {}) },
|
|
113
|
-
{ path: '/new', render: () => h(TodoNew, {}) },
|
|
114
|
-
]
|
|
115
|
-
|
|
116
|
-
/** hash → 内部路径(模板导航约定:location.hash = '#/new'——独立运行可深链/嵌入不污染宿主 pathname) */
|
|
117
|
-
export const pathFromHash = (): string => location.hash.replace(/^#/, '') || '/'
|
|
118
|
-
|
|
119
|
-
/** 应用入口:createTodoApp(root)——独立运行(main.tsx)与嵌入(showcase)复用 */
|
|
120
|
-
export function createTodoApp(root: HTMLElement, _options?: { history?: boolean }): ReturnType<typeof uiServe> {
|
|
121
|
-
// vdom 规范面:UIRouter + uiServe——布局共享精准路由
|
|
122
|
-
const router = new UIRouter()
|
|
123
|
-
for (const r of todoRoutes) {
|
|
124
|
-
router.get(r.path, (req: Request, ctx: any) =>
|
|
125
|
-
(ctx as { stream: (v: unknown) => Response }).stream(r.render()))
|
|
126
|
-
}
|
|
127
|
-
return uiServe(router, { root, browser: createClientBrowser()! })
|
|
128
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* todo 应用模板独立入口——复制本目录即可运行:
|
|
3
|
-
* cd examples/apps/todo && node server.ts → http://localhost:3300
|
|
4
|
-
* hash 桥接:hashchange → 内部路由(独立运行可深链 #/new)
|
|
5
|
-
*/
|
|
6
|
-
import { createTodoApp, pathFromHash } from './app.tsx'
|
|
7
|
-
|
|
8
|
-
const sub = createTodoApp(document.querySelector('#root') as HTMLElement)
|
|
9
|
-
window.addEventListener('hashchange', () => sub.navigate(pathFromHash()))
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* todo 后端——MemorySql CRUD(契约层:演示用内存实现——生产换 postgres() 一行)
|
|
3
|
-
* 独立运行:node server.ts → http://localhost:3300
|
|
4
|
-
*/
|
|
5
|
-
import { serve, Router, ui, createMemorySql } from 'weifuwu'
|
|
6
|
-
import { resolve, dirname } from 'node:path'
|
|
7
|
-
import { fileURLToPath } from 'node:url'
|
|
8
|
-
import { registerTodoApi } from './api.ts'
|
|
9
|
-
|
|
10
|
-
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
11
|
-
const sql = createMemorySql()
|
|
12
|
-
|
|
13
|
-
await sql.unsafe('CREATE TABLE IF NOT EXISTS todos (id serial PRIMARY KEY, name text, done boolean DEFAULT false)')
|
|
14
|
-
|
|
15
|
-
const app = new Router()
|
|
16
|
-
app.use(ui())
|
|
17
|
-
app.get('/app.js', (req, ctx) => ctx.ui.js(resolve(__dirname, 'main.tsx')))
|
|
18
|
-
|
|
19
|
-
registerTodoApi(app, sql)
|
|
20
|
-
|
|
21
|
-
app.get('/*', async (req, ctx) => ctx.ui.html`
|
|
22
|
-
<!DOCTYPE html>
|
|
23
|
-
<html lang="zh-CN">
|
|
24
|
-
<head>
|
|
25
|
-
<meta charset="UTF-8">
|
|
26
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
27
|
-
<link rel="stylesheet" href="/components.css">
|
|
28
|
-
<title>todo 应用模板</title>
|
|
29
|
-
</head>
|
|
30
|
-
<body>
|
|
31
|
-
<div id="root"></div>
|
|
32
|
-
<script src="/app.js"></script>
|
|
33
|
-
</body>
|
|
34
|
-
</html>
|
|
35
|
-
`)
|
|
36
|
-
app.get('/components.css', (req, ctx) => ctx.ui.css('weifuwu/components/style.css'))
|
|
37
|
-
|
|
38
|
-
serve(app, { port: 3300 })
|
|
39
|
-
console.log('todo 模板 → http://localhost:3300')
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* client.ts——客户端入口(纯 SPA 渲染:vdom3 事件流引擎)
|
|
3
|
-
* createRouter(routes, root) —— 监听 location → 匹配 → 事件流渲染
|
|
4
|
-
*/
|
|
5
|
-
import { UIRouter, uiServe } from 'weifuwu/vdom'
|
|
6
|
-
import { routes } from './routes.tsx'
|
|
7
|
-
|
|
8
|
-
// 路由声明 → UIRouter(get 挂载)→ uiServe 渲染(vdom 规范面)
|
|
9
|
-
const router = new UIRouter()
|
|
10
|
-
for (const r of routes) {
|
|
11
|
-
router.get(r.path, (req: Request, ctx: any) =>
|
|
12
|
-
(ctx as { stream: (v: unknown) => Response }).stream(r.render()))
|
|
13
|
-
}
|
|
14
|
-
uiServe(router, { root: document.querySelector('#root') as HTMLElement })
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* routes.tsx——路由声明(两阶段异步组件)
|
|
3
|
-
*
|
|
4
|
-
* 新手必读(概念见 content/guides/component-model.md):
|
|
5
|
-
* - 外层 async 工厂 = mount(只一次):await 数据 / 初始化状态
|
|
6
|
-
* - 内层 async renderFn = 每次渲染:读最新 props / 输出视图
|
|
7
|
-
* - 状态 = 普通对象 let + 改后 ctx.render()(render-only)
|
|
8
|
-
*/
|
|
9
|
-
import { h } from 'weifuwu/vdom'
|
|
10
|
-
|
|
11
|
-
// 两阶段异步组件:工厂层 await 数据(ctx.data——缓存 + 并发合并)
|
|
12
|
-
const Home = async (_init: any, ctx: any) => {
|
|
13
|
-
const data = await ctx.data.get('/api/hello')
|
|
14
|
-
let count = 0 // 组件状态(render-only)
|
|
15
|
-
return async (_props: any) =>
|
|
16
|
-
h('div', { class: 'wf-container wf-stack', style: '--wf-max:640px;--wf-gap:16px;padding:40px 16px;text-align:center' },
|
|
17
|
-
h('h1', { class: 'wf-text-3xl wf-m-0' }, 'weifuwu hello-world'),
|
|
18
|
-
h('p', { class: 'wf-text-secondary' }, `数据管道:${(data as any).msg}`),
|
|
19
|
-
h('button', {
|
|
20
|
-
class: 'wf-btn wf-btn--primary',
|
|
21
|
-
onClick: () => { count++; ctx.render() }, // 改状态 → 显式 render()
|
|
22
|
-
}, `点击 ${count} 次`),
|
|
23
|
-
)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export const routes = [{ path: '/', render: () => h(Home, {}) }]
|