Skip to content

Commit c3c7c2e

Browse files
feat(resource-center): 首页概览统计 + 节点图标选择/录入 + 组态正式发布地址
- 资源中心前台新增概览首页:授权设备数(在线/离线)、相关工单数(按状态)、 各类资源节点数量,普通用户仅统计其授权范围(GET /api/portal/stats) - 资源节点编辑「图标」改为可选择或手动录入(带图标预览) - 组态节点前台优先使用正式发布地址 /scada-editor/share/<token>,未发布回退预览 - 附带 scada-editor、工单、设备等多项在途更新 Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 6b55ffc commit c3c7c2e

44 files changed

Lines changed: 4211 additions & 161 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎agent/app/build.gradle‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ android {
1515
applicationId "com.appmanager.agent"
1616
minSdk 21
1717
targetSdk 34
18-
versionCode 267
19-
versionName "2.3.20"
18+
versionCode 268
19+
versionName "2.3.21"
2020
}
2121

2222
signingConfigs {

‎scada-editor/dist/index.html‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<link rel="icon" type="image/png" sizes="16x16" href="favicon-16.png" />
99
<link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon.png" />
1010
<title>磐石 组态编辑器</title>
11-
<script type="module" crossorigin src="/scada-editor/assets/index-AQ4fIBUO.js"></script>
11+
<script type="module" crossorigin src="/scada-editor/assets/index-Dm4qI0P4.js"></script>
1212
<link rel="stylesheet" crossorigin href="/scada-editor/assets/index-DFghR_IL.css">
1313
</head>
1414
<body>

‎scada-editor/src/components/BindingDrawer.tsx‎

Lines changed: 236 additions & 62 deletions
Large diffs are not rendered by default.

‎scada-editor/src/components/CanvasBoard.tsx‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { useRef, useEffect, useCallback, useState } from 'react'
1+
import { useRef, useEffect, useCallback, useState, useMemo } from 'react'
22
import { useEditorStore } from '@/store/editorStore'
3+
import { resolveGlobalParams } from '@/runtime/expression'
34
import {
45
drawGrid, drawElement, drawSelectionHandles, drawMultiSelectBox, drawMarquee,
56
hitTest, hitTestHandle, hitTestMarquee, snapToGrid, generateId,
@@ -90,9 +91,17 @@ export default function CanvasBoard() {
9091
})
9192

9293
const allElements = canvas?.elements ?? []
94+
const resolvedGlobalParams = useMemo(
95+
() => resolveGlobalParams(store.project.globalParams),
96+
[store.project.globalParams],
97+
)
9398
useInterfaceBindingData({
9499
elements: liveDataOn ? allElements : [],
95100
onData: handlePointData,
101+
scadaCode,
102+
pointData,
103+
globalParams: resolvedGlobalParams,
104+
customFunctions: store.project.customFunctions,
96105
})
97106

98107
// 当前时间日期元件的每秒刷新(编辑器画布直接渲染系统时间)
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/**
2+
* 表达式输入框:CodeMirror 6 + JavaScript,带接口参数表达式作用域的自动补全。
3+
* 补全项 = 内置访问器/时间/工具函数(BUILTIN_EXPR_COMPLETIONS)
4+
* + 全局参数(params.<key> / P('key'))
5+
* + 自定义函数(按名)
6+
* + 组件名(el('name'))
7+
*/
8+
import { useEffect, useRef } from 'react'
9+
import { EditorState } from '@codemirror/state'
10+
import { EditorView, keymap } from '@codemirror/view'
11+
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands'
12+
import { javascript } from '@codemirror/lang-javascript'
13+
import { autocompletion, completionKeymap, closeBrackets, type CompletionContext } from '@codemirror/autocomplete'
14+
import { syntaxHighlighting, defaultHighlightStyle, bracketMatching } from '@codemirror/language'
15+
import { BUILTIN_EXPR_COMPLETIONS } from '@/runtime/expression'
16+
17+
export interface ExprScopeInfo {
18+
/** 全局参数名 */
19+
paramKeys?: string[]
20+
/** 自定义函数名 */
21+
functionNames?: string[]
22+
/** 组件名(用于 el('name') 提示) */
23+
elementNames?: string[]
24+
}
25+
26+
interface Props {
27+
value: string
28+
onChange: (v: string) => void
29+
placeholder?: string
30+
scope?: ExprScopeInfo
31+
height?: number | string
32+
/** 单行模式:压缩高度、禁用换行 */
33+
singleLine?: boolean
34+
}
35+
36+
function buildCompletionSource(scope: ExprScopeInfo | undefined) {
37+
return (context: CompletionContext) => {
38+
const word = context.matchBefore(/[\w.'"()]*/)
39+
if (!word) return null
40+
if (word.from === word.to && !context.explicit) return null
41+
const options = [
42+
...BUILTIN_EXPR_COMPLETIONS.map((c) => ({ label: c.label, detail: c.detail, type: c.type })),
43+
...(scope?.paramKeys ?? []).map((k) => ({ label: `params.${k}`, detail: '全局参数', type: 'variable' as const })),
44+
...(scope?.functionNames ?? []).map((n) => ({ label: `${n}()`, detail: '自定义函数', type: 'function' as const })),
45+
...(scope?.elementNames ?? []).map((n) => ({ label: `el('${n}')`, detail: '组件值', type: 'function' as const })),
46+
]
47+
return { from: word.from, options, validFor: /^[\w.'"()]*$/ }
48+
}
49+
}
50+
51+
export default function ExpressionInput({ value, onChange, placeholder, scope, height = 60, singleLine = false }: Props) {
52+
const hostRef = useRef<HTMLDivElement>(null)
53+
const viewRef = useRef<EditorView | null>(null)
54+
const onChangeRef = useRef(onChange)
55+
onChangeRef.current = onChange
56+
const scopeRef = useRef(scope)
57+
scopeRef.current = scope
58+
59+
const effectiveHeight = singleLine ? 32 : height
60+
61+
useEffect(() => {
62+
if (!hostRef.current) return
63+
const state = EditorState.create({
64+
doc: value,
65+
extensions: [
66+
history(),
67+
bracketMatching(),
68+
closeBrackets(),
69+
javascript(),
70+
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
71+
// 用函数包一层读取 scopeRef.current,保证补全始终用最新作用域
72+
autocompletion({ override: [(ctx) => buildCompletionSource(scopeRef.current)(ctx)] }),
73+
keymap.of([...defaultKeymap, ...historyKeymap, ...completionKeymap]),
74+
singleLine ? [] : EditorView.lineWrapping,
75+
EditorView.theme({
76+
'&': { fontSize: '12px', minHeight: typeof effectiveHeight === 'number' ? `${effectiveHeight}px` : effectiveHeight },
77+
'.cm-scroller': { fontFamily: 'var(--font-mono, monospace)', overflow: singleLine ? 'hidden' : 'auto' },
78+
'.cm-content': { caretColor: 'var(--accent)', minHeight: typeof effectiveHeight === 'number' ? `${effectiveHeight}px` : effectiveHeight },
79+
'&.cm-focused': { outline: 'none' },
80+
}),
81+
EditorView.updateListener.of((u) => {
82+
if (u.docChanged) onChangeRef.current(u.state.doc.toString())
83+
}),
84+
],
85+
})
86+
const view = new EditorView({ state, parent: hostRef.current })
87+
viewRef.current = view
88+
return () => {
89+
// Null the ref first so the [value] effect can't dispatch onto a
90+
// view that is being torn down (row deletion / remount races).
91+
viewRef.current = null
92+
try { view.destroy() } catch { /* view already detached */ }
93+
}
94+
// eslint-disable-next-line react-hooks/exhaustive-deps
95+
}, [singleLine])
96+
97+
useEffect(() => {
98+
const view = viewRef.current
99+
if (!view) return
100+
try {
101+
const cur = view.state.doc.toString()
102+
if (cur !== value) {
103+
view.dispatch({ changes: { from: 0, to: cur.length, insert: value } })
104+
}
105+
} catch { /* view mid-teardown; ignore stale sync */ }
106+
}, [value])
107+
108+
return (
109+
<div
110+
style={{
111+
border: '1px solid var(--border)',
112+
borderRadius: 'var(--radius-sm)',
113+
overflow: 'hidden',
114+
background: 'var(--bg-base)',
115+
position: 'relative',
116+
}}
117+
>
118+
{!value && placeholder && (
119+
<div style={{
120+
position: 'absolute', top: 4, left: 8, fontSize: 12,
121+
color: 'var(--text-muted)', pointerEvents: 'none', zIndex: 1,
122+
fontFamily: 'var(--font-mono, monospace)', whiteSpace: 'pre-wrap',
123+
}}>
124+
{placeholder}
125+
</div>
126+
)}
127+
<div ref={hostRef} />
128+
</div>
129+
)
130+
}

0 commit comments

Comments
 (0)