cardbuddy 0.8.0 → 0.9.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/cli.js +5 -0
- package/dist/preview-editor/assets/index-CrcrVs4F.css +1 -0
- package/dist/preview-editor/assets/index-DnpHHKWl.js +80 -0
- package/dist/preview-editor/index.html +2 -2
- package/dist/server-core/index.js +19 -0
- package/dist/server-core/logStream.js +275 -0
- package/dist/standalone.js +15 -3
- package/dist/vendored/App.vue +12 -7
- package/dist/vendored/ansi.js +185 -0
- package/dist/vendored/api.js +76 -10
- package/dist/vendored/components/CardGallery.vue +2 -2
- package/dist/vendored/components/ComponentPalette.vue +1 -1
- package/dist/vendored/components/ConfirmDialog.vue +2 -2
- package/dist/vendored/components/DebugPanel.vue +276 -32
- package/dist/vendored/components/EventEditor.vue +2 -0
- package/dist/vendored/components/NodeInspector.vue +1 -1
- package/dist/vendored/components/RemotePreview.vue +3 -1
- package/dist/vendored/components/StructureTree.vue +2 -0
- package/dist/vendored/styles/app.css +164 -15
- package/dist/vendored/styles/card-gallery.css +1 -9
- package/dist/vendored/styles/card-select.css +0 -15
- package/dist/vendored/styles/dropdown.css +0 -15
- package/dist/vendored/styles/json-tree.css +1 -1
- package/dist/vendored/styles/node-inspector.css +28 -12
- package/dist/vendored/styles/structure-tree.css +2 -0
- package/package.json +1 -1
- package/dist/preview-editor/assets/index-BQKzMPBq.js +0 -79
- package/dist/preview-editor/assets/index-D3zp47Dg.css +0 -1
package/dist/vendored/api.js
CHANGED
|
@@ -127,6 +127,49 @@ export async function apiFetch(path, opts = {}) {
|
|
|
127
127
|
* @param {(state:'connecting'|'open'|'closed')=>void} [onState] 连接态变化回调,
|
|
128
128
|
* 用于驱动顶栏「连接」图标实时反映服务端运行态(SSE readyState / Vite HMR ws 生命周期)。
|
|
129
129
|
*/
|
|
130
|
+
/**
|
|
131
|
+
* 共享 SSE 连接池:浏览器对同一 origin 的 HTTP/1.1 并发连接约 6 条,而 /__events 是长连接,
|
|
132
|
+
* 每条订阅各开一个 EventSource 会常驻占位(cards-changed + code-bridge 已占 2 条,服务日志
|
|
133
|
+
* 面板再占 1 条后,普通 API 请求只剩 3 条并发余量,截图 / 校验这类慢请求易排队)。
|
|
134
|
+
* 三类事件本就同源同端点,故按 base 复用一条连接、按事件名分发,引用计数归零才真正 close。
|
|
135
|
+
* @type {Map<string, { es: EventSource, states: Set<(s:string)=>void>, refs: number }>}
|
|
136
|
+
*/
|
|
137
|
+
const ssePool = new Map()
|
|
138
|
+
|
|
139
|
+
function acquireSse(base) {
|
|
140
|
+
let entry = ssePool.get(base)
|
|
141
|
+
if (!entry) {
|
|
142
|
+
// EventSource 不能自定义请求头,配置了 token 时改经查询参数携带(服务端 tokenMiddleware 已支持)
|
|
143
|
+
const token = devServiceToken()
|
|
144
|
+
const es = new EventSource(base + '/__events' + (token ? '?token=' + encodeURIComponent(token) : ''))
|
|
145
|
+
entry = { es, states: new Set(), refs: 0 }
|
|
146
|
+
ssePool.set(base, entry)
|
|
147
|
+
es.onopen = () => {
|
|
148
|
+
for (const cb of entry.states) cb('open')
|
|
149
|
+
}
|
|
150
|
+
es.onerror = () => {
|
|
151
|
+
// 断链时 onerror 触发,浏览器会自动重连:
|
|
152
|
+
// readyState===0(CONNECTING) 表示正在重连;===2(CLOSED) 表示已彻底关闭(如显式 close)。
|
|
153
|
+
const state = es.readyState === 2 ? 'closed' : 'connecting'
|
|
154
|
+
for (const cb of entry.states) cb(state)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
entry.refs++
|
|
158
|
+
return entry
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function releaseSse(base, entry) {
|
|
162
|
+
entry.refs = Math.max(0, entry.refs - 1)
|
|
163
|
+
if (entry.refs === 0 && ssePool.get(base) === entry) {
|
|
164
|
+
ssePool.delete(base)
|
|
165
|
+
try {
|
|
166
|
+
entry.es.close()
|
|
167
|
+
} catch (_) {
|
|
168
|
+
/* ignore */
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
130
173
|
/**
|
|
131
174
|
* 通用 SSE 订阅工厂:监听 /__events 上的一组事件名,返回取消订阅函数。
|
|
132
175
|
* base === ''(开发期同源代理)同样走 SSE:请求相对当前页面 host,由 Vite 转发到 8787,
|
|
@@ -140,8 +183,8 @@ function makeSseSubscriber(events, { hmrEvents = [] } = {}) {
|
|
|
140
183
|
const base = resolveApiBase()
|
|
141
184
|
const emit = (s) => { if (typeof onState === 'function') onState(s) }
|
|
142
185
|
if (base !== null) {
|
|
143
|
-
// 本地服务 / 同源代理:SSE
|
|
144
|
-
const
|
|
186
|
+
// 本地服务 / 同源代理:SSE(浏览器原生自动重连),同 base 复用一条连接
|
|
187
|
+
const entry = acquireSse(base)
|
|
145
188
|
const handler = (e) => {
|
|
146
189
|
try {
|
|
147
190
|
cb(e.data ? JSON.parse(e.data) : {})
|
|
@@ -149,15 +192,15 @@ function makeSseSubscriber(events, { hmrEvents = [] } = {}) {
|
|
|
149
192
|
cb({})
|
|
150
193
|
}
|
|
151
194
|
}
|
|
152
|
-
for (const name of events) es.addEventListener(name, handler)
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
195
|
+
for (const name of events) entry.es.addEventListener(name, handler)
|
|
196
|
+
entry.states.add(emit)
|
|
197
|
+
// 复用已建立的连接时立即回写真实态,避免顶栏「连接」图标无谓闪「连接中」
|
|
198
|
+
emit(entry.es.readyState === 1 ? 'open' : entry.es.readyState === 2 ? 'closed' : 'connecting')
|
|
199
|
+
return () => {
|
|
200
|
+
for (const name of events) entry.es.removeEventListener(name, handler)
|
|
201
|
+
entry.states.delete(emit)
|
|
202
|
+
releaseSse(base, entry)
|
|
158
203
|
}
|
|
159
|
-
emit('connecting') // 刚创建即处于 CONNECTING,先置为「连接中」
|
|
160
|
-
return () => es.close()
|
|
161
204
|
}
|
|
162
205
|
// 同源 dev server:沿用 Vite HMR(底层 WebSocket,同样具备连接态)
|
|
163
206
|
if (import.meta.hot && hmrEvents.length) {
|
|
@@ -180,6 +223,29 @@ export const subscribeCardsChanged = makeSseSubscriber(['cards-changed'], { hmrE
|
|
|
180
223
|
/** 订阅外部编辑器代码桥事件(code-bridge-changed / code-bridge-closed)。 */
|
|
181
224
|
export const subscribeCodeBridge = makeSseSubscriber(['code-bridge-changed', 'code-bridge-closed'])
|
|
182
225
|
|
|
226
|
+
/**
|
|
227
|
+
* 订阅本地服务日志流(event: service-log)。
|
|
228
|
+
* 载荷 { lines: [{ seq, t, text }] },text 为含 ANSI 的原始终端行,由 preview/ansi.js 还原配色。
|
|
229
|
+
*
|
|
230
|
+
* 连接开销说明:每条订阅各占一个 EventSource(浏览器对同源的并发连接有限),
|
|
231
|
+
* 因此服务日志采用「面板打开才订阅、关闭即退订」的懒连接策略(见 DebugPanel),
|
|
232
|
+
* 不与常驻的 cards-changed / code-bridge 抢连接。
|
|
233
|
+
*/
|
|
234
|
+
export const subscribeServiceLog = makeSseSubscriber(['service-log'])
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* 取服务日志历史 / 断线补漏(GET /__logs)。
|
|
238
|
+
* @param {{ limit?: number, since?: number }} [opts] since:只取 seq 大于该值的行
|
|
239
|
+
* @returns {Promise<{ ok:boolean, enabled:boolean, seq:number, dropped:number, buffered:number, startedAt:number, pid:number, lines:{seq:number,t:number,text:string}[] }>}
|
|
240
|
+
*/
|
|
241
|
+
export async function fetchServiceLogs({ limit = 300, since = 0 } = {}) {
|
|
242
|
+
const q = new URLSearchParams({ limit: String(limit) })
|
|
243
|
+
if (since) q.set('since', String(since))
|
|
244
|
+
const res = await apiFetch('/__logs?' + q.toString())
|
|
245
|
+
if (!res.ok) throw new Error('HTTP ' + res.status)
|
|
246
|
+
return res.json()
|
|
247
|
+
}
|
|
248
|
+
|
|
183
249
|
/**
|
|
184
250
|
* 代码桥 REST 封装(端点见 server/core/codeBridge.js):
|
|
185
251
|
* open 建立会话并拉起编辑器;fetch 拉取外部保存后的最新代码;
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
搜索框按 id / 名称过滤;Esc 或点遮罩关闭;当前选中卡高亮。
|
|
11
11
|
-->
|
|
12
12
|
<template>
|
|
13
|
-
<transition name="
|
|
13
|
+
<transition name="modal">
|
|
14
14
|
<div v-if="visible" class="cg-mask" @click.self="close">
|
|
15
|
-
<div class="cg-modal" role="dialog" aria-modal="true" aria-label="卡片网格预览">
|
|
15
|
+
<div class="cg-modal modal-card" role="dialog" aria-modal="true" aria-label="卡片网格预览">
|
|
16
16
|
<header class="cg-modal__head">
|
|
17
17
|
<div class="cg-modal__title">
|
|
18
18
|
<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>
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
-->
|
|
6
6
|
<template>
|
|
7
7
|
<div class="cp__mask" @click.self="$emit('close')">
|
|
8
|
-
<div class="cp" role="dialog" aria-label="选择组件类型">
|
|
8
|
+
<div class="cp modal-card" role="dialog" aria-label="选择组件类型">
|
|
9
9
|
<div class="cp__head">
|
|
10
10
|
<span class="cp__title">添加组件</span>
|
|
11
11
|
<span class="cp__hint">选择要插入的组件类型</span>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<transition name="
|
|
2
|
+
<transition name="modal">
|
|
3
3
|
<div v-if="visible" class="confirm-mask" @click.self="$emit('cancel')">
|
|
4
|
-
<div class="confirm-modal" role="alertdialog" aria-modal="true" :aria-label="title">
|
|
4
|
+
<div class="confirm-modal modal-card" role="alertdialog" aria-modal="true" :aria-label="title">
|
|
5
5
|
<div class="confirm-modal__title" :class="{ 'is-danger': severity === 'danger', 'is-warn': severity === 'warn' }">
|
|
6
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
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>
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
RadioButton·CheckGroup 选择 / globalSet 等只写这里),不污染 rootScope、不触发保存
|
|
14
14
|
高亮,事件代码里 `this.data.xxx` 也读这里,调试变更永不落盘。
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
注:「变量绑定」已移至底部状态栏(类 VSCode 面板),点击状态栏指示器在
|
|
17
|
+
底部弹出面板查看,不再作为此处的标签页。
|
|
18
18
|
-->
|
|
19
19
|
<template>
|
|
20
20
|
<aside class="debug">
|
|
@@ -147,16 +147,18 @@
|
|
|
147
147
|
节点属性面板:组件结构 tab 双击节点弹出(可编辑写回)。
|
|
148
148
|
事件编辑已并入本面板的「事件」页签(meta 驱动),不再有独立弹层。
|
|
149
149
|
-->
|
|
150
|
-
<
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
150
|
+
<Transition name="modal">
|
|
151
|
+
<NodeInspector
|
|
152
|
+
v-if="inspectingComp"
|
|
153
|
+
:key="inspectKey"
|
|
154
|
+
:comp="inspectingComp"
|
|
155
|
+
:comp-names="compNames"
|
|
156
|
+
:rt="rt"
|
|
157
|
+
@apply="onApply"
|
|
158
|
+
@commit="onCommit"
|
|
159
|
+
@close="inspectingComp = null"
|
|
160
|
+
/>
|
|
161
|
+
</Transition>
|
|
160
162
|
|
|
161
163
|
<!--
|
|
162
164
|
事件编辑弹框:点击结构树「事件」徽标时单独弹出,与属性面板解耦。
|
|
@@ -182,7 +184,7 @@
|
|
|
182
184
|
不触发保存高亮、调试变更永不落盘。
|
|
183
185
|
-->
|
|
184
186
|
<!--
|
|
185
|
-
底部弹出面板(运行态 / 事件 /
|
|
187
|
+
底部弹出面板(运行态 / 事件 / 变量绑定):Teleport 到 App 的 #app-workside-panels,
|
|
186
188
|
宽度贯穿中右两栏、紧贴右缘(与共同底栏同一容器机制,贴近 VSCode 底部面板)。
|
|
187
189
|
defer:目标与 Teleport 同属本应用组件树,挂载时目标尚未插入 document,须等整个
|
|
188
190
|
应用挂载完成后再解析(Vue 3.5+)。chrome-off 截图模式下禁用 Teleport,回落本面板内。
|
|
@@ -212,7 +214,7 @@
|
|
|
212
214
|
中右两栏共同的底栏(贴近 VSCode 布局)。defer:目标与 Teleport 同属本应用组件树,
|
|
213
215
|
挂载时目标尚未插入 document,须等整个应用挂载完成后再解析(Vue 3.5+)。
|
|
214
216
|
chrome-off 截图模式下禁用 Teleport,回落到本面板内渲染(随面板一起隐藏)。
|
|
215
|
-
|
|
217
|
+
左侧三枚指示器(运行态 / 变量绑定 / 事件)点击弹出或收起对应底部面板;右侧展示当前卡片与卡片总数。
|
|
216
218
|
-->
|
|
217
219
|
<Teleport defer to="#app-workside-bar" :disabled="chromeOff">
|
|
218
220
|
<section class="statusbar">
|
|
@@ -244,17 +246,20 @@
|
|
|
244
246
|
:title="eventStatusTitle"
|
|
245
247
|
>
|
|
246
248
|
<span class="statusbar__icon">{{ eventCount ? (eventErrors.length ? '⚠' : '☰') : '✓' }}</span>
|
|
247
|
-
<span>{{ eventCount ? eventCount + '
|
|
249
|
+
<span>{{ eventCount ? eventCount + ' 条事件记录' : '无事件记录' }}</span>
|
|
248
250
|
</button>
|
|
249
|
-
<!-- 本地开发服务连接状态:连接图标 + 状态文案,颜色与右上角「连接」按钮实时同步(复用 devSvcClass / devSvcTitle / devSvcLabel
|
|
250
|
-
|
|
251
|
+
<!-- 本地开发服务连接状态:连接图标 + 状态文案,颜色与右上角「连接」按钮实时同步(复用 devSvcClass / devSvcTitle / devSvcLabel)。
|
|
252
|
+
可点击:弹出 / 收起底部「服务日志」面板(实时镜像所连 cardbuddy 终端输出),展开态用 is-open 底线标识。 -->
|
|
253
|
+
<button
|
|
251
254
|
class="statusbar__item statusbar__conn"
|
|
252
|
-
:class="devSvcClass"
|
|
253
|
-
|
|
255
|
+
:class="[devSvcClass, { 'is-open': errorPanel === 'svclog' }]"
|
|
256
|
+
type="button"
|
|
257
|
+
:title="devSvcTitle + '(点击查看服务日志)'"
|
|
258
|
+
@click="toggleErrorPanel('svclog')"
|
|
254
259
|
>
|
|
255
260
|
<svg class="statusbar__conn-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>
|
|
256
261
|
<span>{{ devSvcLabel }}</span>
|
|
257
|
-
</
|
|
262
|
+
</button>
|
|
258
263
|
<span class="statusbar__spacer"></span>
|
|
259
264
|
<span class="statusbar__item statusbar__item--right">{{ cardId }}</span>
|
|
260
265
|
<span class="statusbar__item statusbar__item--right">{{ cardCount }} 张卡片</span>
|
|
@@ -266,8 +271,13 @@
|
|
|
266
271
|
<!-- 顶缘拖拽条:与竖向分隔条同规格(命中 6px / 高亮 3px),仅分割边缘触发 -->
|
|
267
272
|
<div class="panelhead__edge" :class="{ 'is-dragging': panelDragging }" title="拖拽调整面板高度" @mousedown.prevent="startResize"></div>
|
|
268
273
|
<div class="errpanel__head">
|
|
269
|
-
<span class="errpanel__title">{{ errorPanel === 'event' ? '
|
|
270
|
-
<!--
|
|
274
|
+
<span class="errpanel__title">{{ errorPanel === 'svclog' ? '服务日志' : errorPanel === 'event' ? '事件记录' : '绑定错误记录' }}</span>
|
|
275
|
+
<!-- 服务日志:标题后给一枚读数(行数 + 实时态),与事件面板 tab 同一位置语言 -->
|
|
276
|
+
<span v-if="errorPanel === 'svclog'" class="svclog__meta">
|
|
277
|
+
{{ svcLogs.length }} 行<span v-if="svcLogDropped"> · 已滚动淘汰 {{ svcLogDropped }}</span>
|
|
278
|
+
<i class="svclog__dot" :class="svcLogLive ? 'is-live' : 'is-off'"></i>{{ svcLogLive ? '实时' : '未连接' }}
|
|
279
|
+
</span>
|
|
280
|
+
<!-- 事件面板:日志 / 错误 tab 并入头部单行(一次只看一类);变量绑定面板无 tab -->
|
|
271
281
|
<div v-if="errorPanel === 'event'" class="errpanel__tabs">
|
|
272
282
|
<button
|
|
273
283
|
type="button"
|
|
@@ -299,14 +309,75 @@
|
|
|
299
309
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none"
|
|
300
310
|
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#icon-trash" /></svg>
|
|
301
311
|
</button>
|
|
312
|
+
<button
|
|
313
|
+
v-if="errorPanel === 'svclog'"
|
|
314
|
+
class="errpanel__iconbtn"
|
|
315
|
+
:class="{ 'is-done': svcCopied }"
|
|
316
|
+
type="button"
|
|
317
|
+
:disabled="!svcLogs.length"
|
|
318
|
+
:title="svcCopied ? '已复制' : '复制全部日志(纯文本,去 ANSI)'"
|
|
319
|
+
@click="copySvcLogs"
|
|
320
|
+
>
|
|
321
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2" /><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" /></svg>
|
|
322
|
+
</button>
|
|
323
|
+
<button
|
|
324
|
+
v-if="errorPanel === 'svclog'"
|
|
325
|
+
class="errpanel__iconbtn svclog__follow"
|
|
326
|
+
:class="{ 'is-on': svcLogFollow }"
|
|
327
|
+
type="button"
|
|
328
|
+
:title="svcLogFollow ? '跟随最新输出(点击暂停)' : '已暂停跟随(点击回到最新)'"
|
|
329
|
+
@click="toggleSvcFollow"
|
|
330
|
+
>
|
|
331
|
+
<!-- 下箭头图标:跟随最新输出 -->
|
|
332
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19" /><polyline points="19 12 12 19 5 12" /></svg>
|
|
333
|
+
</button>
|
|
334
|
+
<button
|
|
335
|
+
v-if="errorPanel === 'svclog'"
|
|
336
|
+
class="errpanel__iconbtn errpanel__clear"
|
|
337
|
+
type="button"
|
|
338
|
+
:disabled="!svcLogs.length"
|
|
339
|
+
title="清空面板(不影响服务端缓冲)"
|
|
340
|
+
@click="clearSvcLogs"
|
|
341
|
+
>
|
|
342
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none"
|
|
343
|
+
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><use href="#icon-trash" /></svg>
|
|
344
|
+
</button>
|
|
302
345
|
<button class="errpanel__iconbtn errpanel__close" type="button" title="关闭" @click="errorPanel = ''">
|
|
303
346
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" 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>
|
|
304
347
|
</button>
|
|
305
348
|
</span>
|
|
306
349
|
</div>
|
|
307
|
-
<div
|
|
350
|
+
<div
|
|
351
|
+
class="errpanel__body"
|
|
352
|
+
:class="{ 'errpanel__body--term': errorPanel === 'svclog' && svcLogs.length }"
|
|
353
|
+
ref="errBody"
|
|
354
|
+
@scroll.passive="onErrBodyScroll"
|
|
355
|
+
>
|
|
356
|
+
<!-- 服务日志面板:本地服务(cardbuddy)终端输出的实时镜像。
|
|
357
|
+
服务端按行镜像 stdout/stderr 并保留原始 ANSI,这里经 preview/ansi.js 还原配色,
|
|
358
|
+
因此面板固定深色终端底(chalk 调色板取自深色令牌,浅底不可读)。 -->
|
|
359
|
+
<template v-if="errorPanel === 'svclog'">
|
|
360
|
+
<div v-if="!svcLogs.length" class="empty-state">
|
|
361
|
+
<span class="empty-state__badge" aria-hidden="true">
|
|
362
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
363
|
+
<polyline points="4 17 10 11 4 5" />
|
|
364
|
+
<line x1="12" y1="19" x2="20" y2="19" />
|
|
365
|
+
</svg>
|
|
366
|
+
</span>
|
|
367
|
+
<p class="debug__empty">{{ svcEmptyText }}</p>
|
|
368
|
+
<p class="empty-state__hint">{{ svcEmptyHint }}</p>
|
|
369
|
+
</div>
|
|
370
|
+
<div v-else class="svclog">
|
|
371
|
+
<div
|
|
372
|
+
v-for="row in svcLogs"
|
|
373
|
+
:key="row.seq || row.t"
|
|
374
|
+
class="svclog__line"
|
|
375
|
+
:class="'svclog__line--' + row.level"
|
|
376
|
+
><span class="svclog__text" v-html="row.html"></span></div>
|
|
377
|
+
</div>
|
|
378
|
+
</template>
|
|
308
379
|
<!-- 事件面板:按当前 tab 只渲染日志或错误一类 -->
|
|
309
|
-
<template v-if="errorPanel === 'event'">
|
|
380
|
+
<template v-else-if="errorPanel === 'event'">
|
|
310
381
|
<template v-if="eventTab === 'log'">
|
|
311
382
|
<!-- 空状态占位:与 API 调用面板同款(虚线徽章 + 线性图标 + 文案 + 弱提示) -->
|
|
312
383
|
<div v-if="!logs.length" class="empty-state">
|
|
@@ -353,7 +424,7 @@
|
|
|
353
424
|
</ul>
|
|
354
425
|
</template>
|
|
355
426
|
</template>
|
|
356
|
-
<!--
|
|
427
|
+
<!-- 变量绑定面板:仅错误列表 -->
|
|
357
428
|
<template v-else>
|
|
358
429
|
<div v-if="!panelErrors.length" class="empty-state">
|
|
359
430
|
<span class="empty-state__badge" aria-hidden="true">
|
|
@@ -394,7 +465,38 @@ import { nodeClipboard } from '@/runtime/clipboard-node.js'
|
|
|
394
465
|
import { CodeJar } from 'codejar'
|
|
395
466
|
import Prism from 'prismjs'
|
|
396
467
|
import 'prismjs/components/prism-json'
|
|
397
|
-
import { apiFetch } from '@/api.js'
|
|
468
|
+
import { apiFetch, subscribeServiceLog, fetchServiceLogs } from '@/api.js'
|
|
469
|
+
// 服务日志面板:终端 ANSI(SGR)→ HTML,1:1 还原 cardbuddy 的 chalk 配色
|
|
470
|
+
import { ansiToHtml, stripAnsi } from '@/ansi.js'
|
|
471
|
+
|
|
472
|
+
/** 服务日志在前端保留的最大行数(与服务端环形缓冲同量级,超出裁剪最旧的)。 */
|
|
473
|
+
const SVC_MAX_LINES = 800
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* 行级别判定:只认 logger.js 的行首标签(HH:MM:SS.mmm INFO/OK/WARN/ERR),
|
|
477
|
+
* 其余输出(console.* / 第三方 / vite)统一 info,避免把卡片内容里的「错误」字样误判。
|
|
478
|
+
*/
|
|
479
|
+
function svcLevelOf(plain) {
|
|
480
|
+
const m = /^\d{2}:\d{2}:\d{2}\.\d{3}\s+(INFO|OK\s|WARN|ERR)\b/.exec(plain)
|
|
481
|
+
if (m) {
|
|
482
|
+
const tag = m[1].trim()
|
|
483
|
+
return tag === 'ERR' ? 'error' : tag === 'WARN' ? 'warn' : tag === 'OK' ? 'ok' : 'info'
|
|
484
|
+
}
|
|
485
|
+
return 'info'
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** 服务端日志行 { seq, t, text } → 面板行(ANSI 转 HTML 只做一次,避免每次重渲染重复解析)。 */
|
|
489
|
+
function toSvcRow(line) {
|
|
490
|
+
const text = String(line && line.text != null ? line.text : '')
|
|
491
|
+
const plain = stripAnsi(text)
|
|
492
|
+
return {
|
|
493
|
+
seq: Number(line && line.seq) || 0,
|
|
494
|
+
t: Number(line && line.t) || 0,
|
|
495
|
+
level: svcLevelOf(plain),
|
|
496
|
+
plain,
|
|
497
|
+
html: ansiToHtml(text),
|
|
498
|
+
}
|
|
499
|
+
}
|
|
398
500
|
|
|
399
501
|
// copyText 已抽到 @/runtime/clipboard.js,供 DebugPanel 与 NodeInspector 共用
|
|
400
502
|
|
|
@@ -437,6 +539,9 @@ export default {
|
|
|
437
539
|
devSvcClass: { type: String, default: 'app__btn--warn' },
|
|
438
540
|
devSvcTitle: { type: String, default: '' },
|
|
439
541
|
devSvcLabel: { type: String, default: '' },
|
|
542
|
+
// 本地服务连接态('' | unconfigured | connecting | open | closed):
|
|
543
|
+
// 服务日志面板据此给出空态文案(未配置 / 连接中 / 已断开)。
|
|
544
|
+
devSvcConn: { type: String, default: '' },
|
|
440
545
|
// 纯净截图模式(?chrome=off):禁用状态栏 Teleport,使其留在本面板内随面板隐藏
|
|
441
546
|
chromeOff: { type: Boolean, default: false },
|
|
442
547
|
},
|
|
@@ -444,13 +549,26 @@ export default {
|
|
|
444
549
|
return {
|
|
445
550
|
copied: false,
|
|
446
551
|
copyTimer: null,
|
|
447
|
-
//
|
|
552
|
+
// 底部面板模式:''(收起) | 'binding'(变量绑定错误) | 'event'(事件日志 / 执行错误)
|
|
448
553
|
errorPanel: '',
|
|
449
554
|
// 事件面板内部 tab:'log'(主动日志) | 'err'(执行错误),打开面板时按是否有错误自动定位
|
|
450
555
|
eventTab: 'log',
|
|
556
|
+
// 服务日志面板(errorPanel === 'svclog'):本地服务终端输出的实时镜像
|
|
557
|
+
svcLogs: [],
|
|
558
|
+
// 已收到的最大 seq:断线重连后用 GET /__logs?since= 精确补齐缺口
|
|
559
|
+
svcLogSeq: 0,
|
|
560
|
+
// 清空面板时记录的水位线:此后取历史不会把已清掉的行再拉回来
|
|
561
|
+
svcLogFloor: 0,
|
|
562
|
+
svcLogLive: false,
|
|
563
|
+
// 自动跟随最新输出;用户向上翻阅即暂停,回到底部自动恢复
|
|
564
|
+
svcLogFollow: true,
|
|
565
|
+
svcLogDropped: 0,
|
|
566
|
+
svcLogLoading: false,
|
|
567
|
+
svcLogErr: '',
|
|
568
|
+
svcCopied: false,
|
|
451
569
|
// 运行态面板:默认收起;点击状态栏「运行态」指示器切换
|
|
452
570
|
statePanelOpen: false,
|
|
453
|
-
// 运行态 /
|
|
571
|
+
// 运行态 / 变量绑定 / 事件 三个底部面板互斥,共用同一高度变量(px);
|
|
454
572
|
// 可被顶部抓手拖拽调整,并持久化到 localStorage(cardbuddy:debug-panel-height)
|
|
455
573
|
panelH: this.loadPanelH(),
|
|
456
574
|
// 底部面板头部拖拽进行中:头部高亮与竖向分隔条 .is-dragging 同一语言
|
|
@@ -476,6 +594,24 @@ export default {
|
|
|
476
594
|
}
|
|
477
595
|
},
|
|
478
596
|
computed: {
|
|
597
|
+
/** 服务日志空态标题:按连接态区分「没配 / 没连上 / 连上了但还没输出」 */
|
|
598
|
+
svcEmptyText() {
|
|
599
|
+
if (this.devSvcConn === 'unconfigured') return '未配置本地开发服务'
|
|
600
|
+
if (this.devSvcConn === 'open') return '暂无服务日志'
|
|
601
|
+
return '本地服务未连接'
|
|
602
|
+
},
|
|
603
|
+
svcEmptyHint() {
|
|
604
|
+
if (this.svcLogErr) {
|
|
605
|
+
return '取历史失败:' + this.svcLogErr + '(本地服务需升级到支持 /__logs 的 cardbuddy 版本)'
|
|
606
|
+
}
|
|
607
|
+
if (this.devSvcConn === 'unconfigured') {
|
|
608
|
+
return '在顶栏「连接」中配置本地开发服务地址后,这里会实时镜像该服务终端的彩色输出'
|
|
609
|
+
}
|
|
610
|
+
if (this.devSvcConn === 'open') {
|
|
611
|
+
return 'cardbuddy 终端的输出(含配色)会实时镜像到这里;试试同步、保存、校验或截图'
|
|
612
|
+
}
|
|
613
|
+
return '连接恢复后自动继续;服务端保留最近 ' + SVC_MAX_LINES + ' 行历史,重连不丢'
|
|
614
|
+
},
|
|
479
615
|
// 依赖 revision,使 setData 后数据绑定实时刷新
|
|
480
616
|
scopeText() {
|
|
481
617
|
void this.rt.revision
|
|
@@ -513,8 +649,8 @@ export default {
|
|
|
513
649
|
return 'is-ok'
|
|
514
650
|
},
|
|
515
651
|
eventStatusTitle() {
|
|
516
|
-
if (this.eventErrors.length) return '
|
|
517
|
-
if (this.logs.length) return '
|
|
652
|
+
if (this.eventErrors.length) return '查看事件(含错误)'
|
|
653
|
+
if (this.logs.length) return '查看事件(含日志)'
|
|
518
654
|
return '无事件记录'
|
|
519
655
|
},
|
|
520
656
|
panelErrors() {
|
|
@@ -544,6 +680,12 @@ export default {
|
|
|
544
680
|
},
|
|
545
681
|
},
|
|
546
682
|
watch: {
|
|
683
|
+
// 服务日志采用懒订阅:面板打开才建 SSE(浏览器对同源并发连接有限,不与
|
|
684
|
+
// cards-changed / code-bridge 常驻连接抢位),关闭即退订,历史由服务端缓冲补。
|
|
685
|
+
errorPanel(mode) {
|
|
686
|
+
if (mode === 'svclog') this.startSvcLog()
|
|
687
|
+
else this.stopSvcLog()
|
|
688
|
+
},
|
|
547
689
|
// 切换卡片时退出 Raw 模式,避免编辑内容与新卡片数据不一致;
|
|
548
690
|
// 同时关闭属性面板,避免展示旧卡片节点;并拉取导入来源元数据。
|
|
549
691
|
cardId: {
|
|
@@ -587,9 +729,10 @@ export default {
|
|
|
587
729
|
// DebugPanel 一般常驻,但保险起见卸载时销毁 CodeJar、清理可能残留的拖拽监听
|
|
588
730
|
this.destroyJar()
|
|
589
731
|
this.cancelResize()
|
|
732
|
+
this.stopSvcLog()
|
|
590
733
|
},
|
|
591
734
|
methods: {
|
|
592
|
-
// 三个底部面板互斥:运行态 /
|
|
735
|
+
// 三个底部面板互斥:运行态 / 变量绑定 / 事件 不会同时出现。
|
|
593
736
|
// 点开运行态即关闭错误/事件面板。
|
|
594
737
|
toggleStatePanel() {
|
|
595
738
|
this.statePanelOpen = !this.statePanelOpen
|
|
@@ -606,7 +749,7 @@ export default {
|
|
|
606
749
|
}
|
|
607
750
|
},
|
|
608
751
|
/**
|
|
609
|
-
* 拖拽调整底部面板(运行态 /
|
|
752
|
+
* 拖拽调整底部面板(运行态 / 变量绑定 / 事件)高度:挂在面板头部(已有 .statepanel__head /
|
|
610
753
|
* .errpanel__head,无专门拖拽节点),与预览区宽度调节同一套 mousedown 机制。
|
|
611
754
|
* 在头部按下后监听 document 的 mousemove / mouseup;向上拖(clientY 减小)增大面板高度,
|
|
612
755
|
* 向下拖减小,符合底部停靠面板的直觉。高度夹在 [120px, 视窗 60%],并实时同步到两个面板
|
|
@@ -804,6 +947,107 @@ export default {
|
|
|
804
947
|
clearEventLogs() {
|
|
805
948
|
this.rt.clearEventLogs()
|
|
806
949
|
},
|
|
950
|
+
/** 打开服务日志:先补历史(含上次清空后的水位线),再建立 SSE 实时订阅。 */
|
|
951
|
+
async startSvcLog() {
|
|
952
|
+
this.svcLogFollow = true
|
|
953
|
+
await this.pullSvcLogHistory(0)
|
|
954
|
+
if (this._svcUnsub) return
|
|
955
|
+
this._svcUnsub = subscribeServiceLog(
|
|
956
|
+
(payload) => this.appendSvcLogs(payload && payload.lines),
|
|
957
|
+
(state) => {
|
|
958
|
+
this.svcLogLive = state === 'open'
|
|
959
|
+
// 断线重连成功后按 seq 补漏:缓冲仍在服务端,不会丢行也不会重复
|
|
960
|
+
if (state === 'open' && this.svcLogSeq) this.pullSvcLogHistory(this.svcLogSeq)
|
|
961
|
+
}
|
|
962
|
+
)
|
|
963
|
+
},
|
|
964
|
+
/** 关闭服务日志:退订 SSE(面板数据保留,下次打开按水位线增量补齐)。 */
|
|
965
|
+
stopSvcLog() {
|
|
966
|
+
if (this._svcUnsub) {
|
|
967
|
+
this._svcUnsub()
|
|
968
|
+
this._svcUnsub = null
|
|
969
|
+
}
|
|
970
|
+
this.svcLogLive = false
|
|
971
|
+
},
|
|
972
|
+
/**
|
|
973
|
+
* 拉取服务日志历史。
|
|
974
|
+
* @param {number} since 0 = 全量(取最新 SVC_MAX_LINES 行);>0 = 只取 seq 大于它的行
|
|
975
|
+
*/
|
|
976
|
+
async pullSvcLogHistory(since) {
|
|
977
|
+
if (this.svcLogLoading) return
|
|
978
|
+
this.svcLogLoading = true
|
|
979
|
+
const from = Math.max(Number(since) || 0, this.svcLogFloor)
|
|
980
|
+
try {
|
|
981
|
+
const snap = await fetchServiceLogs({ limit: SVC_MAX_LINES, since: from })
|
|
982
|
+
this.svcLogErr = ''
|
|
983
|
+
if (snap && snap.enabled === false) {
|
|
984
|
+
this.svcLogErr = '服务端已关闭日志流(CARDBUDDY_STREAM_LOGS=0)'
|
|
985
|
+
} else {
|
|
986
|
+
this.svcLogDropped = (snap && snap.dropped) || 0
|
|
987
|
+
if (snap && snap.seq) this.svcLogSeq = Math.max(this.svcLogSeq, snap.seq)
|
|
988
|
+
const rows = ((snap && snap.lines) || []).map(toSvcRow)
|
|
989
|
+
// from > 0 是断线补漏:走同一条去重/裁剪/跟随逻辑,避免与实时流重叠
|
|
990
|
+
if (from) this.appendSvcLogs(rows)
|
|
991
|
+
else this.svcLogs = rows
|
|
992
|
+
this.scrollSvcToBottom(true)
|
|
993
|
+
}
|
|
994
|
+
} catch (e) {
|
|
995
|
+
this.svcLogErr = (e && e.message) || String(e)
|
|
996
|
+
} finally {
|
|
997
|
+
this.svcLogLoading = false
|
|
998
|
+
}
|
|
999
|
+
},
|
|
1000
|
+
/** 追加实时行:按 seq 去重(历史与实时流可能重叠)、裁剪上限、按需跟随滚动。 */
|
|
1001
|
+
appendSvcLogs(lines) {
|
|
1002
|
+
const list = Array.isArray(lines) ? lines : []
|
|
1003
|
+
if (!list.length) return
|
|
1004
|
+
const rows = []
|
|
1005
|
+
for (const l of list) {
|
|
1006
|
+
// 既接受服务端原始行 { seq, t, text },也接受已转换好的面板行(含 html)
|
|
1007
|
+
const row = l && l.html != null ? l : toSvcRow(l)
|
|
1008
|
+
const seq = Number(row.seq) || 0
|
|
1009
|
+
if (seq && seq <= this.svcLogSeq) continue
|
|
1010
|
+
if (seq) this.svcLogSeq = seq
|
|
1011
|
+
rows.push(row)
|
|
1012
|
+
}
|
|
1013
|
+
if (!rows.length) return
|
|
1014
|
+
const next = this.svcLogs.concat(rows)
|
|
1015
|
+
this.svcLogs = next.length > SVC_MAX_LINES ? next.slice(next.length - SVC_MAX_LINES) : next
|
|
1016
|
+
if (this.svcLogFollow) this.scrollSvcToBottom()
|
|
1017
|
+
},
|
|
1018
|
+
clearSvcLogs() {
|
|
1019
|
+
this.svcLogFloor = this.svcLogSeq
|
|
1020
|
+
this.svcLogs = []
|
|
1021
|
+
this.svcLogDropped = 0
|
|
1022
|
+
},
|
|
1023
|
+
/** 复制全部日志为纯文本(去掉 ANSI),便于贴到工单 / IM 里排障。 */
|
|
1024
|
+
async copySvcLogs() {
|
|
1025
|
+
const ok = await copyText(this.svcLogs.map((r) => r.plain).join('\n'))
|
|
1026
|
+
if (!ok) return
|
|
1027
|
+
this.svcCopied = true
|
|
1028
|
+
if (this._svcCopiedTimer) clearTimeout(this._svcCopiedTimer)
|
|
1029
|
+
this._svcCopiedTimer = setTimeout(() => {
|
|
1030
|
+
this.svcCopied = false
|
|
1031
|
+
}, 1200)
|
|
1032
|
+
},
|
|
1033
|
+
toggleSvcFollow() {
|
|
1034
|
+
this.svcLogFollow = !this.svcLogFollow
|
|
1035
|
+
if (this.svcLogFollow) this.scrollSvcToBottom(true)
|
|
1036
|
+
},
|
|
1037
|
+
scrollSvcToBottom(force) {
|
|
1038
|
+
if (!force && !this.svcLogFollow) return
|
|
1039
|
+
this.$nextTick(() => {
|
|
1040
|
+
const el = this.$refs.errBody
|
|
1041
|
+
if (el) el.scrollTop = el.scrollHeight
|
|
1042
|
+
})
|
|
1043
|
+
},
|
|
1044
|
+
/** 面板体滚动:距底 24px 内视为「在底部」并恢复跟随,否则暂停(用户正在往上翻)。 */
|
|
1045
|
+
onErrBodyScroll() {
|
|
1046
|
+
if (this.errorPanel !== 'svclog') return
|
|
1047
|
+
const el = this.$refs.errBody
|
|
1048
|
+
if (!el) return
|
|
1049
|
+
this.svcLogFollow = el.scrollHeight - el.scrollTop - el.clientHeight < 24
|
|
1050
|
+
},
|
|
807
1051
|
/** 日志 / 错误行首时间戳:HH:MM:SS 等宽对齐,单行多带一份定位信息 */
|
|
808
1052
|
fmtTime(t) {
|
|
809
1053
|
if (!t) return '--:--:--'
|
|
@@ -58,9 +58,11 @@
|
|
|
58
58
|
</svg>
|
|
59
59
|
</button>
|
|
60
60
|
<div v-if="helpOpen" class="ee-help__mask" @click="helpOpen = false"></div>
|
|
61
|
+
<Transition name="pop">
|
|
61
62
|
<div v-if="helpOpen" class="ee-help__pop" @click.stop>
|
|
62
63
|
<p class="ee-help__text">{{ helpText }}</p>
|
|
63
64
|
</div>
|
|
65
|
+
</Transition>
|
|
64
66
|
</aside>
|
|
65
67
|
|
|
66
68
|
<section class="ee-detail">
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
|
+
<transition name="modal">
|
|
2
3
|
<div v-if="visible" class="rp-mask" @click.self="$emit('close')">
|
|
3
|
-
<div class="rp-card" role="dialog" aria-label="远端预览二维码">
|
|
4
|
+
<div class="rp-card modal-card" role="dialog" aria-label="远端预览二维码">
|
|
4
5
|
<div class="rp-head">
|
|
5
6
|
<span class="rp-title">远端预览(真机扫码)</span>
|
|
6
7
|
<button class="rp-x" type="button" @click="$emit('close')" aria-label="关闭">×</button>
|
|
@@ -26,6 +27,7 @@
|
|
|
26
27
|
</div>
|
|
27
28
|
</div>
|
|
28
29
|
</div>
|
|
30
|
+
</transition>
|
|
29
31
|
</template>
|
|
30
32
|
|
|
31
33
|
<script>
|
|
@@ -172,6 +172,7 @@
|
|
|
172
172
|
<line x1="12" y1="17" x2="12" y2="17.01" />
|
|
173
173
|
</svg>
|
|
174
174
|
</button>
|
|
175
|
+
<Transition name="pop">
|
|
175
176
|
<div v-if="helpOpen" class="st__help-pop" @click.stop>
|
|
176
177
|
<div class="st__help-pop-head">
|
|
177
178
|
<span class="st__help-pop-title">快捷键</span>
|
|
@@ -189,6 +190,7 @@
|
|
|
189
190
|
</li>
|
|
190
191
|
</ul>
|
|
191
192
|
</div>
|
|
193
|
+
</Transition>
|
|
192
194
|
</div>
|
|
193
195
|
</div>
|
|
194
196
|
</template>
|