cardbuddy 0.6.0 → 0.8.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 +21 -0
- package/dist/preview-editor/assets/index-BQKzMPBq.js +79 -0
- package/dist/preview-editor/assets/index-D3zp47Dg.css +1 -0
- package/dist/preview-editor/index.html +2 -2
- package/dist/server-core/cardFiles.js +9 -4
- package/dist/server-core/codeBridge.js +359 -0
- package/dist/server-core/index.js +14 -2
- package/dist/server-core/sliceUpload.js +97 -0
- package/dist/standalone.js +18 -3
- package/dist/vendored/App.vue +268 -45
- package/dist/vendored/api.js +102 -32
- package/dist/vendored/assets/svgIcons/icons.svg +5 -0
- package/dist/vendored/components/ApiCallPanel.vue +127 -0
- package/dist/vendored/components/ApiPayloadTree.vue +34 -1
- package/dist/vendored/components/CardGallery.vue +2 -0
- package/dist/vendored/components/CardSelect.vue +6 -0
- package/dist/vendored/components/ComponentPalette.vue +45 -38
- package/dist/vendored/components/ConfirmDialog.vue +20 -5
- package/dist/vendored/components/DebugPanel.vue +168 -127
- package/dist/vendored/components/EventEditor.vue +27 -3
- package/dist/vendored/components/JsonTree.vue +35 -4
- package/dist/vendored/components/LogValue.vue +2 -1
- package/dist/vendored/components/PropControl.vue +6 -2
- package/dist/vendored/components/StructureTree.vue +53 -2
- package/dist/vendored/components/setters/SetCode.vue +111 -3
- package/dist/vendored/styles/app.css +507 -132
- package/dist/vendored/styles/card-gallery.css +23 -2
- package/dist/vendored/styles/card-select.css +15 -0
- package/dist/vendored/styles/dropdown.css +3 -0
- package/dist/vendored/styles/json-tree.css +1 -1
- package/dist/vendored/styles/node-inspector.css +176 -119
- package/dist/vendored/styles/setters.css +91 -2
- package/dist/vendored/styles/structure-tree.css +158 -15
- package/package.json +1 -1
- package/dist/preview-editor/assets/index-BYB0hngG.js +0 -72
- package/dist/preview-editor/assets/index-CpBZEQDy.css +0 -1
package/dist/vendored/api.js
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* 设计目标(前后端分离方案 Phase 2):
|
|
5
5
|
* - 开发期(npm run dev,由 tools/dev.mjs 同起 cardbuddy):apiBase 默认
|
|
6
6
|
* http://127.0.0.1:<FINMALL_CARD_PORT>(.env.finmall,默认 8787),与部署态一致。
|
|
7
|
+
* 若页面不是经回环地址打开(局域网 IP / 其它本机 IP),则退化为同源 '',
|
|
8
|
+
* 由 Vite dev server 代理到 8787,避开浏览器私有网络访问(PNA)拦截。
|
|
7
9
|
* - 部署态(vite build 静态产物):默认读 localStorage 的「本地开发服务」配置;
|
|
8
10
|
* 未配置时 apiBase = null,调用方据此提示「请先配置本地开发服务」。
|
|
9
11
|
* - 本地开发服务(cardbuddy,默认 http://127.0.0.1:8787)经反射 CORS 放行任意
|
|
@@ -37,6 +39,23 @@ const DEFAULT_CONFIG = {
|
|
|
37
39
|
token: '',
|
|
38
40
|
}
|
|
39
41
|
|
|
42
|
+
// 页面是否经「非回环主机名」打开(Vite host:true 会额外打印局域网 IP / 其它本机地址,
|
|
43
|
+
// 例如 http://192.168.x.x:5180、http://2.0.0.1:5180)。此时浏览器把页面归为
|
|
44
|
+
// public/private 地址空间,而 cardbuddy 只绑 127.0.0.1(loopback),Chrome 的私有网络
|
|
45
|
+
// 访问(PNA)规则会以「The request client is not a secure context and the resource is in
|
|
46
|
+
// more-private address space `loopback`」直接拦掉 fetch / EventSource(net::ERR_FAILED),
|
|
47
|
+
// 表现为「本地开发服务已断开」。开发期改走同源(''):请求打到页面自己的 host,
|
|
48
|
+
// 由 vite.config.mjs 的 server.proxy 在 Node 侧转发到 127.0.0.1:<FINMALL_CARD_PORT>
|
|
49
|
+
// —— 服务端转发不受 CORS / PNA 约束。回环访问(localhost / 127.0.0.1)不受影响,
|
|
50
|
+
// 仍直连 8787 跨源链路,与部署态一致。
|
|
51
|
+
const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]', '::1'])
|
|
52
|
+
function isNonLoopbackDevHost() {
|
|
53
|
+
if (typeof window === 'undefined' || !window.location) return false
|
|
54
|
+
const { protocol, hostname } = window.location
|
|
55
|
+
if (protocol !== 'http:' && protocol !== 'https:') return false
|
|
56
|
+
return !LOOPBACK_HOSTS.has(hostname)
|
|
57
|
+
}
|
|
58
|
+
|
|
40
59
|
export function loadDevServiceConfig() {
|
|
41
60
|
try {
|
|
42
61
|
const raw = localStorage.getItem(LS_KEY)
|
|
@@ -68,6 +87,8 @@ export function saveDevServiceConfig(cfg) {
|
|
|
68
87
|
* - Vite dev server(import.meta.env.DEV 且无覆盖)→ 默认本地开发服务
|
|
69
88
|
* http://127.0.0.1:<FINMALL_CARD_PORT>(.env.finmall,默认 8787)
|
|
70
89
|
* (前后端分离:Vite 不再托管卡片后端,改由 tools/dev.mjs 同起的 cardbuddy 提供)
|
|
90
|
+
* 例外:页面经非回环主机名打开时返回 ''(同源),改由 Vite dev server 代理到 8787,
|
|
91
|
+
* 绕开浏览器私有网络访问(PNA)拦截,详见 isNonLoopbackDevHost 注释
|
|
71
92
|
* - 部署态静态产物且未配置 → null(调用方需提示配置)
|
|
72
93
|
*/
|
|
73
94
|
export function resolveApiBase() {
|
|
@@ -76,7 +97,9 @@ export function resolveApiBase() {
|
|
|
76
97
|
if (override) return String(override).replace(/\/+$/, '')
|
|
77
98
|
const cfg = loadDevServiceConfig()
|
|
78
99
|
if (cfg.enabled && cfg.baseUrl) return cfg.baseUrl.replace(/\/+$/, '')
|
|
79
|
-
if (import.meta.env && import.meta.env.DEV)
|
|
100
|
+
if (import.meta.env && import.meta.env.DEV) {
|
|
101
|
+
return isNonLoopbackDevHost() ? '' : DEFAULT_CONFIG.baseUrl
|
|
102
|
+
}
|
|
80
103
|
return null
|
|
81
104
|
}
|
|
82
105
|
|
|
@@ -104,39 +127,86 @@ export async function apiFetch(path, opts = {}) {
|
|
|
104
127
|
* @param {(state:'connecting'|'open'|'closed')=>void} [onState] 连接态变化回调,
|
|
105
128
|
* 用于驱动顶栏「连接」图标实时反映服务端运行态(SSE readyState / Vite HMR ws 生命周期)。
|
|
106
129
|
*/
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
130
|
+
/**
|
|
131
|
+
* 通用 SSE 订阅工厂:监听 /__events 上的一组事件名,返回取消订阅函数。
|
|
132
|
+
* base === ''(开发期同源代理)同样走 SSE:请求相对当前页面 host,由 Vite 转发到 8787,
|
|
133
|
+
* 连接态因此能真实反映 cardbuddy 运行态。
|
|
134
|
+
* @param {string[]} events 事件名列表
|
|
135
|
+
* @param {{ hmrEvents?: string[] }} [opts] hmrEvents:base 为 null(部署态未配置)时
|
|
136
|
+
* 回退到 Vite HMR 通道的事件名(仅 cards-changed 仍有该历史通道)。
|
|
137
|
+
*/
|
|
138
|
+
function makeSseSubscriber(events, { hmrEvents = [] } = {}) {
|
|
139
|
+
return function subscribe(cb, onState) {
|
|
140
|
+
const base = resolveApiBase()
|
|
141
|
+
const emit = (s) => { if (typeof onState === 'function') onState(s) }
|
|
142
|
+
if (base !== null) {
|
|
143
|
+
// 本地服务 / 同源代理:SSE(浏览器原生自动重连)
|
|
144
|
+
const es = new EventSource(base + '/__events')
|
|
145
|
+
const handler = (e) => {
|
|
146
|
+
try {
|
|
147
|
+
cb(e.data ? JSON.parse(e.data) : {})
|
|
148
|
+
} catch (_) {
|
|
149
|
+
cb({})
|
|
150
|
+
}
|
|
118
151
|
}
|
|
152
|
+
for (const name of events) es.addEventListener(name, handler)
|
|
153
|
+
es.onopen = () => emit('open')
|
|
154
|
+
es.onerror = () => {
|
|
155
|
+
// 断链时 onerror 触发,浏览器会自动重连:
|
|
156
|
+
// readyState===0(CONNECTING) 表示正在重连;===2(CLOSED) 表示已彻底关闭(如显式 close)。
|
|
157
|
+
emit(es.readyState === 2 ? 'closed' : 'connecting')
|
|
158
|
+
}
|
|
159
|
+
emit('connecting') // 刚创建即处于 CONNECTING,先置为「连接中」
|
|
160
|
+
return () => es.close()
|
|
119
161
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
// 同源 dev server:沿用 Vite HMR(底层 WebSocket,同样具备连接态)
|
|
131
|
-
if (import.meta.hot) {
|
|
132
|
-
import.meta.hot.on('cards-changed', cb)
|
|
133
|
-
// 页面由同源 dev server 提供,初始即视为已连接;ws 断开 / 重连时更新状态
|
|
134
|
-
import.meta.hot.on('vite:ws:connect', () => emit('open'))
|
|
135
|
-
import.meta.hot.on('vite:ws:disconnect', () => emit('connecting'))
|
|
136
|
-
emit('open')
|
|
137
|
-
return () => {
|
|
138
|
-
/* HMR 无显式 off,noop */
|
|
162
|
+
// 同源 dev server:沿用 Vite HMR(底层 WebSocket,同样具备连接态)
|
|
163
|
+
if (import.meta.hot && hmrEvents.length) {
|
|
164
|
+
for (const name of hmrEvents) import.meta.hot.on(name, cb)
|
|
165
|
+
// 页面由同源 dev server 提供,初始即视为已连接;ws 断开 / 重连时更新状态
|
|
166
|
+
import.meta.hot.on('vite:ws:connect', () => emit('open'))
|
|
167
|
+
import.meta.hot.on('vite:ws:disconnect', () => emit('connecting'))
|
|
168
|
+
emit('open')
|
|
169
|
+
return () => {
|
|
170
|
+
/* HMR 无显式 off,noop */
|
|
171
|
+
}
|
|
139
172
|
}
|
|
173
|
+
return () => {}
|
|
140
174
|
}
|
|
141
|
-
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** 订阅磁盘变更推送(cards-changed)。 */
|
|
178
|
+
export const subscribeCardsChanged = makeSseSubscriber(['cards-changed'], { hmrEvents: ['cards-changed'] })
|
|
179
|
+
|
|
180
|
+
/** 订阅外部编辑器代码桥事件(code-bridge-changed / code-bridge-closed)。 */
|
|
181
|
+
export const subscribeCodeBridge = makeSseSubscriber(['code-bridge-changed', 'code-bridge-closed'])
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* 代码桥 REST 封装(端点见 server/core/codeBridge.js):
|
|
185
|
+
* open 建立会话并拉起编辑器;fetch 拉取外部保存后的最新代码;
|
|
186
|
+
* heartbeat 维持会话(TTL 兜底清理);close 结束会话并删除临时文件。
|
|
187
|
+
*/
|
|
188
|
+
export async function codeBridgeOpen(code, label = '') {
|
|
189
|
+
const res = await apiFetch('/__code-bridge', {
|
|
190
|
+
method: 'POST',
|
|
191
|
+
headers: { 'Content-Type': 'application/json' },
|
|
192
|
+
body: JSON.stringify({ code, label }),
|
|
193
|
+
})
|
|
194
|
+
if (!res.ok) throw new Error(`code-bridge open failed: ${res.status}`)
|
|
195
|
+
return res.json()
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function codeBridgeFetch(id) {
|
|
199
|
+
const res = await apiFetch(`/__code-bridge/${id}`)
|
|
200
|
+
if (!res.ok) throw new Error(`code-bridge fetch failed: ${res.status}`)
|
|
201
|
+
return res.json()
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export async function codeBridgeHeartbeat(id) {
|
|
205
|
+
const res = await apiFetch(`/__code-bridge/${id}/heartbeat`, { method: 'POST' })
|
|
206
|
+
return res.ok
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export async function codeBridgeClose(id) {
|
|
210
|
+
const res = await apiFetch(`/__code-bridge/${id}`, { method: 'DELETE' })
|
|
211
|
+
return res.ok
|
|
142
212
|
}
|
|
@@ -12,11 +12,16 @@
|
|
|
12
12
|
<symbol id="icon-cloud-alert" viewBox="0 0 24 24"><path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"></path><line x1="12" y1="13" x2="12" y2="16"></line><line x1="12" y1="18" x2="12.01" y2="18"></line></symbol>
|
|
13
13
|
<symbol id="icon-cloud" viewBox="0 0 24 24"><path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"></path></symbol>
|
|
14
14
|
<symbol id="icon-upload-cloud" viewBox="0 0 24 24"><path d="M12 13v8"></path><path d="M8 17l4-4 4 4"></path><path d="M20 16.5A4.5 4.5 0 0 0 17.5 8h-1.26A7 7 0 1 0 4 14.9"></path></symbol>
|
|
15
|
+
<!-- 切图上传(image-upload):图片框 + 框内上箭头,语义「把本地切图传到远端图床」,
|
|
16
|
+
与 icon-upload-cloud(版本推送)区分开:一个传素材,一个传卡片版本。 -->
|
|
17
|
+
<symbol id="icon-image-upload" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><path d="M12 19v-7"></path><path d="M9 15l3-3 3 3"></path></symbol>
|
|
15
18
|
<symbol id="icon-grid" viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7" rx="1"></rect><rect x="14" y="3" width="7" height="7" rx="1"></rect><rect x="3" y="14" width="7" height="7" rx="1"></rect><path d="M14 14h3v3M21 14v7h-7v-3"></path></symbol>
|
|
16
19
|
<symbol id="icon-sync-box" viewBox="0 0 24 24"><rect x="3" y="5" width="14" height="14" rx="2" ry="2"></rect><path d="M21 7v10a2 2 0 0 1-2 2H7"></path></symbol>
|
|
17
20
|
<symbol id="icon-locate" viewBox="0 0 24 24"><circle cx="12" cy="12" r="7"></circle><line x1="12" y1="1" x2="12" y2="5"></line><line x1="12" y1="19" x2="12" y2="23"></line><line x1="1" y1="12" x2="5" y2="12"></line><line x1="19" y1="12" x2="23" y2="12"></line></symbol>
|
|
18
21
|
<symbol id="icon-gear" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"></path></symbol>
|
|
19
22
|
<symbol id="icon-no-entry" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"></circle><line x1="6" y1="6" x2="18" y2="18"></line></symbol>
|
|
23
|
+
<!-- 警告三角(alert-triangle):确认框 warn 级标题图标(警告操作,橙黄档);danger 级用 icon-no-entry -->
|
|
24
|
+
<symbol id="icon-alert-triangle" viewBox="0 0 24 24"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></symbol>
|
|
20
25
|
<symbol id="icon-copy-sm" viewBox="0 0 24 24"><rect x="9" y="9" width="11" height="11" rx="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></symbol>
|
|
21
26
|
<symbol id="icon-trash" viewBox="0 0 24 24"><path d="M3 6h18"></path><path d="M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2"></path><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></symbol>
|
|
22
27
|
<!-- 连接(link):本地开发服务「连接前后端」语义图标,呼应主题【连接】 -->
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
接口调用面板:右侧常驻独立面板(由 App 顶栏按钮 / 面板内 ✕ 控制展开收起)。
|
|
3
|
+
展示 ynet.callJSAPI / postMessage 的调用流水,内容原属调试面板的「接口调用」tab,
|
|
4
|
+
拆出后调试面板聚焦「绑定数据 / 组件结构 / 卡片来源」,接口流水可常驻可视。
|
|
5
|
+
-->
|
|
6
|
+
<template>
|
|
7
|
+
<aside class="apipanel">
|
|
8
|
+
<div class="apipanel__head">
|
|
9
|
+
<span class="apipanel__title">
|
|
10
|
+
API调用
|
|
11
|
+
<span v-if="apiCalls.length" class="debug__count">{{ apiCalls.length }}</span>
|
|
12
|
+
</span>
|
|
13
|
+
<span class="apipanel__spacer"></span>
|
|
14
|
+
<button
|
|
15
|
+
class="apipanel__btn"
|
|
16
|
+
type="button"
|
|
17
|
+
:disabled="!apiCalls.length"
|
|
18
|
+
title="清空日志"
|
|
19
|
+
@click="clearApiLog"
|
|
20
|
+
>
|
|
21
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none"
|
|
22
|
+
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#icon-trash" /></svg>
|
|
23
|
+
</button>
|
|
24
|
+
<button
|
|
25
|
+
class="apipanel__btn"
|
|
26
|
+
type="button"
|
|
27
|
+
title="收起接口调用面板"
|
|
28
|
+
@click="$emit('close')"
|
|
29
|
+
>
|
|
30
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none"
|
|
31
|
+
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="5" x2="19" y2="19" /><line x1="19" y1="5" x2="5" y2="19" /></svg>
|
|
32
|
+
</button>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="apipanel__body">
|
|
35
|
+
<!-- 空状态占位:虚线圆形徽章 + 插头图标(与全局 24 网格线性图标同语言)+ 文案 + 弱提示 -->
|
|
36
|
+
<div v-if="!apiCalls.length" class="empty-state">
|
|
37
|
+
<span class="empty-state__badge" aria-hidden="true">
|
|
38
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
39
|
+
<path d="M9 7V3" />
|
|
40
|
+
<path d="M15 7V3" />
|
|
41
|
+
<path d="M6 7h12v4a6 6 0 0 1-12 0V7z" />
|
|
42
|
+
<path d="M12 17v4" />
|
|
43
|
+
</svg>
|
|
44
|
+
</span>
|
|
45
|
+
<p class="debug__empty">暂无API调用</p>
|
|
46
|
+
<p class="empty-state__hint">卡片事件触发 ynet.callJSAPI / postMessage 后,调用流水会实时显示在这里</p>
|
|
47
|
+
</div>
|
|
48
|
+
<ul class="debug__list">
|
|
49
|
+
<li v-for="(c, i) in apiCalls" :key="i" class="debug__item debug__item--api">
|
|
50
|
+
<div class="debug__api-row">
|
|
51
|
+
<span class="debug__kind">{{ c.api }}</span>
|
|
52
|
+
<button
|
|
53
|
+
class="debug__copy-ic"
|
|
54
|
+
type="button"
|
|
55
|
+
:title="copiedIndex === i ? '已复制 ✓' : '复制数据(不含方法名)'"
|
|
56
|
+
@click="copyApiPayload(c, i)"
|
|
57
|
+
>
|
|
58
|
+
<svg v-if="copiedIndex !== i" width="15" height="15" viewBox="0 0 24 24" fill="none"
|
|
59
|
+
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#icon-copy-sm" /></svg>
|
|
60
|
+
<span v-else class="debug__copy-ok">✓</span>
|
|
61
|
+
</button>
|
|
62
|
+
</div>
|
|
63
|
+
<!-- 返回数据:结构化折叠树(Chrome 控制台风格) -->
|
|
64
|
+
<ApiPayloadTree :value="c.payload" />
|
|
65
|
+
</li>
|
|
66
|
+
</ul>
|
|
67
|
+
</div>
|
|
68
|
+
</aside>
|
|
69
|
+
</template>
|
|
70
|
+
|
|
71
|
+
<script>
|
|
72
|
+
import ApiPayloadTree from './ApiPayloadTree.vue'
|
|
73
|
+
import { copyText } from '@/runtime/clipboard.js'
|
|
74
|
+
|
|
75
|
+
export default {
|
|
76
|
+
name: 'ApiCallPanel',
|
|
77
|
+
components: { ApiPayloadTree },
|
|
78
|
+
props: {
|
|
79
|
+
rt: { type: Object, required: true },
|
|
80
|
+
},
|
|
81
|
+
emits: ['close'],
|
|
82
|
+
data() {
|
|
83
|
+
return {
|
|
84
|
+
// 刚复制过数据的条目下标(用于图标变 ✓ 反馈),null=无
|
|
85
|
+
copiedIndex: null,
|
|
86
|
+
apiCopyTimer: null,
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
computed: {
|
|
90
|
+
// 依赖 apiRev,使接口调用流水在 pushApiCall / clearApiCalls 后实时刷新
|
|
91
|
+
// (apiCalls 本身是非响应式数组,模板直接读它不会触发重渲染)
|
|
92
|
+
//
|
|
93
|
+
// ⚠️ 必须 slice() 返回**新数组**,不能直接 return 原数组引用:
|
|
94
|
+
// Vue 3.4+ 引入了「computed 值稳定性」——重算后若新值与旧值 Object.is 相等,
|
|
95
|
+
// 就不再通知下游 effect。原数组是同一个引用,即使 apiRev 自增触发了重算,
|
|
96
|
+
// 渲染 effect 也收不到通知,界面纹丝不动。
|
|
97
|
+
apiCalls() {
|
|
98
|
+
void this.rt.apiRev
|
|
99
|
+
return this.rt.apiCalls.slice()
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
methods: {
|
|
103
|
+
/** 清空接口调用流水:调用运行时 clearApiCalls(清空数组 + 自增 apiRev),
|
|
104
|
+
* 因 apiCalls computed 返回 slice() 新引用,触发实时重渲染 */
|
|
105
|
+
clearApiLog() {
|
|
106
|
+
this.rt.clearApiCalls()
|
|
107
|
+
},
|
|
108
|
+
/** 复制某条接口调用的「返回数据」(即 payload 全文),不含方法名(api) */
|
|
109
|
+
async copyApiPayload(c, i) {
|
|
110
|
+
let text = ''
|
|
111
|
+
try {
|
|
112
|
+
text = JSON.stringify(c.payload, null, 2)
|
|
113
|
+
} catch {
|
|
114
|
+
text = String(c.payload)
|
|
115
|
+
}
|
|
116
|
+
const ok = await copyText(text)
|
|
117
|
+
if (ok) {
|
|
118
|
+
this.copiedIndex = i
|
|
119
|
+
if (this.apiCopyTimer) clearTimeout(this.apiCopyTimer)
|
|
120
|
+
this.apiCopyTimer = setTimeout(() => {
|
|
121
|
+
if (this.copiedIndex === i) this.copiedIndex = null
|
|
122
|
+
}, 1500)
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
</script>
|
|
@@ -15,7 +15,26 @@
|
|
|
15
15
|
</div>
|
|
16
16
|
|
|
17
17
|
<template v-else>
|
|
18
|
-
|
|
18
|
+
<!-- 日志场景(rootCollapsed):根对象/数组先收成一行摘要、点击展开(Chrome 控制台风格),
|
|
19
|
+
避免每个实参默认逐键占行稀释日志密度;接口调用面板不传该 prop,行为不变 -->
|
|
20
|
+
<div
|
|
21
|
+
v-if="rootCollapsed && isRoot"
|
|
22
|
+
class="apt__branch apt__branch--root"
|
|
23
|
+
@click="rootOpen = !rootOpen"
|
|
24
|
+
>
|
|
25
|
+
<span class="apt__caret" :class="{ 'apt__caret--open': rootOpen }">▶</span>
|
|
26
|
+
<span class="jt__bracket">{{ isArrayRoot ? '[' : '{' }}</span>
|
|
27
|
+
<span v-if="!rootOpen" class="jt__preview">{{ preview(value) }}</span>
|
|
28
|
+
<template v-else>
|
|
29
|
+
<span class="jt__count">{{ rootCount }} {{ isArrayRoot ? '项' : '键' }}</span>
|
|
30
|
+
</template>
|
|
31
|
+
<span v-if="!rootOpen" class="jt__bracket">{{ isArrayRoot ? ']' : '}' }}</span>
|
|
32
|
+
</div>
|
|
33
|
+
<div
|
|
34
|
+
v-show="!(rootCollapsed && isRoot) || rootOpen"
|
|
35
|
+
class="apt__list"
|
|
36
|
+
:class="{ 'apt__list--nested': rootCollapsed && isRoot && rootOpen }"
|
|
37
|
+
>
|
|
19
38
|
<div v-for="(entry, i) in entries" :key="i" class="apt__node">
|
|
20
39
|
<!-- 复合类型且未超最大深度:可折叠分支 -->
|
|
21
40
|
<div v-if="canExpand(entry)" class="apt__branch" @click="toggle(entry)">
|
|
@@ -58,6 +77,10 @@
|
|
|
58
77
|
</div>
|
|
59
78
|
</div>
|
|
60
79
|
</div>
|
|
80
|
+
<div v-if="rootCollapsed && isRoot && rootOpen" class="apt__close">
|
|
81
|
+
<span class="jt__bracket">{{ isArrayRoot ? ']' : '}' }}</span>
|
|
82
|
+
<span class="jt__count">{{ rootCount }} {{ isArrayRoot ? '项' : '键' }}</span>
|
|
83
|
+
</div>
|
|
61
84
|
</template>
|
|
62
85
|
</div>
|
|
63
86
|
</template>
|
|
@@ -81,17 +104,27 @@ export default {
|
|
|
81
104
|
// 最大可展开深度(保护):达到该深度的复合节点不再可展开,
|
|
82
105
|
// 避免日志中循环引用 / 超深结构在展开时无限递归。默认 100(接口调用场景基本无感)。
|
|
83
106
|
maxDepth: { type: Number, default: 100 },
|
|
107
|
+
// 根节点默认折叠成一行摘要(事件日志使用);接口调用面板保持逐行铺开
|
|
108
|
+
rootCollapsed: { type: Boolean, default: false },
|
|
84
109
|
},
|
|
85
110
|
data() {
|
|
86
111
|
return {
|
|
87
112
|
// 若未传入共享状态(理论只发生在根),自行创建,保证递归子组件拿到同一引用
|
|
88
113
|
localOpenState: this.openState || reactive({}),
|
|
114
|
+
// 根摘要行的展开态(仅 rootCollapsed 场景使用)
|
|
115
|
+
rootOpen: false,
|
|
89
116
|
}
|
|
90
117
|
},
|
|
91
118
|
computed: {
|
|
92
119
|
isRoot() {
|
|
93
120
|
return this.depth === 0
|
|
94
121
|
},
|
|
122
|
+
isArrayRoot() {
|
|
123
|
+
return Array.isArray(this.value)
|
|
124
|
+
},
|
|
125
|
+
rootCount() {
|
|
126
|
+
return this.isArrayRoot ? this.value.length : Object.keys(this.value || {}).length
|
|
127
|
+
},
|
|
95
128
|
state() {
|
|
96
129
|
return this.openState || this.localOpenState
|
|
97
130
|
},
|
|
@@ -56,6 +56,8 @@
|
|
|
56
56
|
@click="choose(card)"
|
|
57
57
|
>
|
|
58
58
|
<div class="cg-tile__thumb">
|
|
59
|
+
<!-- 导入标记:仅含 .import-meta.json 侧车的卡(经远端同步导入)显示,悬浮于缩略图右上角 -->
|
|
60
|
+
<span v-if="card.imported" class="cg-tile__tag" title="导入卡片(含 .import-meta.json 来源信息)">导入</span>
|
|
59
61
|
<img
|
|
60
62
|
v-if="visibleIds.has(card.id) && !failed.has(card.id)"
|
|
61
63
|
class="cg-tile__img"
|
|
@@ -43,6 +43,12 @@
|
|
|
43
43
|
@mousemove="highlighted = i"
|
|
44
44
|
@click="choose(opt)"
|
|
45
45
|
>
|
|
46
|
+
<span
|
|
47
|
+
class="csel__dot"
|
|
48
|
+
:class="{ 'csel__dot--imported': opt.imported }"
|
|
49
|
+
:title="opt.imported ? '导入卡片(含 .import-meta.json 来源信息)' : '本地卡片(无导入来源信息)'"
|
|
50
|
+
aria-hidden="true"
|
|
51
|
+
></span>
|
|
46
52
|
<span class="csel__item-id">{{ opt.id }}</span>
|
|
47
53
|
<span class="csel__item-name">{{ opt.name }}</span>
|
|
48
54
|
<svg v-if="opt.id === value" class="csel__check" viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><use href="#icon-check" /></svg>
|
|
@@ -129,11 +129,13 @@ export default {
|
|
|
129
129
|
.cp__mask {
|
|
130
130
|
position: fixed;
|
|
131
131
|
inset: 0;
|
|
132
|
-
|
|
132
|
+
/* 与确认框 .confirm-mask 同款模糊遮罩:暗色半透明 + 2px 背景模糊 */
|
|
133
|
+
background: var(--scrim-modal);
|
|
134
|
+
backdrop-filter: blur(var(--scrim-modal-blur));
|
|
133
135
|
display: flex;
|
|
134
136
|
align-items: center;
|
|
135
137
|
justify-content: center;
|
|
136
|
-
z-index:
|
|
138
|
+
z-index: var(--z-modal);
|
|
137
139
|
}
|
|
138
140
|
|
|
139
141
|
.cp {
|
|
@@ -142,8 +144,8 @@ export default {
|
|
|
142
144
|
max-height: calc(100vh - 64px);
|
|
143
145
|
background: var(--bg-panel);
|
|
144
146
|
border: 1px solid var(--border-strong);
|
|
145
|
-
border-radius:
|
|
146
|
-
box-shadow:
|
|
147
|
+
border-radius: var(--radius-lg);
|
|
148
|
+
box-shadow: var(--shadow-4);
|
|
147
149
|
display: flex;
|
|
148
150
|
flex-direction: column;
|
|
149
151
|
overflow: hidden;
|
|
@@ -152,29 +154,35 @@ export default {
|
|
|
152
154
|
|
|
153
155
|
.cp__head {
|
|
154
156
|
display: flex;
|
|
155
|
-
align-items:
|
|
157
|
+
align-items: center;
|
|
156
158
|
gap: 10px;
|
|
157
|
-
padding:
|
|
158
|
-
border-bottom: 1px solid var(--border
|
|
159
|
+
padding: 10px 16px;
|
|
160
|
+
border-bottom: 1px solid var(--border);
|
|
159
161
|
}
|
|
160
162
|
.cp__title {
|
|
161
|
-
font-size:
|
|
163
|
+
font-size: 13px;
|
|
162
164
|
font-weight: 600;
|
|
163
165
|
}
|
|
164
166
|
.cp__hint {
|
|
165
|
-
font-size:
|
|
166
|
-
color: var(--text-
|
|
167
|
+
font-size: 11px;
|
|
168
|
+
color: var(--text-faint);
|
|
167
169
|
}
|
|
168
170
|
.cp__close {
|
|
169
171
|
margin-left: auto;
|
|
172
|
+
display: inline-flex;
|
|
173
|
+
align-items: center;
|
|
174
|
+
justify-content: center;
|
|
175
|
+
width: 22px;
|
|
176
|
+
height: 22px;
|
|
177
|
+
padding: 0;
|
|
170
178
|
background: transparent;
|
|
171
179
|
border: none;
|
|
172
|
-
|
|
173
|
-
|
|
180
|
+
border-radius: 6px;
|
|
181
|
+
color: var(--text-faint);
|
|
182
|
+
font-size: 15px;
|
|
174
183
|
cursor: pointer;
|
|
175
184
|
line-height: 1;
|
|
176
|
-
|
|
177
|
-
border-radius: 4px;
|
|
185
|
+
transition: background 0.15s, color 0.15s;
|
|
178
186
|
}
|
|
179
187
|
.cp__close:hover {
|
|
180
188
|
background: var(--bg-hover);
|
|
@@ -182,16 +190,16 @@ export default {
|
|
|
182
190
|
}
|
|
183
191
|
|
|
184
192
|
.cp__body {
|
|
185
|
-
padding:
|
|
193
|
+
padding: 10px 16px 16px;
|
|
186
194
|
overflow-y: auto;
|
|
187
195
|
}
|
|
188
196
|
|
|
189
197
|
.cp__group {
|
|
190
|
-
margin-top:
|
|
198
|
+
margin-top: 10px;
|
|
191
199
|
}
|
|
192
200
|
.cp__group-title {
|
|
193
|
-
margin: 0 0
|
|
194
|
-
font-size:
|
|
201
|
+
margin: 0 0 6px;
|
|
202
|
+
font-size: 11px;
|
|
195
203
|
font-weight: 600;
|
|
196
204
|
color: var(--text-faint);
|
|
197
205
|
letter-spacing: 0.04em;
|
|
@@ -200,7 +208,7 @@ export default {
|
|
|
200
208
|
.cp__grid {
|
|
201
209
|
display: grid;
|
|
202
210
|
grid-template-columns: repeat(4, 1fr);
|
|
203
|
-
gap:
|
|
211
|
+
gap: 6px;
|
|
204
212
|
}
|
|
205
213
|
|
|
206
214
|
.cp__item {
|
|
@@ -208,21 +216,17 @@ export default {
|
|
|
208
216
|
flex-direction: column;
|
|
209
217
|
align-items: center;
|
|
210
218
|
gap: 4px;
|
|
211
|
-
padding:
|
|
212
|
-
background:
|
|
219
|
+
padding: 8px 6px;
|
|
220
|
+
background: transparent;
|
|
213
221
|
border: 1px solid var(--border);
|
|
214
|
-
border-radius:
|
|
222
|
+
border-radius: 6px;
|
|
215
223
|
cursor: pointer;
|
|
216
224
|
color: var(--text);
|
|
217
|
-
transition: border-color 0.
|
|
225
|
+
transition: border-color 0.15s, background 0.15s;
|
|
218
226
|
}
|
|
219
227
|
.cp__item:hover {
|
|
220
|
-
border-color: var(--accent);
|
|
221
|
-
background: var(--
|
|
222
|
-
transform: translateY(-1px);
|
|
223
|
-
}
|
|
224
|
-
.cp__item:active {
|
|
225
|
-
transform: translateY(0);
|
|
228
|
+
border-color: var(--accent-border);
|
|
229
|
+
background: var(--accent-soft);
|
|
226
230
|
}
|
|
227
231
|
|
|
228
232
|
.cp__avatar {
|
|
@@ -234,14 +238,17 @@ export default {
|
|
|
234
238
|
justify-content: center;
|
|
235
239
|
font-size: 15px;
|
|
236
240
|
font-weight: 600;
|
|
237
|
-
color: var(--
|
|
238
|
-
background: var(--
|
|
241
|
+
color: var(--text-dim);
|
|
242
|
+
background: var(--bg-hover);
|
|
239
243
|
}
|
|
244
|
+
/* 分组语义色:布局 = accent、基础 = ok,均用 soft 底 + 同色线性图标,与全局轻风格一致 */
|
|
240
245
|
.cp__avatar[data-g='layout'] {
|
|
241
|
-
|
|
246
|
+
color: var(--accent);
|
|
247
|
+
background: var(--accent-soft);
|
|
242
248
|
}
|
|
243
249
|
.cp__avatar[data-g='basic'] {
|
|
244
|
-
|
|
250
|
+
color: var(--ok);
|
|
251
|
+
background: var(--ok-soft);
|
|
245
252
|
}
|
|
246
253
|
.cp__avatar svg {
|
|
247
254
|
display: block;
|
|
@@ -272,14 +279,14 @@ export default {
|
|
|
272
279
|
.cp__search {
|
|
273
280
|
box-sizing: border-box;
|
|
274
281
|
width: 100%;
|
|
275
|
-
height:
|
|
282
|
+
height: 28px;
|
|
276
283
|
margin-bottom: 10px;
|
|
277
|
-
padding: 0
|
|
278
|
-
font-size:
|
|
284
|
+
padding: 0 8px;
|
|
285
|
+
font-size: 12px;
|
|
279
286
|
color: var(--text);
|
|
280
287
|
background: var(--bg-input);
|
|
281
288
|
border: 1px solid var(--border);
|
|
282
|
-
border-radius:
|
|
289
|
+
border-radius: 6px;
|
|
283
290
|
outline: none;
|
|
284
291
|
}
|
|
285
292
|
.cp__search:focus {
|
|
@@ -287,7 +294,7 @@ export default {
|
|
|
287
294
|
}
|
|
288
295
|
.cp__empty {
|
|
289
296
|
margin: 8px 0;
|
|
290
|
-
color: var(--text-
|
|
297
|
+
color: var(--text-faint);
|
|
291
298
|
font-size: 12px;
|
|
292
299
|
text-align: center;
|
|
293
300
|
}
|
|
@@ -2,15 +2,16 @@
|
|
|
2
2
|
<transition name="confirm-fade">
|
|
3
3
|
<div v-if="visible" class="confirm-mask" @click.self="$emit('cancel')">
|
|
4
4
|
<div class="confirm-modal" role="alertdialog" aria-modal="true" :aria-label="title">
|
|
5
|
-
<div class="confirm-modal__title" :class="{ 'is-danger': danger }">
|
|
6
|
-
<svg v-if="danger" class="confirm-modal__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-no-entry" /></svg>
|
|
5
|
+
<div class="confirm-modal__title" :class="{ 'is-danger': severity === 'danger', 'is-warn': severity === 'warn' }">
|
|
6
|
+
<svg v-if="severity === 'danger'" class="confirm-modal__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-no-entry" /></svg>
|
|
7
|
+
<svg v-else-if="severity === 'warn'" class="confirm-modal__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-alert-triangle" /></svg>
|
|
7
8
|
<svg v-else class="confirm-modal__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-gear" /></svg>
|
|
8
9
|
{{ title }}
|
|
9
10
|
</div>
|
|
10
11
|
<div class="confirm-modal__desc">{{ message }}</div>
|
|
11
12
|
<div class="confirm-modal__actions">
|
|
12
13
|
<button class="confirm-modal__btn confirm-modal__btn--ghost" type="button" @click="$emit('cancel')">{{ cancelText }}</button>
|
|
13
|
-
<button class="confirm-modal__btn" :class="
|
|
14
|
+
<button class="confirm-modal__btn" :class="confirmBtnClass" type="button" @click="$emit('confirm')">{{ confirmText }}</button>
|
|
14
15
|
</div>
|
|
15
16
|
</div>
|
|
16
17
|
</div>
|
|
@@ -31,9 +32,23 @@ export default {
|
|
|
31
32
|
confirmText: { type: String, default: '继续' },
|
|
32
33
|
/** 取消按钮文案,默认「取消」 */
|
|
33
34
|
cancelText: { type: String, default: '取消' },
|
|
34
|
-
/**
|
|
35
|
-
|
|
35
|
+
/** 严重级:primary=常规确认(蓝)/ warn=警告操作(橙黄+警告三角图标,如版本推送等覆盖推送类)/
|
|
36
|
+
* danger=明确负面结果(红+禁止图标,如全量同步覆盖本地卡等不可逆数据丢失)。
|
|
37
|
+
* 分级与配色见 design/深浅主题配色规范手册.md §2.7 / §3.4。 */
|
|
38
|
+
severity: {
|
|
39
|
+
type: String,
|
|
40
|
+
default: 'primary',
|
|
41
|
+
validator: (v) => ['primary', 'warn', 'danger'].includes(v),
|
|
42
|
+
},
|
|
36
43
|
},
|
|
37
44
|
emits: ['confirm', 'cancel'],
|
|
45
|
+
computed: {
|
|
46
|
+
/** 确认按钮配色档:与 severity 一一对应 */
|
|
47
|
+
confirmBtnClass() {
|
|
48
|
+
if (this.severity === 'danger') return 'confirm-modal__btn--danger'
|
|
49
|
+
if (this.severity === 'warn') return 'confirm-modal__btn--warn'
|
|
50
|
+
return 'confirm-modal__btn--primary'
|
|
51
|
+
},
|
|
52
|
+
},
|
|
38
53
|
}
|
|
39
54
|
</script>
|