create-windy 0.6.8 → 0.6.10

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.
Files changed (80) hide show
  1. package/README.md +4 -1
  2. package/dist/cli.js +1012 -329
  3. package/package.json +1 -1
  4. package/template/.windy-template.json +3 -3
  5. package/template/README.md +8 -2
  6. package/template/apps/agent-server/src/southwind_agent_server/contracts.py +2 -0
  7. package/template/apps/agent-server/tests/test_multimodal.py +30 -0
  8. package/template/apps/agent-server/tests/test_postgres_store.py +1 -0
  9. package/template/apps/agent-server/tests/test_sqlite_store.py +1 -0
  10. package/template/apps/server/src/agent/remote-runtime.test.ts +1 -0
  11. package/template/apps/server/src/agent/run-audit.ts +1 -0
  12. package/template/apps/server/src/agent/run-contracts.ts +1 -0
  13. package/template/apps/server/src/agent/run-facade.ts +2 -2
  14. package/template/apps/server/src/agent/run-operation-registry.ts +7 -0
  15. package/template/apps/server/src/agent/run-output-schema.ts +15 -0
  16. package/template/apps/server/src/agent/run-release.ts +11 -0
  17. package/template/apps/server/src/agent/supervisor-output-schema.test.ts +95 -0
  18. package/template/apps/server/src/agent/vision-input.ts +37 -2
  19. package/template/apps/server/src/license/pairing-http.ts +138 -0
  20. package/template/apps/server/src/license/pairing-routes.test.ts +282 -0
  21. package/template/apps/server/src/license/pairing-routes.ts +257 -0
  22. package/template/apps/server/src/license/pairing-service.test.ts +320 -0
  23. package/template/apps/server/src/license/pairing-service.ts +321 -0
  24. package/template/apps/server/src/license/restriction.test.ts +11 -0
  25. package/template/apps/server/src/license/restriction.ts +5 -0
  26. package/template/apps/server/src/license/routes.ts +8 -1
  27. package/template/apps/server/src/license/runtime-service.ts +20 -0
  28. package/template/apps/server/src/runtime-development.ts +1 -0
  29. package/template/apps/web/package.json +6 -4
  30. package/template/apps/web/src/components/license/LicensePairingCard.vue +177 -0
  31. package/template/apps/web/src/components/license/LicensePairingCard.webtest.ts +113 -0
  32. package/template/apps/web/src/composables/useLicensePairing.ts +168 -0
  33. package/template/apps/web/src/composables/useLicenseRuntime.ts +11 -0
  34. package/template/apps/web/src/layout/AppSidebar.vue +1 -0
  35. package/template/apps/web/src/pages/license/LicensePage.vue +8 -0
  36. package/template/apps/web/src/pages/ui/UiComponentPage.vue +7 -0
  37. package/template/apps/web/src/pages/ui/UiComponentPage.webtest.ts +94 -0
  38. package/template/apps/web/src/pages/ui/component-catalog.ts +248 -0
  39. package/template/apps/web/src/pages/ui/components/ComponentPreviewCard.vue +50 -0
  40. package/template/apps/web/src/pages/ui/components/DataComponentShowcases.vue +131 -0
  41. package/template/apps/web/src/pages/ui/components/FeedbackComponentShowcases.vue +158 -0
  42. package/template/apps/web/src/pages/ui/components/FoundationComponentShowcases.vue +129 -0
  43. package/template/apps/web/src/pages/ui/components/NavigationComponentShowcases.vue +155 -0
  44. package/template/apps/web/src/pages/ui/components/ProjectComponentShowcases.vue +31 -0
  45. package/template/apps/web/src/pages/ui/components/UiComponentGallery.vue +238 -0
  46. package/template/apps/web/src/router/index.ts +2 -0
  47. package/template/apps/web/src/router/manifest-routes.webtest.ts +1 -0
  48. package/template/apps/web/src/router/platform-admin-route-bindings.ts +3 -1
  49. package/template/apps/web/src/services/license-api.ts +60 -0
  50. package/template/apps/web/src/style.webtest.ts +5 -6
  51. package/template/docs/architecture/ai-runtime.md +5 -0
  52. package/template/docs/development/business-web-routes.md +3 -1
  53. package/template/docs/development/ui-component-gallery.md +44 -0
  54. package/template/docs/license/license-pairing-activation.md +67 -0
  55. package/template/docs/operations/incremental-delivery.md +10 -9
  56. package/template/docs/platform/agent-supervisor-output-contract.md +80 -0
  57. package/template/packages/ai-client/package.json +1 -1
  58. package/template/packages/ai-client/src/agent/contracts/final-answer.test.ts +67 -0
  59. package/template/packages/ai-client/src/agent/contracts/final-answer.ts +53 -0
  60. package/template/packages/ai-client/src/agent/contracts/index.ts +1 -0
  61. package/template/packages/modules/index.ts +1 -0
  62. package/template/packages/modules/package.json +1 -1
  63. package/template/packages/modules/src/system-admin-routes.ts +8 -0
  64. package/template/packages/modules/src/system-admin-ui-catalog.ts +6 -2
  65. package/template/packages/modules/src/system-agent-supervisor.test.ts +48 -0
  66. package/template/packages/modules/src/system-agent-supervisor.ts +101 -0
  67. package/template/packages/modules/src/system-api-control-plane.ts +37 -0
  68. package/template/packages/modules/src/system-features.ts +5 -0
  69. package/template/packages/modules/src/system-license-pairing.test.ts +44 -0
  70. package/template/packages/modules/src/system-module-catalog.ts +10 -19
  71. package/template/packages/modules/src/system-module-menu-selection.ts +15 -0
  72. package/template/packages/modules/src/system-module-schema-catalog.ts +14 -0
  73. package/template/packages/modules/src/system-modules.ts +13 -0
  74. package/template/packages/modules/src/system-permissions.ts +2 -0
  75. package/template/packages/modules/src/system-ui-components.test.ts +36 -0
  76. package/template/packages/server-sdk/index.ts +5 -0
  77. package/template/packages/server-sdk/package.json +3 -1
  78. package/template/packages/server-sdk/src/ai-operation-registration.ts +4 -0
  79. package/template/packages/server-sdk/src/system-agent-supervisor.test.ts +87 -0
  80. package/template/packages/server-sdk/src/system-agent-supervisor.ts +89 -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",
