-
MOSAIC H5 STUDIO
+
MOSAIC H5 STUDIO
预览 · {{device==='phone'?'390 × 844':'375 × 667'}}{{schema.title}}
diff --git a/apps/editor/tsconfig.json b/apps/editor/tsconfig.json
index b3ed883..d2cf333 100644
--- a/apps/editor/tsconfig.json
+++ b/apps/editor/tsconfig.json
@@ -7,6 +7,7 @@
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"types": ["vite/client"],
"allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
diff --git a/apps/h5/index.html b/apps/h5/index.html
index f83f8fb..52acca3 100644
--- a/apps/h5/index.html
+++ b/apps/h5/index.html
@@ -1,3 +1,9 @@
+
+
+
H5 预览
diff --git a/apps/h5/package.json b/apps/h5/package.json
index 9a0c035..02918d7 100644
--- a/apps/h5/package.json
+++ b/apps/h5/package.json
@@ -9,7 +9,8 @@
"dependencies": {
"vue": "^3.5.17",
"@demo/page-schema": "*",
- "@demo/renderer": "*"
+ "@demo/renderer": "*",
+ "@demo/shared": "*"
},
"devDependencies": {
"vite": "^7.0.6",
diff --git a/apps/h5/src/main.ts b/apps/h5/src/main.ts
index fc5d2ac..1589e57 100644
--- a/apps/h5/src/main.ts
+++ b/apps/h5/src/main.ts
@@ -1,6 +1,7 @@
import { createApp, defineComponent, ref, onMounted } from 'vue';
import { PageRenderer } from '@demo/renderer';
import type { PageSchema } from '@demo/page-schema';
+import { previewOrigin, type PreviewMessage } from '@demo/shared';
import './style.css';
import './enhanced.css';
const api = `${location.protocol}//${location.hostname}:3000/api/pages/`;
@@ -11,20 +12,32 @@ const App = defineComponent({
error = ref('');
onMounted(async () => {
addEventListener('message', (e) => {
- if (e.data?.type === 'preview') schema.value = e.data.schema;
+ if (e.origin !== previewOrigin()) return;
+ const data = e.data as PreviewMessage;
+ if (data?.type === 'preview') schema.value = data.schema;
});
- if (!location.search.includes('preview'))
+ if (!location.search.includes('preview')) {
+ const id = new URLSearchParams(location.search).get('id') || 'demo-store';
+ const cacheKey = 'page:' + id;
+ try {
+ const cached = sessionStorage.getItem(cacheKey);
+ if (cached) {
+ const parsed = JSON.parse(cached);
+ if (parsed?.schema) schema.value = parsed.schema as PageSchema;
+ }
+ } catch {
+ /* ignore corrupt cache */
+ }
try {
- const id = new URLSearchParams(location.search).get('id') || 'demo-store';
- const cached = sessionStorage.getItem('page:' + id);
- if (cached) schema.value = JSON.parse(cached);
const r = await fetch(api + id + '/published');
if (!r.ok) throw Error('该模板尚未发布');
- schema.value = await r.json();
- sessionStorage.setItem('page:' + id, JSON.stringify(schema.value));
+ const next: PageSchema = await r.json();
+ schema.value = next;
+ sessionStorage.setItem(cacheKey, JSON.stringify({ version: next.version, schema: next }));
} catch (e: any) {
- error.value = e.message;
+ if (!schema.value) error.value = e.message;
}
+ }
});
return { schema, error };
},
diff --git a/apps/h5/tsconfig.json b/apps/h5/tsconfig.json
index b3ed883..d2cf333 100644
--- a/apps/h5/tsconfig.json
+++ b/apps/h5/tsconfig.json
@@ -7,6 +7,7 @@
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"types": ["vite/client"],
"allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
diff --git a/apps/server/package.json b/apps/server/package.json
index c59f442..89c42c0 100644
--- a/apps/server/package.json
+++ b/apps/server/package.json
@@ -2,7 +2,7 @@
"name": "@demo/server",
"type": "module",
"scripts": {
- "dev": "node src/index.mjs"
+ "dev": "node --watch src/index.mjs"
},
"dependencies": {
"express": "^5.1.0",
diff --git a/apps/server/src/index.mjs b/apps/server/src/index.mjs
index 518fc69..74c678d 100644
--- a/apps/server/src/index.mjs
+++ b/apps/server/src/index.mjs
@@ -2,6 +2,7 @@ import express from 'express';
import cors from 'cors';
import fs from 'node:fs';
import path from 'node:path';
+import { fileURLToPath } from 'node:url';
const app = express(),
file = path.resolve('data.json');
@@ -18,26 +19,33 @@ const seed = {
versions: [],
};
const read = () => {
- const x = fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 'utf8')) : structuredClone(seed);
+ let x;
+ if (fs.existsSync(file)) {
+ try {
+ x = JSON.parse(fs.readFileSync(file, 'utf8'));
+ } catch (error) {
+ console.error('data.json 解析失败,已回退到种子数据', error);
+ x = structuredClone(seed);
+ }
+ } else {
+ x = structuredClone(seed);
+ }
x.versions ||= x.published ? [x.published] : [];
return x;
};
-const write = (x) => fs.writeFileSync(file, JSON.stringify(x, null, 2));
-const allowed = new Set([
- 'banner',
- 'title',
- 'text',
- 'image',
- 'button',
- 'product-list',
- 'divider',
- 'spacer',
- 'two-column',
- 'benefit-product',
- 'benefit-actions',
- 'benefit-notice',
- 'benefit-product-list',
-]);
+const write = (x) => {
+ const tmp = file + '.tmp';
+ fs.writeFileSync(tmp, JSON.stringify(x, null, 2));
+ fs.renameSync(tmp, file);
+};
+const allowed = new Set(
+ JSON.parse(
+ fs.readFileSync(
+ fileURLToPath(new URL('../../../packages/page-schema/component-types.json', import.meta.url)),
+ 'utf8',
+ ),
+ ),
+);
const isSafe = (s) =>
s &&
typeof s.id === 'string' &&
@@ -60,8 +68,9 @@ app.put('/api/pages/:id/draft', (q, s) => {
s.json(x.draft);
});
app.post('/api/pages/:id/publish', (q, s) => {
- const x = read(),
- next = (x.versions.at(-1)?.version || 0) + 1,
+ const x = read();
+ if (!isSafe(x.draft)) return s.status(400).json({ error: 'draft invalid' });
+ const next = (x.versions.at(-1)?.version || 0) + 1,
snapshot = structuredClone({
...x.draft,
id: q.params.id,
@@ -69,6 +78,7 @@ app.post('/api/pages/:id/publish', (q, s) => {
updatedAt: new Date().toISOString(),
});
x.versions.push(snapshot);
+ x.versions = x.versions.slice(-20);
x.published = snapshot;
write(x);
s.json(x.published);
diff --git a/package-lock.json b/package-lock.json
index e84c2d3..a5f455b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -30,6 +30,7 @@
"@demo/materials": "*",
"@demo/page-schema": "*",
"@demo/renderer": "*",
+ "@demo/shared": "*",
"vue": "^3.5.17"
},
"devDependencies": {
@@ -42,6 +43,7 @@
"dependencies": {
"@demo/page-schema": "*",
"@demo/renderer": "*",
+ "@demo/shared": "*",
"vue": "^3.5.17"
},
"devDependencies": {
@@ -3861,7 +3863,10 @@
},
"packages/shared": {
"name": "@demo/shared",
- "version": "1.0.0"
+ "version": "1.0.0",
+ "dependencies": {
+ "@demo/page-schema": "*"
+ }
}
}
}
diff --git a/packages/page-schema/component-types.json b/packages/page-schema/component-types.json
new file mode 100644
index 0000000..c528bf8
--- /dev/null
+++ b/packages/page-schema/component-types.json
@@ -0,0 +1,15 @@
+[
+ "banner",
+ "title",
+ "text",
+ "image",
+ "button",
+ "product-list",
+ "divider",
+ "spacer",
+ "two-column",
+ "benefit-product",
+ "benefit-actions",
+ "benefit-notice",
+ "benefit-product-list"
+]
diff --git a/packages/page-schema/src/index.ts b/packages/page-schema/src/index.ts
index 7b1ae83..980dff2 100644
--- a/packages/page-schema/src/index.ts
+++ b/packages/page-schema/src/index.ts
@@ -1,17 +1,33 @@
-export type ComponentType =
- | 'banner'
- | 'title'
- | 'text'
- | 'image'
- | 'button'
- | 'product-list'
- | 'divider'
- | 'spacer'
- | 'two-column'
- | 'benefit-product'
- | 'benefit-actions'
- | 'benefit-notice'
- | 'benefit-product-list';
+import componentTypesMirror from '../component-types.json';
+
+// Canonical list + compile-time union in one place.
+// component-types.json is the runtime mirror read by the Node server; the guard
+// below fails loudly (editor / H5 / e2e) if the two ever drift out of sync.
+export const COMPONENT_TYPES = [
+ 'banner',
+ 'title',
+ 'text',
+ 'image',
+ 'button',
+ 'product-list',
+ 'divider',
+ 'spacer',
+ 'two-column',
+ 'benefit-product',
+ 'benefit-actions',
+ 'benefit-notice',
+ 'benefit-product-list',
+] as const;
+export type ComponentType = (typeof COMPONENT_TYPES)[number];
+
+{
+ const canonical = [...COMPONENT_TYPES].sort().join(',');
+ const mirror = [...(componentTypesMirror as string[])].sort().join(',');
+ if (canonical !== mirror)
+ throw new Error(
+ `component-types.json out of sync with COMPONENT_TYPES:\n json: [${mirror}]\n const: [${canonical}]`,
+ );
+}
export interface Block {
id: string;
type: ComponentType;
@@ -35,23 +51,7 @@ export function validSchema(x: unknown): x is PageSchema {
Array.isArray(p.blocks) &&
p.blocks.every(
(b) =>
- b &&
- typeof b.id === 'string' &&
- [
- 'banner',
- 'title',
- 'text',
- 'image',
- 'button',
- 'product-list',
- 'divider',
- 'spacer',
- 'two-column',
- 'benefit-product',
- 'benefit-actions',
- 'benefit-notice',
- 'benefit-product-list',
- ].includes(b.type),
+ b && typeof b.id === 'string' && (COMPONENT_TYPES as readonly string[]).includes(b.type),
)
);
}
diff --git a/packages/shared/package.json b/packages/shared/package.json
index dabecde..8476ae0 100644
--- a/packages/shared/package.json
+++ b/packages/shared/package.json
@@ -2,5 +2,8 @@
"name": "@demo/shared",
"version": "1.0.0",
"type": "module",
- "exports": "./src/index.ts"
+ "exports": "./src/index.ts",
+ "dependencies": {
+ "@demo/page-schema": "*"
+ }
}
diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts
index 5e1df64..e2eb50d 100644
--- a/packages/shared/src/index.ts
+++ b/packages/shared/src/index.ts
@@ -1,2 +1,5 @@
-export type PreviewMessage = { type: 'select'; id: string } | { type: 'ready' };
-export const send = (m: PreviewMessage) => parent.postMessage(m, '*');
+import type { PageSchema } from '@demo/page-schema';
+
+export type PreviewMessage = { type: 'preview'; schema: PageSchema };
+export const EDITOR_PORT = 5173;
+export const previewOrigin = () => `${location.protocol}//${location.hostname}:${EDITOR_PORT}`;