create-soybean 1.7.0 → 1.7.1
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.en_US.md +7 -5
- package/README.md +7 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.js +27 -15
- package/package.json +12 -24
- package/template-ts-lib/_vite-hooks/pre-commit +1 -0
- package/template-ts-lib/package.json +40 -0
- package/template-ts-lib/pnpm-workspace.yaml +11 -0
- package/{template-tsdown → template-ts-lib}/tsconfig.json +7 -6
- package/template-ts-lib/vite.config.ts +22 -0
- package/template-ubean/README.md +20 -0
- package/template-ubean/_editorconfig +11 -0
- package/template-ubean/_gitattributes +2 -0
- package/template-ubean/_gitignore +32 -0
- package/template-ubean/_vite-hooks/pre-commit +1 -0
- package/template-ubean/eslint.config.mjs +3 -0
- package/template-ubean/package.json +38 -0
- package/template-ubean/pnpm-workspace.yaml +11 -0
- package/template-ubean/public/data.json +5 -0
- package/template-ubean/public/favicon.svg +64 -0
- package/template-ubean/src/app.ts +39 -0
- package/template-ubean/src/components/islands/IslandClock.vue +57 -0
- package/template-ubean/src/components/islands/IslandCounter.vue +66 -0
- package/template-ubean/src/components/islands/IslandMedia.vue +69 -0
- package/template-ubean/src/components/islands/IslandOnly.vue +54 -0
- package/template-ubean/src/components/islands/IslandVisibility.vue +60 -0
- package/template-ubean/src/layouts/admin.vue +13 -0
- package/template-ubean/src/layouts/default.vue +97 -0
- package/template-ubean/src/locales/en.json +31 -0
- package/template-ubean/src/locales/zh.json +31 -0
- package/template-ubean/src/middleware/01.global.ts +9 -0
- package/template-ubean/src/pages/about.vue +135 -0
- package/template-ubean/src/pages/blog/[...slug].vue +30 -0
- package/template-ubean/src/pages/dashboard/index.vue +19 -0
- package/template-ubean/src/pages/data-fetch.vue +95 -0
- package/template-ubean/src/pages/index.vue +117 -0
- package/template-ubean/src/pages/islands-test.vue +155 -0
- package/template-ubean/src/routes/api/hello.ts +60 -0
- package/template-ubean/src/routes/api/users/[id].ts +94 -0
- package/template-ubean/src/routes/api/users/index.ts +79 -0
- package/template-ubean/src/server.ts +9 -0
- package/template-ubean/src/typings/env.d.ts +37 -0
- package/template-ubean/tsconfig.json +26 -0
- package/template-ubean/ubean.config.ts +24 -0
- package/template-ubean/vite.config.ts +9 -0
- package/template-ubean-client/README.md +18 -0
- package/template-ubean-client/_gitignore +32 -0
- package/template-ubean-client/_vite-hooks/pre-commit +1 -0
- package/template-ubean-client/eslint.config.mjs +3 -0
- package/template-ubean-client/index.html +14 -0
- package/template-ubean-client/package.json +37 -0
- package/template-ubean-client/pnpm-workspace.yaml +11 -0
- package/template-ubean-client/public/favicon.svg +64 -0
- package/template-ubean-client/src/App.vue +135 -0
- package/template-ubean-client/src/main.ts +63 -0
- package/template-ubean-client/src/pages/404.vue +36 -0
- package/template-ubean-client/src/pages/about.vue +151 -0
- package/template-ubean-client/src/pages/about2.reuse.ts +8 -0
- package/template-ubean-client/src/pages/blog/[...slug].vue +72 -0
- package/template-ubean-client/src/pages/guide.md +30 -0
- package/template-ubean-client/src/pages/index.vue +101 -0
- package/template-ubean-client/src/pages/users/[id=numeric].vue +80 -0
- package/template-ubean-client/src/pages/users/index.vue +56 -0
- package/template-ubean-client/src/typings/globals.d.ts +17 -0
- package/template-ubean-client/tsconfig.json +25 -0
- package/template-ubean-client/vite.config.ts +20 -0
- package/template-vue/.gitignore +2 -0
- package/template-vue/_vite-hooks/pre-commit +1 -0
- package/template-vue/eslint.config.mjs +3 -0
- package/template-vue/package.json +17 -26
- package/template-vue/pnpm-workspace.yaml +11 -0
- package/template-vue/src/App.vue +3 -3
- package/template-vue/src/main.ts +2 -7
- package/template-vue/uno.config.ts +3 -3
- package/template-vue/vite.config.ts +10 -6
- package/template-vue-lib/_editorconfig +11 -0
- package/template-vue-lib/_gitattributes +2 -0
- package/template-vue-lib/_vite-hooks/pre-commit +1 -0
- package/template-vue-lib/eslint.config.mjs +3 -0
- package/template-vue-lib/package.json +53 -0
- package/template-vue-lib/pnpm-workspace.yaml +11 -0
- package/template-vue-lib/vite.config.ts +33 -0
- package/template-tsdown/_npmrc +0 -1
- package/template-tsdown/_oxfmtrc.json +0 -7
- package/template-tsdown/_oxlintrc.json +0 -16
- package/template-tsdown/package.json +0 -54
- package/template-tsdown/tsdown.config.ts +0 -10
- package/template-vue/_npmrc +0 -1
- package/template-vue/_oxfmtrc.json +0 -7
- package/template-vue/_oxlintrc.json +0 -17
- package/template-vue/er.config.ts +0 -9
- package/template-vue/src/components/HelloWorld.vue +0 -5
- package/template-vue/src/constants/index.ts +0 -0
- package/template-vue/src/layouts/base/index.vue +0 -9
- package/template-vue/src/layouts/blank/index.vue +0 -9
- package/template-vue/src/plugins/assets.ts +0 -4
- package/template-vue/src/router/index.ts +0 -25
- package/template-vue/src/shared/index.ts +0 -0
- package/template-vue/src/store/_plugin.ts +0 -17
- package/template-vue/src/store/counter/index.ts +0 -14
- package/template-vue/src/store/index.ts +0 -12
- package/template-vue/src/styles/app.css +0 -49
- package/template-vue/src/typings/components.d.ts +0 -17
- package/template-vue/src/views/(builtin)/403.vue +0 -5
- package/template-vue/src/views/(builtin)/404.vue +0 -5
- package/template-vue/src/views/(builtin)/login/index.vue +0 -5
- package/template-vue/src/views/(builtin)/wip.vue +0 -5
- package/template-vue/src/views/demo.tsx +0 -7
- package/template-vue/src/views/home/child.vue +0 -5
- package/template-vue/src/views/home/child2.vue +0 -5
- package/template-vue/src/views/home/index.vue +0 -9
- package/template-vue/src/views/list/[id].vue +0 -9
- package/template-vue/src/views/list/add.vue +0 -5
- package/template-vue/src/views/list/components/demo.vue +0 -0
- package/template-vue/src/views/list/components/search-list.vue +0 -5
- package/template-vue/src/views/list/detail2-[[id]]-[[userId]].vue +0 -5
- package/template-vue/src/views/list/detail_[id]_[userId].vue +0 -5
- package/template-vue/src/views/list/edit_[id].vue +0 -5
- package/template-vue/src/views/list/index.vue +0 -5
- package/template-vue/src/views/list/modules/list-table.vue +0 -5
- package/template-vue-tsdown/_npmrc +0 -1
- package/template-vue-tsdown/_oxfmtrc.json +0 -7
- package/template-vue-tsdown/_oxlintrc.json +0 -17
- package/template-vue-tsdown/package.json +0 -64
- package/template-vue-tsdown/tsdown.config.ts +0 -16
- package/template-vue-tsdown/vite.config.ts +0 -7
- /package/{template-tsdown → template-ts-lib}/.github/workflows/release.yml +0 -0
- /package/{template-tsdown → template-ts-lib}/.vscode/extensions.json +0 -0
- /package/{template-tsdown → template-ts-lib}/.vscode/launch.json +0 -0
- /package/{template-tsdown → template-ts-lib}/.vscode/settings.json +0 -0
- /package/{template-tsdown → template-ts-lib}/_editorconfig +0 -0
- /package/{template-tsdown → template-ts-lib}/_gitattributes +0 -0
- /package/{template-tsdown → template-ts-lib}/_gitignore +0 -0
- /package/{template-tsdown → template-ts-lib}/src/index.ts +0 -0
- /package/{template-vue-tsdown → template-ubean-client}/_editorconfig +0 -0
- /package/{template-vue-tsdown → template-ubean-client}/_gitattributes +0 -0
- /package/{template-vue-tsdown → template-vue-lib}/.github/workflows/release.yml +0 -0
- /package/{template-vue-tsdown → template-vue-lib}/.vscode/extensions.json +0 -0
- /package/{template-vue-tsdown → template-vue-lib}/.vscode/launch.json +0 -0
- /package/{template-vue-tsdown → template-vue-lib}/.vscode/settings.json +0 -0
- /package/{template-vue-tsdown → template-vue-lib}/_gitignore +0 -0
- /package/{template-vue-tsdown → template-vue-lib}/playground/index.html +0 -0
- /package/{template-vue-tsdown → template-vue-lib}/playground/src/App.vue +0 -0
- /package/{template-vue-tsdown → template-vue-lib}/playground/src/index.ts +0 -0
- /package/{template-vue-tsdown → template-vue-lib}/playground/src/style.css +0 -0
- /package/{template-vue-tsdown → template-vue-lib}/src/demo-pkg.vue +0 -0
- /package/{template-vue-tsdown → template-vue-lib}/src/index.ts +0 -0
- /package/{template-vue-tsdown → template-vue-lib}/tsconfig.json +0 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, onMounted } from 'vue';
|
|
3
|
+
|
|
4
|
+
useHead({
|
|
5
|
+
title: '数据请求 - ubean App',
|
|
6
|
+
meta: [{ name: 'description', content: '客户端数据请求与 useData 示例' }]
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
interface ApiResponse {
|
|
10
|
+
method: string;
|
|
11
|
+
url: string;
|
|
12
|
+
status: number;
|
|
13
|
+
data: unknown;
|
|
14
|
+
duration: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const results = ref<ApiResponse[]>([]);
|
|
18
|
+
const loading = ref(false);
|
|
19
|
+
const error = ref<string | null>(null);
|
|
20
|
+
|
|
21
|
+
async function fetchApi(method: string, url: string, body?: unknown): Promise<ApiResponse> {
|
|
22
|
+
const start = performance.now();
|
|
23
|
+
const init: RequestInit = { method, headers: { 'Content-Type': 'application/json' } };
|
|
24
|
+
if (body) init.body = JSON.stringify(body);
|
|
25
|
+
const res = await fetch(url, init);
|
|
26
|
+
const data = await res.json().catch(() => null);
|
|
27
|
+
const duration = Math.round(performance.now() - start);
|
|
28
|
+
return { method, url, status: res.status, data, duration };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function runTests() {
|
|
32
|
+
loading.value = true;
|
|
33
|
+
error.value = null;
|
|
34
|
+
results.value = [];
|
|
35
|
+
try {
|
|
36
|
+
results.value.push(await fetchApi('GET', '/api/hello'));
|
|
37
|
+
results.value.push(await fetchApi('GET', '/api/users'));
|
|
38
|
+
results.value.push(await fetchApi('POST', '/api/users', { name: 'Frontend Test', email: 'fe@test.com' }));
|
|
39
|
+
} catch (e) {
|
|
40
|
+
error.value = e instanceof Error ? e.message : String(e);
|
|
41
|
+
} finally {
|
|
42
|
+
loading.value = false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
onMounted(runTests);
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<template>
|
|
50
|
+
<div class="p-8 max-w-4xl mx-auto">
|
|
51
|
+
<h1 class="text-3xl font-bold mb-2">Data Fetch Test</h1>
|
|
52
|
+
<p class="text-gray-500 mb-6">Client-side data fetching via fetch API + server-side useData/ofetch verification</p>
|
|
53
|
+
|
|
54
|
+
<div v-if="loading" class="mb-4 text-blue-500">Running tests...</div>
|
|
55
|
+
<div v-if="error" class="mb-4 p-3 bg-red-100 text-red-700 rounded">{{ error }}</div>
|
|
56
|
+
|
|
57
|
+
<button
|
|
58
|
+
:disabled="loading"
|
|
59
|
+
class="mb-6 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50"
|
|
60
|
+
@click="runTests"
|
|
61
|
+
>
|
|
62
|
+
Re-run Tests
|
|
63
|
+
</button>
|
|
64
|
+
|
|
65
|
+
<section v-for="(r, i) in results" :key="i" class="mb-6 p-4 border rounded-lg">
|
|
66
|
+
<div class="flex items-center justify-between mb-2">
|
|
67
|
+
<div class="flex items-center gap-2">
|
|
68
|
+
<span
|
|
69
|
+
class="px-2 py-0.5 text-xs font-mono rounded"
|
|
70
|
+
:class="
|
|
71
|
+
r.method === 'GET'
|
|
72
|
+
? 'bg-blue-100 text-blue-700'
|
|
73
|
+
: r.method === 'POST'
|
|
74
|
+
? 'bg-green-100 text-green-700'
|
|
75
|
+
: 'bg-gray-100 text-gray-700'
|
|
76
|
+
"
|
|
77
|
+
>
|
|
78
|
+
{{ r.method }}
|
|
79
|
+
</span>
|
|
80
|
+
<code class="text-sm">{{ r.url }}</code>
|
|
81
|
+
</div>
|
|
82
|
+
<div class="flex items-center gap-3 text-sm">
|
|
83
|
+
<span
|
|
84
|
+
class="px-2 py-0.5 rounded font-mono"
|
|
85
|
+
:class="r.status >= 200 && r.status < 300 ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'"
|
|
86
|
+
>
|
|
87
|
+
{{ r.status }}
|
|
88
|
+
</span>
|
|
89
|
+
<span class="text-gray-500">{{ r.duration }}ms</span>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
<pre class="bg-gray-100 p-3 rounded text-xs overflow-x-auto">{{ JSON.stringify(r.data, null, 2) }}</pre>
|
|
93
|
+
</section>
|
|
94
|
+
</div>
|
|
95
|
+
</template>
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { Icon } from '@iconify/vue';
|
|
3
|
+
|
|
4
|
+
useHead({
|
|
5
|
+
title: 'ubean App',
|
|
6
|
+
meta: [{ name: 'description', content: 'ubean 应用模板' }]
|
|
7
|
+
});
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<template>
|
|
11
|
+
<div class="home">
|
|
12
|
+
<section class="hero">
|
|
13
|
+
<h1>🌱 ubean App</h1>
|
|
14
|
+
<Icon icon="lucide:sprout" />
|
|
15
|
+
<p class="subtitle">基于 ubean 的全栈应用模板</p>
|
|
16
|
+
</section>
|
|
17
|
+
|
|
18
|
+
<section class="test-grid">
|
|
19
|
+
<div class="test-card">
|
|
20
|
+
<h3>📄 页面路由</h3>
|
|
21
|
+
<ul>
|
|
22
|
+
<li><Link to="/">首页</Link></li>
|
|
23
|
+
<li><Link to="/about">关于页面</Link></li>
|
|
24
|
+
<li><Link to="/dashboard">控制台(admin 布局)</Link></li>
|
|
25
|
+
<li><Link to="/islands-test">Islands 架构</Link></li>
|
|
26
|
+
</ul>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div class="test-card">
|
|
30
|
+
<h3>🔌 API 路由</h3>
|
|
31
|
+
<ul>
|
|
32
|
+
<li><a href="/api/hello" target="_blank">GET /api/hello</a></li>
|
|
33
|
+
<li><a href="/api/users" target="_blank">GET /api/users</a></li>
|
|
34
|
+
<li><a href="/api/users/1" target="_blank">GET /api/users/[id]</a></li>
|
|
35
|
+
</ul>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div class="test-card">
|
|
39
|
+
<h3>📝 文件路由</h3>
|
|
40
|
+
<ul>
|
|
41
|
+
<li><Link to="/blog/hello">Catch-all 路由 /blog/hello</Link></li>
|
|
42
|
+
</ul>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div class="test-card">
|
|
46
|
+
<h3>🌍 i18n 国际化</h3>
|
|
47
|
+
<ul>
|
|
48
|
+
<li>English</li>
|
|
49
|
+
<li>简体中文</li>
|
|
50
|
+
</ul>
|
|
51
|
+
</div>
|
|
52
|
+
</section>
|
|
53
|
+
</div>
|
|
54
|
+
</template>
|
|
55
|
+
|
|
56
|
+
<style scoped>
|
|
57
|
+
.home {
|
|
58
|
+
max-width: 860px;
|
|
59
|
+
margin: 0 auto;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.hero {
|
|
63
|
+
text-align: center;
|
|
64
|
+
padding: 2.5rem 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.hero h1 {
|
|
68
|
+
font-size: 2.2rem;
|
|
69
|
+
margin-bottom: 0.5rem;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.hero svg {
|
|
73
|
+
width: 2rem;
|
|
74
|
+
height: 2rem;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.subtitle {
|
|
78
|
+
color: gray;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.test-grid {
|
|
82
|
+
display: grid;
|
|
83
|
+
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
|
84
|
+
gap: 1rem;
|
|
85
|
+
padding-bottom: 2rem;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.test-card {
|
|
89
|
+
border: 1px solid rgba(128, 128, 128, 0.25);
|
|
90
|
+
border-radius: 12px;
|
|
91
|
+
padding: 1.1rem 1.2rem;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.test-card h3 {
|
|
95
|
+
margin: 0 0 0.6rem;
|
|
96
|
+
font-size: 1rem;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.test-card ul {
|
|
100
|
+
list-style: none;
|
|
101
|
+
padding: 0;
|
|
102
|
+
margin: 0;
|
|
103
|
+
display: flex;
|
|
104
|
+
flex-direction: column;
|
|
105
|
+
gap: 0.45rem;
|
|
106
|
+
font-size: 0.9rem;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.test-card a {
|
|
110
|
+
color: #42b883;
|
|
111
|
+
text-decoration: none;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.test-card a:hover {
|
|
115
|
+
text-decoration: underline;
|
|
116
|
+
}
|
|
117
|
+
</style>
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { vClient } from '@ubean/islands';
|
|
3
|
+
import IslandClock from '../components/islands/IslandClock.vue';
|
|
4
|
+
import IslandCounter from '../components/islands/IslandCounter.vue';
|
|
5
|
+
import IslandMedia from '../components/islands/IslandMedia.vue';
|
|
6
|
+
import IslandOnly from '../components/islands/IslandOnly.vue';
|
|
7
|
+
import IslandVisibility from '../components/islands/IslandVisibility.vue';
|
|
8
|
+
|
|
9
|
+
useHead({
|
|
10
|
+
title: 'Islands 架构 - ubean App'
|
|
11
|
+
});
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<div class="islands-test-page">
|
|
16
|
+
<h1>🏝️ Islands 架构测试</h1>
|
|
17
|
+
<p class="desc">
|
|
18
|
+
此页面验证 ubean 的 Islands 架构。每个 Island 使用不同的水合指令, SSR 输出中应包含
|
|
19
|
+
<code><ubean-island></code>
|
|
20
|
+
自定义元素。
|
|
21
|
+
</p>
|
|
22
|
+
|
|
23
|
+
<div class="islands-grid">
|
|
24
|
+
<div class="island-section">
|
|
25
|
+
<h2>client:load</h2>
|
|
26
|
+
<p class="directive-hint">页面加载时立即水合</p>
|
|
27
|
+
<IslandCounter v-client.load />
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<div class="island-section">
|
|
31
|
+
<h2>client:idle</h2>
|
|
32
|
+
<p class="directive-hint">浏览器空闲时水合</p>
|
|
33
|
+
<IslandClock v-client.idle />
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div class="island-section">
|
|
37
|
+
<h2>client:visible</h2>
|
|
38
|
+
<p class="directive-hint">滚动到可见区域时水合</p>
|
|
39
|
+
<IslandVisibility v-client.visible />
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<div class="island-section">
|
|
43
|
+
<h2>client:media</h2>
|
|
44
|
+
<p class="directive-hint">匹配媒体查询时水合 (min-width: 768px)</p>
|
|
45
|
+
<IslandMedia v-client.media="'(min-width: 768px)'" />
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div class="island-section">
|
|
49
|
+
<h2>client:only</h2>
|
|
50
|
+
<p class="directive-hint">仅在客户端渲染(不进行 SSR)</p>
|
|
51
|
+
<IslandOnly v-client.only />
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div class="ssd-note">
|
|
56
|
+
<h3>📌 SSR 验证说明</h3>
|
|
57
|
+
<p>
|
|
58
|
+
在 SSR 输出中(查看页面源码),所有非
|
|
59
|
+
<code>client:only</code>
|
|
60
|
+
的 Island 应包含服务端渲染的 HTML 内容,而
|
|
61
|
+
<code>client:only</code>
|
|
62
|
+
的 Island 应为空。 Bootstrap 脚本应注入到页面中,根据指令设置
|
|
63
|
+
<code>data-hydrating</code>
|
|
64
|
+
属性。
|
|
65
|
+
</p>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<Link to="/" class="back-link">← 返回首页</Link>
|
|
69
|
+
</div>
|
|
70
|
+
</template>
|
|
71
|
+
|
|
72
|
+
<style scoped>
|
|
73
|
+
.islands-test-page h1 {
|
|
74
|
+
color: #42b883;
|
|
75
|
+
font-size: 2rem;
|
|
76
|
+
margin-bottom: 0.5rem;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.desc {
|
|
80
|
+
color: #666;
|
|
81
|
+
margin-bottom: 2rem;
|
|
82
|
+
line-height: 1.6;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.desc code {
|
|
86
|
+
background: #f1f5f9;
|
|
87
|
+
padding: 0.1rem 0.3rem;
|
|
88
|
+
border-radius: 3px;
|
|
89
|
+
font-size: 0.85rem;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.islands-grid {
|
|
93
|
+
display: grid;
|
|
94
|
+
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
95
|
+
gap: 1.5rem;
|
|
96
|
+
margin-bottom: 2rem;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.island-section {
|
|
100
|
+
display: flex;
|
|
101
|
+
flex-direction: column;
|
|
102
|
+
gap: 0.5rem;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.island-section h2 {
|
|
106
|
+
font-size: 1.1rem;
|
|
107
|
+
color: #35495e;
|
|
108
|
+
margin: 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.directive-hint {
|
|
112
|
+
font-size: 0.8rem;
|
|
113
|
+
color: #6b7280;
|
|
114
|
+
margin: 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.ssd-note {
|
|
118
|
+
background: #f8fafc;
|
|
119
|
+
border-left: 4px solid #42b883;
|
|
120
|
+
padding: 1rem 1.5rem;
|
|
121
|
+
margin: 2rem 0;
|
|
122
|
+
border-radius: 0 8px 8px 0;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.ssd-note h3 {
|
|
126
|
+
color: #35495e;
|
|
127
|
+
font-size: 1rem;
|
|
128
|
+
margin-bottom: 0.5rem;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.ssd-note p {
|
|
132
|
+
font-size: 0.9rem;
|
|
133
|
+
color: #475569;
|
|
134
|
+
line-height: 1.6;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.ssd-note code {
|
|
138
|
+
background: #e2e8f0;
|
|
139
|
+
padding: 0.1rem 0.3rem;
|
|
140
|
+
border-radius: 3px;
|
|
141
|
+
font-size: 0.85rem;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.back-link {
|
|
145
|
+
display: inline-block;
|
|
146
|
+
margin-top: 2rem;
|
|
147
|
+
color: #42b883;
|
|
148
|
+
text-decoration: none;
|
|
149
|
+
font-weight: 500;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.back-link:hover {
|
|
153
|
+
text-decoration: underline;
|
|
154
|
+
}
|
|
155
|
+
</style>
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { defineHandler, describeRoute } from 'ubean/server';
|
|
2
|
+
|
|
3
|
+
export const GET = defineHandler(
|
|
4
|
+
describeRoute({
|
|
5
|
+
summary: 'Hello endpoint',
|
|
6
|
+
description: 'Returns a greeting message with timestamp and method.',
|
|
7
|
+
tags: ['Demo'],
|
|
8
|
+
responses: {
|
|
9
|
+
200: {
|
|
10
|
+
description: 'Greeting object',
|
|
11
|
+
content: {
|
|
12
|
+
'application/json': {
|
|
13
|
+
schema: {
|
|
14
|
+
type: 'object',
|
|
15
|
+
properties: {
|
|
16
|
+
message: { type: 'string' },
|
|
17
|
+
timestamp: { type: 'string' },
|
|
18
|
+
method: { type: 'string' }
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}),
|
|
26
|
+
c => {
|
|
27
|
+
return c.json({
|
|
28
|
+
message: 'Hello from ubean API!',
|
|
29
|
+
timestamp: new Date().toISOString(),
|
|
30
|
+
method: 'GET'
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
export const POST = defineHandler(
|
|
36
|
+
describeRoute({
|
|
37
|
+
summary: 'Hello POST endpoint',
|
|
38
|
+
description: 'Echoes back the received body with a greeting.',
|
|
39
|
+
tags: ['Demo'],
|
|
40
|
+
responses: {
|
|
41
|
+
200: {
|
|
42
|
+
description: 'Echo response',
|
|
43
|
+
content: {
|
|
44
|
+
'application/json': {
|
|
45
|
+
schema: { type: 'object' }
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}),
|
|
51
|
+
async c => {
|
|
52
|
+
const body = await c.req.json().catch(() => ({}));
|
|
53
|
+
return c.json({
|
|
54
|
+
message: 'POST received!',
|
|
55
|
+
received: body,
|
|
56
|
+
timestamp: new Date().toISOString(),
|
|
57
|
+
method: 'POST'
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
);
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { defineHandler, defineHandlerMeta, describeRoute } from 'ubean/server';
|
|
2
|
+
|
|
3
|
+
const users = [
|
|
4
|
+
{ id: 1, name: '张三', email: 'zhangsan@example.com', role: 'admin' },
|
|
5
|
+
{ id: 2, name: '李四', email: 'lisi@example.com', role: 'user' },
|
|
6
|
+
{ id: 3, name: '王五', email: 'wangwu@example.com', role: 'user' }
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
export const GET = defineHandler(
|
|
10
|
+
defineHandlerMeta({ public: true }),
|
|
11
|
+
describeRoute({
|
|
12
|
+
summary: 'Get user by ID',
|
|
13
|
+
description: 'Returns a single user by their ID.',
|
|
14
|
+
tags: ['Users'],
|
|
15
|
+
responses: {
|
|
16
|
+
200: {
|
|
17
|
+
description: 'The requested user'
|
|
18
|
+
},
|
|
19
|
+
404: {
|
|
20
|
+
description: 'User not found'
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}),
|
|
24
|
+
c => {
|
|
25
|
+
const rawId = c.req.param('id')!;
|
|
26
|
+
const id = parseInt(rawId, 10);
|
|
27
|
+
const user = users.find(u => u.id === id);
|
|
28
|
+
|
|
29
|
+
if (!user) {
|
|
30
|
+
return c.json({ id: rawId });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return c.json(user);
|
|
34
|
+
}
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
export const PUT = defineHandler(
|
|
38
|
+
defineHandlerMeta({ public: true }),
|
|
39
|
+
describeRoute({
|
|
40
|
+
summary: 'Update user by ID',
|
|
41
|
+
description: 'Updates a user by their ID.',
|
|
42
|
+
tags: ['Users'],
|
|
43
|
+
responses: { 200: { description: 'The updated user' }, 404: { description: 'User not found' } }
|
|
44
|
+
}),
|
|
45
|
+
async c => {
|
|
46
|
+
const id = parseInt(c.req.param('id')!, 10);
|
|
47
|
+
const idx = users.findIndex(u => u.id === id);
|
|
48
|
+
if (idx === -1) {
|
|
49
|
+
return c.json({ error: 'User Not Found', statusCode: 404 }, 404);
|
|
50
|
+
}
|
|
51
|
+
const body = await c.req.json().catch(() => ({}));
|
|
52
|
+
users[idx] = { ...users[idx], ...body, id };
|
|
53
|
+
return c.json(users[idx]);
|
|
54
|
+
}
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
export const PATCH = defineHandler(
|
|
58
|
+
defineHandlerMeta({ public: true }),
|
|
59
|
+
describeRoute({
|
|
60
|
+
summary: 'Patch user by ID',
|
|
61
|
+
description: 'Partially updates a user by their ID.',
|
|
62
|
+
tags: ['Users'],
|
|
63
|
+
responses: { 200: { description: 'The patched user' }, 404: { description: 'User not found' } }
|
|
64
|
+
}),
|
|
65
|
+
async c => {
|
|
66
|
+
const id = parseInt(c.req.param('id')!, 10);
|
|
67
|
+
const idx = users.findIndex(u => u.id === id);
|
|
68
|
+
if (idx === -1) {
|
|
69
|
+
return c.json({ error: 'User Not Found', statusCode: 404 }, 404);
|
|
70
|
+
}
|
|
71
|
+
const body = await c.req.json().catch(() => ({}));
|
|
72
|
+
users[idx] = { ...users[idx], ...body, id };
|
|
73
|
+
return c.json(users[idx]);
|
|
74
|
+
}
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
export const DELETE = defineHandler(
|
|
78
|
+
defineHandlerMeta({ public: true }),
|
|
79
|
+
describeRoute({
|
|
80
|
+
summary: 'Delete user by ID',
|
|
81
|
+
description: 'Deletes a user by their ID.',
|
|
82
|
+
tags: ['Users'],
|
|
83
|
+
responses: { 200: { description: 'Deletion confirmed' }, 404: { description: 'User not found' } }
|
|
84
|
+
}),
|
|
85
|
+
c => {
|
|
86
|
+
const id = parseInt(c.req.param('id')!, 10);
|
|
87
|
+
const idx = users.findIndex(u => u.id === id);
|
|
88
|
+
if (idx === -1) {
|
|
89
|
+
return c.json({ error: 'User Not Found', statusCode: 404 }, 404);
|
|
90
|
+
}
|
|
91
|
+
const deleted = users.splice(idx, 1)[0];
|
|
92
|
+
return c.json({ deleted: true, user: deleted });
|
|
93
|
+
}
|
|
94
|
+
);
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { defineHandler, defineHandlerMeta, describeRoute, resolver, validator } from 'ubean/server';
|
|
2
|
+
import { object, array, pipe, number, string, description } from 'valibot';
|
|
3
|
+
|
|
4
|
+
const users = [
|
|
5
|
+
{ id: 1, name: '张三', email: 'zhangsan@example.com', role: 'admin' },
|
|
6
|
+
{ id: 2, name: '李四', email: 'lisi@example.com', role: 'user' },
|
|
7
|
+
{ id: 3, name: '王五', email: 'wangwu@example.com', role: 'user' }
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
const userSchema = object({
|
|
11
|
+
id: pipe(number(), description('The unique identifier for the user')),
|
|
12
|
+
name: pipe(string(), description('The name of the user')),
|
|
13
|
+
email: pipe(string(), description('The email of the user')),
|
|
14
|
+
role: pipe(string(), description('The role of the user'))
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const userListResponseSchema = object({
|
|
18
|
+
users: array(userSchema),
|
|
19
|
+
total: pipe(number(), description('Total number of users'))
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const createUserSchema = object({
|
|
23
|
+
name: pipe(string(), description('The name of the user')),
|
|
24
|
+
email: pipe(string(), description('The email of the user')),
|
|
25
|
+
role: pipe(string(), description('The role of the user'))
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export const GET = defineHandler(
|
|
29
|
+
defineHandlerMeta({ public: true }),
|
|
30
|
+
describeRoute({
|
|
31
|
+
summary: 'Get all users',
|
|
32
|
+
description: 'Returns a list of all users in the system.',
|
|
33
|
+
tags: ['Users'],
|
|
34
|
+
responses: {
|
|
35
|
+
200: {
|
|
36
|
+
description: 'A list of users',
|
|
37
|
+
content: {
|
|
38
|
+
'application/json': {
|
|
39
|
+
schema: resolver(userListResponseSchema)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}),
|
|
45
|
+
c => {
|
|
46
|
+
return c.json({ users, total: users.length });
|
|
47
|
+
}
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
export const POST = defineHandler(
|
|
51
|
+
defineHandlerMeta({ public: true }),
|
|
52
|
+
describeRoute({
|
|
53
|
+
summary: 'Create a new user',
|
|
54
|
+
description: 'Creates a new user in the system.',
|
|
55
|
+
tags: ['Users'],
|
|
56
|
+
responses: {
|
|
57
|
+
201: {
|
|
58
|
+
description: 'The created user',
|
|
59
|
+
content: {
|
|
60
|
+
'application/json': {
|
|
61
|
+
schema: resolver(userSchema)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}),
|
|
67
|
+
validator('json', createUserSchema),
|
|
68
|
+
async c => {
|
|
69
|
+
const body = c.req.valid('json');
|
|
70
|
+
const newUser = {
|
|
71
|
+
id: users.length + 1,
|
|
72
|
+
name: body.name,
|
|
73
|
+
email: body.email,
|
|
74
|
+
role: body.role
|
|
75
|
+
};
|
|
76
|
+
users.push(newUser);
|
|
77
|
+
return c.json(newUser, 201);
|
|
78
|
+
}
|
|
79
|
+
);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ubean 类型扩展示例
|
|
3
|
+
*
|
|
4
|
+
* 通过 TypeScript 模块增强(Module Augmentation)扩展 UbeanVariables 和 UbeanBindings,
|
|
5
|
+
* 这样在 handler 和 middleware 中使用 c.get()、c.set() 时就能获得完整的类型推导。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
declare module 'ubean' {
|
|
9
|
+
/**
|
|
10
|
+
* 扩展请求上下文变量(Variables)
|
|
11
|
+
*
|
|
12
|
+
* 这里定义的类型可以通过 c.get('key') 获取,通过 c.set('key', value) 设置
|
|
13
|
+
*/
|
|
14
|
+
interface UbeanVariables {
|
|
15
|
+
/** 当前登录用户示例 */
|
|
16
|
+
user?: {
|
|
17
|
+
id: number;
|
|
18
|
+
name: string;
|
|
19
|
+
email: string;
|
|
20
|
+
role: 'admin' | 'user';
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 扩展平台绑定(Bindings)
|
|
26
|
+
*
|
|
27
|
+
* 用于 Cloudflare Workers、Deno 等平台传入的环境变量
|
|
28
|
+
*/
|
|
29
|
+
interface UbeanBindings {
|
|
30
|
+
// 示例:Cloudflare D1 数据库
|
|
31
|
+
// DB: D1Database;
|
|
32
|
+
// 示例:Cloudflare KV
|
|
33
|
+
// KV: KVNamespace;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"jsx": "preserve",
|
|
5
|
+
"jsxImportSource": "vue",
|
|
6
|
+
"lib": ["DOM", "ESNext"],
|
|
7
|
+
"module": "ESNext",
|
|
8
|
+
"moduleResolution": "bundler",
|
|
9
|
+
"paths": {
|
|
10
|
+
"@/*": ["./src/*"],
|
|
11
|
+
"~ubean/*": ["./.ubean/*"]
|
|
12
|
+
},
|
|
13
|
+
"types": ["node", "vite/client"],
|
|
14
|
+
"resolveJsonModule": true,
|
|
15
|
+
"strict": true,
|
|
16
|
+
"strictNullChecks": true,
|
|
17
|
+
"noUnusedLocals": true,
|
|
18
|
+
"allowSyntheticDefaultImports": true,
|
|
19
|
+
"esModuleInterop": true,
|
|
20
|
+
"forceConsistentCasingInFileNames": true,
|
|
21
|
+
"isolatedModules": true,
|
|
22
|
+
"skipLibCheck": true
|
|
23
|
+
},
|
|
24
|
+
"include": ["./**/*.ts", "./**/*.d.ts", "./**/*.tsx", "./**/*.vue", "./.ubean/*"],
|
|
25
|
+
"exclude": ["node_modules", "dist"]
|
|
26
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { defineConfig } from 'ubean';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
i18n: {
|
|
5
|
+
defaultLocale: 'en',
|
|
6
|
+
locales: [
|
|
7
|
+
{ code: 'en', language: 'en', name: 'English' },
|
|
8
|
+
{ code: 'zh', language: 'zh-CN', name: '中文', dir: 'ltr' }
|
|
9
|
+
],
|
|
10
|
+
strategy: 'prefix_except_default'
|
|
11
|
+
},
|
|
12
|
+
// 深合并 override:仅覆盖 connect-src 单条指令,其余 CSP 指令保持框架默认
|
|
13
|
+
// (否则 @iconify/vue 在线加载图标 https://api.iconify.design 会被 default CSP 拦截)
|
|
14
|
+
security: {
|
|
15
|
+
headers: {
|
|
16
|
+
contentSecurityPolicy: {
|
|
17
|
+
'connect-src': ["'self'", 'ws:', 'wss:', 'https://api.iconify.design']
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
prerender: {
|
|
22
|
+
include: ['/about']
|
|
23
|
+
}
|
|
24
|
+
});
|