@@ -30,6 +30,34 @@ interface LicenseInstallResponse {
30
30
  snapshot: PlatformLicenseSnapshot;
31
31
  }
32
32
 
33
+ export type LicensePairingSessionStatus =
34
+ | "waiting"
35
+ | "delivered"
36
+ | "expired"
37
+ | "locked"
38
+ | "consumed";
39
+
40
+ export interface LicensePairingSession {
41
+ id: string;
42
+ token: string;
43
+ nonce: string;
44
+ productCode: string;
45
+ machineCode: string;
46
+ expiresAt: string;
47
+ qrPayload: string;
48
+ status: "waiting";
49
+ }
50
+
51
+ export interface LicensePairingSessionSnapshot {
52
+ id: string;
53
+ status: LicensePairingSessionStatus;
54
+ expiresAt: string;
55
+ license?: {
56
+ licenseVersion: { key: string; name: string };
57
+ expiresAt: string;
58
+ };
59
+ }
60
+
33
61
  export function loadPlatformLicense(): Promise<PlatformLicenseSnapshot> {
34
62
  return apiRequest<PlatformLicenseSnapshot>("/license/runtime");
35
63
  }
@@ -51,3 +79,35 @@ export function installPlatformActivationCode(
51
79
  body: JSON.stringify({ activationCode }),
52
80
  });
53
81
  }
82
+
83
+ export function createPlatformLicensePairingSession(
84
+ callbackOrigin: string,
85
+ ): Promise<LicensePairingSession> {
86
+ return apiRequest<LicensePairingSession>(
87
+ "/license/runtime/pairing/sessions",
88
+ { method: "POST", body: JSON.stringify({ callbackOrigin }) },
89
+ );
90
+ }
91
+
92
+ export function loadPlatformLicensePairingSession(
93
+ sessionId: string,
94
+ ): Promise<LicensePairingSessionSnapshot> {
95
+ return apiRequest<LicensePairingSessionSnapshot>(
96
+ `/license/runtime/pairing/sessions/${encodeURIComponent(sessionId)}`,
97
+ undefined,
98
+ { suppressFailureHandler: true },
99
+ );
100
+ }
101
+
102
+ export function confirmPlatformLicensePairing(
103
+ sessionId: string,
104
+ pairingCode: string,
105
+ ): Promise<LicenseInstallResponse> {
106
+ return apiRequest<LicenseInstallResponse>(
107
+ `/license/runtime/pairing/sessions/${encodeURIComponent(sessionId)}/confirm`,
108
+ {
109
+ method: "POST",
110
+ body: JSON.stringify({ pairingCode }),
111
+ },
112
+ );
113
+ }
@@ -1,12 +1,11 @@
1
- import { describe, expect, test } from "vitest";
2
- // @ts-expect-error Vitest runs this contract test in Node; the browser bundle omits Node types.
3
- import { readFileSync } from "node:fs";
4
- // @ts-expect-error Vitest runs this contract test in Node; the browser bundle omits Node types.
5
- import { resolve } from "node:path";
1
+ import { describe, expect, test, vi } from "vitest";
6
2
 
7
3
  declare const process: { cwd(): string };
8
4
 
9
- const styles = readFileSync(resolve(process.cwd(), "src/style.css"), "utf8");
5
+ const { readFileSync } = await vi.importActual<{
6
+ readFileSync(path: string, encoding: "utf8"): string;
7
+ }>("node:fs");
8
+ const styles = readFileSync(`${process.cwd()}/src/style.css`, "utf8");
10
9
 
11
10
  describe("全局页面背景", () => {
12
11
  test("html、body 与应用根节点共同使用主题背景,避免深色滚动露白", () => {