@whyshu/argent-erp 0.0.0-stage → 0.2.2
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 +179 -2
- package/cordis.patch.yml +6 -0
- package/dist/client.js +614 -0
- package/package.json +55 -4
- package/presets/argent-office.patch.yml +78 -0
- package/src/data.mjs +297 -0
- package/src/index.js +123 -0
- package/src/office-guard.js +76 -0
- package/src/vendor/account-token.mjs +41 -0
- package/src/vendor/redact.mjs +37 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* argent-erp · 客户端(dsh browser bundle 格式,**手写维护**)
|
|
3
|
+
*
|
|
4
|
+
* 形态:一个 `conversation.view` tab(对话顶部的 Chat / 轨迹 / **ERP** 切换项),
|
|
5
|
+
* 渲染 ERP 的只读视图:办公室现场 / 待办 / 时间线 / 货件 / 聊一聊。
|
|
6
|
+
*
|
|
7
|
+
* 为什么不是侧栏面板:会话视图与"正在做的那一单"同屏——看 ERP 不用离开会话。
|
|
8
|
+
*
|
|
9
|
+
* 数据全部来自宿主端点 `/api/argent-erp/*`(token 在宿主侧,浏览器拿不到)。
|
|
10
|
+
* ⛔ 不做写操作;重编辑一律跳 ERP web。
|
|
11
|
+
*
|
|
12
|
+
* id 必须是**整包名**(与 package.json#name 一致),否则客户端拒绝加载。
|
|
13
|
+
*/
|
|
14
|
+
window.__ModuleLoader__.load({
|
|
15
|
+
id: "@whyshu/argent-erp",
|
|
16
|
+
factory: (require) => {
|
|
17
|
+
const React = require("react")
|
|
18
|
+
const h = React.createElement
|
|
19
|
+
const module = { exports: {} }
|
|
20
|
+
const exports = module.exports
|
|
21
|
+
|
|
22
|
+
const NS = "argent-erp"
|
|
23
|
+
const API = "/api/argent-erp"
|
|
24
|
+
const WEB = "https://whyshu.com/admin/argent/"
|
|
25
|
+
|
|
26
|
+
/* ───────────────────────── 样式(只注入一次;全部用 dsh 主题 token) ───────────────────────── */
|
|
27
|
+
|
|
28
|
+
const CSS = `
|
|
29
|
+
.aerp-root{display:flex;flex-direction:column;height:100%;min-height:0;color:var(--dsw-alias-label-primary);
|
|
30
|
+
font:14px/1.55 var(--dsw-font-family,system-ui,-apple-system,"PingFang SC","Microsoft YaHei",sans-serif)}
|
|
31
|
+
.aerp-head{display:flex;align-items:center;gap:8px;padding:10px 18px 0;flex-wrap:wrap}
|
|
32
|
+
.aerp-title{font-weight:600;font-size:14px;letter-spacing:.2px}
|
|
33
|
+
.aerp-dim{color:var(--dsw-alias-label-secondary);font-size:12px}
|
|
34
|
+
.aerp-spacer{flex:1 1 auto}
|
|
35
|
+
.aerp-btn{border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-primary);
|
|
36
|
+
border-radius:8px;padding:4px 10px;font:12px/1.4 inherit;cursor:pointer}
|
|
37
|
+
.aerp-btn:hover{background:rgba(127,127,127,.10)}
|
|
38
|
+
.aerp-btn:disabled{opacity:.5;cursor:default}
|
|
39
|
+
.aerp-btn.primary{border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-brand-primary)}
|
|
40
|
+
.aerp-sel{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);
|
|
41
|
+
border-radius:8px;padding:4px 8px;font:12px/1.4 inherit}
|
|
42
|
+
.aerp-tabs{display:flex;gap:4px;padding:10px 18px 0;flex-wrap:wrap}
|
|
43
|
+
.aerp-tab{border:0;background:transparent;color:var(--dsw-alias-label-secondary);border-radius:8px;
|
|
44
|
+
padding:5px 11px;font:13px/1.4 inherit;cursor:pointer}
|
|
45
|
+
.aerp-tab:hover{background:rgba(127,127,127,.10);color:var(--dsw-alias-label-primary)}
|
|
46
|
+
.aerp-tab[data-on="1"]{background:rgba(127,127,127,.16);color:var(--dsw-alias-label-primary);font-weight:600}
|
|
47
|
+
.aerp-body{flex:1 1 auto;min-height:0;overflow:auto;padding:12px 18px 28px;display:flex;flex-direction:column;gap:12px}
|
|
48
|
+
.aerp-card{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);border-radius:12px;padding:12px 14px}
|
|
49
|
+
.aerp-card h4{margin:0 0 10px;font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary)}
|
|
50
|
+
.aerp-kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(112px,1fr));gap:10px}
|
|
51
|
+
.aerp-kpi{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);border-radius:12px;padding:10px 12px}
|
|
52
|
+
.aerp-kpi b{display:block;font-size:22px;line-height:1.15;font-weight:600}
|
|
53
|
+
.aerp-kpi span{display:block;font-size:11px;color:var(--dsw-alias-label-secondary);margin-top:2px}
|
|
54
|
+
.aerp-list{display:flex;flex-direction:column;gap:6px}
|
|
55
|
+
.aerp-row{display:flex;align-items:flex-start;gap:10px;padding:8px 10px;border-radius:10px;cursor:pointer;
|
|
56
|
+
border:1px solid transparent;background:rgba(127,127,127,.06)}
|
|
57
|
+
.aerp-row:hover{background:rgba(127,127,127,.13);border-color:var(--dsw-alias-border-l1)}
|
|
58
|
+
.aerp-row .t{font-weight:600;font-size:13px}
|
|
59
|
+
.aerp-row .s{font-size:11px;color:var(--dsw-alias-label-secondary);margin-top:2px}
|
|
60
|
+
.aerp-row .r{margin-left:auto;font-size:11px;color:var(--dsw-alias-label-secondary);white-space:nowrap}
|
|
61
|
+
.aerp-dot{width:7px;height:7px;border-radius:50%;margin-top:6px;flex:0 0 auto;background:var(--dsw-alias-state-idle-primary)}
|
|
62
|
+
.aerp-dot[data-s="in_progress"],.aerp-dot[data-s="working"]{background:var(--dsw-alias-state-success-primary)}
|
|
63
|
+
.aerp-dot[data-s="pending"]{background:var(--dsw-alias-state-warn-primary)}
|
|
64
|
+
.aerp-dot[data-s="blocked"],.aerp-dot[data-s="failed"]{background:var(--dsw-alias-state-error-primary)}
|
|
65
|
+
.aerp-dot[data-s="done"],.aerp-dot[data-s="completed"]{background:var(--dsw-alias-state-idle-primary)}
|
|
66
|
+
.aerp-people{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px}
|
|
67
|
+
.aerp-person{display:flex;gap:8px;align-items:flex-start;padding:8px 10px;border-radius:10px;background:rgba(127,127,127,.06)}
|
|
68
|
+
.aerp-person .nm{font-weight:600;font-size:13px}
|
|
69
|
+
.aerp-person .rl{font-size:11px;color:var(--dsw-alias-label-secondary)}
|
|
70
|
+
.aerp-person .cu{font-size:11px;color:var(--dsw-alias-label-secondary);margin-top:3px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
71
|
+
.aerp-empty,.aerp-loading{padding:26px 0;text-align:center;color:var(--dsw-alias-label-secondary);font-size:12px}
|
|
72
|
+
.aerp-err{margin:0;padding:12px 14px;border-radius:12px;border:1px solid var(--dsw-alias-state-error-primary);
|
|
73
|
+
background:rgba(248,113,113,.10);color:var(--dsw-alias-label-primary);font-size:13px;white-space:pre-wrap}
|
|
74
|
+
.aerp-note{font-size:12px;color:var(--dsw-alias-label-secondary);background:rgba(127,127,127,.08);
|
|
75
|
+
border-radius:10px;padding:10px 12px}
|
|
76
|
+
.aerp-note b{color:var(--dsw-alias-label-primary)}
|
|
77
|
+
.aerp-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}
|
|
78
|
+
.aerp-link{color:var(--dsw-alias-brand-primary);cursor:pointer;text-decoration:none;font-size:12px}
|
|
79
|
+
.aerp-link:hover{text-decoration:underline}
|
|
80
|
+
.aerp-detail-head{display:flex;align-items:center;gap:8px;margin-bottom:2px}
|
|
81
|
+
.aerp-nodes{display:flex;flex-direction:column;gap:0;margin-top:6px}
|
|
82
|
+
.aerp-node{display:flex;gap:10px;padding:8px 0;border-bottom:1px dashed var(--dsw-alias-border-l1)}
|
|
83
|
+
.aerp-node:last-child{border-bottom:0}
|
|
84
|
+
.aerp-node .k{font-size:13px;font-weight:600}
|
|
85
|
+
.aerp-node .m{font-size:11px;color:var(--dsw-alias-label-secondary)}
|
|
86
|
+
.aerp-node .f{font-size:11px;color:var(--dsw-alias-label-secondary);margin-top:3px}
|
|
87
|
+
.aerp-chip{display:inline-block;border-radius:999px;padding:1px 8px;font-size:11px;
|
|
88
|
+
background:rgba(127,127,127,.14);color:var(--dsw-alias-label-secondary)}
|
|
89
|
+
.aerp-chip[data-on="1"]{background:rgba(52,211,153,.16);color:var(--dsw-alias-state-success-primary)}
|
|
90
|
+
.aerp-search{display:flex;gap:8px;align-items:center}
|
|
91
|
+
.aerp-search input{flex:1 1 auto;min-width:0;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;
|
|
92
|
+
background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);padding:6px 10px;font:13px/1.4 inherit}
|
|
93
|
+
`
|
|
94
|
+
|
|
95
|
+
function injectCss() {
|
|
96
|
+
try {
|
|
97
|
+
if (typeof document === "undefined" || !document.head) return
|
|
98
|
+
if (document.getElementById("argent-erp-css")) return
|
|
99
|
+
const style = document.createElement("style")
|
|
100
|
+
style.id = "argent-erp-css"
|
|
101
|
+
style.textContent = CSS
|
|
102
|
+
document.head.appendChild(style)
|
|
103
|
+
} catch { /* noop */ }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* ───────────────────────── 纯函数(可单测;见 exports.__test__) ───────────────────────── */
|
|
107
|
+
|
|
108
|
+
const pad2 = (n) => (n < 10 ? "0" + n : String(n))
|
|
109
|
+
|
|
110
|
+
/** 24 小时制(全站口径):2026-11-07 14:05 */
|
|
111
|
+
function fmtDateTime(iso) {
|
|
112
|
+
if (!iso) return ""
|
|
113
|
+
const d = new Date(iso)
|
|
114
|
+
if (Number.isNaN(d.getTime())) return String(iso)
|
|
115
|
+
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}`
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** 相对时间:刚刚 / 5 分钟前 / 3 小时前 / 2 天前 */
|
|
119
|
+
function fmtRelative(iso, now = Date.now()) {
|
|
120
|
+
if (!iso) return ""
|
|
121
|
+
const t = Date.parse(iso)
|
|
122
|
+
if (Number.isNaN(t)) return ""
|
|
123
|
+
const diff = Math.max(0, now - t)
|
|
124
|
+
const min = Math.floor(diff / 60000)
|
|
125
|
+
if (min < 1) return "刚刚"
|
|
126
|
+
if (min < 60) return min + " 分钟前"
|
|
127
|
+
const hr = Math.floor(min / 60)
|
|
128
|
+
if (hr < 24) return hr + " 小时前"
|
|
129
|
+
const day = Math.floor(hr / 24)
|
|
130
|
+
if (day < 30) return day + " 天前"
|
|
131
|
+
return fmtDateTime(iso).slice(0, 10)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const STATUS_LABEL = {
|
|
135
|
+
pending: "待处理", in_progress: "在办", working: "在岗", blocked: "异常",
|
|
136
|
+
done: "已完成", completed: "已完成", idle: "空闲", failed: "失败",
|
|
137
|
+
}
|
|
138
|
+
function statusLabel(status) {
|
|
139
|
+
return STATUS_LABEL[status] || status || ""
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** 时间线:days[] → 扁平条目(按天分组,日期递减) */
|
|
143
|
+
function flattenTimeline(data) {
|
|
144
|
+
if (!data || !Array.isArray(data.days)) return []
|
|
145
|
+
const out = []
|
|
146
|
+
for (const day of data.days) {
|
|
147
|
+
for (const ev of day.events || []) out.push(Object.assign({ date: day.date }, ev))
|
|
148
|
+
}
|
|
149
|
+
return out
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** 节点完成度(0..100),用于详情顶部那条进度条文字 */
|
|
153
|
+
function flowProgress(nodes) {
|
|
154
|
+
const list = Array.isArray(nodes) ? nodes : []
|
|
155
|
+
if (!list.length) return 0
|
|
156
|
+
const done = list.filter((n) => n && (n.status === "done" || n.status === "completed")).length
|
|
157
|
+
return Math.round((done / list.length) * 100)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/* ───────────────────────── 数据 ───────────────────────── */
|
|
161
|
+
|
|
162
|
+
function useApi(path, autoMs) {
|
|
163
|
+
const [st, set] = React.useState({ loading: true, data: null, error: null, at: null })
|
|
164
|
+
const seq = React.useRef(0)
|
|
165
|
+
const load = React.useCallback(() => {
|
|
166
|
+
const mine = ++seq.current
|
|
167
|
+
set((s) => Object.assign({}, s, { loading: s.data === null, error: null }))
|
|
168
|
+
fetch(API + path, { headers: { accept: "application/json" } })
|
|
169
|
+
.then((r) => r.json().then((j) => ({ ok: r.ok, j })))
|
|
170
|
+
.then(({ ok, j }) => {
|
|
171
|
+
if (mine !== seq.current) return
|
|
172
|
+
if (j && j.ok) set({ loading: false, data: j.data, error: null, at: new Date().toISOString() })
|
|
173
|
+
else set({ loading: false, data: null, error: (j && (j.message || j.error)) || ("HTTP " + (ok ? "?" : "错误")), at: new Date().toISOString() })
|
|
174
|
+
})
|
|
175
|
+
.catch((e) => {
|
|
176
|
+
if (mine !== seq.current) return
|
|
177
|
+
set({ loading: false, data: null, error: String((e && e.message) || e), at: new Date().toISOString() })
|
|
178
|
+
})
|
|
179
|
+
}, [path])
|
|
180
|
+
React.useEffect(() => { load() }, [load])
|
|
181
|
+
React.useEffect(() => {
|
|
182
|
+
if (!autoMs) return undefined
|
|
183
|
+
const t = setInterval(load, autoMs)
|
|
184
|
+
return () => clearInterval(t)
|
|
185
|
+
}, [autoMs, load])
|
|
186
|
+
return { loading: st.loading, data: st.data, error: st.error, at: st.at, reload: load }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/* ───────────────────────── 小组件 ───────────────────────── */
|
|
190
|
+
|
|
191
|
+
function ErrorCard({ error, onRetry }) {
|
|
192
|
+
return h("div", { className: "aerp-err" },
|
|
193
|
+
"ERP 取不到数据:" + String(error || "未知错误"),
|
|
194
|
+
h("div", { className: "aerp-actions" },
|
|
195
|
+
onRetry ? h("button", { type: "button", className: "aerp-btn", onClick: onRetry }, "重试") : null,
|
|
196
|
+
h("a", { className: "aerp-link", href: WEB, target: "_blank", rel: "noopener noreferrer" }, "在 ERP 里打开 ↗")))
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function Kpis({ kpis }) {
|
|
200
|
+
const items = [
|
|
201
|
+
{ label: "待处理", value: kpis.pending },
|
|
202
|
+
{ label: "在办", value: kpis.inProgress },
|
|
203
|
+
{ label: "今日完成", value: kpis.doneToday },
|
|
204
|
+
{ label: kpis.agentsLabel || "执行机在线", value: `${kpis.agentsOnline}/${kpis.agentsTotal}` },
|
|
205
|
+
]
|
|
206
|
+
return h("div", { className: "aerp-kpis" }, items.map((it, i) =>
|
|
207
|
+
h("div", { className: "aerp-kpi", key: i }, h("b", null, String(it.value)), h("span", null, it.label))))
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function Tabs({ items, value, onChange }) {
|
|
211
|
+
return h("div", { className: "aerp-tabs", role: "tablist" }, items.map((it) =>
|
|
212
|
+
h("button", {
|
|
213
|
+
key: it.id, type: "button", role: "tab", className: "aerp-tab",
|
|
214
|
+
"data-on": value === it.id ? "1" : "0",
|
|
215
|
+
"aria-selected": value === it.id ? "true" : "false",
|
|
216
|
+
onClick: () => onChange(it.id),
|
|
217
|
+
}, it.label)))
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function Row({ status, title, sub, right, onClick }) {
|
|
221
|
+
return h("div", {
|
|
222
|
+
className: "aerp-row", role: onClick ? "button" : undefined, tabIndex: onClick ? 0 : undefined,
|
|
223
|
+
onClick: onClick || undefined,
|
|
224
|
+
onKeyDown: onClick ? (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onClick() } } : undefined,
|
|
225
|
+
},
|
|
226
|
+
h("i", { className: "aerp-dot", "data-s": status || "" }),
|
|
227
|
+
h("div", { style: { minWidth: 0 } }, h("div", { className: "t" }, title), sub ? h("div", { className: "s" }, sub) : null),
|
|
228
|
+
right ? h("div", { className: "r" }, right) : null)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/* ───────────────────────── 详情 ───────────────────────── */
|
|
232
|
+
|
|
233
|
+
function FlowDetail({ tenant, flowId, onBack }) {
|
|
234
|
+
const q = useApi(`/flow?tenant=${encodeURIComponent(tenant || "")}&id=${encodeURIComponent(flowId)}`)
|
|
235
|
+
return h("div", { className: "aerp-card" },
|
|
236
|
+
h("div", { className: "aerp-detail-head" },
|
|
237
|
+
h("button", { type: "button", className: "aerp-btn", onClick: onBack }, "← 返回"),
|
|
238
|
+
h("span", { className: "aerp-dim" }, "时间线详情")),
|
|
239
|
+
q.loading ? h("div", { className: "aerp-loading" }, "加载中…")
|
|
240
|
+
: q.error ? h(ErrorCard, { error: q.error, onRetry: q.reload })
|
|
241
|
+
: h("div", null,
|
|
242
|
+
h("div", { style: { fontWeight: 600, fontSize: 14 } }, q.data.title || flowId),
|
|
243
|
+
h("div", { className: "aerp-dim", style: { marginTop: 4 } },
|
|
244
|
+
[q.data.shipmentNo, q.data.sku, q.data.store, q.data.statusLabel].filter(Boolean).join(" · ")),
|
|
245
|
+
h("div", { className: "aerp-dim", style: { marginTop: 6 } }, `节点完成 ${flowProgress(q.data.nodes)}%`),
|
|
246
|
+
h("div", { className: "aerp-nodes" }, (q.data.nodes || []).map((n, i) =>
|
|
247
|
+
h("div", { className: "aerp-node", key: n.key || i },
|
|
248
|
+
h("i", { className: "aerp-dot", "data-s": n.status || "" }),
|
|
249
|
+
h("div", { style: { minWidth: 0 } },
|
|
250
|
+
h("div", { className: "k" }, n.title || n.key),
|
|
251
|
+
h("div", { className: "m" }, [n.statusLabel || statusLabel(n.status), n.ownerName, fmtDateTime(n.at)].filter(Boolean).join(" · ")),
|
|
252
|
+
(n.fields || []).length
|
|
253
|
+
? h("div", { className: "f" }, n.fields.slice(0, 6).map((f) => `${f.label}: ${f.value}`).join(";"))
|
|
254
|
+
: null))))))
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function ShipmentDetail({ tenant, recordId, onBack }) {
|
|
258
|
+
const q = useApi(`/shipment?tenant=${encodeURIComponent(tenant || "")}&recordId=${encodeURIComponent(recordId)}`)
|
|
259
|
+
return h("div", { className: "aerp-card" },
|
|
260
|
+
h("div", { className: "aerp-detail-head" },
|
|
261
|
+
h("button", { type: "button", className: "aerp-btn", onClick: onBack }, "← 返回"),
|
|
262
|
+
h("span", { className: "aerp-dim" }, "货件详情")),
|
|
263
|
+
q.loading ? h("div", { className: "aerp-loading" }, "加载中…")
|
|
264
|
+
: q.error ? h(ErrorCard, { error: q.error, onRetry: q.reload })
|
|
265
|
+
: h("div", null,
|
|
266
|
+
h("div", { style: { fontWeight: 600, fontSize: 14 } }, q.data.shipmentNo || recordId),
|
|
267
|
+
h("div", { className: "aerp-dim", style: { marginTop: 4 } },
|
|
268
|
+
[q.data.store, q.data.sku, q.data.fullStatus, q.data.currentStep, q.data.boxCount ? q.data.boxCount + " 箱" : ""].filter(Boolean).join(" · ")),
|
|
269
|
+
h("div", { className: "aerp-dim", style: { marginTop: 6 } },
|
|
270
|
+
`发票:${q.data.invoice && q.data.invoice.status || "未知"}${q.data.invoice && q.data.invoice.ready ? "(已就绪)" : ""}`),
|
|
271
|
+
h("div", { className: "aerp-nodes" }, (q.data.nodes || []).map((n, i) =>
|
|
272
|
+
h("div", { className: "aerp-node", key: i },
|
|
273
|
+
h("i", { className: "aerp-dot", "data-s": n.status || "" }),
|
|
274
|
+
h("div", null,
|
|
275
|
+
h("div", { className: "k" }, n.label || ""),
|
|
276
|
+
h("div", { className: "m" }, [statusLabel(n.status), fmtDateTime(n.at)].filter(Boolean).join(" · "))))))))
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/* ───────────────────────── 面板 ───────────────────────── */
|
|
280
|
+
|
|
281
|
+
function OfficePane({ overview, tenant }) {
|
|
282
|
+
if (overview.loading && !overview.data) return h("div", { className: "aerp-loading" }, "加载办公室现场…")
|
|
283
|
+
if (overview.error) return h(ErrorCard, { error: overview.error, onRetry: overview.reload })
|
|
284
|
+
const d = overview.data || {}
|
|
285
|
+
const employees = d.employees || []
|
|
286
|
+
const machines = (d.agents && d.agents.machines) || []
|
|
287
|
+
const activity = d.activity || []
|
|
288
|
+
return h(React.Fragment, null,
|
|
289
|
+
h(Kpis, { kpis: d.kpis || {} }),
|
|
290
|
+
h("div", { className: "aerp-card" },
|
|
291
|
+
h("h4", null, "在岗(" + employees.length + ")"),
|
|
292
|
+
employees.length
|
|
293
|
+
? h("div", { className: "aerp-people" }, employees.map((e, i) =>
|
|
294
|
+
h("div", { className: "aerp-person", key: e.id || i },
|
|
295
|
+
h("i", { className: "aerp-dot", "data-s": e.status || "" }),
|
|
296
|
+
h("div", { style: { minWidth: 0 } },
|
|
297
|
+
h("div", { className: "nm" }, e.name || "—"),
|
|
298
|
+
h("div", { className: "rl" }, [e.roleLabel, statusLabel(e.status)].filter(Boolean).join(" · ")),
|
|
299
|
+
h("div", { className: "cu", title: e.current || "" }, e.current || "空闲")))))
|
|
300
|
+
: h("div", { className: "aerp-empty" }, "当前没有在岗记录")),
|
|
301
|
+
machines.length
|
|
302
|
+
? h("div", { className: "aerp-card" },
|
|
303
|
+
h("h4", null, "执行机"),
|
|
304
|
+
h("div", { className: "aerp-list" }, machines.map((m, i) =>
|
|
305
|
+
h(Row, {
|
|
306
|
+
key: m.machineId || i, status: m.online ? "working" : "idle",
|
|
307
|
+
title: m.name || m.machineId,
|
|
308
|
+
sub: [m.online ? "在线" : "离线", m.defaultPreset, m.lastSeenAt ? "最后上报 " + fmtRelative(m.lastSeenAt) : ""].filter(Boolean).join(" · "),
|
|
309
|
+
}))))
|
|
310
|
+
: null,
|
|
311
|
+
h("div", { className: "aerp-card" },
|
|
312
|
+
h("h4", null, "现场动态"),
|
|
313
|
+
activity.length
|
|
314
|
+
? h("div", { className: "aerp-list" }, activity.map((a, i) =>
|
|
315
|
+
h(Row, { key: a.id || i, status: a.actorKind === "agent" ? "working" : "idle",
|
|
316
|
+
title: a.text || "", sub: [a.actor, fmtRelative(a.ts)].filter(Boolean).join(" · ") })))
|
|
317
|
+
: h("div", { className: "aerp-empty" }, "暂无动态")))
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function TodoPane({ overview, onOpenFlow, onOpenShipment }) {
|
|
321
|
+
if (overview.loading && !overview.data) return h("div", { className: "aerp-loading" }, "加载待办…")
|
|
322
|
+
if (overview.error) return h(ErrorCard, { error: overview.error, onRetry: overview.reload })
|
|
323
|
+
const items = (overview.data && overview.data.workItems) || []
|
|
324
|
+
const sorted = items.slice().sort((a, b) => {
|
|
325
|
+
const rank = (s) => (s === "in_progress" ? 0 : s === "blocked" ? 1 : 2)
|
|
326
|
+
return rank(a.status) - rank(b.status) || String(b.updatedAt || "").localeCompare(String(a.updatedAt || ""))
|
|
327
|
+
})
|
|
328
|
+
if (!sorted.length) return h("div", { className: "aerp-empty" }, "没有待办 🎉")
|
|
329
|
+
return h("div", { className: "aerp-card" },
|
|
330
|
+
h("h4", null, `待办(${sorted.length})`),
|
|
331
|
+
h("div", { className: "aerp-list" }, sorted.map((it, i) =>
|
|
332
|
+
h(Row, {
|
|
333
|
+
key: it.id || i, status: it.status,
|
|
334
|
+
title: it.title || it.shipmentNo || "—",
|
|
335
|
+
sub: [it.ownerName, it.nodeLabel, fmtRelative(it.updatedAt)].filter(Boolean).join(" · "),
|
|
336
|
+
right: it.progress ? it.progress + "%" : "",
|
|
337
|
+
onClick: () => {
|
|
338
|
+
// 抢预约任务没有货件 recordId(shipmentId 是任务 id)⇒ 只能跳 ERP
|
|
339
|
+
if (it.type === "appointment") { try { window.open(WEB + "#/appointments", "_blank", "noopener,noreferrer") } catch { /* noop */ } }
|
|
340
|
+
else onOpenShipment(it.shipmentId)
|
|
341
|
+
},
|
|
342
|
+
}))))
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function TimelinePane({ tenant, onOpenFlow }) {
|
|
346
|
+
const q = useApi(`/timeline?tenant=${encodeURIComponent(tenant || "")}&days=14`)
|
|
347
|
+
if (q.loading) return h("div", { className: "aerp-loading" }, "加载时间线…")
|
|
348
|
+
if (q.error) return h(ErrorCard, { error: q.error, onRetry: q.reload })
|
|
349
|
+
const rows = flattenTimeline(q.data)
|
|
350
|
+
if (!rows.length) return h("div", { className: "aerp-empty" }, "最近 14 天没有更新")
|
|
351
|
+
const groups = new Map()
|
|
352
|
+
for (const r of rows) {
|
|
353
|
+
if (!groups.has(r.date)) groups.set(r.date, [])
|
|
354
|
+
groups.get(r.date).push(r)
|
|
355
|
+
}
|
|
356
|
+
return h("div", { style: { display: "flex", flexDirection: "column", gap: 12 } },
|
|
357
|
+
[...groups.entries()].map(([date, evs]) =>
|
|
358
|
+
h("div", { className: "aerp-card", key: date },
|
|
359
|
+
h("h4", null, date),
|
|
360
|
+
h("div", { className: "aerp-list" }, evs.map((ev, i) =>
|
|
361
|
+
h(Row, {
|
|
362
|
+
key: ev.id || i, status: ev.todoCount > 0 ? "pending" : "idle",
|
|
363
|
+
title: ev.title || ev.shipmentNo || ev.id,
|
|
364
|
+
sub: [ev.sku, ev.store, ev.currentStepLabel || ev.statusLabel].filter(Boolean).join(" · "),
|
|
365
|
+
right: ev.todoCount > 0 ? ev.todoCount + " 待办" : "",
|
|
366
|
+
onClick: () => onOpenFlow(ev.id),
|
|
367
|
+
}))))))
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function ShipmentPane({ tenant, onOpenShipment }) {
|
|
371
|
+
const [q, setQ] = React.useState("")
|
|
372
|
+
const [draft, setDraft] = React.useState("")
|
|
373
|
+
const qs = useApi(`/shipments?tenant=${encodeURIComponent(tenant || "")}&q=${encodeURIComponent(q)}&limit=30`)
|
|
374
|
+
const submit = (e) => { e.preventDefault(); setQ(draft.trim()) }
|
|
375
|
+
const list = (qs.data && (qs.data.shipments || qs.data.items || qs.data.rows)) || []
|
|
376
|
+
return h(React.Fragment, null,
|
|
377
|
+
h("form", { className: "aerp-search", onSubmit: submit },
|
|
378
|
+
h("input", { value: draft, onChange: (e) => setDraft(e.target.value), placeholder: "搜货件号 / SKU / 店铺…", "aria-label": "搜索货件" }),
|
|
379
|
+
h("button", { type: "submit", className: "aerp-btn primary" }, "搜索")),
|
|
380
|
+
qs.loading ? h("div", { className: "aerp-loading" }, "加载货件…")
|
|
381
|
+
: qs.error ? h(ErrorCard, { error: qs.error, onRetry: qs.reload })
|
|
382
|
+
: list.length
|
|
383
|
+
? h("div", { className: "aerp-card" }, h("div", { className: "aerp-list" }, list.map((s, i) =>
|
|
384
|
+
h(Row, {
|
|
385
|
+
key: s.recordId || s.sourceRecordId || s._id || i,
|
|
386
|
+
status: s.fullStatus || s.status || "",
|
|
387
|
+
title: s.shipmentNo || s.appointmentShipmentNo || s.sourceRecordId || "—",
|
|
388
|
+
sub: [s.shop || s.store || s.storeName, s.sku, s.currentStep].filter(Boolean).join(" · "),
|
|
389
|
+
onClick: () => onOpenShipment(s.recordId || s.sourceRecordId || s._id),
|
|
390
|
+
}))))
|
|
391
|
+
: h("div", { className: "aerp-empty" }, q ? "没有匹配的货件" : "输入关键词搜索货件"))
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* 「办公室聊一聊」——本视图只做**入口 + 说明**,不重造聊天 UI。
|
|
396
|
+
*
|
|
397
|
+
* 结论(2026-11-07 实证):ERP 的办公室聊一聊是**服务端** LLM+工具循环(POST .../office/chat,
|
|
398
|
+
* 自定义 SSE:meta/tool/card/proposal/delta/done),会话落 OfficeChatSession(tenantId+userId 双隔离),
|
|
399
|
+
* 计费走 pointsBilling —— 它与 dsh 的 Session 事件**不是同一种协议**,
|
|
400
|
+
* 因此 dsh 的 Chat 视图不能直接渲染它(Chat 视图硬绑 target="chat",见 README)。
|
|
401
|
+
*
|
|
402
|
+
* 两条正确的路:
|
|
403
|
+
* ① 在 ERP 里聊(原样,保留计费/工具/会话);
|
|
404
|
+
* ② 开一个 **dsh 会话**聊 —— 那就是 dsh 原生 Chat 视图(本按钮)。
|
|
405
|
+
*/
|
|
406
|
+
/** 办公室助理 preset 的 id(与 presets/argent-office.patch.yml 的 config.id 一致) */
|
|
407
|
+
const OFFICE_PRESET = "argent-office"
|
|
408
|
+
/**
|
|
409
|
+
* 新会话钉死的模型:问述代理(走问述积分)。
|
|
410
|
+
* provider id = `whyshu`(由 @whyshu/argent 的 llm-whyshu 注册),model 见其模型目录。
|
|
411
|
+
*/
|
|
412
|
+
const OFFICE_MODEL = { provider: "whyshu", model: "deepseek-flash" }
|
|
413
|
+
|
|
414
|
+
function ChatPane({ tenant, props }) {
|
|
415
|
+
const [busy, setBusy] = React.useState(false)
|
|
416
|
+
const [msg, setMsg] = React.useState("")
|
|
417
|
+
const history = useApi(`/chat/sessions?tenant=${encodeURIComponent(tenant || "")}&limit=20`)
|
|
418
|
+
const sessions = props && props.sessions
|
|
419
|
+
const nav = props && props.workspaceNav // ctx.uiWorkspace(openSession / connectWorkspace)
|
|
420
|
+
const remote = props && props.remote // ctx.remote(session.selectModel)
|
|
421
|
+
// 当前会话所属 workspace(没有就退回第一个)——不传 workspaceId 会掉进「请选择 workspace」,
|
|
422
|
+
// 用户选完 workspace 时 UI 会**另建一个 blank 会话**,preset 就被重置成默认了(2026-11-07 实测)。
|
|
423
|
+
const useWorkspaces = (props && props.useWorkspaces) || (() => null)
|
|
424
|
+
const workspaces = (() => { try { return useWorkspaces((x) => x) } catch { return null } })()
|
|
425
|
+
const wsList = (workspaces && workspaces.items) || []
|
|
426
|
+
const myWs = (props && props.sessionId
|
|
427
|
+
? wsList.find((w) => w && Array.isArray(w.sessionIds) && w.sessionIds.includes(props.sessionId))
|
|
428
|
+
: null) || wsList[0] || null
|
|
429
|
+
|
|
430
|
+
/** 把新会话模型钉成问述代理;失败不算致命,返回提示文案 */
|
|
431
|
+
const pinModel = async (sid) => {
|
|
432
|
+
try {
|
|
433
|
+
const api = remote && remote.session
|
|
434
|
+
if (!api || typeof api.selectModel !== "function") return "(模型未钉选:当前 dsh 没有 remote.session.selectModel,可在输入框上方的模型选择里手动选「问述代理 · deepseek-flash」)"
|
|
435
|
+
const r = await api.selectModel({ sessionId: sid, provider: OFFICE_MODEL.provider, model: OFFICE_MODEL.model })
|
|
436
|
+
if (r && r.ok === false) {
|
|
437
|
+
const err = r.error || {}
|
|
438
|
+
return `(模型钉选失败:${err.code || ""} ${err.message || ""}——可在模型选择里手动选「问述代理 · deepseek-flash」)`
|
|
439
|
+
}
|
|
440
|
+
return ""
|
|
441
|
+
} catch (e) {
|
|
442
|
+
return "(模型钉选失败:" + String((e && e.message) || e) + "——可在模型选择里手动选「问述代理 · deepseek-flash」)"
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const startDshChat = async () => {
|
|
447
|
+
setBusy(true)
|
|
448
|
+
setMsg("")
|
|
449
|
+
try {
|
|
450
|
+
let sid = ""
|
|
451
|
+
let warn = ""
|
|
452
|
+
// 首选:workspace 原生路径(会复用/新建该 workspace 的 blank,再按 preset 落位)
|
|
453
|
+
if (nav && typeof nav.connectWorkspace === "function" && myWs && myWs.workspaceId) {
|
|
454
|
+
try {
|
|
455
|
+
sid = await nav.connectWorkspace(myWs.workspaceId, { agentPreset: OFFICE_PRESET })
|
|
456
|
+
} catch (e) {
|
|
457
|
+
// workspace 路径失败(权限/迁移等)⇒ 退回 sessions.create,带上 workspaceId
|
|
458
|
+
console.warn("[argent-erp] connectWorkspace 失败,退回 sessions.create:", (e && e.message) || e)
|
|
459
|
+
sid = ""
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (!sid) {
|
|
463
|
+
if (!(sessions && typeof sessions.create === "function")) {
|
|
464
|
+
setMsg("当前 dsh 版本没有可用的建会话接口(uiWorkspace.connectWorkspace / sessions.create);请在左侧手动新建。")
|
|
465
|
+
return
|
|
466
|
+
}
|
|
467
|
+
sid = await sessions.create(Object.assign({ agentPreset: OFFICE_PRESET }, myWs && myWs.workspaceId ? { workspaceId: myWs.workspaceId } : {}))
|
|
468
|
+
}
|
|
469
|
+
warn = await pinModel(sid)
|
|
470
|
+
if (nav && typeof nav.openSession === "function") nav.openSession(sid)
|
|
471
|
+
setMsg((sid ? "已新建会话 " + sid.slice(0, 12) + "…" : "已新建会话") + (warn ? " " + warn : ""))
|
|
472
|
+
} catch (e) {
|
|
473
|
+
const text = String((e && e.message) || e)
|
|
474
|
+
setMsg(/preset/i.test(text)
|
|
475
|
+
? "未找到「六禾园办公室助理」preset(" + text + ")——重装/重启 dsh 后再试。"
|
|
476
|
+
: "新建会话失败:" + text)
|
|
477
|
+
} finally {
|
|
478
|
+
setBusy(false)
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return h(React.Fragment, null,
|
|
483
|
+
h("div", { className: "aerp-note" },
|
|
484
|
+
h("b", null, "和办公室聊一聊 = 一个 dsh 会话"),
|
|
485
|
+
"(角色「六禾园办公室助理」):只聊六禾园办公室,只有 ERP 只读工具,没有本机/终端/联网能力——边界写死在 preset 里,不靠模型自觉。",
|
|
486
|
+
"会话建在你**当前 workspace** 里,模型钉死为「问述代理 · deepseek-flash」(走问述积分),原生 Chat 的轨迹、统计、中断全都在。"),
|
|
487
|
+
h("div", { className: "aerp-actions" },
|
|
488
|
+
h("button", { type: "button", className: "aerp-btn primary", disabled: busy, onClick: startDshChat },
|
|
489
|
+
busy ? "正在新建…" : "和办公室聊一聊(dsh 会话)"),
|
|
490
|
+
h("a", { className: "aerp-btn", href: WEB + "#/office", target: "_blank", rel: "noopener noreferrer" }, "在 ERP 里聊 ↗")),
|
|
491
|
+
msg ? h("div", { className: "aerp-note", style: { marginTop: 8 } }, msg) : null,
|
|
492
|
+
h("div", { className: "aerp-dim", style: { marginTop: 2, fontSize: 11 } },
|
|
493
|
+
"说明:ERP 网页里的「办公室聊一聊」仍跑在 ERP 服务端(自定义 SSE + 服务端计费),与这里的 dsh 会话是两条链路;上面的历史只读列表是 ERP 那一侧的记录。"),
|
|
494
|
+
h("div", { className: "aerp-card", style: { marginTop: 4 } },
|
|
495
|
+
h("h4", null, "ERP 侧历史会话(只读)"),
|
|
496
|
+
history.loading ? h("div", { className: "aerp-loading" }, "加载中…")
|
|
497
|
+
: history.error ? h(ErrorCard, { error: history.error, onRetry: history.reload })
|
|
498
|
+
: (history.data && history.data.sessions || []).length
|
|
499
|
+
? h("div", { className: "aerp-list" }, history.data.sessions.map((s, i) =>
|
|
500
|
+
h(Row, {
|
|
501
|
+
key: s.id || i, status: "idle",
|
|
502
|
+
title: s.title || "(无标题会话)",
|
|
503
|
+
sub: [s.messageCount ? s.messageCount + " 条" : "", fmtDateTime(s.updatedAt)].filter(Boolean).join(" · "),
|
|
504
|
+
right: h("a", { className: "aerp-link", href: WEB + "#/office", target: "_blank", rel: "noopener noreferrer" }, "打开 ↗"),
|
|
505
|
+
})))
|
|
506
|
+
: h("div", { className: "aerp-empty" }, "还没有 ERP 侧聊天记录")))
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/* ───────────────────────── 主视图 ───────────────────────── */
|
|
510
|
+
|
|
511
|
+
const TABS = [
|
|
512
|
+
{ id: "office", label: "办公室" },
|
|
513
|
+
{ id: "todo", label: "待办" },
|
|
514
|
+
{ id: "timeline", label: "时间线" },
|
|
515
|
+
{ id: "shipments", label: "货件" },
|
|
516
|
+
{ id: "chat", label: "聊一聊" },
|
|
517
|
+
]
|
|
518
|
+
|
|
519
|
+
function ErpView(props) {
|
|
520
|
+
const [tab, setTab] = React.useState("office")
|
|
521
|
+
const [tenant, setTenant] = React.useState("")
|
|
522
|
+
const [detail, setDetail] = React.useState(null) // { kind:'flow'|'shipment', id }
|
|
523
|
+
const [nonce, setNonce] = React.useState(0) // 刷新时让各自取数的面板重挂
|
|
524
|
+
// 渲染信标:只有切到 ERP tab 才会打 ⇒ /health 的 viewPingAt 能证明"真的渲染了"
|
|
525
|
+
React.useEffect(() => { try { fetch(API + "/ping?from=view") } catch { /* noop */ } }, [])
|
|
526
|
+
const who = useApi("/whoami")
|
|
527
|
+
const health = useApi("/health")
|
|
528
|
+
const overview = useApi(`/overview?tenant=${encodeURIComponent(tenant || "")}`, 60000)
|
|
529
|
+
|
|
530
|
+
React.useEffect(() => {
|
|
531
|
+
if (!tenant && who.data && who.data.defaultTenant) setTenant(who.data.defaultTenant)
|
|
532
|
+
}, [who.data, tenant])
|
|
533
|
+
|
|
534
|
+
const tenants = (who.data && who.data.tenants) || []
|
|
535
|
+
const host = (who.data && who.data.tokenSource) || ""
|
|
536
|
+
const at = overview.at || (who.at || "")
|
|
537
|
+
const openFlow = (id) => setDetail({ kind: "flow", id })
|
|
538
|
+
const openShipment = (id) => setDetail({ kind: "shipment", id })
|
|
539
|
+
|
|
540
|
+
let body
|
|
541
|
+
if (detail) {
|
|
542
|
+
body = detail.kind === "flow"
|
|
543
|
+
? h(FlowDetail, { tenant, flowId: detail.id, onBack: () => setDetail(null) })
|
|
544
|
+
: h(ShipmentDetail, { tenant, recordId: detail.id, onBack: () => setDetail(null) })
|
|
545
|
+
} else if (tab === "office") {
|
|
546
|
+
body = h(OfficePane, { overview, tenant })
|
|
547
|
+
} else if (tab === "todo") {
|
|
548
|
+
body = h(TodoPane, { overview, onOpenFlow: openFlow, onOpenShipment: openShipment })
|
|
549
|
+
} else if (tab === "timeline") {
|
|
550
|
+
body = h(TimelinePane, { key: "tl-" + nonce, tenant, onOpenFlow: openFlow })
|
|
551
|
+
} else if (tab === "shipments") {
|
|
552
|
+
body = h(ShipmentPane, { key: "sh-" + nonce, tenant, onOpenShipment: openShipment })
|
|
553
|
+
} else {
|
|
554
|
+
body = h(ChatPane, { key: "chat-" + nonce, tenant, props })
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return h("div", { className: "aerp-root" },
|
|
558
|
+
h("div", { className: "aerp-head" },
|
|
559
|
+
h("span", { className: "aerp-title" }, "Argent ERP"),
|
|
560
|
+
tenants.length > 1
|
|
561
|
+
? h("select", {
|
|
562
|
+
className: "aerp-sel", value: tenant, onChange: (e) => { setTenant(e.target.value); setDetail(null) },
|
|
563
|
+
"aria-label": "租户",
|
|
564
|
+
}, tenants.map((t) => h("option", { key: t.appId, value: t.appId }, t.name || t.appId)))
|
|
565
|
+
: null,
|
|
566
|
+
h("span", { className: "aerp-spacer" }),
|
|
567
|
+
at ? h("span", { className: "aerp-dim" }, "更新于 " + fmtDateTime(at).slice(11)) : null,
|
|
568
|
+
h("button", { type: "button", className: "aerp-btn", onClick: () => { overview.reload(); setNonce((n) => n + 1) } }, "刷新"),
|
|
569
|
+
h("a", { className: "aerp-btn", href: WEB, target: "_blank", rel: "noopener noreferrer" }, "完整版 ↗")),
|
|
570
|
+
h(Tabs, { items: TABS, value: tab, onChange: (id) => { setTab(id); setDetail(null) } }),
|
|
571
|
+
(health.data && health.data.erpTools === 0)
|
|
572
|
+
? h("div", { className: "aerp-err", style: { margin: "8px 18px 0" } },
|
|
573
|
+
"未检测到 ERP 工具(mcp__argent__erp_*):办公室角色需要同机安装 @whyshu/argent(提供 ERP MCP 网关与问述 provider)。只装本插件时,角色会被安全降级为「无工具、只按提示词回答」。")
|
|
574
|
+
: null,
|
|
575
|
+
h("div", { className: "aerp-body" }, body),
|
|
576
|
+
host === "none" ? h("div", { className: "aerp-dim", style: { padding: "0 18px 10px" } }, "未检测到问述账号 token") : null)
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/* ───────────────────────── 注册 ───────────────────────── */
|
|
580
|
+
|
|
581
|
+
const name = "argent-erp-client"
|
|
582
|
+
const inject = ["slots"]
|
|
583
|
+
|
|
584
|
+
function apply(ctx) {
|
|
585
|
+
injectCss()
|
|
586
|
+
// 自检信标:宿主 /health 会回报 clientPingAt ⇒ 能区分"bundle 没加载"与"加载了但没显示"
|
|
587
|
+
try { fetch(API + "/ping?from=client").catch(() => {}) } catch { /* noop */ }
|
|
588
|
+
|
|
589
|
+
// ⛔ 永不抛出:注册失败只影响这个 tab,不能让 web boot 报 "entry did not activate"
|
|
590
|
+
try {
|
|
591
|
+
ctx.slots.inject("conversation.view", () => ctx.slots.register({
|
|
592
|
+
name: "conversation.view",
|
|
593
|
+
id: "erp",
|
|
594
|
+
order: 5,
|
|
595
|
+
label: () => "ERP",
|
|
596
|
+
inject: () => ({
|
|
597
|
+
sessions: (() => { try { return ctx.get("sessions") } catch { return undefined } })(),
|
|
598
|
+
workspaceNav: (() => { try { return ctx.get("uiWorkspace") } catch { return undefined } })(),
|
|
599
|
+
remote: (() => { try { return ctx.get("remote") } catch { return undefined } })(),
|
|
600
|
+
}),
|
|
601
|
+
}, ErpView))
|
|
602
|
+
} catch (e) {
|
|
603
|
+
console.warn("[argent-erp] conversation.view 注册失败(不影响 dsh):", (e && e.message) || e)
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
exports.name = name
|
|
608
|
+
exports.inject = inject
|
|
609
|
+
exports.apply = apply
|
|
610
|
+
// 仅测试用:纯函数导出,避免为了验证格式再重启一次
|
|
611
|
+
exports.__test__ = { fmtDateTime, fmtRelative, flattenTimeline, flowProgress, statusLabel }
|
|
612
|
+
return module.exports
|
|
613
|
+
},
|
|
614
|
+
})
|