create-windy 0.6.8 → 0.6.9
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 +152 -24
- package/package.json +1 -1
- package/template/.windy-template.json +2 -2
- package/template/README.md +4 -2
- package/template/apps/server/src/runtime-development.ts +1 -0
- package/template/apps/web/src/layout/AppSidebar.vue +1 -0
- package/template/apps/web/src/pages/ui/UiComponentPage.vue +7 -0
- package/template/apps/web/src/pages/ui/UiComponentPage.webtest.ts +94 -0
- package/template/apps/web/src/pages/ui/component-catalog.ts +248 -0
- package/template/apps/web/src/pages/ui/components/ComponentPreviewCard.vue +50 -0
- package/template/apps/web/src/pages/ui/components/DataComponentShowcases.vue +131 -0
- package/template/apps/web/src/pages/ui/components/FeedbackComponentShowcases.vue +158 -0
- package/template/apps/web/src/pages/ui/components/FoundationComponentShowcases.vue +129 -0
- package/template/apps/web/src/pages/ui/components/NavigationComponentShowcases.vue +155 -0
- package/template/apps/web/src/pages/ui/components/ProjectComponentShowcases.vue +31 -0
- package/template/apps/web/src/pages/ui/components/UiComponentGallery.vue +238 -0
- package/template/apps/web/src/router/index.ts +2 -0
- package/template/apps/web/src/router/manifest-routes.webtest.ts +1 -0
- package/template/apps/web/src/router/platform-admin-route-bindings.ts +3 -1
- package/template/docs/development/business-web-routes.md +3 -1
- package/template/docs/development/ui-component-gallery.md +44 -0
- package/template/docs/operations/incremental-delivery.md +10 -9
- package/template/packages/modules/package.json +1 -1
- package/template/packages/modules/src/system-admin-routes.ts +8 -0
- package/template/packages/modules/src/system-admin-ui-catalog.ts +1 -1
- package/template/packages/modules/src/system-features.ts +5 -0
- package/template/packages/modules/src/system-module-catalog.ts +2 -1
- package/template/packages/modules/src/system-modules.ts +11 -0
- package/template/packages/modules/src/system-permissions.ts +2 -0
- package/template/packages/modules/src/system-ui-components.test.ts +36 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ArrowRightIcon, PlusIcon, Trash2Icon } from "@lucide/vue";
|
|
3
|
+
import { shallowRef } from "vue";
|
|
4
|
+
import { Badge } from "@/components/ui/badge";
|
|
5
|
+
import { Button } from "@/components/ui/button";
|
|
6
|
+
import { Checkbox } from "@/components/ui/checkbox";
|
|
7
|
+
import { Input } from "@/components/ui/input";
|
|
8
|
+
import { Separator } from "@/components/ui/separator";
|
|
9
|
+
import { Skeleton } from "@/components/ui/skeleton";
|
|
10
|
+
import { Switch } from "@/components/ui/switch";
|
|
11
|
+
import { componentCatalogById, type ComponentId } from "../component-catalog";
|
|
12
|
+
import ComponentPreviewCard from "./ComponentPreviewCard.vue";
|
|
13
|
+
|
|
14
|
+
const props = defineProps<{
|
|
15
|
+
visibleIds: readonly ComponentId[];
|
|
16
|
+
}>();
|
|
17
|
+
|
|
18
|
+
const query = shallowRef("");
|
|
19
|
+
const selected = shallowRef(true);
|
|
20
|
+
const enabled = shallowRef(true);
|
|
21
|
+
|
|
22
|
+
function isVisible(id: ComponentId) {
|
|
23
|
+
return props.visibleIds.includes(id);
|
|
24
|
+
}
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<template>
|
|
28
|
+
<ComponentPreviewCard
|
|
29
|
+
v-if="isVisible('button')"
|
|
30
|
+
:item="componentCatalogById.button"
|
|
31
|
+
>
|
|
32
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
33
|
+
<Button><PlusIcon />新建记录</Button>
|
|
34
|
+
<Button variant="outline">取消</Button>
|
|
35
|
+
<Button variant="secondary">保存草稿</Button>
|
|
36
|
+
<Button variant="destructive"><Trash2Icon />删除</Button>
|
|
37
|
+
<Button variant="link">查看规范<ArrowRightIcon /></Button>
|
|
38
|
+
</div>
|
|
39
|
+
</ComponentPreviewCard>
|
|
40
|
+
|
|
41
|
+
<ComponentPreviewCard
|
|
42
|
+
v-if="isVisible('badge')"
|
|
43
|
+
:item="componentCatalogById.badge"
|
|
44
|
+
>
|
|
45
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
46
|
+
<Badge>已启用</Badge>
|
|
47
|
+
<Badge variant="secondary">草稿</Badge>
|
|
48
|
+
<Badge variant="outline">待处理</Badge>
|
|
49
|
+
<Badge variant="destructive">异常</Badge>
|
|
50
|
+
</div>
|
|
51
|
+
</ComponentPreviewCard>
|
|
52
|
+
|
|
53
|
+
<ComponentPreviewCard
|
|
54
|
+
v-if="isVisible('input')"
|
|
55
|
+
:item="componentCatalogById.input"
|
|
56
|
+
>
|
|
57
|
+
<label class="grid w-full max-w-sm gap-2 text-sm font-medium">
|
|
58
|
+
搜索组件
|
|
59
|
+
<Input v-model="query" placeholder="输入名称或用途" />
|
|
60
|
+
<span class="font-normal text-muted-foreground">
|
|
61
|
+
当前输入:{{ query || "暂无" }}
|
|
62
|
+
</span>
|
|
63
|
+
</label>
|
|
64
|
+
</ComponentPreviewCard>
|
|
65
|
+
|
|
66
|
+
<ComponentPreviewCard
|
|
67
|
+
v-if="isVisible('checkbox')"
|
|
68
|
+
:item="componentCatalogById.checkbox"
|
|
69
|
+
>
|
|
70
|
+
<label class="flex items-center gap-3 text-sm">
|
|
71
|
+
<Checkbox v-model="selected" aria-label="包含已停用组件" />
|
|
72
|
+
<span>
|
|
73
|
+
<span class="block font-medium">包含已停用组件</span>
|
|
74
|
+
<span class="text-muted-foreground">
|
|
75
|
+
{{ selected ? "已包含" : "仅显示可用组件" }}
|
|
76
|
+
</span>
|
|
77
|
+
</span>
|
|
78
|
+
</label>
|
|
79
|
+
</ComponentPreviewCard>
|
|
80
|
+
|
|
81
|
+
<ComponentPreviewCard
|
|
82
|
+
v-if="isVisible('switch')"
|
|
83
|
+
:item="componentCatalogById.switch"
|
|
84
|
+
>
|
|
85
|
+
<div
|
|
86
|
+
class="flex w-full items-center justify-between gap-4 rounded-lg border bg-background p-3"
|
|
87
|
+
>
|
|
88
|
+
<div>
|
|
89
|
+
<p class="text-sm font-medium">组件预览</p>
|
|
90
|
+
<p class="text-xs text-muted-foreground">
|
|
91
|
+
{{ enabled ? "预览已启用" : "预览已暂停" }}
|
|
92
|
+
</p>
|
|
93
|
+
</div>
|
|
94
|
+
<Switch v-model="enabled" aria-label="切换组件预览" />
|
|
95
|
+
</div>
|
|
96
|
+
</ComponentPreviewCard>
|
|
97
|
+
|
|
98
|
+
<ComponentPreviewCard
|
|
99
|
+
v-if="isVisible('separator')"
|
|
100
|
+
:item="componentCatalogById.separator"
|
|
101
|
+
>
|
|
102
|
+
<div class="w-full max-w-sm text-sm">
|
|
103
|
+
<p class="font-medium">组件信息</p>
|
|
104
|
+
<p class="mt-1 text-muted-foreground">由 Windy Web 统一维护。</p>
|
|
105
|
+
<Separator class="my-4" />
|
|
106
|
+
<div class="flex h-5 items-center gap-3">
|
|
107
|
+
<span>稳定</span>
|
|
108
|
+
<Separator orientation="vertical" />
|
|
109
|
+
<span>可访问</span>
|
|
110
|
+
<Separator orientation="vertical" />
|
|
111
|
+
<span>可复用</span>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
</ComponentPreviewCard>
|
|
115
|
+
|
|
116
|
+
<ComponentPreviewCard
|
|
117
|
+
v-if="isVisible('skeleton')"
|
|
118
|
+
:item="componentCatalogById.skeleton"
|
|
119
|
+
>
|
|
120
|
+
<div class="flex w-full max-w-sm items-center gap-3">
|
|
121
|
+
<Skeleton class="size-10 rounded-full" />
|
|
122
|
+
<div class="grid flex-1 gap-2">
|
|
123
|
+
<Skeleton class="h-3 w-1/3" />
|
|
124
|
+
<Skeleton class="h-3 w-full" />
|
|
125
|
+
<Skeleton class="h-3 w-4/5" />
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
</ComponentPreviewCard>
|
|
129
|
+
</template>
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
BellIcon,
|
|
4
|
+
ChevronDownIcon,
|
|
5
|
+
LayoutDashboardIcon,
|
|
6
|
+
SettingsIcon,
|
|
7
|
+
} from "@lucide/vue";
|
|
8
|
+
import { shallowRef } from "vue";
|
|
9
|
+
import {
|
|
10
|
+
Breadcrumb,
|
|
11
|
+
BreadcrumbItem,
|
|
12
|
+
BreadcrumbLink,
|
|
13
|
+
BreadcrumbList,
|
|
14
|
+
BreadcrumbPage,
|
|
15
|
+
BreadcrumbSeparator,
|
|
16
|
+
} from "@/components/ui/breadcrumb";
|
|
17
|
+
import { Button } from "@/components/ui/button";
|
|
18
|
+
import {
|
|
19
|
+
DropdownMenu,
|
|
20
|
+
DropdownMenuCheckboxItem,
|
|
21
|
+
DropdownMenuContent,
|
|
22
|
+
DropdownMenuItem,
|
|
23
|
+
DropdownMenuLabel,
|
|
24
|
+
DropdownMenuSeparator,
|
|
25
|
+
DropdownMenuShortcut,
|
|
26
|
+
DropdownMenuTrigger,
|
|
27
|
+
} from "@/components/ui/dropdown-menu";
|
|
28
|
+
import {
|
|
29
|
+
SidebarGroup,
|
|
30
|
+
SidebarGroupContent,
|
|
31
|
+
SidebarGroupLabel,
|
|
32
|
+
SidebarMenu,
|
|
33
|
+
SidebarMenuBadge,
|
|
34
|
+
SidebarMenuItem,
|
|
35
|
+
} from "@/components/ui/sidebar";
|
|
36
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
37
|
+
import { componentCatalogById, type ComponentId } from "../component-catalog";
|
|
38
|
+
import ComponentPreviewCard from "./ComponentPreviewCard.vue";
|
|
39
|
+
|
|
40
|
+
const props = defineProps<{
|
|
41
|
+
visibleIds: readonly ComponentId[];
|
|
42
|
+
}>();
|
|
43
|
+
|
|
44
|
+
const compactMode = shallowRef(false);
|
|
45
|
+
|
|
46
|
+
function isVisible(id: ComponentId) {
|
|
47
|
+
return props.visibleIds.includes(id);
|
|
48
|
+
}
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<template>
|
|
52
|
+
<ComponentPreviewCard
|
|
53
|
+
v-if="isVisible('breadcrumb')"
|
|
54
|
+
:item="componentCatalogById.breadcrumb"
|
|
55
|
+
>
|
|
56
|
+
<Breadcrumb>
|
|
57
|
+
<BreadcrumbList>
|
|
58
|
+
<BreadcrumbItem>
|
|
59
|
+
<BreadcrumbLink href="/admin">工作台</BreadcrumbLink>
|
|
60
|
+
</BreadcrumbItem>
|
|
61
|
+
<BreadcrumbSeparator />
|
|
62
|
+
<BreadcrumbItem>
|
|
63
|
+
<BreadcrumbLink href="/ui/component">UI 组件</BreadcrumbLink>
|
|
64
|
+
</BreadcrumbItem>
|
|
65
|
+
<BreadcrumbSeparator />
|
|
66
|
+
<BreadcrumbItem>
|
|
67
|
+
<BreadcrumbPage>面包屑</BreadcrumbPage>
|
|
68
|
+
</BreadcrumbItem>
|
|
69
|
+
</BreadcrumbList>
|
|
70
|
+
</Breadcrumb>
|
|
71
|
+
</ComponentPreviewCard>
|
|
72
|
+
|
|
73
|
+
<ComponentPreviewCard
|
|
74
|
+
v-if="isVisible('tabs')"
|
|
75
|
+
:item="componentCatalogById.tabs"
|
|
76
|
+
>
|
|
77
|
+
<Tabs default-value="preview" class="w-full">
|
|
78
|
+
<TabsList aria-label="组件页签示例">
|
|
79
|
+
<TabsTrigger value="preview">预览</TabsTrigger>
|
|
80
|
+
<TabsTrigger value="usage">使用说明</TabsTrigger>
|
|
81
|
+
</TabsList>
|
|
82
|
+
<TabsContent
|
|
83
|
+
value="preview"
|
|
84
|
+
class="rounded-lg border bg-background p-3 text-sm"
|
|
85
|
+
>
|
|
86
|
+
在真实主题和间距系统中检查组件。
|
|
87
|
+
</TabsContent>
|
|
88
|
+
<TabsContent
|
|
89
|
+
value="usage"
|
|
90
|
+
class="rounded-lg border bg-background p-3 text-sm"
|
|
91
|
+
>
|
|
92
|
+
从组件目录导入,不复制基础组件源码。
|
|
93
|
+
</TabsContent>
|
|
94
|
+
</Tabs>
|
|
95
|
+
</ComponentPreviewCard>
|
|
96
|
+
|
|
97
|
+
<ComponentPreviewCard
|
|
98
|
+
v-if="isVisible('dropdown-menu')"
|
|
99
|
+
:item="componentCatalogById['dropdown-menu']"
|
|
100
|
+
>
|
|
101
|
+
<DropdownMenu>
|
|
102
|
+
<DropdownMenuTrigger as-child>
|
|
103
|
+
<Button variant="outline">页面操作<ChevronDownIcon /></Button>
|
|
104
|
+
</DropdownMenuTrigger>
|
|
105
|
+
<DropdownMenuContent align="start" class="w-52">
|
|
106
|
+
<DropdownMenuLabel>显示设置</DropdownMenuLabel>
|
|
107
|
+
<DropdownMenuCheckboxItem v-model="compactMode">
|
|
108
|
+
紧凑模式
|
|
109
|
+
</DropdownMenuCheckboxItem>
|
|
110
|
+
<DropdownMenuSeparator />
|
|
111
|
+
<DropdownMenuItem>
|
|
112
|
+
刷新组件
|
|
113
|
+
<DropdownMenuShortcut>⌘R</DropdownMenuShortcut>
|
|
114
|
+
</DropdownMenuItem>
|
|
115
|
+
</DropdownMenuContent>
|
|
116
|
+
</DropdownMenu>
|
|
117
|
+
</ComponentPreviewCard>
|
|
118
|
+
|
|
119
|
+
<ComponentPreviewCard
|
|
120
|
+
v-if="isVisible('sidebar')"
|
|
121
|
+
:item="componentCatalogById.sidebar"
|
|
122
|
+
>
|
|
123
|
+
<div
|
|
124
|
+
class="w-full max-w-xs overflow-hidden rounded-lg border bg-sidebar text-sidebar-foreground"
|
|
125
|
+
>
|
|
126
|
+
<SidebarGroup>
|
|
127
|
+
<SidebarGroupLabel>组件导航</SidebarGroupLabel>
|
|
128
|
+
<SidebarGroupContent>
|
|
129
|
+
<SidebarMenu>
|
|
130
|
+
<SidebarMenuItem>
|
|
131
|
+
<Button
|
|
132
|
+
variant="ghost"
|
|
133
|
+
class="w-full justify-start bg-sidebar-accent"
|
|
134
|
+
>
|
|
135
|
+
<LayoutDashboardIcon />总览
|
|
136
|
+
</Button>
|
|
137
|
+
</SidebarMenuItem>
|
|
138
|
+
<SidebarMenuItem>
|
|
139
|
+
<Button variant="ghost" class="w-full justify-start">
|
|
140
|
+
<SettingsIcon />表单组件
|
|
141
|
+
</Button>
|
|
142
|
+
<SidebarMenuBadge class="top-1.5">7</SidebarMenuBadge>
|
|
143
|
+
</SidebarMenuItem>
|
|
144
|
+
<SidebarMenuItem>
|
|
145
|
+
<Button variant="ghost" class="w-full justify-start">
|
|
146
|
+
<BellIcon />反馈组件
|
|
147
|
+
</Button>
|
|
148
|
+
<SidebarMenuBadge class="top-1.5">5</SidebarMenuBadge>
|
|
149
|
+
</SidebarMenuItem>
|
|
150
|
+
</SidebarMenu>
|
|
151
|
+
</SidebarGroupContent>
|
|
152
|
+
</SidebarGroup>
|
|
153
|
+
</div>
|
|
154
|
+
</ComponentPreviewCard>
|
|
155
|
+
</template>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { Badge } from "@/components/ui/badge";
|
|
3
|
+
import type { ComponentCatalogItem } from "../component-catalog";
|
|
4
|
+
import ComponentPreviewCard from "./ComponentPreviewCard.vue";
|
|
5
|
+
|
|
6
|
+
defineProps<{
|
|
7
|
+
items: readonly ComponentCatalogItem[];
|
|
8
|
+
}>();
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<template>
|
|
12
|
+
<ComponentPreviewCard v-for="item in items" :key="item.id" :item="item">
|
|
13
|
+
<div class="grid w-full gap-3">
|
|
14
|
+
<p class="text-sm text-muted-foreground">
|
|
15
|
+
已从当前项目的组件入口自动发现。交互预览可在项目内按需补充。
|
|
16
|
+
</p>
|
|
17
|
+
<div class="flex flex-wrap gap-2">
|
|
18
|
+
<Badge
|
|
19
|
+
v-for="exportName in item.exports"
|
|
20
|
+
:key="exportName"
|
|
21
|
+
variant="outline"
|
|
22
|
+
>
|
|
23
|
+
{{ exportName }}
|
|
24
|
+
</Badge>
|
|
25
|
+
<Badge v-if="item.exports.length === 0" variant="secondary">
|
|
26
|
+
暂无具名导出
|
|
27
|
+
</Badge>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</ComponentPreviewCard>
|
|
31
|
+
</template>
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { BlocksIcon, SearchIcon, XIcon } from "@lucide/vue";
|
|
3
|
+
import { computed, shallowRef } from "vue";
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
5
|
+
import { Input } from "@/components/ui/input";
|
|
6
|
+
import {
|
|
7
|
+
componentCatalog,
|
|
8
|
+
componentCategories,
|
|
9
|
+
type ComponentCategoryId,
|
|
10
|
+
} from "../component-catalog";
|
|
11
|
+
import DataComponentShowcases from "./DataComponentShowcases.vue";
|
|
12
|
+
import FeedbackComponentShowcases from "./FeedbackComponentShowcases.vue";
|
|
13
|
+
import FoundationComponentShowcases from "./FoundationComponentShowcases.vue";
|
|
14
|
+
import NavigationComponentShowcases from "./NavigationComponentShowcases.vue";
|
|
15
|
+
import ProjectComponentShowcases from "./ProjectComponentShowcases.vue";
|
|
16
|
+
|
|
17
|
+
type CategoryFilter = "all" | ComponentCategoryId;
|
|
18
|
+
|
|
19
|
+
const searchQuery = shallowRef("");
|
|
20
|
+
const activeCategory = shallowRef<CategoryFilter>("all");
|
|
21
|
+
const normalizedQuery = computed(() =>
|
|
22
|
+
searchQuery.value.trim().toLocaleLowerCase(),
|
|
23
|
+
);
|
|
24
|
+
const visibleComponents = computed(() =>
|
|
25
|
+
componentCatalog.filter((component) => {
|
|
26
|
+
if (
|
|
27
|
+
activeCategory.value !== "all" &&
|
|
28
|
+
component.category !== activeCategory.value
|
|
29
|
+
) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
if (!normalizedQuery.value) return true;
|
|
33
|
+
return [
|
|
34
|
+
component.name,
|
|
35
|
+
component.label,
|
|
36
|
+
component.description,
|
|
37
|
+
component.exportPath,
|
|
38
|
+
...component.keywords,
|
|
39
|
+
].some((value) =>
|
|
40
|
+
value.toLocaleLowerCase().includes(normalizedQuery.value),
|
|
41
|
+
);
|
|
42
|
+
}),
|
|
43
|
+
);
|
|
44
|
+
const visibleIds = computed(() => visibleComponents.value.map(({ id }) => id));
|
|
45
|
+
const visibleProjectComponents = computed(() =>
|
|
46
|
+
visibleComponents.value.filter(({ category }) => category === "project"),
|
|
47
|
+
);
|
|
48
|
+
const visibleCategories = computed(() =>
|
|
49
|
+
componentCategories.filter((category) =>
|
|
50
|
+
visibleComponents.value.some(
|
|
51
|
+
(component) => component.category === category.id,
|
|
52
|
+
),
|
|
53
|
+
),
|
|
54
|
+
);
|
|
55
|
+
const categoryCounts = Object.fromEntries(
|
|
56
|
+
componentCategories.map((category) => [
|
|
57
|
+
category.id,
|
|
58
|
+
componentCatalog.filter((component) => component.category === category.id)
|
|
59
|
+
.length,
|
|
60
|
+
]),
|
|
61
|
+
) as Readonly<Record<ComponentCategoryId, number>>;
|
|
62
|
+
|
|
63
|
+
function selectCategory(category: CategoryFilter) {
|
|
64
|
+
activeCategory.value = category;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function clearFilters() {
|
|
68
|
+
searchQuery.value = "";
|
|
69
|
+
activeCategory.value = "all";
|
|
70
|
+
}
|
|
71
|
+
</script>
|
|
72
|
+
|
|
73
|
+
<template>
|
|
74
|
+
<div class="mx-auto grid w-full max-w-7xl gap-6">
|
|
75
|
+
<header
|
|
76
|
+
class="overflow-hidden rounded-2xl border bg-card text-card-foreground"
|
|
77
|
+
>
|
|
78
|
+
<div
|
|
79
|
+
class="grid gap-6 p-6 lg:grid-cols-[minmax(0,1fr)_minmax(24rem,0.72fr)] lg:p-8"
|
|
80
|
+
>
|
|
81
|
+
<div class="max-w-2xl">
|
|
82
|
+
<div
|
|
83
|
+
class="mb-4 flex size-11 items-center justify-center rounded-xl bg-primary text-primary-foreground"
|
|
84
|
+
>
|
|
85
|
+
<BlocksIcon class="size-5" />
|
|
86
|
+
</div>
|
|
87
|
+
<p
|
|
88
|
+
class="text-xs font-semibold tracking-[0.18em] text-muted-foreground uppercase"
|
|
89
|
+
>
|
|
90
|
+
Windy UI Registry
|
|
91
|
+
</p>
|
|
92
|
+
<h1 class="mt-2 text-3xl font-semibold tracking-tight">组件总览</h1>
|
|
93
|
+
<p class="mt-3 max-w-xl text-sm leading-6 text-muted-foreground">
|
|
94
|
+
在真实主题、间距和交互上下文中检查 Web 基础组件。每个示例均直接引用
|
|
95
|
+
<code class="rounded bg-muted px-1 py-0.5">components/ui</code>
|
|
96
|
+
中的封装。
|
|
97
|
+
</p>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<div class="grid content-start gap-2 rounded-xl border bg-muted/20 p-4">
|
|
101
|
+
<div class="flex items-center justify-between gap-4 border-b pb-3">
|
|
102
|
+
<span class="text-sm font-medium">注册清单</span>
|
|
103
|
+
<code class="text-xs text-muted-foreground">
|
|
104
|
+
{{ componentCatalog.length }} 组件族 ·
|
|
105
|
+
{{ componentCategories.length }} 类
|
|
106
|
+
</code>
|
|
107
|
+
</div>
|
|
108
|
+
<div
|
|
109
|
+
v-for="category in componentCategories"
|
|
110
|
+
:key="category.id"
|
|
111
|
+
class="grid grid-cols-[6rem_1fr_auto] items-center gap-3 py-1 text-xs"
|
|
112
|
+
>
|
|
113
|
+
<code class="text-muted-foreground">{{ category.id }}</code>
|
|
114
|
+
<span>{{ category.label }}</span>
|
|
115
|
+
<span class="tabular-nums text-muted-foreground">
|
|
116
|
+
{{ categoryCounts[category.id] }}
|
|
117
|
+
</span>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
</header>
|
|
122
|
+
|
|
123
|
+
<section
|
|
124
|
+
aria-label="组件筛选"
|
|
125
|
+
class="sticky top-0 z-10 rounded-xl border bg-background/95 p-3 shadow-sm backdrop-blur"
|
|
126
|
+
>
|
|
127
|
+
<div
|
|
128
|
+
class="flex flex-col gap-3 xl:flex-row xl:items-center xl:justify-between"
|
|
129
|
+
>
|
|
130
|
+
<div class="relative w-full xl:max-w-sm">
|
|
131
|
+
<SearchIcon
|
|
132
|
+
class="pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-muted-foreground"
|
|
133
|
+
/>
|
|
134
|
+
<Input
|
|
135
|
+
v-model="searchQuery"
|
|
136
|
+
type="search"
|
|
137
|
+
aria-label="搜索组件"
|
|
138
|
+
placeholder="搜索名称、用途或导出路径"
|
|
139
|
+
class="pl-8"
|
|
140
|
+
/>
|
|
141
|
+
</div>
|
|
142
|
+
<div class="flex flex-wrap gap-2" aria-label="按分类筛选">
|
|
143
|
+
<Button
|
|
144
|
+
size="sm"
|
|
145
|
+
:variant="activeCategory === 'all' ? 'default' : 'outline'"
|
|
146
|
+
data-category-id="all"
|
|
147
|
+
@click="selectCategory('all')"
|
|
148
|
+
>
|
|
149
|
+
全部
|
|
150
|
+
<span class="tabular-nums opacity-70">{{
|
|
151
|
+
componentCatalog.length
|
|
152
|
+
}}</span>
|
|
153
|
+
</Button>
|
|
154
|
+
<Button
|
|
155
|
+
v-for="category in componentCategories"
|
|
156
|
+
:key="category.id"
|
|
157
|
+
size="sm"
|
|
158
|
+
:variant="activeCategory === category.id ? 'default' : 'outline'"
|
|
159
|
+
:data-category-id="category.id"
|
|
160
|
+
@click="selectCategory(category.id)"
|
|
161
|
+
>
|
|
162
|
+
{{ category.label }}
|
|
163
|
+
<span class="tabular-nums opacity-70">{{
|
|
164
|
+
categoryCounts[category.id]
|
|
165
|
+
}}</span>
|
|
166
|
+
</Button>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
<p class="mt-2 text-xs text-muted-foreground" aria-live="polite">
|
|
170
|
+
当前显示 {{ visibleComponents.length }} /
|
|
171
|
+
{{ componentCatalog.length }} 个组件族
|
|
172
|
+
</p>
|
|
173
|
+
</section>
|
|
174
|
+
|
|
175
|
+
<template v-if="visibleComponents.length">
|
|
176
|
+
<section
|
|
177
|
+
v-for="category in visibleCategories"
|
|
178
|
+
:key="category.id"
|
|
179
|
+
:aria-labelledby="`category-${category.id}`"
|
|
180
|
+
class="grid gap-4"
|
|
181
|
+
>
|
|
182
|
+
<header
|
|
183
|
+
class="flex flex-wrap items-end justify-between gap-2 border-b pb-3"
|
|
184
|
+
>
|
|
185
|
+
<div>
|
|
186
|
+
<p class="text-xs font-medium text-muted-foreground">
|
|
187
|
+
{{ category.id }}
|
|
188
|
+
</p>
|
|
189
|
+
<h2
|
|
190
|
+
:id="`category-${category.id}`"
|
|
191
|
+
class="mt-1 text-xl font-semibold"
|
|
192
|
+
>
|
|
193
|
+
{{ category.label }}
|
|
194
|
+
</h2>
|
|
195
|
+
</div>
|
|
196
|
+
<p class="text-sm text-muted-foreground">
|
|
197
|
+
{{ category.description }}
|
|
198
|
+
</p>
|
|
199
|
+
</header>
|
|
200
|
+
|
|
201
|
+
<div class="grid items-start gap-4 lg:grid-cols-2">
|
|
202
|
+
<FoundationComponentShowcases
|
|
203
|
+
v-if="category.id === 'foundation'"
|
|
204
|
+
:visible-ids="visibleIds"
|
|
205
|
+
/>
|
|
206
|
+
<NavigationComponentShowcases
|
|
207
|
+
v-else-if="category.id === 'navigation'"
|
|
208
|
+
:visible-ids="visibleIds"
|
|
209
|
+
/>
|
|
210
|
+
<FeedbackComponentShowcases
|
|
211
|
+
v-else-if="category.id === 'feedback'"
|
|
212
|
+
:visible-ids="visibleIds"
|
|
213
|
+
/>
|
|
214
|
+
<DataComponentShowcases
|
|
215
|
+
v-else-if="category.id === 'data'"
|
|
216
|
+
:visible-ids="visibleIds"
|
|
217
|
+
/>
|
|
218
|
+
<ProjectComponentShowcases v-else :items="visibleProjectComponents" />
|
|
219
|
+
</div>
|
|
220
|
+
</section>
|
|
221
|
+
</template>
|
|
222
|
+
|
|
223
|
+
<section
|
|
224
|
+
v-else
|
|
225
|
+
class="grid min-h-64 place-items-center rounded-xl border border-dashed p-8 text-center"
|
|
226
|
+
>
|
|
227
|
+
<div>
|
|
228
|
+
<p class="font-medium">没有匹配的组件</p>
|
|
229
|
+
<p class="mt-2 text-sm text-muted-foreground">
|
|
230
|
+
调整关键词或清除分类后重试。
|
|
231
|
+
</p>
|
|
232
|
+
<Button variant="outline" class="mt-4" @click="clearFilters">
|
|
233
|
+
<XIcon />清除筛选
|
|
234
|
+
</Button>
|
|
235
|
+
</div>
|
|
236
|
+
</section>
|
|
237
|
+
</div>
|
|
238
|
+
</template>
|
|
@@ -29,6 +29,7 @@ import SystemResourceRoutePage from "@/pages/system/SystemResourceRoutePage.vue"
|
|
|
29
29
|
import GlobalSettingsPage from "@/pages/settings/GlobalSettingsPage.vue";
|
|
30
30
|
// @windy-module system.settings end
|
|
31
31
|
import PersonalSettingsPage from "@/pages/profile/PersonalSettingsPage.vue";
|
|
32
|
+
import UiComponentPage from "@/pages/ui/UiComponentPage.vue";
|
|
32
33
|
// @windy-module system.configuration begin
|
|
33
34
|
import ModuleConfigurationPage from "@/pages/configuration/ModuleConfigurationPage.vue";
|
|
34
35
|
// @windy-module system.configuration end
|
|
@@ -84,6 +85,7 @@ const platformAdminPageComponents: Partial<
|
|
|
84
85
|
> = {
|
|
85
86
|
resource: SystemResourceRoutePage,
|
|
86
87
|
profile: PersonalSettingsPage,
|
|
88
|
+
"ui-components": UiComponentPage,
|
|
87
89
|
// @windy-module system.settings begin
|
|
88
90
|
settings: GlobalSettingsPage,
|
|
89
91
|
// @windy-module system.settings end
|
|
@@ -47,6 +47,7 @@ describe("Manifest 菜单与 Router 契约", () => {
|
|
|
47
47
|
expect(definitions.some(({ path }) => path === "/admin/profile")).toBe(
|
|
48
48
|
true,
|
|
49
49
|
);
|
|
50
|
+
expect(definitions.some(({ path }) => path === "/ui/component")).toBe(true);
|
|
50
51
|
expect(definitions.some(({ path }) => path === "/admin/work-orders")).toBe(
|
|
51
52
|
false,
|
|
52
53
|
);
|
|
@@ -8,7 +8,8 @@ export type PlatformAdminPageKey =
|
|
|
8
8
|
| "notification"
|
|
9
9
|
| "operations"
|
|
10
10
|
| "ai-runtime"
|
|
11
|
-
| "configuration"
|
|
11
|
+
| "configuration"
|
|
12
|
+
| "ui-components";
|
|
12
13
|
|
|
13
14
|
export interface PlatformAdminRouteBinding {
|
|
14
15
|
readonly routeKey: string;
|
|
@@ -37,6 +38,7 @@ export const platformAdminRouteBindings: readonly PlatformAdminRouteBinding[] =
|
|
|
37
38
|
resourceKey: "dictionaries",
|
|
38
39
|
},
|
|
39
40
|
{ routeKey: "system.feature", page: "resource", resourceKey: "features" },
|
|
41
|
+
{ routeKey: "system.ui-components", page: "ui-components" },
|
|
40
42
|
// @windy-module system.license begin
|
|
41
43
|
{
|
|
42
44
|
routeKey: "system.license",
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
整个 `apps/web/src` 都是 project-owned 应用源码。布局、页面、组件库封装、Router、
|
|
6
6
|
导航呈现与 Host 实现均可由项目修改、替换或删除;普通 `windy update` 和模块命令不能
|
|
7
|
-
|
|
7
|
+
为了接入入口而整文件重写这些文件。一次性兼容迁移可以使用版本 recipe 修改已登记的
|
|
8
|
+
窄锚点,具体例外见[项目 UI 组件展厅](./ui-component-gallery.md)。
|
|
8
9
|
|
|
9
10
|
## package 默认贡献
|
|
10
11
|
|
|
@@ -63,6 +64,7 @@ Foundation 访问快照改善导航和直达体验。`admin.visible=false` 只
|
|
|
63
64
|
- 客户保留、修改与删除均由项目决定;
|
|
64
65
|
- 删除以 tombstone 跨版本保留;
|
|
65
66
|
- 模块 package 更新只改变依赖与自身导出,不改客户 layout、Router 或页面;
|
|
67
|
+
- 已登记版本 recipe 只追加明确的兼容锚点,缺失或不唯一时 fail closed;
|
|
66
68
|
- Host 不再实现协议时,CLI/构建只诊断并 fail closed,不能覆盖客户实现。
|
|
67
69
|
|
|
68
70
|
未来若提供整套 Admin 预设更新,它必须是独立、显式选择的三方合并能力;当前没有
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# 项目 UI 组件展厅
|
|
2
|
+
|
|
3
|
+
更新时间:2026-08-21。
|
|
4
|
+
|
|
5
|
+
## 能力边界
|
|
6
|
+
|
|
7
|
+
`/ui/component` 展示当前项目在
|
|
8
|
+
`apps/web/src/components/ui/*/index.ts` 中封装的全部组件族。页面不维护第二份目录清单:
|
|
9
|
+
Vite 在构建当前项目时扫描每个 `index.ts`,因此下游新增 `radio-group`、`textarea` 等
|
|
10
|
+
组件后,会自动出现在“项目扩展”分组并列出真实具名导出。
|
|
11
|
+
|
|
12
|
+
Windy 自带组件提供可操作的主题内示例。项目扩展组件默认只展示发现结果与导出名,因为
|
|
13
|
+
任意组件所需的 props、slot 和业务上下文无法安全推断;项目可继续在自己的页面中增加
|
|
14
|
+
针对性预览,组件入口本身仍由项目拥有。
|
|
15
|
+
|
|
16
|
+
该页面属于 `system` Module,统一使用以下 Manifest 事实:
|
|
17
|
+
|
|
18
|
+
- Feature:`system.ui-components`;
|
|
19
|
+
- Permission:`system.ui-components.read`;
|
|
20
|
+
- Admin Route:`system.ui-components`,路径 `/ui/component`;
|
|
21
|
+
- 菜单入口与直达路由均经过现有 Feature/RBAC Guard。
|
|
22
|
+
|
|
23
|
+
## 下游升级
|
|
24
|
+
|
|
25
|
+
`create-windy@0.6.9` 的 `starter-0.6.8-to-0.6.9` recipe 会创建新的展厅页面文件,并在
|
|
26
|
+
下游项目现有 Router 中只追加两处登记内容:
|
|
27
|
+
|
|
28
|
+
1. `platformAdminPageComponents` 中的懒加载页面 binding;
|
|
29
|
+
2. `PlatformAdminPageKey` 与 `system.ui-components` Route binding。
|
|
30
|
+
|
|
31
|
+
这不是对整个 Admin 预设的重新同步。其余 `apps/web/src` 文件仍为 project-owned,
|
|
32
|
+
客户的 layout、业务 Route、页面和组件不会被模板覆盖。recipe 只接受唯一的稳定锚点;
|
|
33
|
+
若项目已经完全替换 Host,或锚点缺失/不唯一,dry-run 会报告冲突并在写入前停止。完成
|
|
34
|
+
升级后建议检查:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
bun run windy update --dry-run
|
|
38
|
+
bun run windy update
|
|
39
|
+
bun run windy doctor
|
|
40
|
+
bun run --cwd apps/web typecheck
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
验收时应确认 `/ui/component` 可访问,并且组件数量与当前项目
|
|
44
|
+
`apps/web/src/components/ui` 下含 `index.ts` 的目录数量一致。
|