cardbuddy 0.9.1 → 0.10.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/README.md +17 -2
- package/dist/cli.js +3 -0
- package/dist/config.js +7 -0
- package/dist/portGuard.js +293 -0
- package/dist/preview-editor/assets/index-C_UKS0NF.css +1 -0
- package/dist/preview-editor/assets/{index-B9MOriNv.js → index-D1iIJ4sl.js} +14 -13
- package/dist/preview-editor/index.html +2 -2
- package/dist/server-core/assetProxy.js +3 -22
- package/dist/server-core/buildDataFromSchema.js +36 -4
- package/dist/server-core/cardListCache.js +0 -17
- package/dist/server-core/index.js +3 -5
- package/dist/server-core/logStream.js +1 -6
- package/dist/standalone.js +24 -8
- package/dist/vendored/App.vue +136 -46
- package/dist/vendored/components/ComponentPalette.vue +1 -1
- package/dist/vendored/components/DebugPanel.vue +735 -90
- package/dist/vendored/components/NumberStepper.vue +125 -0
- package/dist/vendored/components/ProgressSlider.vue +9 -1
- package/dist/vendored/components/StructureTree.vue +2 -2
- package/dist/vendored/runtime/clipboard-node.js +0 -5
- package/dist/vendored/styles/app.css +618 -210
- package/dist/vendored/styles/card-gallery.css +3 -3
- package/dist/vendored/styles/card-select.css +13 -6
- package/dist/vendored/styles/dropdown.css +1 -2
- package/dist/vendored/styles/json-tree.css +3 -3
- package/dist/vendored/styles/node-inspector.css +7 -294
- package/dist/vendored/styles/setters.css +48 -49
- package/dist/vendored/styles/structure-tree.css +6 -11
- package/package.json +1 -1
- package/dist/preview-editor/assets/index-CwD14gm3.css +0 -1
- package/dist/vendored/assets/logo.png +0 -0
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
<link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon-16-Cg6Nrosk.png" />
|
|
9
9
|
<link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon-32-B_1744sp.png" />
|
|
10
10
|
<link rel="apple-touch-icon" sizes="180x180" href="/assets/apple-touch-icon-BQcES9QK.png" />
|
|
11
|
-
<script type="module" crossorigin src="/assets/index-
|
|
12
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
11
|
+
<script type="module" crossorigin src="/assets/index-D1iIJ4sl.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-C_UKS0NF.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
|
15
15
|
<div id="app"></div>
|
|
@@ -87,13 +87,9 @@ async function fetchUpstream(pathname, upstream) {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
/**
|
|
90
|
-
* 资源代理 +
|
|
91
|
-
*
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* 资源代理 + 落盘缓存中间件(与 Express / Vite middleware 容器解耦)。
|
|
95
|
-
* 既可由 Vite 插件 configureServer 挂载,也可由 server/core 的 mountCoreMiddlewares
|
|
96
|
-
* 直接挂载到 cardbuddy(8787),实现「Vite 零后端插件、8787 是唯一后端」。
|
|
90
|
+
* 资源代理 + 落盘缓存中间件(回源地址从传入的 upstream 取;与 Express / Vite middleware 容器解耦)。
|
|
91
|
+
* 由 server/core 的 mountCoreMiddlewares 挂载到 cardbuddy(8787),
|
|
92
|
+
* 实现「Vite 零后端插件、8787 是唯一后端」。
|
|
97
93
|
* @param {{ root: string, upstream: string }} opts
|
|
98
94
|
* @returns {(req: import('http').IncomingMessage, res: import('http').ServerResponse, next: Function) => void}
|
|
99
95
|
*/
|
|
@@ -137,18 +133,3 @@ export function createAssetProxyMiddleware({ root, upstream }) {
|
|
|
137
133
|
return res.end(PLACEHOLDER)
|
|
138
134
|
}
|
|
139
135
|
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Vite 插件适配层(保留以兼容纯 vite dev 场景):configureServer 阶段把
|
|
143
|
-
* createAssetProxyMiddleware 挂到 server.middlewares。前后分离架构下已由 server/core
|
|
144
|
-
* 的 mountCoreMiddlewares 挂载到 cardbuddy(8787),Vite 不再需要此插件。
|
|
145
|
-
* @param {{ root: string, upstream: string }} opts
|
|
146
|
-
*/
|
|
147
|
-
export function createAssetProxyPlugin({ root, upstream }) {
|
|
148
|
-
return {
|
|
149
|
-
name: 'finmall-asset-proxy',
|
|
150
|
-
configureServer(server) {
|
|
151
|
-
server.middlewares.use(createAssetProxyMiddleware({ root, upstream }))
|
|
152
|
-
},
|
|
153
|
-
}
|
|
154
|
-
}
|
|
@@ -8,13 +8,21 @@
|
|
|
8
8
|
* 仅当 defaultValue 为空 / 空对象 / 未定义时,回退到 param.desc 里的 JSON 兜底
|
|
9
9
|
* (desc 以 '{' 开头且解析后含该 key 时生效)。
|
|
10
10
|
*
|
|
11
|
+
* mock 覆盖(dictionary.mocks):
|
|
12
|
+
* 平台变量面板的「配置变量 mock」把设计期覆盖值写在 dictionary.mocks(key → 值),
|
|
13
|
+
* 与「默认值」inputParams[].defaultValue 分仓:defaultValue 是随卡发布的缺省数据,
|
|
14
|
+
* mocks 仅供设计期预览覆盖(编辑器画布),不进入 data.json。故默认(options.includeMocks
|
|
15
|
+
* falsy)只按 defaultValue 派生 —— 服务端写盘 / 同步落盘走这条;编辑器预览态传
|
|
16
|
+
* { includeMocks: true },在 defaultValue 之上按 key 叠加非空 mock 值。
|
|
17
|
+
*
|
|
11
18
|
* 本文件是纯 ESM(无任何 node / 浏览器专有依赖),因此既能被服务端(Node)import,
|
|
12
19
|
* 也能被预览前端(Vite 打包)直接 import,避免逻辑漂移。
|
|
13
20
|
*/
|
|
14
|
-
export function buildDataFromSchema(schema) {
|
|
21
|
+
export function buildDataFromSchema(schema, options = {}) {
|
|
15
22
|
// 稳健性:schema 缺失 / 非对象(如外部改坏 schema.json)时返回空对象,
|
|
16
23
|
// 不抛错,避免 GET /__card 在损坏卡片上返回 500 堆栈。
|
|
17
24
|
if (!schema || typeof schema !== 'object') return {}
|
|
25
|
+
const mocks = options.includeMocks ? readMocks(schema) : null
|
|
18
26
|
const data = {}
|
|
19
27
|
for (const param of schema.dictionary?.inputParams || []) {
|
|
20
28
|
const fallback = parseDescFallback(param)
|
|
@@ -24,15 +32,39 @@ export function buildDataFromSchema(schema) {
|
|
|
24
32
|
typeof defaultValue === 'object' &&
|
|
25
33
|
!Array.isArray(defaultValue) &&
|
|
26
34
|
Object.keys(defaultValue).length === 0
|
|
27
|
-
|
|
35
|
+
let value =
|
|
28
36
|
fallback !== undefined && (defaultValue === undefined || defaultValue === '' || emptyObject)
|
|
29
37
|
? fallback
|
|
30
|
-
: defaultValue
|
|
31
|
-
|
|
38
|
+
: defaultValue
|
|
39
|
+
// mock 覆盖优先于默认值;空 mock(未配置)不覆盖,回退 defaultValue / desc 兜底
|
|
40
|
+
if (mocks && hasMockValue(mocks, param.key)) value = mocks[param.key]
|
|
41
|
+
data[param.key] = clone(value)
|
|
32
42
|
}
|
|
33
43
|
return data
|
|
34
44
|
}
|
|
35
45
|
|
|
46
|
+
/**
|
|
47
|
+
* 读取 dictionary.mocks(key → mock 值)。历史卡 / 同步脚本可能写成数组或缺省,
|
|
48
|
+
* 一律归一为 null(视为「没有 mock 表」),调用方无需再判形。
|
|
49
|
+
*/
|
|
50
|
+
export function readMocks(schema) {
|
|
51
|
+
const mocks = schema && schema.dictionary && schema.dictionary.mocks
|
|
52
|
+
return mocks && typeof mocks === 'object' && !Array.isArray(mocks) ? mocks : null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 某 key 是否「配置了 mock」:undefined / null / 空串 / 空容器 视为未配置
|
|
57
|
+
* (与变量列表的 m 徽标同一判据,避免空壳 mock 被当成有效覆盖)。
|
|
58
|
+
*/
|
|
59
|
+
export function hasMockValue(mocks, key) {
|
|
60
|
+
if (!mocks || !Object.prototype.hasOwnProperty.call(mocks, key)) return false
|
|
61
|
+
const value = mocks[key]
|
|
62
|
+
if (value === undefined || value === null || value === '') return false
|
|
63
|
+
if (Array.isArray(value)) return value.length > 0
|
|
64
|
+
if (typeof value === 'object') return Object.keys(value).length > 0
|
|
65
|
+
return true
|
|
66
|
+
}
|
|
67
|
+
|
|
36
68
|
function parseDescFallback(param) {
|
|
37
69
|
if (!param.desc || typeof param.desc !== 'string') return undefined
|
|
38
70
|
const text = param.desc.trim()
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
const cardIndex = new Map(); // cardId(String) -> { cardId, code, cardName }
|
|
16
|
-
let lastRefreshedAt = 0;
|
|
17
16
|
|
|
18
17
|
/**
|
|
19
18
|
* 写入/合并卡片列表。
|
|
@@ -27,7 +26,6 @@ export function refreshCardList(cards, { full = false } = {}) {
|
|
|
27
26
|
const id = String(c.cardId);
|
|
28
27
|
cardIndex.set(id, { cardId: id, code: c.code, cardName: c.cardName });
|
|
29
28
|
}
|
|
30
|
-
lastRefreshedAt = Date.now();
|
|
31
29
|
}
|
|
32
30
|
|
|
33
31
|
/** 卡片是否存在于缓存列表(按 cardId / pageId 查)。 */
|
|
@@ -35,16 +33,6 @@ export function hasCardId(cardId) {
|
|
|
35
33
|
return cardIndex.has(String(cardId));
|
|
36
34
|
}
|
|
37
35
|
|
|
38
|
-
/** 取缓存中的卡片元信息,不存在返回 null。 */
|
|
39
|
-
export function getCard(cardId) {
|
|
40
|
-
return cardIndex.get(String(cardId)) || null;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** 缓存是否已非空(至少同步过一次全量列表)。 */
|
|
44
|
-
export function cacheSize() {
|
|
45
|
-
return cardIndex.size;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
36
|
/**
|
|
49
37
|
* 推送前存在性校验:目标卡片是否确实在远端卡片列表中(按 cardId/pageId 查)。
|
|
50
38
|
* - 缓存为空(本次 dev server 尚未全量同步过):放行,返回 allowed=true,避免误拦截首推。
|
|
@@ -61,8 +49,3 @@ export function checkPushAllowed(pageId) {
|
|
|
61
49
|
reason: `卡片 pageId=${pageId} 不在远端卡片列表中,疑似已删除或 pageId 错位,请先「远程同步」刷新列表后再推送`,
|
|
62
50
|
};
|
|
63
51
|
}
|
|
64
|
-
|
|
65
|
-
/** 上次刷新时间戳(ms),未刷新为 0。 */
|
|
66
|
-
export function lastRefreshed() {
|
|
67
|
-
return lastRefreshedAt;
|
|
68
|
-
}
|
|
@@ -3,11 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 设计:
|
|
5
5
|
* - 每个端点都是独立 `(req, res, next)` 中间件工厂(见同目录各文件),与 Express / Vite
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* - mountCoreMiddlewares(app, opts) 把全部中间件挂到任意 Connect 风格容器(Vite 的
|
|
10
|
-
* server.middlewares 或 connect 实例皆可),两处共用、改一处两处同步。
|
|
6
|
+
* 中间件容器解耦,由本地开发服务 packages/cardbuddy 统一装配(dev 与发布态同一份)。
|
|
7
|
+
* - mountCoreMiddlewares(app, opts) 把全部中间件挂到任意 Connect 风格容器(cardbuddy 的
|
|
8
|
+
* connect 实例,或 tools/regress-data.mjs 里的临时容器),改一处全链路同步。
|
|
11
9
|
*
|
|
12
10
|
* 注意:renderStatus / nodeModels 需要活着的 vite 实例做 ssrLoadModule,故 mountCoreMiddlewares
|
|
13
11
|
* 接收 viteServer 参数透传;无 SSR 需求的场景可传 null(这两个端点会返回明确错误)。
|
|
@@ -166,11 +166,6 @@ export function subscribeLog(cb) {
|
|
|
166
166
|
return () => subscribers.delete(cb)
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
-
/** 当前订阅者数量(诊断用)。 */
|
|
170
|
-
export function logSubscriberCount() {
|
|
171
|
-
return subscribers.size
|
|
172
|
-
}
|
|
173
|
-
|
|
174
169
|
let mirrorInstalled = false
|
|
175
170
|
|
|
176
171
|
/**
|
|
@@ -218,7 +213,7 @@ export function installLogMirror() {
|
|
|
218
213
|
|
|
219
214
|
/**
|
|
220
215
|
* 日志流 HTTP 端点:GET /__logs(历史/补漏)、POST /__logs/ingest(外部进程注入)。
|
|
221
|
-
* 挂在 mountCoreMiddlewares
|
|
216
|
+
* 挂在 mountCoreMiddlewares 里,随 cardbuddy 本地服务一并生效。
|
|
222
217
|
*/
|
|
223
218
|
export function createLogMiddleware() {
|
|
224
219
|
const sendJson = (res, status, payload) => {
|
package/dist/standalone.js
CHANGED
|
@@ -24,6 +24,7 @@ import chalk from 'chalk'
|
|
|
24
24
|
|
|
25
25
|
import { mountCoreMiddlewares, installLogMirror, subscribeLog } from './core.js'
|
|
26
26
|
import { loadConfig } from './config.js'
|
|
27
|
+
import { ensurePortFree } from './portGuard.js'
|
|
27
28
|
|
|
28
29
|
const MIME_TYPES = {
|
|
29
30
|
'.html': 'text/html; charset=utf-8',
|
|
@@ -45,6 +46,13 @@ const MIME_TYPES = {
|
|
|
45
46
|
'.map': 'application/json; charset=utf-8',
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
// 被 dev.mjs 拉起时(DSL_LAUNCHER='dev'),来源标签由启动器用带背景色的 chip 标注,
|
|
50
|
+
// 这里让出前缀避免双重来源标签;直接运行 / dev:prod(stdio:inherit)则自带标签。
|
|
51
|
+
const srcTag =
|
|
52
|
+
process.env.DSL_LAUNCHER === 'dev'
|
|
53
|
+
? ''
|
|
54
|
+
: chalk.magenta('▌ ') + chalk.magenta.bold('cardbuddy') + ' '
|
|
55
|
+
|
|
48
56
|
function decodePathname(url) {
|
|
49
57
|
if (!url) return '/'
|
|
50
58
|
const raw = url.split('?')[0]
|
|
@@ -382,6 +390,21 @@ async function resolveTls(config) {
|
|
|
382
390
|
*/
|
|
383
391
|
export async function startServer(userConfig = {}) {
|
|
384
392
|
const config = { ...loadConfig(), ...userConfig }
|
|
393
|
+
|
|
394
|
+
// 日志镜像尽早安装(幂等,CLI 入口已先行装过一次):让端口接管等启动期日志也能进入
|
|
395
|
+
// 编辑器底部「服务日志」面板的环形缓冲。
|
|
396
|
+
installLogMirror()
|
|
397
|
+
|
|
398
|
+
// 端口前置检查:被「上一个 cardbuddy 实例」占用时主动结束它再接管(见 src/portGuard.js)。
|
|
399
|
+
// 放在最前面,避免白跑一遍 TLS / Vite 初始化后才以 EADDRINUSE 失败。
|
|
400
|
+
await ensurePortFree({
|
|
401
|
+
port: config.port,
|
|
402
|
+
tls: config.https,
|
|
403
|
+
token: config.token,
|
|
404
|
+
mode: config.portTakeover,
|
|
405
|
+
log: (msg) => console.log(srcTag + msg),
|
|
406
|
+
})
|
|
407
|
+
|
|
385
408
|
const app = connect()
|
|
386
409
|
|
|
387
410
|
app.use(corsMiddleware)
|
|
@@ -403,8 +426,7 @@ export async function startServer(userConfig = {}) {
|
|
|
403
426
|
|
|
404
427
|
// 服务日志流:镜像本进程 stdout/stderr(含 chalk ANSI)→ 环形缓冲 → SSE 批量推送。
|
|
405
428
|
// 编辑器底部「服务日志」面板订阅 event: service-log;历史与断线补漏走 GET /__logs。
|
|
406
|
-
//
|
|
407
|
-
installLogMirror()
|
|
429
|
+
// 镜像本体在函数开头安装,这里只负责把新增行转推给 SSE 订阅者。
|
|
408
430
|
subscribeLog((lines) => events.notify({ lines }, 'service-log'))
|
|
409
431
|
|
|
410
432
|
const vite = await createEmbeddedVite(config)
|
|
@@ -437,12 +459,6 @@ export async function startServer(userConfig = {}) {
|
|
|
437
459
|
})
|
|
438
460
|
|
|
439
461
|
const bind = `${scheme}://127.0.0.1:${config.port}`
|
|
440
|
-
// 被 dev.mjs 拉起时(DSL_LAUNCHER='dev'),来源标签由启动器用带背景色的 chip 标注,
|
|
441
|
-
// 这里让出前缀避免双重来源标签;直接运行 / dev:prod(stdio:inherit)则自带标签。
|
|
442
|
-
const srcTag =
|
|
443
|
-
process.env.DSL_LAUNCHER === 'dev'
|
|
444
|
-
? ''
|
|
445
|
-
: chalk.magenta('▌ ') + chalk.magenta.bold('cardbuddy') + ' '
|
|
446
462
|
console.log(
|
|
447
463
|
srcTag +
|
|
448
464
|
chalk.green(`本地开发服务已启动:${bind}`) +
|
package/dist/vendored/App.vue
CHANGED
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
<svg class="app__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="#icon-grid" /></svg>
|
|
37
37
|
</button>
|
|
38
38
|
<div
|
|
39
|
+
ref="validateBadgeEl"
|
|
39
40
|
class="app__badge"
|
|
40
41
|
:class="'app__badge--' + validateBadge.state"
|
|
41
42
|
:title="badgeTitle"
|
|
@@ -69,7 +70,7 @@
|
|
|
69
70
|
<svg class="app__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="#icon-save" /></svg>
|
|
70
71
|
<span>保存</span>
|
|
71
72
|
</button>
|
|
72
|
-
<button class="app__btn app__btn--op" :disabled="!preview" @click="copySchema" title="复制 schema
|
|
73
|
+
<button class="app__btn app__btn--op" :disabled="!preview" @click="copySchema" title="复制 schema(含变量 mock 编辑后的数据)">
|
|
73
74
|
<svg class="app__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="#icon-copy" /></svg>
|
|
74
75
|
<span>{{ copiedSchema ? '已复制 ✓' : '复制' }}</span>
|
|
75
76
|
</button>
|
|
@@ -198,29 +199,38 @@
|
|
|
198
199
|
>
|
|
199
200
|
<svg class="app__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><use href="#icon-link" /></svg>
|
|
200
201
|
</button>
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
<
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
<
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
202
|
+
<!-- 点遮罩关闭:与事件帮助气泡等轻量浮层同一交互 -->
|
|
203
|
+
<div v-if="devServiceOpen" class="app__dev-svc-mask" @click="devServiceOpen = false"></div>
|
|
204
|
+
<Transition name="pop">
|
|
205
|
+
<div v-if="devServiceOpen" class="app__dev-svc-panel" @click.stop>
|
|
206
|
+
<div class="app__dev-svc-head">
|
|
207
|
+
<span class="app__dev-svc-title">本地开发服务</span>
|
|
208
|
+
<button class="app__dev-svc-close" type="button" title="关闭" @click="devServiceOpen = false"></button>
|
|
209
|
+
</div>
|
|
210
|
+
<div class="app__dev-svc-body">
|
|
211
|
+
<label class="app__dev-svc-row">
|
|
212
|
+
<span>启用</span>
|
|
213
|
+
<Switch v-model="devServiceForm.enabled" />
|
|
214
|
+
</label>
|
|
215
|
+
<label class="app__dev-svc-row app__dev-svc-row--col">
|
|
216
|
+
<span>服务地址</span>
|
|
217
|
+
<input type="text" v-model="devServiceForm.baseUrl" placeholder="http://127.0.0.1:8787" />
|
|
218
|
+
</label>
|
|
219
|
+
<label class="app__dev-svc-row app__dev-svc-row--col">
|
|
220
|
+
<span>访问令牌(可选)</span>
|
|
221
|
+
<input type="text" v-model="devServiceForm.token" placeholder="留空则不校验" />
|
|
222
|
+
</label>
|
|
223
|
+
<p class="app__dev-svc-hint" v-if="apiBaseResolved === null">
|
|
224
|
+
<template v-if="devSvcBlockReason === 'disabled'">已填写服务地址,但「启用」开关未打开。请开启「启用」开关并点击保存,本地开发服务才会生效。</template>
|
|
225
|
+
<template v-else>请填写本机 cardbuddy 服务地址(默认 http://127.0.0.1:8787),打开「启用」开关并保存后,编辑功能才会生效。</template>
|
|
226
|
+
</p>
|
|
227
|
+
<div class="app__dev-svc-actions">
|
|
228
|
+
<button class="ni-btn ni-btn--ghost" type="button" @click="devServiceOpen = false">关闭</button>
|
|
229
|
+
<button class="ni-btn" type="button" @click="saveDevService">保存</button>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
218
232
|
</div>
|
|
219
|
-
|
|
220
|
-
<template v-if="devSvcBlockReason === 'disabled'">已填写服务地址,但「启用」开关未打开。请开启「启用」开关并点击保存,本地开发服务才会生效。</template>
|
|
221
|
-
<template v-else>请填写本机 cardbuddy 服务地址(默认 http://127.0.0.1:8787),打开「启用」开关并保存后,编辑功能才会生效。</template>
|
|
222
|
-
</p>
|
|
223
|
-
</div>
|
|
233
|
+
</Transition>
|
|
224
234
|
</div>
|
|
225
235
|
</div>
|
|
226
236
|
<!-- revision 依赖锚点:setData 自增后驱动本组件重渲染 -->
|
|
@@ -536,7 +546,9 @@ import logoUrl from '@/assets/site-icon.png'
|
|
|
536
546
|
import { apiFetch, resolveApiBase, subscribeCardsChanged, loadDevServiceConfig, saveDevServiceConfig } from '@/api.js'
|
|
537
547
|
// 由 schema 单向派生 data 的共享实现(与 server/core 同源,避免逻辑漂移)。
|
|
538
548
|
// 预览渲染注入的数据绑定、本地基线比较、历史快照还原均使用此函数,使 data 永不读回 data.json 文件。
|
|
539
|
-
import { buildDataFromSchema } from '../server/core/buildDataFromSchema.js'
|
|
549
|
+
import { buildDataFromSchema, readMocks } from '../server/core/buildDataFromSchema.js'
|
|
550
|
+
// 编辑器预览态派生 data 时叠加 dictionary.mocks(设计期覆盖);服务端写盘仍只用 defaultValue。
|
|
551
|
+
const WITH_MOCKS = { includeMocks: true }
|
|
540
552
|
|
|
541
553
|
// 编辑器自身「保存到本地 / 保存至远端」写盘后,cardbuddy 的 watcher 会把这次落盘经 SSE
|
|
542
554
|
// 推回 cards-changed。这是「自己改自己」的回声,并非外部修改,用此窗口吸收,避免误弹外部重载
|
|
@@ -547,8 +559,8 @@ const SELF_SAVE_SUPPRESS_MS = 2500
|
|
|
547
559
|
// 内部 rootScope 可经 setRootScope 整体替换而引用不变。
|
|
548
560
|
const adapter = new Adapter()
|
|
549
561
|
const cardRuntime = createCardRuntime({}, adapter)
|
|
550
|
-
// 开发期暴露 runtime 单例句柄,供 E2E 回归(如 L2-12
|
|
551
|
-
// 直接驱动 rt.setRootScope —— 与 DebugPanel
|
|
562
|
+
// 开发期暴露 runtime 单例句柄,供 E2E 回归(如 L2-12 rootScope 整体替换实时同步画布)
|
|
563
|
+
// 直接驱动 rt.setRootScope —— 与 DebugPanel「变量 → 配置变量 mock」Raw「应用」同源。生产构建不暴露。
|
|
552
564
|
if (typeof window !== 'undefined' && import.meta.env.DEV) window.__rt = cardRuntime
|
|
553
565
|
|
|
554
566
|
export default {
|
|
@@ -607,9 +619,9 @@ export default {
|
|
|
607
619
|
localSchema: null,
|
|
608
620
|
localData: null,
|
|
609
621
|
selectedNodeId: '',
|
|
610
|
-
// 当前激活的调试面板 tab
|
|
622
|
+
// 当前激活的调试面板 tab(变量/组件结构/卡片来源)。
|
|
611
623
|
// 画布 Ctrl/⌘ 点击会自动切到「组件结构」tab 并在节点树聚焦选中,无需预先停在结构树 tab。
|
|
612
|
-
activeTab: '
|
|
624
|
+
activeTab: 'vars',
|
|
613
625
|
// 右侧常驻「接口调用」面板:展开/收起由顶栏按钮与面板内 ✕ 控制,
|
|
614
626
|
// 初始状态持久化到 localStorage(cardbuddy:api-panel-open)
|
|
615
627
|
apiPanelOpen: true,
|
|
@@ -666,7 +678,7 @@ export default {
|
|
|
666
678
|
baseline: null,
|
|
667
679
|
_commitTimer: null, // 编辑落定防抖计时器,避免连续编辑产生过多快照
|
|
668
680
|
viewportHeight: 700, // 手机视口实际高度(高度自适应填充后由 ResizeObserver 实测更新)
|
|
669
|
-
// 画布背景模式:checker(棋盘格) / white(白色) / gray(浅灰)
|
|
681
|
+
// 画布背景模式:checker(棋盘格) / white(白色) / gray(浅灰) / black(近黑,深色卡片对色用)
|
|
670
682
|
canvasBg: 'gray',
|
|
671
683
|
// 刷新按钮进行态:图标自转 + 禁用,避免连点与「点了没反应」
|
|
672
684
|
refreshing: false,
|
|
@@ -674,6 +686,7 @@ export default {
|
|
|
674
686
|
{ key: 'checker', label: '棋盘' },
|
|
675
687
|
{ key: 'white', label: '白色' },
|
|
676
688
|
{ key: 'gray', label: '浅灰' },
|
|
689
|
+
{ key: 'black', label: '黑色' },
|
|
677
690
|
],
|
|
678
691
|
// —— 预览区宽度可调(拖拽分隔条 / 预设尺寸,画布按实际像素宽度布局,无 transform 缩放)——
|
|
679
692
|
_previewWidthPx: 372, // 预览列宽度(像素,持久化到 localStorage);第一次打开未主动设置时默认 372px
|
|
@@ -934,9 +947,10 @@ export default {
|
|
|
934
947
|
this.selectedId = initial
|
|
935
948
|
await this.loadCard(this.selectedId)
|
|
936
949
|
}
|
|
937
|
-
// 数据绑定(rt.rootScope
|
|
950
|
+
// 数据绑定(rt.rootScope)深度监听:任意编辑实时回写 schema 的权威数据位 ——
|
|
951
|
+
// 已配置 mock 的 key 回写 dictionary.mocks[key],其余回写 inputParams[].defaultValue,
|
|
938
952
|
// 保证「保存到本地」基于最新数据绑定;预览也经 rt 响应式实时刷新。
|
|
939
|
-
// ⚠️
|
|
953
|
+
// ⚠️ 此处【不】写历史——历史由「变量 → 配置变量 mock」视图的「应用」按钮(schema-commit)落定,
|
|
940
954
|
// 与节点属性/事件面板一致:实时编辑只更新预览与内存,点「应用」才记一笔撤销记录。
|
|
941
955
|
this.$watch(
|
|
942
956
|
() => this.rt.rootScope,
|
|
@@ -1215,18 +1229,89 @@ export default {
|
|
|
1215
1229
|
// 渲染端点异常不强行判错(避免 dev server 预热期 SSR 冷启动误报)
|
|
1216
1230
|
const renderOk = r ? !!r.renderOk : true
|
|
1217
1231
|
const ok = contractOk && renderOk
|
|
1232
|
+
const next = ok ? 'ok' : 'error'
|
|
1233
|
+
const changed = this.validateBadge.state !== next
|
|
1218
1234
|
this.validateBadge = {
|
|
1219
|
-
state:
|
|
1235
|
+
state: next,
|
|
1220
1236
|
errors: (v.errors || []).length,
|
|
1221
1237
|
warnings: (v.warnings || []).length,
|
|
1222
1238
|
renderOk,
|
|
1223
1239
|
ts: Date.now(),
|
|
1224
1240
|
}
|
|
1241
|
+
if (changed && (next === 'ok' || next === 'error')) this.celebrateValidate(next)
|
|
1225
1242
|
} catch (e) {
|
|
1226
|
-
// 网络/服务不可用:保底不阻断编辑,标记 fail
|
|
1243
|
+
// 网络/服务不可用:保底不阻断编辑,标记 fail(fail 不触发动画)
|
|
1227
1244
|
this.validateBadge = { ...this.validateBadge, state: 'fail' }
|
|
1228
1245
|
}
|
|
1229
1246
|
},
|
|
1247
|
+
/** 状态跳变时,在徽标局部触发庆祝动画:徽标弹跳 + 碎玻璃碎屑沿轮廓放射爆开,碎屑颜色跟随目标状态主色。仅局部,不铺屏。 */
|
|
1248
|
+
celebrateValidate(state = 'ok') {
|
|
1249
|
+
const el = this.$refs.validateBadgeEl
|
|
1250
|
+
if (!el) return
|
|
1251
|
+
if (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) return
|
|
1252
|
+
// 徽标弹跳
|
|
1253
|
+
el.classList.remove('is-celebrating')
|
|
1254
|
+
void el.offsetWidth // 重排以重启动画
|
|
1255
|
+
el.classList.add('is-celebrating')
|
|
1256
|
+
setTimeout(() => el.classList.remove('is-celebrating'), 600)
|
|
1257
|
+
// 碎屑颜色跟随目标状态徽标主色(--ok / --danger / --warn / --text-faint)
|
|
1258
|
+
const r = el.getBoundingClientRect()
|
|
1259
|
+
const colorVar = { ok: '--ok', error: '--danger', pending: '--warn', fail: '--text-faint', nopython: '--text-faint' }[state] || '--ok'
|
|
1260
|
+
const fallback = { ok: '#52c41a', error: '#F5222D', pending: '#FAAD14', fail: '#8c8c8c', nopython: '#8c8c8c' }[state] || '#52c41a'
|
|
1261
|
+
const badgeColor = getComputedStyle(el).getPropertyValue(colorVar).trim() || fallback
|
|
1262
|
+
const box = document.createElement('div')
|
|
1263
|
+
box.className = 'app__confetti'
|
|
1264
|
+
box.style.left = r.left + r.width / 2 + 'px'
|
|
1265
|
+
box.style.top = r.top + r.height / 2 + 'px'
|
|
1266
|
+
// 碎玻璃碎片:从徽标矩形轮廓迸出,沿外法线方向飞散;不规则尖锐多边形 + 尺寸不一
|
|
1267
|
+
const count = 22
|
|
1268
|
+
const clips = [
|
|
1269
|
+
'polygon(50% 0%, 0% 100%, 100% 100%)',
|
|
1270
|
+
'polygon(62% 0%, 100% 40%, 45% 100%, 0% 52%)',
|
|
1271
|
+
'polygon(0% 0%, 100% 18%, 82% 100%, 12% 70%)',
|
|
1272
|
+
'polygon(22% 0%, 100% 8%, 88% 100%, 0% 76%)',
|
|
1273
|
+
'polygon(0% 32%, 100% 0%, 76% 100%, 18% 86%)',
|
|
1274
|
+
'polygon(8% 0%, 100% 22%, 70% 100%, 0% 64%)',
|
|
1275
|
+
'polygon(0% 12%, 100% 0%, 100% 78%, 30% 100%, 12% 55%)',
|
|
1276
|
+
]
|
|
1277
|
+
const halfW = r.width / 2
|
|
1278
|
+
const halfH = r.height / 2
|
|
1279
|
+
for (let i = 0; i < count; i++) {
|
|
1280
|
+
const bit = document.createElement('span')
|
|
1281
|
+
bit.className = 'app__confetti-bit'
|
|
1282
|
+
// 在徽标矩形轮廓上随机取一点作为迸发起点
|
|
1283
|
+
const edge = (Math.random() * 4) | 0
|
|
1284
|
+
let px, py
|
|
1285
|
+
if (edge === 0) { px = (Math.random() * 2 - 1) * halfW; py = -halfH }
|
|
1286
|
+
else if (edge === 1) { px = halfW; py = (Math.random() * 2 - 1) * halfH }
|
|
1287
|
+
else if (edge === 2) { px = (Math.random() * 2 - 1) * halfW; py = halfH }
|
|
1288
|
+
else { px = -halfW; py = (Math.random() * 2 - 1) * halfH }
|
|
1289
|
+
// 沿轮廓点的外法线方向飞出 + 微抖动
|
|
1290
|
+
const len = Math.hypot(px, py) || 1
|
|
1291
|
+
const a = Math.atan2(py / len, px / len) + (Math.random() - 0.5) * 0.5
|
|
1292
|
+
const dist = 16 + Math.random() * 26
|
|
1293
|
+
bit.style.setProperty('--sx', px.toFixed(1) + 'px')
|
|
1294
|
+
bit.style.setProperty('--sy', py.toFixed(1) + 'px')
|
|
1295
|
+
bit.style.setProperty('--tx', (Math.cos(a) * dist).toFixed(1) + 'px')
|
|
1296
|
+
bit.style.setProperty('--ty', (Math.sin(a) * dist).toFixed(1) + 'px')
|
|
1297
|
+
bit.style.setProperty('--rot', (Math.random() * 720 - 360).toFixed(0) + 'deg')
|
|
1298
|
+
bit.style.setProperty('--clip', clips[(Math.random() * clips.length) | 0])
|
|
1299
|
+
bit.style.setProperty('--c', badgeColor)
|
|
1300
|
+
// 每片独立透明度,半透明玻璃感
|
|
1301
|
+
bit.style.setProperty('--o', (0.68 + Math.random() * 0.27).toFixed(2))
|
|
1302
|
+
// 三层尺寸分布:长薄片 / 中等碎块 / 细微粉尘,强化碎裂参差感
|
|
1303
|
+
const roll = Math.random()
|
|
1304
|
+
let w, h
|
|
1305
|
+
if (roll < 0.35) { w = 1.5 + Math.random() * 2; h = 10 + Math.random() * 10 } // 细长薄片
|
|
1306
|
+
else if (roll < 0.75) { w = 4 + Math.random() * 5; h = w * (0.6 + Math.random() * 0.8) } // 中等碎块
|
|
1307
|
+
else { w = 2 + Math.random() * 2.5; h = w } // 细微粉尘
|
|
1308
|
+
bit.style.width = w.toFixed(0) + 'px'
|
|
1309
|
+
bit.style.height = h.toFixed(0) + 'px'
|
|
1310
|
+
box.appendChild(bit)
|
|
1311
|
+
}
|
|
1312
|
+
document.body.appendChild(box)
|
|
1313
|
+
setTimeout(() => box.remove(), 800)
|
|
1314
|
+
},
|
|
1230
1315
|
/** 实时从磁盘读取并载入指定卡片(GET /__card/<id>),不读任何缓存 */
|
|
1231
1316
|
async loadCard(id, opts = {}) {
|
|
1232
1317
|
if (!id) return
|
|
@@ -1251,7 +1336,7 @@ export default {
|
|
|
1251
1336
|
this.rt.clearApiCalls()
|
|
1252
1337
|
// schema 被事件编辑器改后重建时保留当前数据绑定编辑,避免被磁盘 data(由 schema 派生)覆盖。
|
|
1253
1338
|
// 数据绑定改为由 schema 单向派生,不再读回 data.json 文件。
|
|
1254
|
-
if (!opts.keepScope) this.rt.setRootScope(buildDataFromSchema(fresh.schema))
|
|
1339
|
+
if (!opts.keepScope) this.rt.setRootScope(buildDataFromSchema(fresh.schema, WITH_MOCKS))
|
|
1255
1340
|
this.currentCard = { schema: fresh.schema }
|
|
1256
1341
|
this.currentSchema = fresh.schema
|
|
1257
1342
|
// 重置并在载入后拉取导入来源元数据(Phase E):本地新建卡无 .import-meta.json → null,
|
|
@@ -1261,7 +1346,7 @@ export default {
|
|
|
1261
1346
|
// 记录本地真实版本基线(磁盘当前内容),供「保存到本地」按钮判断与本地是否一致。
|
|
1262
1347
|
// localData 由 schema 派生(与渲染注入同源),保证纯数据编辑也能触发脏标记。
|
|
1263
1348
|
this.localSchema = this.cloneSchema(fresh.schema)
|
|
1264
|
-
this.localData = this.cloneData(buildDataFromSchema(fresh.schema))
|
|
1349
|
+
this.localData = this.cloneData(buildDataFromSchema(fresh.schema, WITH_MOCKS))
|
|
1265
1350
|
this.preview = buildPreview(fresh.schema)
|
|
1266
1351
|
// Task #5:把 Property 树模型森林暴露到 window,供浏览器内 AI / skill 直接读取节点可编辑结构
|
|
1267
1352
|
if (typeof window !== 'undefined') window.__cardModel = buildModelView(fresh.schema)
|
|
@@ -1339,7 +1424,7 @@ export default {
|
|
|
1339
1424
|
if (!id || !snapshot) return
|
|
1340
1425
|
const schema = snapshot.schema || snapshot
|
|
1341
1426
|
// 快照只存 schema;数据绑定由其派生。若老快照带 data 则优先沿用(历史兼容)。
|
|
1342
|
-
const data = snapshot.data != null ? snapshot.data : buildDataFromSchema(schema)
|
|
1427
|
+
const data = snapshot.data != null ? snapshot.data : buildDataFromSchema(schema, WITH_MOCKS)
|
|
1343
1428
|
this.currentSchema = this.cloneSchema(schema)
|
|
1344
1429
|
this.rt.setRootScope(this.cloneData(data))
|
|
1345
1430
|
this.preview = buildPreview(this.currentSchema)
|
|
@@ -1355,7 +1440,7 @@ export default {
|
|
|
1355
1440
|
commitHistory() {
|
|
1356
1441
|
if (!this.currentSchema || !this.baseline) return
|
|
1357
1442
|
const sameSchema = this.structuralEqual(this.currentSchema, this.baseline.schema)
|
|
1358
|
-
const sameData = this.dataEqual(this.rt.rootScope, buildDataFromSchema(this.baseline.schema))
|
|
1443
|
+
const sameData = this.dataEqual(this.rt.rootScope, buildDataFromSchema(this.baseline.schema, WITH_MOCKS))
|
|
1359
1444
|
if (sameSchema && sameData) return
|
|
1360
1445
|
const previous = { schema: this.baseline.schema }
|
|
1361
1446
|
this.past.push(previous)
|
|
@@ -1396,7 +1481,7 @@ export default {
|
|
|
1396
1481
|
// 取末尾最多 30 个作为可回退的上一版本(最新在末尾;仅 schema,data 由 schema 派生)
|
|
1397
1482
|
this.past = snaps.slice(-30).map((s) => ({
|
|
1398
1483
|
schema: s.schema,
|
|
1399
|
-
data: s.data != null ? s.data : buildDataFromSchema(s.schema),
|
|
1484
|
+
data: s.data != null ? s.data : buildDataFromSchema(s.schema, WITH_MOCKS),
|
|
1400
1485
|
}))
|
|
1401
1486
|
} catch (e) {
|
|
1402
1487
|
console.warn('[加载编辑历史失败]', e)
|
|
@@ -1461,10 +1546,11 @@ export default {
|
|
|
1461
1546
|
this.commitHistory()
|
|
1462
1547
|
},
|
|
1463
1548
|
/**
|
|
1464
|
-
* 把数据绑定(rt.rootScope
|
|
1465
|
-
*
|
|
1466
|
-
*
|
|
1467
|
-
*
|
|
1549
|
+
* 把数据绑定(rt.rootScope)的编辑实时同步回 schema 的权威数据位:
|
|
1550
|
+
* key 已在 dictionary.mocks 中 → 回写 mocks[key](设计期覆盖仓);
|
|
1551
|
+
* 否则回写 schema.dictionary.inputParams[i].defaultValue(随卡发布的缺省仓)。
|
|
1552
|
+
* 这样「本地保存」时 schema.json 携带最新两仓数据,而 data.json 仍只由 defaultValue 派生,
|
|
1553
|
+
* mock 不会泄漏进发布数据;两仓也不会互相覆盖。
|
|
1468
1554
|
*/
|
|
1469
1555
|
syncDataToSchema() {
|
|
1470
1556
|
const schema = this.currentSchema
|
|
@@ -1472,13 +1558,14 @@ export default {
|
|
|
1472
1558
|
const root = this.rt && this.rt.rootScope
|
|
1473
1559
|
if (!root || typeof root !== 'object') return
|
|
1474
1560
|
const params = schema.dictionary.inputParams
|
|
1561
|
+
const mocks = readMocks(schema)
|
|
1475
1562
|
for (const key of Object.keys(root)) {
|
|
1476
1563
|
const param = params.find((p) => p && (p.key === key || p.name === key))
|
|
1477
1564
|
if (!param) continue
|
|
1478
1565
|
// 深拷贝,避免数据绑定与 schema 共享同一引用(setRootScope 重建后会失联)
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1566
|
+
const value = JSON.parse(JSON.stringify(root[key], (k, v) => (k === '_schemaNode' ? undefined : v)))
|
|
1567
|
+
if (mocks && Object.prototype.hasOwnProperty.call(mocks, key)) mocks[key] = value
|
|
1568
|
+
else param.defaultValue = value
|
|
1482
1569
|
}
|
|
1483
1570
|
},
|
|
1484
1571
|
/** 结构树选中节点 */
|
|
@@ -2283,7 +2370,10 @@ export default {
|
|
|
2283
2370
|
// 写盘成功后,当前显示版本即成为本地真实版本:刷新本地基线,使顶栏
|
|
2284
2371
|
// 「保存到本地」按钮在「当前版本 == 本地版本」时自动置灰。
|
|
2285
2372
|
this.localSchema = this.cloneSchema(this.currentSchema)
|
|
2286
|
-
|
|
2373
|
+
// 基线数据必须与载入 / 历史快照同源(含 mocks):rt.rootScope 是含 mock 覆盖的,
|
|
2374
|
+
// 若此处漏传 WITH_MOCKS,只要存在 mock 数据,保存后 rootScope 与 localData 永远
|
|
2375
|
+
// 不等 → historyDirty 恒真,「保存到本地」按钮保存成功也保持高亮。
|
|
2376
|
+
this.localData = this.cloneData(buildDataFromSchema(this.currentSchema, WITH_MOCKS))
|
|
2287
2377
|
this.savedCard = true
|
|
2288
2378
|
// 本地保存后磁盘已更新,刷新整卡校验徽标(端点读磁盘)
|
|
2289
2379
|
this.refreshValidateBadge()
|