用 bun fmt 格式化全部代码和文档
CI/CD / test (push) Canceled after 0s
CI/CD / deploy (push) Canceled after 0s

- bunfig.toml 的 [fmt]:不写分号、单引号、一行最长 160,和原来的写法一致
- .prettierignore:docs/教材摘要/ 是书上的摘录,不格式化
- 模板里一行写两条语句的事件处理(@keydown.delete、@dblclick)改成调用方法:去掉分号后拆成两行,Vue 编译不过

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-26 14:02:33 +08:00
1 parent d4b8bf9241
commit de277d3e9d
102 files changed
+1971 -1059

No files matched your search

+60 -11
View File
@@ -2,14 +2,46 @@
// 图表(WinForms 的 Chart,教材项目 8 的 7 天温度折线图):运行器发来各个系列的点,用 Chart.js 画。
// 图表类型按 WinForms 的 SeriesChartType 对应;配色是 WinForms 默认的 BrightPastel;图例默认在右边;不要动画。
import {
ArcElement, BarController, BarElement, CategoryScale, Chart, DoughnutController, Filler, Legend, LinearScale, LineController,
LineElement, PieController, PointElement, RadarController, RadialLinearScale, Title, Tooltip, type ChartConfiguration,
ArcElement,
BarController,
BarElement,
CategoryScale,
Chart,
DoughnutController,
Filler,
Legend,
LinearScale,
LineController,
LineElement,
PieController,
PointElement,
RadarController,
RadialLinearScale,
Title,
Tooltip,
type ChartConfiguration,
} from 'chart.js'
import { computed, onBeforeUnmount, onMounted, ref, watch, type CSSProperties } from 'vue'
import type { ControlNode } from '@iot/shared'
Chart.register(ArcElement, BarController, BarElement, CategoryScale, DoughnutController, Filler, Legend, LinearScale, LineController,
LineElement, PieController, PointElement, RadarController, RadialLinearScale, Title, Tooltip)
Chart.register(
ArcElement,
BarController,
BarElement,
CategoryScale,
DoughnutController,
Filler,
Legend,
LinearScale,
LineController,
LineElement,
PieController,
PointElement,
RadarController,
RadialLinearScale,
Title,
Tooltip,
)
interface ChartSeries {
name: string
@@ -33,7 +65,23 @@ interface ChartData {
const props = defineProps<{ node: ControlNode; style: CSSProperties }>()
/** WinForms 图表默认的 BrightPastel 配色 */
const PALETTE = ['#418CF0', '#FCB441', '#E0400A', '#056492', '#BFBFBF', '#1A3B69', '#FFE382', '#129CDD', '#CA6B4B', '#005CDB', '#F3D288', '#506381', '#F1B9A8', '#E0830A', '#7893BE']
const PALETTE = [
'#418CF0',
'#FCB441',
'#E0400A',
'#056492',
'#BFBFBF',
'#1A3B69',
'#FFE382',
'#129CDD',
'#CA6B4B',
'#005CDB',
'#F3D288',
'#506381',
'#F1B9A8',
'#E0830A',
'#7893BE',
]
const data = computed(() => props.node.props.Chart as ChartData | undefined)
const canvas = ref<HTMLCanvasElement>()
@@ -84,12 +132,13 @@ function config(d: ChartData): ChartConfiguration {
legend: { display: d.series.some((s) => s.inLegend), position, labels: { filter: (item) => d.series[item.datasetIndex ?? 0]?.inLegend ?? true } },
title: { display: d.titles.length > 0, text: d.titles },
},
scales: pie || chartKind === 'radar'
? {}
: {
x: { stacked, grid: { color: '#d9d9d9' } },
y: { stacked, min: d.yMin ?? undefined, max: d.yMax ?? undefined, beginAtZero: true, grid: { color: '#d9d9d9' } },
},
scales:
pie || chartKind === 'radar'
? {}
: {
x: { stacked, grid: { color: '#d9d9d9' } },
y: { stacked, min: d.yMin ?? undefined, max: d.yMax ?? undefined, beginAtZero: true, grid: { color: '#d9d9d9' } },
},
},
} as ChartConfiguration
}
+2 -12
View File
@@ -95,13 +95,7 @@ const itemHeight = computed(() => `${(p.value.ItemHeight as number | undefined)
<template>
<!-- Button -->
<button
v-if="node.type === 'Button'"
class="wf-control wf-button"
:style="style"
:disabled="!enabled"
@click="act({ action: 'click' })"
>
<button v-if="node.type === 'Button'" class="wf-control wf-button" :style="style" :disabled="!enabled" @click="act({ action: 'click' })">
{{ displayText(p.Text) }}
</button>
@@ -230,11 +224,7 @@ const itemHeight = computed(() => `${(p.value.ItemHeight as number | undefined)
</div>
<!-- 教材控件库:LED 显示屏 -->
<LedControl
v-else-if="node.type === 'WinFormsDeviceControlLibrary.LED'"
:node="node"
:style="style"
/>
<LedControl v-else-if="node.type === 'WinFormsDeviceControlLibrary.LED'" :node="node" :style="style" />
<!-- 其他容器(Panel、UserControl 等):按普通容器渲染 -->
<div v-else class="wf-control wf-panel" :style="style" :title="node.type">
+7 -7
View File
@@ -53,6 +53,11 @@ function click(row: number, column: number) {
if (col?.check && !col.readOnly && p.value.ReadOnly !== true) props.act({ action: 'cellEdit', row, column, value: cell === true ? 'False' : 'True' })
}
function doubleClick(row: number, column: number) {
props.act({ action: 'cellDoubleClick', row, column })
startEdit(row, column)
}
function startEdit(row: number, column: number, initial?: string) {
const col = columns.value.find((c) => c.index === column)
if (!props.enabled || !col || col.readOnly || col.check || p.value.ReadOnly === true) return
@@ -137,12 +142,7 @@ function columnStyle(column: Column): CSSProperties {
</tr>
</thead>
<tbody>
<tr
v-for="(row, r) in rows.items"
:key="r"
:class="{ 'wf-dgv-selected': selectedRows.has(r) }"
:style="{ height: `${rows.height}px` }"
>
<tr v-for="(row, r) in rows.items" :key="r" :class="{ 'wf-dgv-selected': selectedRows.has(r) }" :style="{ height: `${rows.height}px` }">
<th v-if="rowHeaders" class="wf-dgv-rowheader" @click="click(r, -1)">
{{ current?.[0] === r ? '▶' : row.isNew ? '*' : '' }}
</th>
@@ -152,7 +152,7 @@ function columnStyle(column: Column): CSSProperties {
:class="{ 'wf-dgv-current': isCurrent(r, column.index) }"
:style="columnStyle(column)"
@click="click(r, column.index)"
@dblclick="act({ action: 'cellDoubleClick', row: r, column: column.index }); startEdit(r, column.index)"
@dblclick="doubleClick(r, column.index)"
>
<input
v-if="editing && editing.row === r && editing.column === column.index"
+1 -2
View File
@@ -33,8 +33,7 @@ function pick(event: Event) {
if (!picked) return
const [date, time] = value.value.split('T')
const withSeconds = (t: string) => (t.length === 5 ? `${t}:00` : t)
const next =
inputType.value === 'date' ? `${picked}T${time}` : inputType.value === 'time' ? `${date}T${withSeconds(picked)}` : withSeconds(picked)
const next = inputType.value === 'date' ? `${picked}T${time}` : inputType.value === 'time' ? `${date}T${withSeconds(picked)}` : withSeconds(picked)
props.act({ action: 'pick', value: next })
}
+4 -1
View File
@@ -26,7 +26,10 @@ const props = defineProps<{
const p = computed(() => props.form.props)
const runImages = inject(RUN_IMAGES, new Map<string, string>())
provide(AMBIENT_BACK_COLOR, computed(() => (p.value.BackColor as string | null) ?? DEFAULT_BACK_COLOR))
provide(
AMBIENT_BACK_COLOR,
computed(() => (p.value.BackColor as string | null) ?? DEFAULT_BACK_COLOR),
)
const borderless = computed(() => p.value.FormBorderStyle === 'None')
// WinForms 中先加入 Controls 的控件在最上层,所以倒序渲染
const childrenTopLast = computed(() => [...props.form.children].reverse())
+32 -6
View File
@@ -8,13 +8,39 @@ const emit = defineEmits<{ reply: [result: string] }>()
const BUTTONS: Record<string, [string, string][]> = {
OK: [['OK', '确定']],
OKCancel: [['OK', '确定'], ['Cancel', '取消']],
YesNo: [['Yes', '是(Y)'], ['No', '否(N)']],
YesNoCancel: [['Yes', '是(Y)'], ['No', '否(N)'], ['Cancel', '取消']],
RetryCancel: [['Retry', '重试(R)'], ['Cancel', '取消']],
AbortRetryIgnore: [['Abort', '中止(A)'], ['Retry', '重试(R)'], ['Ignore', '忽略(I)']],
OKCancel: [
['OK', '确定'],
['Cancel', '取消'],
],
YesNo: [
['Yes', '是(Y)'],
['No', '否(N)'],
],
YesNoCancel: [
['Yes', '是(Y)'],
['No', '否(N)'],
['Cancel', '取消'],
],
RetryCancel: [
['Retry', '重试(R)'],
['Cancel', '取消'],
],
AbortRetryIgnore: [
['Abort', '中止(A)'],
['Retry', '重试(R)'],
['Ignore', '忽略(I)'],
],
}
const ICONS: Record<string, string> = {
Error: '⛔',
Hand: '⛔',
Stop: '⛔',
Warning: '⚠️',
Exclamation: '⚠️',
Information: 'ℹ️',
Asterisk: 'ℹ️',
Question: '❓',
}
const ICONS: Record<string, string> = { Error: '⛔', Hand: '⛔', Stop: '⛔', Warning: '⚠️', Exclamation: '⚠️', Information: 'ℹ️', Asterisk: 'ℹ️', Question: '❓' }
const buttons = computed(() => BUTTONS[props.box.buttons] ?? BUTTONS.OK)
const icon = computed(() => ICONS[props.box.icon])
+1 -1
View File
@@ -22,7 +22,7 @@ const vertical = computed(() => p.value.Orientation === 'Vertical')
const tickStyle = computed(() => String(p.value.TickStyle ?? 'BottomRight'))
const size = computed(() => p.value.Size as { width: number; height: number } | undefined)
/** 轨道方向上的长度 */
const length = computed(() => (vertical.value ? size.value?.height ?? 104 : size.value?.width ?? 104))
const length = computed(() => (vertical.value ? (size.value?.height ?? 104) : (size.value?.width ?? 104)))
/** 值 → 沿轨道的位置(像素);竖向时最小值在下面 */
function position(v: number) {
+1 -2
View File
@@ -28,8 +28,7 @@ export function imageSources(value: unknown, images: ReadonlyMap<string, string>
}
/** <img> 的 srcset:有两倍图时浏览器按屏幕分辨率选 */
export const srcset = (sources: { src: string; hidpi?: string } | undefined) =>
sources?.hidpi ? `${sources.src} 1x, ${sources.hidpi} 2x` : undefined
export const srcset = (sources: { src: string; hidpi?: string } | undefined) => (sources?.hidpi ? `${sources.src} 1x, ${sources.hidpi} 2x` : undefined)
/** 背景图片的 CSS:BackgroundImageLayout 为 Tile(默认,平铺)、None(左上角一张)、Center、Stretch、Zoom */
export function backgroundCss(value: unknown, layout: unknown, images: ReadonlyMap<string, string>): CSSProperties {
+7 -7
View File
@@ -5,7 +5,7 @@
background: #f0f0f0;
border: 1px solid #1883d7;
box-shadow: 0 6px 24px rgb(0 0 0 / 0.25);
font-family: "SimSun", "Songti SC", "Noto Serif CJK SC", "Noto Serif SC", serif;
font-family: 'SimSun', 'Songti SC', 'Noto Serif CJK SC', 'Noto Serif SC', serif;
font-size: 12px;
color: #000;
user-select: none;
@@ -26,7 +26,7 @@
.wf-title {
flex: 1;
padding-left: 8px;
font-family: "Microsoft YaHei", "PingFang SC", "Noto Sans CJK SC", sans-serif;
font-family: 'Microsoft YaHei', 'PingFang SC', 'Noto Sans CJK SC', sans-serif;
font-size: 12px;
white-space: nowrap;
overflow: hidden;
@@ -373,7 +373,7 @@
flex-direction: column;
background: #000;
color: #9aa4b2;
font-family: "Microsoft YaHei", "PingFang SC", sans-serif;
font-family: 'Microsoft YaHei', 'PingFang SC', sans-serif;
font-size: 11px;
overflow: hidden;
}
@@ -456,15 +456,15 @@
border-radius: 50%;
}
.wf-checkbox-box[data-checked="true"]::after {
content: "✓";
.wf-checkbox-box[data-checked='true']::after {
content: '✓';
display: block;
font: bold 11px/11px sans-serif;
text-align: center;
color: #333;
}
.wf-radio-dot[data-checked="true"] {
.wf-radio-dot[data-checked='true'] {
background: radial-gradient(circle, #333 0 3px, #fff 3.5px);
}
@@ -551,7 +551,7 @@
align-items: center;
padding: 24px 28px;
background: #fff;
font-family: "Microsoft YaHei", "PingFang SC", "Noto Sans CJK SC", sans-serif;
font-family: 'Microsoft YaHei', 'PingFang SC', 'Noto Sans CJK SC', sans-serif;
}
.wf-messagebox-icon {