@finesoft/create-app 0.1.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/dist/index.mjs +129 -0
- package/package.json +26 -0
- package/templates/react/env.d.ts +1 -0
- package/templates/react/index.html +13 -0
- package/templates/react/package.json +21 -0
- package/templates/react/src/App.tsx +20 -0
- package/templates/react/src/actions.ts +14 -0
- package/templates/react/src/bootstrap.ts +36 -0
- package/templates/react/src/components/Layout.tsx +63 -0
- package/templates/react/src/components/Loading.tsx +3 -0
- package/templates/react/src/components/Navigation.tsx +51 -0
- package/templates/react/src/components/PageRenderer.tsx +41 -0
- package/templates/react/src/components/ProductCard.tsx +35 -0
- package/templates/react/src/lib/controllers/about.ts +18 -0
- package/templates/react/src/lib/controllers/error.ts +11 -0
- package/templates/react/src/lib/controllers/home.ts +53 -0
- package/templates/react/src/lib/controllers/product-detail.ts +52 -0
- package/templates/react/src/lib/controllers/search.ts +58 -0
- package/templates/react/src/lib/guards/auth.ts +17 -0
- package/templates/react/src/lib/guards/seo.ts +15 -0
- package/templates/react/src/lib/mappers/product.ts +27 -0
- package/templates/react/src/lib/models/product.ts +48 -0
- package/templates/react/src/lib/services/api-client.ts +28 -0
- package/templates/react/src/main.tsx +49 -0
- package/templates/react/src/pages/About.tsx +16 -0
- package/templates/react/src/pages/Home.tsx +34 -0
- package/templates/react/src/pages/NotFound.tsx +25 -0
- package/templates/react/src/pages/ProductDetail.tsx +34 -0
- package/templates/react/src/pages/Search.tsx +23 -0
- package/templates/react/src/ssr.tsx +23 -0
- package/templates/react/tsconfig.json +17 -0
- package/templates/react/vite.config.ts +20 -0
- package/templates/react-minimal/env.d.ts +1 -0
- package/templates/react-minimal/index.html +13 -0
- package/templates/react-minimal/package.json +21 -0
- package/templates/react-minimal/src/App.tsx +22 -0
- package/templates/react-minimal/src/bootstrap.ts +6 -0
- package/templates/react-minimal/src/lib/controllers/home.ts +14 -0
- package/templates/react-minimal/src/main.tsx +44 -0
- package/templates/react-minimal/src/ssr.tsx +25 -0
- package/templates/react-minimal/tsconfig.json +17 -0
- package/templates/react-minimal/vite.config.ts +12 -0
- package/templates/svelte/env.d.ts +9 -0
- package/templates/svelte/index.html +13 -0
- package/templates/svelte/package.json +18 -0
- package/templates/svelte/src/App.svelte +85 -0
- package/templates/svelte/src/actions.ts +14 -0
- package/templates/svelte/src/bootstrap.ts +36 -0
- package/templates/svelte/src/components/Layout.svelte +19 -0
- package/templates/svelte/src/components/Loading.svelte +1 -0
- package/templates/svelte/src/components/Navigation.svelte +75 -0
- package/templates/svelte/src/components/PageRenderer.svelte +22 -0
- package/templates/svelte/src/components/ProductCard.svelte +37 -0
- package/templates/svelte/src/lib/controllers/about.ts +18 -0
- package/templates/svelte/src/lib/controllers/error.ts +11 -0
- package/templates/svelte/src/lib/controllers/home.ts +53 -0
- package/templates/svelte/src/lib/controllers/product-detail.ts +52 -0
- package/templates/svelte/src/lib/controllers/search.ts +58 -0
- package/templates/svelte/src/lib/framework-svelte.ts +34 -0
- package/templates/svelte/src/lib/guards/auth.ts +17 -0
- package/templates/svelte/src/lib/guards/seo.ts +15 -0
- package/templates/svelte/src/lib/mappers/product.ts +27 -0
- package/templates/svelte/src/lib/models/product.ts +47 -0
- package/templates/svelte/src/lib/render.ts +15 -0
- package/templates/svelte/src/lib/services/api-client.ts +28 -0
- package/templates/svelte/src/main.ts +27 -0
- package/templates/svelte/src/pages/About.svelte +10 -0
- package/templates/svelte/src/pages/Home.svelte +34 -0
- package/templates/svelte/src/pages/NotFound.svelte +23 -0
- package/templates/svelte/src/pages/ProductDetail.svelte +32 -0
- package/templates/svelte/src/pages/Search.svelte +29 -0
- package/templates/svelte/src/ssr.ts +12 -0
- package/templates/svelte/tsconfig.json +16 -0
- package/templates/svelte/vite.config.ts +21 -0
- package/templates/svelte-minimal/env.d.ts +9 -0
- package/templates/svelte-minimal/index.html +13 -0
- package/templates/svelte-minimal/package.json +18 -0
- package/templates/svelte-minimal/src/App.svelte +63 -0
- package/templates/svelte-minimal/src/bootstrap.ts +6 -0
- package/templates/svelte-minimal/src/lib/controllers/home.ts +15 -0
- package/templates/svelte-minimal/src/lib/framework-svelte.ts +34 -0
- package/templates/svelte-minimal/src/lib/render.ts +12 -0
- package/templates/svelte-minimal/src/main.ts +22 -0
- package/templates/svelte-minimal/src/pages/Home.svelte +10 -0
- package/templates/svelte-minimal/src/ssr.ts +18 -0
- package/templates/svelte-minimal/tsconfig.json +16 -0
- package/templates/svelte-minimal/vite.config.ts +12 -0
- package/templates/vue/env.d.ts +7 -0
- package/templates/vue/index.html +13 -0
- package/templates/vue/package.json +18 -0
- package/templates/vue/src/App.vue +29 -0
- package/templates/vue/src/actions.ts +14 -0
- package/templates/vue/src/bootstrap.ts +42 -0
- package/templates/vue/src/components/Layout.vue +18 -0
- package/templates/vue/src/components/Loading.vue +3 -0
- package/templates/vue/src/components/Navigation.vue +58 -0
- package/templates/vue/src/components/PageRenderer.vue +26 -0
- package/templates/vue/src/components/ProductCard.vue +39 -0
- package/templates/vue/src/lib/controllers/about.ts +18 -0
- package/templates/vue/src/lib/controllers/error.ts +11 -0
- package/templates/vue/src/lib/controllers/home.ts +53 -0
- package/templates/vue/src/lib/controllers/product-detail.ts +52 -0
- package/templates/vue/src/lib/controllers/search.ts +58 -0
- package/templates/vue/src/lib/guards/auth.ts +17 -0
- package/templates/vue/src/lib/guards/seo.ts +15 -0
- package/templates/vue/src/lib/mappers/product.ts +27 -0
- package/templates/vue/src/lib/models/product.ts +47 -0
- package/templates/vue/src/lib/services/api-client.ts +31 -0
- package/templates/vue/src/main.ts +46 -0
- package/templates/vue/src/pages/About.vue +16 -0
- package/templates/vue/src/pages/Home.vue +38 -0
- package/templates/vue/src/pages/NotFound.vue +22 -0
- package/templates/vue/src/pages/ProductDetail.vue +34 -0
- package/templates/vue/src/pages/Search.vue +33 -0
- package/templates/vue/src/ssr.ts +24 -0
- package/templates/vue/tsconfig.json +17 -0
- package/templates/vue/vite.config.ts +20 -0
- package/templates/vue-minimal/env.d.ts +7 -0
- package/templates/vue-minimal/index.html +13 -0
- package/templates/vue-minimal/package.json +18 -0
- package/templates/vue-minimal/src/App.vue +21 -0
- package/templates/vue-minimal/src/bootstrap.ts +6 -0
- package/templates/vue-minimal/src/lib/controllers/home.ts +15 -0
- package/templates/vue-minimal/src/main.ts +41 -0
- package/templates/vue-minimal/src/pages/Home.vue +12 -0
- package/templates/vue-minimal/src/ssr.ts +28 -0
- package/templates/vue-minimal/tsconfig.json +17 -0
- package/templates/vue-minimal/vite.config.ts +12 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { startBrowserApp, type BasePage, type Framework } from "@finesoft/front";
|
|
2
|
+
import { hydrate } from "svelte";
|
|
3
|
+
import App from "./App.svelte";
|
|
4
|
+
import { bootstrap } from "./bootstrap";
|
|
5
|
+
import type { AppPage } from "./lib/models/product";
|
|
6
|
+
|
|
7
|
+
void startBrowserApp({
|
|
8
|
+
bootstrap,
|
|
9
|
+
mount(target: HTMLElement, { framework }: { framework: Framework; locale: string }) {
|
|
10
|
+
// Svelte 5 中必须用 hydrate() 激活 SSR 输出,new Component() 已被移除
|
|
11
|
+
const app = hydrate(App, { target, props: { framework } });
|
|
12
|
+
|
|
13
|
+
return (props: { page: Promise<BasePage> | BasePage; isFirstPage?: boolean }) => {
|
|
14
|
+
(app as { updatePage: (p: Promise<AppPage> | AppPage) => void }).updatePage(
|
|
15
|
+
props.page as Promise<AppPage> | AppPage,
|
|
16
|
+
);
|
|
17
|
+
};
|
|
18
|
+
},
|
|
19
|
+
callbacks: {
|
|
20
|
+
onNavigate(pathname) {
|
|
21
|
+
console.log("[navigate]", pathname);
|
|
22
|
+
},
|
|
23
|
+
onModal(page) {
|
|
24
|
+
console.log("[modal]", page.title);
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import ProductCard from "../components/ProductCard.svelte";
|
|
3
|
+
import type { HomePage } from "../lib/models/product";
|
|
4
|
+
|
|
5
|
+
let { page }: { page: HomePage } = $props();
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<div>
|
|
9
|
+
<h1>{page.title}</h1>
|
|
10
|
+
<p>{page.description}</p>
|
|
11
|
+
|
|
12
|
+
{#each page.shelves as shelf}
|
|
13
|
+
<section>
|
|
14
|
+
<h2>{shelf.title}</h2>
|
|
15
|
+
<div class="shelf" class:horizontal={shelf.isHorizontal}>
|
|
16
|
+
{#each shelf.items as item}
|
|
17
|
+
<ProductCard {item} />
|
|
18
|
+
{/each}
|
|
19
|
+
</div>
|
|
20
|
+
</section>
|
|
21
|
+
{/each}
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<style>
|
|
25
|
+
.shelf {
|
|
26
|
+
display: flex;
|
|
27
|
+
gap: 1rem;
|
|
28
|
+
flex-wrap: wrap;
|
|
29
|
+
}
|
|
30
|
+
.shelf.horizontal {
|
|
31
|
+
flex-wrap: nowrap;
|
|
32
|
+
overflow-x: auto;
|
|
33
|
+
}
|
|
34
|
+
</style>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { NAV_ACTIONS } from "../actions";
|
|
3
|
+
import { getPerform } from "../lib/framework-svelte";
|
|
4
|
+
import type { ErrorPage } from "../lib/models/product";
|
|
5
|
+
|
|
6
|
+
let { page }: { page: ErrorPage } = $props();
|
|
7
|
+
|
|
8
|
+
const perform = getPerform();
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<div>
|
|
12
|
+
<h1>{page.title}</h1>
|
|
13
|
+
<p>{page.description}</p>
|
|
14
|
+
<a
|
|
15
|
+
href="/"
|
|
16
|
+
onclick={(e) => {
|
|
17
|
+
if (perform) {
|
|
18
|
+
e.preventDefault();
|
|
19
|
+
void perform(NAV_ACTIONS.home);
|
|
20
|
+
}
|
|
21
|
+
}}>← Go Home</a
|
|
22
|
+
>
|
|
23
|
+
</div>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { NAV_ACTIONS } from "../actions";
|
|
3
|
+
import { getPerform } from "../lib/framework-svelte";
|
|
4
|
+
import type { ProductPage } from "../lib/models/product";
|
|
5
|
+
|
|
6
|
+
let { page }: { page: ProductPage } = $props();
|
|
7
|
+
|
|
8
|
+
const perform = getPerform();
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<div>
|
|
12
|
+
<a
|
|
13
|
+
href="/"
|
|
14
|
+
onclick={(e) => {
|
|
15
|
+
if (perform) {
|
|
16
|
+
e.preventDefault();
|
|
17
|
+
void perform(NAV_ACTIONS.home);
|
|
18
|
+
}
|
|
19
|
+
}}>← Back</a
|
|
20
|
+
>
|
|
21
|
+
<h1>{page.product.name}</h1>
|
|
22
|
+
<p class="price">${page.product.price.toFixed(2)}</p>
|
|
23
|
+
<p>{page.product.description}</p>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<style>
|
|
27
|
+
.price {
|
|
28
|
+
font-size: 1.5rem;
|
|
29
|
+
color: #007bff;
|
|
30
|
+
font-weight: bold;
|
|
31
|
+
}
|
|
32
|
+
</style>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import ProductCard from "../components/ProductCard.svelte";
|
|
3
|
+
import type { SearchPage } from "../lib/models/product";
|
|
4
|
+
|
|
5
|
+
let { page }: { page: SearchPage } = $props();
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<div>
|
|
9
|
+
<h1>{page.title}</h1>
|
|
10
|
+
<p>{page.description}</p>
|
|
11
|
+
|
|
12
|
+
<div class="results">
|
|
13
|
+
{#each page.results as item}
|
|
14
|
+
<ProductCard {item} />
|
|
15
|
+
{/each}
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
{#if page.results.length === 0}
|
|
19
|
+
<p>No products found.</p>
|
|
20
|
+
{/if}
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<style>
|
|
24
|
+
.results {
|
|
25
|
+
display: flex;
|
|
26
|
+
gap: 1rem;
|
|
27
|
+
flex-wrap: wrap;
|
|
28
|
+
}
|
|
29
|
+
</style>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createSSRRender, serializeServerData } from "@finesoft/front";
|
|
2
|
+
import { bootstrap } from "./bootstrap";
|
|
3
|
+
import { getErrorPage } from "./lib/controllers/error";
|
|
4
|
+
import { renderApp } from "./lib/render";
|
|
5
|
+
|
|
6
|
+
export const render = createSSRRender({
|
|
7
|
+
bootstrap,
|
|
8
|
+
getErrorPage,
|
|
9
|
+
renderApp,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export { serializeServerData };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"strict": true,
|
|
4
|
+
"target": "ESNext",
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"forceConsistentCasingInFileNames": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"noEmit": true,
|
|
11
|
+
"paths": {
|
|
12
|
+
"@/*": ["./src/*"]
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*", "env.d.ts"]
|
|
16
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { finesoftFrontViteConfig } from "@finesoft/front";
|
|
2
|
+
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
|
3
|
+
import { defineConfig } from "vite-plus";
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
plugins: [
|
|
7
|
+
svelte(),
|
|
8
|
+
finesoftFrontViteConfig({
|
|
9
|
+
ssr: { entry: "src/ssr.ts" },
|
|
10
|
+
locales: ["en", "zh"],
|
|
11
|
+
defaultLocale: "en",
|
|
12
|
+
proxies: [
|
|
13
|
+
{
|
|
14
|
+
prefix: "/api",
|
|
15
|
+
target: "https://jsonplaceholder.typicode.com",
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
adapter: "auto",
|
|
19
|
+
}),
|
|
20
|
+
],
|
|
21
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="<!--ssr-lang-->">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<!--ssr-head-->
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="app"><!--ssr-body--></div>
|
|
10
|
+
<!--ssr-data-->
|
|
11
|
+
<script type="module" src="/src/main.ts"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "finesoft-svelte-minimal",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "vp dev",
|
|
7
|
+
"build": "vp build",
|
|
8
|
+
"preview": "vp preview"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@finesoft/front": "workspace:*",
|
|
12
|
+
"svelte": "^5.0.0"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
|
16
|
+
"vite-plus": "catalog:"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { type BasePage, Framework } from "@finesoft/front";
|
|
3
|
+
import { untrack } from "svelte";
|
|
4
|
+
import { setFrameworkContext } from "./lib/framework-svelte";
|
|
5
|
+
import Home from "./pages/Home.svelte";
|
|
6
|
+
|
|
7
|
+
type Props = {
|
|
8
|
+
framework?: Framework;
|
|
9
|
+
page?: BasePage | null;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
let props: Props = $props();
|
|
13
|
+
|
|
14
|
+
// context 只需在初始化时设置一次,用 untrack 明确声明仅读取初始值
|
|
15
|
+
const framework = untrack(() => props.framework);
|
|
16
|
+
if (framework) {
|
|
17
|
+
setFrameworkContext(framework);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** 将导航 Promise 的 rejection 转为错误页,避免 {:catch} 暴露原始异常 */
|
|
21
|
+
function wrapError(p: Promise<BasePage>): Promise<BasePage> {
|
|
22
|
+
return p.catch(
|
|
23
|
+
(err): BasePage => ({
|
|
24
|
+
id: "error",
|
|
25
|
+
pageType: "error",
|
|
26
|
+
title: "Error",
|
|
27
|
+
description:
|
|
28
|
+
err instanceof Error ? err.message : "Failed to load page",
|
|
29
|
+
}),
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 当前页面数据:
|
|
35
|
+
* - BasePage: SSR 直出 或 已完成的导航
|
|
36
|
+
* - Promise<BasePage>: SPA 导航中
|
|
37
|
+
* - null: 初始空白态
|
|
38
|
+
*/
|
|
39
|
+
let currentPage = $state<Promise<BasePage> | BasePage | null>(
|
|
40
|
+
untrack(() => props.page ?? null),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
/** 由 main.ts 的 mount 回调调用,驱动 SPA 导航 */
|
|
44
|
+
export function updatePage(newPage: Promise<BasePage> | BasePage): void {
|
|
45
|
+
if (newPage instanceof Promise) {
|
|
46
|
+
currentPage = wrapError(newPage);
|
|
47
|
+
} else {
|
|
48
|
+
currentPage = newPage;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<main style="padding: 1rem">
|
|
54
|
+
{#if currentPage instanceof Promise}
|
|
55
|
+
{#await currentPage}
|
|
56
|
+
<p style="text-align: center; color: #999">Loading…</p>
|
|
57
|
+
{:then page}
|
|
58
|
+
<Home {page} />
|
|
59
|
+
{/await}
|
|
60
|
+
{:else if currentPage}
|
|
61
|
+
<Home page={currentPage} />
|
|
62
|
+
{/if}
|
|
63
|
+
</main>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type Framework, defineRoutes } from "@finesoft/front";
|
|
2
|
+
import { HomeController } from "./lib/controllers/home";
|
|
3
|
+
|
|
4
|
+
export function bootstrap(framework: Framework): void {
|
|
5
|
+
defineRoutes(framework, [{ path: "/", intentId: "home", controller: new HomeController() }]);
|
|
6
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { BaseController, type BasePage } from "@finesoft/front";
|
|
2
|
+
|
|
3
|
+
export class HomeController extends BaseController<Record<string, string>, BasePage> {
|
|
4
|
+
readonly intentId = "home";
|
|
5
|
+
|
|
6
|
+
execute(): BasePage {
|
|
7
|
+
return {
|
|
8
|
+
id: "home",
|
|
9
|
+
pageType: "home",
|
|
10
|
+
url: "/",
|
|
11
|
+
title: "Home",
|
|
12
|
+
description: "Welcome to Finesoft Front",
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Svelte Context 集成 — Framework 实例的 Svelte context 传递
|
|
3
|
+
*
|
|
4
|
+
* 在根组件(App.svelte)中调用 setFrameworkContext,
|
|
5
|
+
* 子组件通过 getPerform 获取 perform 函数,无需逐层传递 onaction 回调。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Action } from "@finesoft/front";
|
|
9
|
+
import { Framework } from "@finesoft/front";
|
|
10
|
+
import { getContext, setContext } from "svelte";
|
|
11
|
+
|
|
12
|
+
const FRAMEWORK_KEY = "framework";
|
|
13
|
+
|
|
14
|
+
/** 在根组件初始化时设置 Framework 实例到 Svelte context */
|
|
15
|
+
export function setFrameworkContext(framework: Framework): void {
|
|
16
|
+
setContext(FRAMEWORK_KEY, framework);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** 获取 Framework 实例;SSR 或未设置时返回 undefined */
|
|
20
|
+
export function getFramework(): Framework | undefined {
|
|
21
|
+
return getContext<Framework | undefined>(FRAMEWORK_KEY);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 获取 perform 函数,供导航/操作类组件在事件处理器中调用。
|
|
26
|
+
* 在组件初始化阶段(<script> 顶层)调用,不能在事件回调内调用。
|
|
27
|
+
*
|
|
28
|
+
* @returns perform 函数;SSR 或无 context 时返回 undefined
|
|
29
|
+
*/
|
|
30
|
+
export function getPerform(): ((action: Action) => Promise<void>) | undefined {
|
|
31
|
+
const fw = getFramework();
|
|
32
|
+
if (!fw) return undefined;
|
|
33
|
+
return (action: Action) => fw.perform(action);
|
|
34
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { BasePage } from "@finesoft/front";
|
|
2
|
+
import { render } from "svelte/server";
|
|
3
|
+
import App from "../App.svelte";
|
|
4
|
+
|
|
5
|
+
export function renderApp(page: BasePage, _locale: string) {
|
|
6
|
+
const result = render(App, { props: { page } });
|
|
7
|
+
return {
|
|
8
|
+
html: result.body ?? "",
|
|
9
|
+
head: `<title>${page.title}</title>${result.head ?? ""}`,
|
|
10
|
+
css: "",
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { startBrowserApp, type BasePage, type Framework } from "@finesoft/front";
|
|
2
|
+
import { hydrate } from "svelte";
|
|
3
|
+
import App from "./App.svelte";
|
|
4
|
+
import { bootstrap } from "./bootstrap";
|
|
5
|
+
|
|
6
|
+
void startBrowserApp({
|
|
7
|
+
bootstrap,
|
|
8
|
+
mount(target: HTMLElement, { framework }: { framework: Framework; locale: string }) {
|
|
9
|
+
// Svelte 5 中必须用 hydrate() 激活 SSR 输出,new Component() 已被移除
|
|
10
|
+
const app = hydrate(App, { target, props: { framework } });
|
|
11
|
+
|
|
12
|
+
return (props: { page: Promise<BasePage> | BasePage; isFirstPage?: boolean }) => {
|
|
13
|
+
(app as { updatePage: (p: Promise<BasePage> | BasePage) => void }).updatePage(
|
|
14
|
+
props.page,
|
|
15
|
+
);
|
|
16
|
+
};
|
|
17
|
+
},
|
|
18
|
+
callbacks: {
|
|
19
|
+
onNavigate() {},
|
|
20
|
+
onModal() {},
|
|
21
|
+
},
|
|
22
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createSSRRender, serializeServerData } from "@finesoft/front";
|
|
2
|
+
import { bootstrap } from "./bootstrap";
|
|
3
|
+
import { renderApp } from "./lib/render";
|
|
4
|
+
|
|
5
|
+
export const render = createSSRRender({
|
|
6
|
+
bootstrap,
|
|
7
|
+
getErrorPage(status, message) {
|
|
8
|
+
return {
|
|
9
|
+
id: "error",
|
|
10
|
+
pageType: "error",
|
|
11
|
+
title: `Error ${status}`,
|
|
12
|
+
description: message,
|
|
13
|
+
};
|
|
14
|
+
},
|
|
15
|
+
renderApp,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export { serializeServerData };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"strict": true,
|
|
4
|
+
"target": "ESNext",
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"forceConsistentCasingInFileNames": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"noEmit": true,
|
|
11
|
+
"paths": {
|
|
12
|
+
"@/*": ["./src/*"]
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*", "env.d.ts"]
|
|
16
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { finesoftFrontViteConfig } from "@finesoft/front";
|
|
2
|
+
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
|
3
|
+
import { defineConfig } from "vite-plus";
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
plugins: [
|
|
7
|
+
svelte(),
|
|
8
|
+
finesoftFrontViteConfig({
|
|
9
|
+
ssr: { entry: "src/ssr.ts" },
|
|
10
|
+
}),
|
|
11
|
+
],
|
|
12
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="<!--ssr-lang-->">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<!--ssr-head-->
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="app"><!--ssr-body--></div>
|
|
10
|
+
<!--ssr-data-->
|
|
11
|
+
<script type="module" src="/src/main.ts"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "finesoft-vue",
|
|
3
|
+
"private": true,
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "vp dev",
|
|
7
|
+
"build": "vp build",
|
|
8
|
+
"preview": "vp preview"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@finesoft/front": "workspace:*",
|
|
12
|
+
"vue": "^3.5.0"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@vitejs/plugin-vue": "^5.0.0",
|
|
16
|
+
"vite-plus": "catalog:"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Action } from "@finesoft/front";
|
|
3
|
+
import { computed } from "vue";
|
|
4
|
+
import Layout from "./components/Layout.vue";
|
|
5
|
+
import Loading from "./components/Loading.vue";
|
|
6
|
+
import PageRenderer from "./components/PageRenderer.vue";
|
|
7
|
+
import type { AppPage } from "./lib/models/product";
|
|
8
|
+
|
|
9
|
+
// state: reactive object for CSR; page: initial prop for SSR
|
|
10
|
+
const {
|
|
11
|
+
state,
|
|
12
|
+
page: ssrPage,
|
|
13
|
+
onAction,
|
|
14
|
+
} = defineProps<{
|
|
15
|
+
state?: { page: AppPage | null; loading: boolean };
|
|
16
|
+
page?: AppPage;
|
|
17
|
+
onAction?: (action: Action) => void;
|
|
18
|
+
}>();
|
|
19
|
+
|
|
20
|
+
const currentPage = computed(() => state?.page ?? ssrPage ?? null);
|
|
21
|
+
const loading = computed(() => state?.loading ?? false);
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<template>
|
|
25
|
+
<Layout :current-path="currentPage?.url ?? '/'" :on-action="onAction">
|
|
26
|
+
<Loading v-if="loading" />
|
|
27
|
+
<PageRenderer v-else-if="currentPage" :page="currentPage" :on-action="onAction" />
|
|
28
|
+
</Layout>
|
|
29
|
+
</template>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { makeExternalUrlAction, makeFlowAction, type Action } from "@finesoft/front";
|
|
2
|
+
|
|
3
|
+
/** 创建指向商品详情的 FlowAction */
|
|
4
|
+
export function productDetailAction(id: string): Action {
|
|
5
|
+
return makeFlowAction(`/products/${id}`);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** 导航 Actions */
|
|
9
|
+
export const NAV_ACTIONS = {
|
|
10
|
+
home: makeFlowAction("/"),
|
|
11
|
+
search: makeFlowAction("/search"),
|
|
12
|
+
about: makeFlowAction("/about"),
|
|
13
|
+
github: makeExternalUrlAction("https://github.com/nicepkg/finesoft"),
|
|
14
|
+
} as const;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type Framework, defineRoutes } from "@finesoft/front";
|
|
2
|
+
import { AboutController } from "./lib/controllers/about";
|
|
3
|
+
import { HomeController } from "./lib/controllers/home";
|
|
4
|
+
import { ProductDetailController } from "./lib/controllers/product-detail";
|
|
5
|
+
import { SearchController } from "./lib/controllers/search";
|
|
6
|
+
import { authGuard } from "./lib/guards/auth";
|
|
7
|
+
import { seoGuard } from "./lib/guards/seo";
|
|
8
|
+
|
|
9
|
+
export function bootstrap(framework: Framework): void {
|
|
10
|
+
// Global afterLoad guard — SEO URL normalization
|
|
11
|
+
framework.afterLoad(seoGuard);
|
|
12
|
+
|
|
13
|
+
defineRoutes(framework, [
|
|
14
|
+
// SSR routes (default)
|
|
15
|
+
{ path: "/", intentId: "home", controller: new HomeController() },
|
|
16
|
+
{
|
|
17
|
+
path: "/products/:id",
|
|
18
|
+
intentId: "product-detail",
|
|
19
|
+
controller: new ProductDetailController(),
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
path: "/search",
|
|
23
|
+
intentId: "search",
|
|
24
|
+
controller: new SearchController(),
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
// CSR-only route
|
|
28
|
+
{
|
|
29
|
+
path: "/about",
|
|
30
|
+
intentId: "about",
|
|
31
|
+
controller: new AboutController(),
|
|
32
|
+
renderMode: "csr",
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
// Guarded route — requires auth cookie
|
|
36
|
+
{
|
|
37
|
+
path: "/admin",
|
|
38
|
+
intentId: "home",
|
|
39
|
+
beforeLoad: [authGuard],
|
|
40
|
+
},
|
|
41
|
+
]);
|
|
42
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Action } from "@finesoft/front";
|
|
3
|
+
import Navigation from "./Navigation.vue";
|
|
4
|
+
|
|
5
|
+
const { currentPath = "/", onAction } = defineProps<{
|
|
6
|
+
currentPath?: string;
|
|
7
|
+
onAction?: (action: Action) => void;
|
|
8
|
+
}>();
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<template>
|
|
12
|
+
<div>
|
|
13
|
+
<Navigation :current-path="currentPath" :on-action="onAction" />
|
|
14
|
+
<main style="padding: 1rem">
|
|
15
|
+
<slot />
|
|
16
|
+
</main>
|
|
17
|
+
</div>
|
|
18
|
+
</template>
|