feat(v2): T-308 group monitor/settings into white cards for visual separation
Adds a card() surface (white rounded panel, 1px #C8D4E3 stroke, 8px radius) matching the light-Windows spec, and groups the monitor (video / status+ controls / gallery) and settings (camera / params / deps) into cards so the gray-blue page shows through the gutters. Components were previously flat on one background with no separation. Cross-compiles for Windows. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -622,3 +622,12 @@
|
|||||||
- 阻塞:`cmd/silver-pose` 全量构建需 Windows+CGo(onnxruntime)+Gio;窗口目视/表单交互需 Windows。
|
- 阻塞:`cmd/silver-pose` 全量构建需 Windows+CGo(onnxruntime)+Gio;窗口目视/表单交互需 Windows。
|
||||||
- 决策:账号密码按用户要求进设置(内网演示),但仅存未跟踪 config.local.json、UI 掩码、不入日志/版本/事件;source.id 用固定/原值避免把内网 IP 写进 JSONL。设置改动"下次开始监控生效"经控制器重载 config 实现(对齐 V1 草稿→运行快照语义)。
|
- 决策:账号密码按用户要求进设置(内网演示),但仅存未跟踪 config.local.json、UI 掩码、不入日志/版本/事件;source.id 用固定/原值避免把内网 IP 写进 JSONL。设置改动"下次开始监控生效"经控制器重载 config 实现(对齐 V1 草稿→运行快照语义)。
|
||||||
- 下一步:用户 Windows `go run ./cmd/silver-pose` 目视——80% 窗口、2×3 画廊、Esc 关图、按钮灰显、设置页可编辑并保存后重启监控生效。
|
- 下一步:用户 Windows `go run ./cmd/silver-pose` 目视——80% 窗口、2×3 画廊、Esc 关图、按钮灰显、设置页可编辑并保存后重启监控生效。
|
||||||
|
|
||||||
|
## 【2026-07-23】T-308 收尾:监控/设置组件加白色卡片边框
|
||||||
|
|
||||||
|
- 状态:DOING(卡片布局代码完成 + 交叉编译通过;观感需 Windows 目视)
|
||||||
|
- 变更:新增 `card()`(白圆角面板 `#FFFFFF` + `#C8D4E3` 1px 细边 + 8px 圆角,`op.Record` 量出内容再画底/描边),对齐 UI 规范"浅灰蓝底+白卡片"。监控页分三卡:视频卡、状态控制卡(运行/检测状态+开始停止+事件)、画廊卡(标题+2×3 缩略图);设置页分三卡:摄像头连接、常用参数、运行依赖。新增 `verticalRows` 给卡内行加统一间距。此前组件平铺在同一底色上、无分隔,偏离了规范;本次补齐。
|
||||||
|
- 验证:`gofmt` 干净;`GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -mod=vendor ./internal/ui/... ./cmd/ui-spike/...` 退出码 0(含 `card()` 的 `op.Record`/`clip.UniformRRect.Push` 等 API)。
|
||||||
|
- 阻塞:卡片间距/描边深浅、缩略图是否再加细边等观感需 Windows 目视微调。
|
||||||
|
- 决策:按功能分组成少量卡(视频/状态控制/画廊、摄像头/参数/依赖),不逐行套框以免过碎;卡间留 8–12px 浅灰蓝缝。
|
||||||
|
- 下一步:用户 Windows 目视卡片分隔观感;据反馈微调后 T-308 置 DONE。
|
||||||
|
|||||||
+106
-54
@@ -474,10 +474,12 @@ func (window *Window) layoutMonitor(gtx layout.Context, model ViewModel, hasFram
|
|||||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||||
layout.Flexed(7, func(gtx layout.Context) layout.Dimensions {
|
layout.Flexed(7, func(gtx layout.Context) layout.Dimensions {
|
||||||
return layout.UniformInset(unit.Dp(8)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
return layout.UniformInset(unit.Dp(8)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||||
if !hasFrame {
|
return card(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||||
return layout.Center.Layout(gtx, material.Body1(window.theme, model.ConnectionText).Layout)
|
if !hasFrame {
|
||||||
}
|
return layout.Center.Layout(gtx, material.Body1(window.theme, model.ConnectionText).Layout)
|
||||||
return widget.Image{Src: window.imageOp, Fit: widget.Contain, Position: layout.Center}.Layout(gtx)
|
}
|
||||||
|
return widget.Image{Src: window.imageOp, Fit: widget.Contain, Position: layout.Center}.Layout(gtx)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
layout.Flexed(3, func(gtx layout.Context) layout.Dimensions {
|
layout.Flexed(3, func(gtx layout.Context) layout.Dimensions {
|
||||||
@@ -493,57 +495,86 @@ func (window *Window) layoutRightPanel(gtx layout.Context, model ViewModel, runn
|
|||||||
if running && window.stopButton.Clicked(gtx) {
|
if running && window.stopButton.Clicked(gtx) {
|
||||||
window.stop()
|
window.stop()
|
||||||
}
|
}
|
||||||
return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
return layout.UniformInset(unit.Dp(8)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||||
// Row 1: connection status and detection status on one line.
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
return card(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||||
layout.Rigid(material.Body1(window.theme, model.ConnectionText).Layout),
|
return window.statusControlsGroup(gtx, model, running)
|
||||||
layout.Rigid(layout.Spacer{Width: unit.Dp(16)}.Layout),
|
})
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
||||||
label := material.Body1(window.theme, model.StateText)
|
|
||||||
if model.Critical {
|
|
||||||
label.Color = colorAlert
|
|
||||||
}
|
|
||||||
return label.Layout(gtx)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}),
|
}),
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||||
// Row 2: compact start/stop buttons on one line.
|
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
return card(gtx, window.galleryGroup)
|
||||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
||||||
return stateButton(gtx, window.theme, &window.startButton, "开始监控", !running)
|
|
||||||
}),
|
|
||||||
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
||||||
return stateButton(gtx, window.theme, &window.stopButton, "停止监控", running)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}),
|
}),
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
|
||||||
// Row 3: event text and the red-note on one line.
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
||||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
|
||||||
layout.Rigid(material.Body1(window.theme, model.EventText).Layout),
|
|
||||||
layout.Rigid(layout.Spacer{Width: unit.Dp(12)}.Layout),
|
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
|
||||||
note := material.Body2(window.theme, "红色=确认摔倒")
|
|
||||||
note.Color = colorSubtle
|
|
||||||
return note.Layout(gtx)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
|
||||||
layout.Rigid(material.Body2(window.theme, "最近确认截图(双击看原图)").Layout),
|
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
|
|
||||||
// Remaining area: 3x3 reverse-chronological screenshot gallery.
|
|
||||||
layout.Flexed(1, window.layoutGallery),
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (window *Window) statusControlsGroup(gtx layout.Context, model ViewModel, running bool) layout.Dimensions {
|
||||||
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||||
|
layout.Rigid(material.Body1(window.theme, model.ConnectionText).Layout),
|
||||||
|
layout.Rigid(layout.Spacer{Width: unit.Dp(16)}.Layout),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
label := material.Body1(window.theme, model.StateText)
|
||||||
|
if model.Critical {
|
||||||
|
label.Color = colorAlert
|
||||||
|
}
|
||||||
|
return label.Layout(gtx)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return stateButton(gtx, window.theme, &window.startButton, "开始监控", !running)
|
||||||
|
}),
|
||||||
|
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return stateButton(gtx, window.theme, &window.stopButton, "停止监控", running)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||||
|
layout.Rigid(material.Body1(window.theme, model.EventText).Layout),
|
||||||
|
layout.Rigid(layout.Spacer{Width: unit.Dp(12)}.Layout),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
note := material.Body2(window.theme, "红色=确认摔倒")
|
||||||
|
note.Color = colorSubtle
|
||||||
|
return note.Layout(gtx)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (window *Window) galleryGroup(gtx layout.Context) layout.Dimensions {
|
||||||
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||||
|
layout.Rigid(material.Body2(window.theme, "最近确认截图(双击看原图)").Layout),
|
||||||
|
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
|
||||||
|
layout.Flexed(1, window.layoutGallery),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// card wraps content in a white rounded surface with a subtle stroke, so the
|
||||||
|
// gray-blue page shows through the gutters between components (per the UI spec).
|
||||||
|
func card(gtx layout.Context, inner layout.Widget) layout.Dimensions {
|
||||||
|
border := widget.Border{Color: colorStroke, Width: unit.Dp(1), CornerRadius: unit.Dp(8)}
|
||||||
|
return border.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||||
|
macro := op.Record(gtx.Ops)
|
||||||
|
dims := layout.UniformInset(unit.Dp(10)).Layout(gtx, inner)
|
||||||
|
content := macro.Stop()
|
||||||
|
defer clip.UniformRRect(image.Rectangle{Max: dims.Size}, gtx.Dp(8)).Push(gtx.Ops).Pop()
|
||||||
|
paint.Fill(gtx.Ops, colorSurface)
|
||||||
|
content.Add(gtx.Ops)
|
||||||
|
return dims
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func smallButton(gtx layout.Context, theme *material.Theme, click *widget.Clickable, label string) layout.Dimensions {
|
func smallButton(gtx layout.Context, theme *material.Theme, click *widget.Clickable, label string) layout.Dimensions {
|
||||||
return stateButton(gtx, theme, click, label, true)
|
return stateButton(gtx, theme, click, label, true)
|
||||||
}
|
}
|
||||||
@@ -660,7 +691,7 @@ func (window *Window) layoutSettings(gtx layout.Context, model ViewModel) layout
|
|||||||
if window.saveParams.Clicked(gtx) {
|
if window.saveParams.Clicked(gtx) {
|
||||||
window.onSaveParams()
|
window.onSaveParams()
|
||||||
}
|
}
|
||||||
rows := []layout.Widget{
|
cameraCard := verticalRows(
|
||||||
window.heading("摄像头连接(账号密码仅存本地配置,下次启动生效)"),
|
window.heading("摄像头连接(账号密码仅存本地配置,下次启动生效)"),
|
||||||
window.editorRow("IP / 主机", &window.editHost),
|
window.editorRow("IP / 主机", &window.editHost),
|
||||||
window.editorRow("端口", &window.editPort),
|
window.editorRow("端口", &window.editPort),
|
||||||
@@ -671,7 +702,8 @@ func (window *Window) layoutSettings(gtx layout.Context, model ViewModel) layout
|
|||||||
window.editorRow("连接超时 (秒)", &window.editTimeout),
|
window.editorRow("连接超时 (秒)", &window.editTimeout),
|
||||||
window.checkRow("低延迟", &window.lowLatency),
|
window.checkRow("低延迟", &window.lowLatency),
|
||||||
window.buttonRow(&window.saveCamera, "保存摄像头"),
|
window.buttonRow(&window.saveCamera, "保存摄像头"),
|
||||||
window.spacerRow(),
|
)
|
||||||
|
paramsCard := verticalRows(
|
||||||
window.heading("常用检测参数(下次启动生效)"),
|
window.heading("常用检测参数(下次启动生效)"),
|
||||||
window.editorRow("关键点置信度", &window.editKeypoint),
|
window.editorRow("关键点置信度", &window.editKeypoint),
|
||||||
window.editorRow("确认窗口 (秒, 1-3)", &window.editConfirm),
|
window.editorRow("确认窗口 (秒, 1-3)", &window.editConfirm),
|
||||||
@@ -681,18 +713,38 @@ func (window *Window) layoutSettings(gtx layout.Context, model ViewModel) layout
|
|||||||
window.checkRow("要求膝踝清晰", &window.requireLower),
|
window.checkRow("要求膝踝清晰", &window.requireLower),
|
||||||
window.buttonRow(&window.saveParams, "保存参数"),
|
window.buttonRow(&window.saveParams, "保存参数"),
|
||||||
window.labelRow(window.settingsStatus),
|
window.labelRow(window.settingsStatus),
|
||||||
window.spacerRow(),
|
)
|
||||||
|
depsCard := verticalRows(
|
||||||
window.heading("运行依赖(只读)"),
|
window.heading("运行依赖(只读)"),
|
||||||
window.labelRow("模型 SHA-256:" + shortHash(window.settings.ModelSHA256)),
|
window.labelRow("模型 SHA-256:"+shortHash(window.settings.ModelSHA256)),
|
||||||
window.labelRow("运行依赖:" + window.settings.RuntimeSummary),
|
window.labelRow("运行依赖:"+window.settings.RuntimeSummary),
|
||||||
|
)
|
||||||
|
sections := []layout.Widget{
|
||||||
|
func(gtx layout.Context) layout.Dimensions { return card(gtx, cameraCard) },
|
||||||
|
func(gtx layout.Context) layout.Dimensions { return card(gtx, paramsCard) },
|
||||||
|
func(gtx layout.Context) layout.Dimensions { return card(gtx, depsCard) },
|
||||||
}
|
}
|
||||||
return layout.UniformInset(unit.Dp(12)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
return layout.UniformInset(unit.Dp(8)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||||
return material.List(window.theme, &window.settingsList).Layout(gtx, len(rows), func(gtx layout.Context, index int) layout.Dimensions {
|
return material.List(window.theme, &window.settingsList).Layout(gtx, len(sections), func(gtx layout.Context, index int) layout.Dimensions {
|
||||||
return layout.UniformInset(unit.Dp(4)).Layout(gtx, rows[index])
|
return layout.UniformInset(unit.Dp(6)).Layout(gtx, sections[index])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// verticalRows stacks row widgets with a small gap between them.
|
||||||
|
func verticalRows(rows ...layout.Widget) layout.Widget {
|
||||||
|
return func(gtx layout.Context) layout.Dimensions {
|
||||||
|
children := make([]layout.FlexChild, 0, len(rows)*2)
|
||||||
|
for i := range rows {
|
||||||
|
if i > 0 {
|
||||||
|
children = append(children, layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout))
|
||||||
|
}
|
||||||
|
children = append(children, layout.Rigid(rows[i]))
|
||||||
|
}
|
||||||
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, children...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (window *Window) heading(text string) layout.Widget {
|
func (window *Window) heading(text string) layout.Widget {
|
||||||
return func(gtx layout.Context) layout.Dimensions {
|
return func(gtx layout.Context) layout.Dimensions {
|
||||||
return material.Body1(window.theme, text).Layout(gtx)
|
return material.Body1(window.theme, text).Layout(gtx)
|
||||||
|
|||||||
Reference in New Issue
Block a user