Commit Graph
255 Commits
Author SHA1 Message Date
pnoker 0be2d69ffa feat(menu): multilingual menu titles (form + render utility)
Replaces the "store an i18n key in menu_ext.content.title and resolve
it via frontend locales" design with per-locale titles maintained by
the admin in the menu CRUD form. The menu store type gains a
titles: Record<string,string> field, and MenuEditForm now asks for
Chinese / English display names directly instead of an nav.* key.

A new resolveMenuTitle(node) helper in utils/MenuUtil.ts gives the
rest of the UI a single place to pick the title with the right
fallback chain: titles[currentLocale] → titles.en → any populated
locale → legacy content.title (via $t for nav.* keys still in the
database) → menuName. Any time a menu is rendered — sidebar,
breadcrumb, settings left nav — it should go through this helper.

Old rows that still carry content.title keep rendering via the
legacy-fallback branch until the dc3-docker migration converts them
to titles; no hard cutover required.
2026-05-03 02:38:39 +08:00
pnoker 6a56bf0f82 feat(filter): RangeSegmented component + rangeKey API propagation
Extract the "last 24h / 7d / 30d" (+ today) time-range picker into a
reusable RangeSegmented component and replace six ad-hoc el-segmented
occurrences across the dashboard, event, and point-value pages:

- TrendChart / AnalyticsTabs / LatencyChart / ActivityHeatmap on Home
- EventTable filter bar (also drops the orphaned (sourceId) suffix on
  the entity column so the table shows only the resolved name)
- PointValueTool (also repositions filters so the time range sits last:
  Point Name → Enable → Time Range)

On the wire the component emits rangeKey strings ("today"/"24h"/"7d"
/"30d") that the new backend TimeRangeUtil resolves into a concrete
from-timestamp; all dashboard API helpers in src/api/dashboard.ts
(timeseries / top / latency / activity / alertPage) gain an optional
rangeKey parameter alongside legacy rangeHours / days. Two newly added
endpoints (alertTrend / alertTopSources) round out the API surface
used by the event-overview page.
2026-05-03 02:37:54 +08:00
pnoker cbfe670456 feat(event): overview widgets (trend chart + top sources + recent unconfirmed)
Three components powering the event overview page:

- EventTrendChart renders the 30-day alarm volume split by
  device/driver source, consuming /alert/trend
- TopSourcesChart lists the Top N noisiest event sources by count,
  consuming /alert/top-sources
- RecentUnconfirmed shows the most recent unconfirmed alarms with
  one-click drill-in to the Event page
2026-05-03 02:36:54 +08:00
pnoker e5b6f8d516 refactor(form): extract shared validation rules into FormRuleUtil
Create src/utils/FormRuleUtil.ts with reusable form rule factories:
- nameRules(t, entityName) — required + length(2-32) + CJK-safe pattern
- remarkRules(t) — max 300 characters
- NAME_PATTERN / DECIMAL_PATTERN — shared regex constants

Replaces 6x duplicated name rules and 7x duplicated remark rules
across device/profile/point add/edit forms.
2026-05-02 23:50:39 +08:00
pnoker 91f31d5ccc refactor: replace literal CJK char with unicode escape range in form validation regex patterns 2026-05-02 23:38:58 +08:00
pnoker 83cb9c3dfe refactor(i18n): consolidate duplicate keys into common.* namespace
Extract semantically duplicated i18n keys into common.*:
- common.ranges.h24/d7/d30 (from home.ranges)
- common.levelInfo/levelWarn/levelError (from home.alertList)
- common.confirmed/unconfirmed (from home.alertList, settings.event)
- common.remarkLength/nameRequired/nameLength/nameFormat (from
  device/profile/point add/edit sections)

common.nameRequired uses {name} placeholder for entity-specific
messages. Updated 15 Vue/TS files to reference common.* keys.
2026-05-02 23:34:55 +08:00
pnoker 9dba65f422 feat(filter): add time range segmented controls to Event and PointValue pages
Add All/Last 24h/Last 7d/Last 30d segmented filters to Driver Event,
Device Event, and PointValue list pages. Backend passes rangeHours to
mapper for SQL-level time filtering. Fix Jackson deserialization error
by filtering empty strings before API calls.
2026-05-02 23:34:41 +08:00
pnoker 42d401f4e9 feat(dashboard): today alarms + total data volume subtitle
- Rename Driver Alerts → Today's Driver Alarms, Device Alerts → Today's Device Alarms
- Alarm cards now show today's counts instead of all-time
- Data card subtitle shows total data volume (累计 X 条)
- Add todayTotal i18n key for zh/en
2026-05-02 22:36:11 +08:00
pnoker 2ca73cc938 fix(dashboard): Recent Alerts badge shows actual list count
Badge now displays rows.length (items visible in the list) instead of
the system-wide unconfirmed total, so the number always matches what
the user sees below.
2026-05-02 21:38:40 +08:00
pnoker 9257f9fd00 fix(tool): default Enable filter to ALL across all search bars
Initialize formData.enableFlag to '' so el-segmented shows ALL as
selected on mount. Also restore the default after reset in the 5 tool
components that use manual key-deletion (Menu, Api, Resource, Role, User).

Affected: MenuTool, ApiTool, ResourceTool, RoleTool, UserTool,
DriverTool, DeviceTool, ProfileTool, PointTool, PointValueTool.
2026-05-02 21:31:23 +08:00
pnoker 92f9963416 feat(ui): sidebar language submenu, Event overview fix, About page content
- Layout: language switch as inline click-to-expand submenu under avatar
  dropdown (replaces hover-based clipped panel)
- Layout: breadcrumb name mapping for Settings sub-pages
- Settings sidebar: add ROUTE_ALIAS for settingsEventOverview navigation,
  reorder Event children (Overview, Driver Event, Device Event)
- About page: add platform intro, architecture overview, design objectives,
  and license sections from README
- Event Overview: rewrite to use shared StatCard component
- i18n: fix plural/singular mismatch in English nav labels, add Event
  overview translations
2026-05-02 21:30:58 +08:00
pnoker e779ccc503 feat(dashboard): split alert cards by source + 6-column grid + trend for all cards
- Move StatCard to shared component directory (components/card/stat/)
- Add 6th stat card for device alerts, switch to 6-column grid layout
- Alert card shows driver alerts, Device Alerts card shows device alerts
- All stat cards now display sparkline trend with direction indicator
- AlertList badge computes count from per-source fields (driverAlerts +
  deviceAlerts) instead of merged total
- LiveDataFeed converts to timeline format with driver tag display
- Reduce banner-to-cards gap
2026-05-02 21:30:38 +08:00
pnoker a9b2b4bd2a refactor(settings): split Event into Device/Driver sub-pages + Overview
Event.vue was a single merged table over dc3_device_event and
dc3_driver_event, which forced magic column names like "Source ID"
that meant different things per row and a Point ID column that only
made sense for device rows. Splitting along the source boundary
removes the ambiguity.

- views/settings/event/Overview.vue — landing page for the parent
  menu, 4 stat cards (device total/unconfirmed, driver total/
  unconfirmed) linking into the detail pages.
- views/settings/event/DeviceEvent.vue + DriverEvent.vue — thin
  wrappers around a shared EventTable.vue component that takes the
  source as a prop. EventTable hides Point ID on driver pages, looks
  up device / driver names via the right facade, and keeps all the
  confirm / unconfirm / bulk plumbing in one place.
- router adds settingsDeviceEvent / settingsDriverEvent; Layout
  breadcrumb nameMap and zh/en i18n extended to match.
2026-05-02 17:45:43 +08:00
pnoker 89f6a7166b feat(home): AlertList shows the alert message
Pulls row.message (now populated by /dashboard/alert/latest) into
each timeline item as a second line under the tags, clamped to two
lines with an ellipsis — tooltip on hover gives the full text. The
device/driver name moved inline with the level/source chips so the
message gets the visual weight it deserves.
2026-05-02 17:11:41 +08:00
pnoker 0a3c5322d0 chore(i18n): rename "Data" labels to "Point Value" on the home page
Align the stat card and live feed titles with the PointValue menu
(/settings renamed that route a while back). Two labels, both value
changes only — keys stay the same so no other code needs to follow.

- home.todayData:   "Today's Data"       -> "Today's Point Value"
                    "今日数据量"           -> "今日位号数据"
- home.liveFeed.title: "Live Data Feed"  -> "Live Point Value Feed"
                       "最新采集数据"     -> "实时位号数据流"
2026-05-02 17:06:55 +08:00
pnoker 9dfaaf3b73 refactor(settings): merge Alarm into Event
Alarm.vue was a near-duplicate of Event.vue with eventTypeFlag pinned
to 1 — and the Event toolbar already exposes that same filter. Keeping
both pages meant two places to maintain the exact same confirm / undo
/ bulk plumbing.

- delete src/views/settings/alarm/
- drop settingsAlarm route
- drop settingsAlarm from Layout breadcrumb nameMap and both locale
  nav blocks
- prune settings.alarm.* i18n (Event page uses settings.event.*)

Event.vue already shipped ALARM as a type filter, so no user-visible
feature regresses.
2026-05-02 17:04:13 +08:00
pnoker 60589dc652 refactor(home): timeline alerts, device banner, stat refresh, popconfirms
Post-feedback polish round covering the 10-item list:

- AlertList: flat rows replaced by el-timeline grouped by date; each
  item shows level tag (info/warn/error), source tag and device/driver
  name. Confirmed rows render a hollow dot so the eye can tell them
  apart from the active ones.
- HomeBanner: adds a Devices row (total / online, same dot tone
  scheme as Drivers). LiveFeed's interval picker becomes el-segmented
  for consistency with the rest of the page. Service dots aligned by
  giving each service chip a min-width.
- StatCard: optional onRefresh prop — when set, a text-only refresh
  button surfaces on hover in the top-right. Home's 5 cards each
  provide a reload closure so users can repoll without a full page
  refresh. Clicks don't bubble, so refresh doesn't navigate.
- Event + Alarm pages: migrate from ad-hoc inline toolbar to ToolCard
  (same filter-card style as User / Role / Resource pages). Every
  confirm action goes through an el-popconfirm — inline, and bulk via
  a selection toolbar that appears when rows are checked. Bulk action
  hits a single /dashboard/alert/bulk-confirm round-trip.
- About page: service health section inherits the new min-width
  alignment so dots line up in a column.
- Element-plus icons: drop the hand-maintained ICONS allow-list; every
  icon exported from @element-plus/icons-vue is now globally registered
  so template strings like `<component :is="'Bell'" />` resolve for any
  menu whose menu_ext.content.icon names a real icon. Fixes the
  Event/Alarm/About menus rendering with a blank icon slot.
2026-05-02 16:56:32 +08:00
pnoker 6b9f4a6e1b feat(settings): add About, Event, Alarm pages
Three new Settings children under /settings:

- /settings/about — Platform card (name / version from package.json /
  license / docs / repo / copyright) plus a Service Health card that
  reuses the /dashboard/system/health endpoint the banner already
  polls. Manual refresh via the header button.
- /settings/event — Paged event list across dc3_device_event and
  dc3_driver_event, filterable by source / event type / confirm
  state. Row actions include inline confirm. Backed by the new
  /dashboard/alert/page endpoint.
- /settings/alarm — Same table shape as Events but pins
  eventTypeFlag=1 on every request; users only pick source / confirm
  state. Confirm flips the row, list refreshes.

Router, breadcrumb nameMap and zh/en i18n extended so each page shows
up correctly in the sidebar and crumbs.
2026-05-02 16:18:49 +08:00
pnoker 5181b6ecc2 feat(home): wire sparklines for all 5 stat cards
Previously only Today's Data had a sparkline. Now:

- Driver / Device / Point cards show the last 7 days of new rows
  (from /manager/dashboard/growth)
- Alert card shows the last 24 hours of event counts hour-by-hour
  (from the new sparkline24h field on /dashboard/alert/stats)
- Today's Data keeps its hourly 24h series

All five cards render the same sparkline component at the same height,
so the strip reads as one consistent row.
2026-05-02 16:07:41 +08:00
pnoker ef8abdceaa refactor(home): consistent card headers, segmented ranges, refresh buttons
Five card-level polish items after a once-over pass:

- Card header padding: AnalyticsTabs was using 4px 12px 0 (tabs-tuned)
  which made its header visibly shorter than TrendChart / LatencyChart
  / ActivityHeatmap / LiveFeed / AlertList. All now use 12px 16px so
  the whole row lines up.
- Range selectors: AnalyticsTabs top-N range swapped el-select (drop-
  down) -> el-segmented (tab bar), matching TrendChart / LatencyChart
  / ActivityHeatmap. One control style across the page.
- Refresh buttons: AnalyticsTabs, TrendChart, LatencyChart and
  ActivityHeatmap now carry a small circle refresh icon in the header,
  on par with LiveDataFeed and AlertList. Re-fetching a chart no
  longer requires a full page reload.
- ActivityHeatmap default range is now 24h (was 7d). The 24h single-
  row view is the most common check, and the segmented control covers
  the wider windows on demand.
- HomeBanner clock/date now localize via the active vue-i18n locale
  (zh -> zh-CN, en -> en-US) instead of always toLocaleTimeString(
  'zh-CN', ...). Switching language updates the banner on next tick.
2026-05-02 16:02:18 +08:00
pnoker 2a8e235742 feat(home): full service/infra/drivers health on the banner
HomeBanner now polls /dashboard/system/health every 30s and renders
three grouped rows on the right:

- Center: auth / data / manager
- Infra:  database / mq / gateway
- Drivers: N / total with partial-state (amber) when some online

Banner owns the request; Home.vue no longer passes a status prop and
index.ts no longer derives service up/down from stat request success.
Adds home.banner.serviceDatabase / serviceMq / serviceGateway and the
group labels to zh/en i18n.
2026-05-02 15:39:35 +08:00
pnoker 73649b2101 refactor(home): compact 2-column layout + forced 5-wide stat strip
Three adjustments tied together:

- Hourly Data Activity adds a 24h range next to 7d / 30d. The heatmap
  degrades gracefully to a single-day row rather than being
  unavailable for that window.
- Home layout moves to the double-column shape the user picked:
    Row 2  AnalyticsTabs(16)  LiveFeed(8)
    Row 3  TrendChart(16)     AlertList(8)
    Row 4  Latency(12)        Heatmap(12)
  AlertList is no longer full-width; the page is shorter and the
  analytics row mirrors directly beneath the Tabs row.
- Stat strip is now CSS grid (repeat(5, 1fr)) instead of el-col
  spans. Five cards always fit on one line on desktop (>=1280px)
  regardless of how many indicators get added later. Below 1280px
  fall back to 2 columns so cards keep their usable width, 1 column
  below 640px.
2026-05-02 15:31:24 +08:00
pnoker 73760142ee feat(home): latency histogram + hourly activity heatmap
- LatencyChart: bar chart over 6 fixed latency bins with 24h/7d/30d
  segmented range selector. Colours grade from green (fast) to red
  (slow) so an eye-only glance tells you if the pipeline is healthy.
- ActivityHeatmap: 7 x 24 grid of (day-of-week x hour-of-day) cell
  counts using G2's sequential blue palette; 7d/30d range selector.
  Surfaces ingestion rhythm (peak hours, idle weekends, etc.) without
  needing to query the raw timeseries by hand.
- dashboard.ts: two new one-liners (statsLatency, statsActivity)
  following the shared axios params convention.
- Wired into a new Row 5 on Home.vue, half-width each; i18n keys
  added under home.latency.* and home.activity.*.
2026-05-02 15:16:32 +08:00
pnoker 89ed1e6b88 refactor(home): drop QuickActions, align StatCard heights
- QuickActions component removed: the main nav already covers those
  jumps, so the panel was adding visual noise without helping.
- StatCard: sparkline slot is always rendered (transparent when empty),
  card min-height pinned at 132px, body uses a column flex so the
  spark area drops to the bottom regardless of trend/subtitle presence.
  Every card in the strip now lines up at the same size.
- Home.vue row 3 collapses to a full-width AlertList.
2026-05-02 15:10:10 +08:00
pnoker 49b2125c99 refactor(icons): expose every element-plus icon via iconMap
The hand-maintained whitelist (~35 icons) meant operators saw a very
short drop-down and any icon name written to dc3_menu outside the list
rendered blank. Switch to a namespace import — iconMap is built from
every capitalized export of @element-plus/icons-vue (~280 icons), so
the menu picker covers the full library shown at
https://element-plus.org/zh-CN/component/icon and new menus never land
on an unknown glyph.

The select is already filterable, so finding a specific icon is a type
away even with the larger list.
2026-05-02 14:21:00 +08:00
pnoker cc16cd08b6 refactor(api): extract gateway base paths to src/config/constant/api.ts
Every api/*.ts module was hardcoding the same three prefixes
('api/v3/auth', 'api/v3/data', 'api/v3/manager'). Pull them into
API_AUTH_BASE / API_DATA_BASE / API_MANAGER_BASE so the prefix lives
in one place — aligns with the other static-constant files under
src/config/constant/ and makes it trivial to retarget the gateway
mount point or add a new service base.

All 17 api files (including dashboard.ts, which used to declare local
DATA_BASE/MANAGER_BASE) now import from the shared module.
2026-05-02 14:12:17 +08:00
pnoker 627fb1ff92 refactor(ui): tidy dashboard api + rename PointValue + menu icon polish
- api/dashboard.ts now passes query params via axios config (matches the
  one-liner-per-endpoint style used by api/device.ts / api/driver.ts);
  no more hand-rolled URLSearchParams.
- nav.data renamed to nav.pointValue across i18n + Layout breadcrumb
  map so the main menu and breadcrumb both read "PointValue" / "位号数据"
  instead of "Data" — matches the route name.
- Menu list: icon column renders the actual el-icon component (with the
  name beside it for scannability) rather than the raw string.
- Menu edit dialog: icon field is now a filterable el-select; each
  option previews the icon glyph so users pick visually instead of
  guessing strings.
- icons.ts expanded to cover every name used by the seed menus plus
  common spares, and exports iconNames as a stable alphabetized list
  for the picker.
2026-05-02 13:43:27 +08:00
pnoker c7765ae5d3 feat(home): add banner, quick actions, alert list and wire alert card
Adds three panels to the dashboard and connects the 5th indicator card
to real data:
- HomeBanner: greeting + live clock + per-service status dots derived
  from whether each stat request succeeded (no separate /health probe)
- QuickActions: 6 navigation shortcuts into driver/device/profile/
  data/alert/settings with tone-matched icons
- AlertList: latest N alerts from /dashboard/alert/latest with device/
  driver name resolution and unconfirmed badge

i18n (zh/en) updated for the new keys; alert indicator card subtitle
now shows unconfirmed count.
2026-05-02 11:13:20 +08:00
pnoker 4c0d972cf1 feat(home): add analytics tabs + trend chart to dashboard
D2 of the home-page revamp. Two new components land on the page:

- AnalyticsTabs.vue: six-tab card backed by four endpoints
  (manager driver/stats + device/stats for distributions, data
  /top for rankings). Tabs:
  * deviceStatus — doughnut of devices by enable flag
  * protocol — bar of drivers by type (CLIENT/SERVER/GATEWAY/CONNECT)
  * profile — horizontal bar, top profile bindings (names resolved)
  * topDevice / topPoint / topDriver — horizontal top-10, with a
    time-range select (24h / 7d / 30d)
  Id → name resolution uses the existing batch endpoints; names are
  cached per kind to avoid refetching on tab switches.

- TrendChart.vue: area + smoothed line over
  /data/dashboard/stats/timeseries. A segmented control swaps between
  Last 24h (hour buckets), Last 7d (day) and Last 30d (day).

api/dashboard.ts picks up driverStats() / deviceStats() for the
manager aggregates. Home.vue replaces the row-2 placeholder with
AnalyticsTabs + LiveDataFeed side by side, and adds row 3 hosting
the trend chart. The dashed placeholder styles are gone.
2026-05-02 01:55:29 +08:00
pnoker 47e6ac29a9 feat(home): rebuild home page skeleton with stat cards + live feed
First slice of the home-page revamp (P1 of the P1+P2+P3 plan we signed
off on):

- StatCard.vue: reusable indicator card with title + big number +
  trend badge + optional G2 sparkline area. 5 tone presets
  (blue / green / orange / purple / red) matching the page's
  existing palette so nothing looks plastic.
- LiveDataFeed.vue: right-column panel backed by GET /dashboard/stream.
  Manual refresh button + an auto-refresh select (off / 5s / 10s / 30s).
  Each row labels the value type (STR / INT / DOUBLE / …) and resolves
  device/point ids to names via the existing batch endpoints, matching
  Resource page UX.
- api/dashboard.ts: thin fetch wrappers for the four new endpoints
  landing in dc3-center-data.
- Home.vue + index.ts rewritten: replaced the static 4-card + mock
  heatmap with a 12-gutter grid where each indicator is a StatCard
  backed by real totals (driver/device/point/profile/today), plus the
  live feed column. The tab card + trend chart + quick actions
  placeholders are marked for D2/D3.
- i18n zh/en get a full home.* block for every string used by the new
  components, including the upcoming tabs / ranges / alertList /
  quickActions / banner blocks so later phases just drop in.
2026-05-02 01:38:19 +08:00
pnoker 80a66862f7 style: fix Settings page layout spacing 2026-05-02 01:07:12 +08:00
pnoker 1197f5a702 style(layout): tighten breadcrumb bottom margin to 1px
5px felt too loose; a 1px gap is enough to prevent the edge-touching
on settings pages without opening a visible gutter elsewhere.
2026-05-02 01:03:26 +08:00
pnoker c76791549a style(layout): gap the breadcrumb from page content
.breadcrumb had margin-bottom: 0 and relied on whatever padding the
first child component brought to create visual separation from the
page. That works for list pages (tool-card sits inside el-card with
default 20px padding) but collapses on Settings — both the sidebar
card and the main pane there zero out their el-card__body / el-main
padding, so the breadcrumb ended up sharing an edge with the
sidebar menu.

Give the breadcrumb a constant 5px bottom margin so every page has
a consistent gap regardless of the child's own spacing.
2026-05-02 01:02:22 +08:00
pnoker 0b2b27869f style(toolbar): hide left divider when actions slot renders nothing
The $slots.actions guard was too permissive: DriverTool supplies the
slot but templates its Add button behind v-if="add", so the DOM is
empty when add=false and the divider ended up dangling next to just
Search/Reset. Always render the divider template-side, then hide it
via CSS :first-child when nothing precedes it. :first-child ignores
Vue's comment-node placeholders for v-if'd children, which is exactly
the case this rule needs to catch.
2026-05-02 00:55:09 +08:00
pnoker d656d998a2 style(toolbar): add divider between actions and search/reset cluster
Mirrors the right-cluster divider between pagination and refresh/sort.
Only renders when both groups actually exist: the `actions` slot is
filled (Add / Import / …) *and* the default Search/Reset pair is
visible (no custom `buttons` slot). Otherwise a stray bar would show
up next to nothing.

Promoted .tool-card-footer-divider to the shared footer scope so both
sides reuse the same rule instead of duplicating styles.
2026-05-02 00:52:21 +08:00
pnoker 5fa44883f5 style(toolbar): insert vertical divider between pagination and refresh/sort
A 1x18px divider separates the pagination sizer dropdown from the
refresh / sort circle buttons so the two control groups read as
distinct clusters instead of running into each other.
2026-05-02 00:49:48 +08:00
pnoker 9e3a803475 style(toolbar): add breathing room + divider between filters and footer
After moving Search/Reset into the footer the filter grid and the
action row ended up almost touching, which made the card feel cramped.
Widen the top margin to 16px and add a 1px lighter border + 12px
padding above the footer so there's a clear visual break between the
filter area and the action bar.
2026-05-02 00:45:07 +08:00
pnoker 6ce61e28e2 style(toolbar): merge default Search/Reset into footer action row
Previously the default Search/Reset pair lived on its own line under
the filter grid, while Add / Import sat alone in the footer — two
button rows for no good reason once the filter grid wraps. Move the
default Search/Reset into the footer actions slot so every toolbar has
a single unified button row.

Pages that override the `buttons` slot (currently just PointTool with
its inline prev/next chunks) keep their body-level button row — that
path stays triggered via v-if="$slots.buttons".
2026-05-02 00:40:12 +08:00
pnoker 21e221c394 style(layout): align language-switcher emoji flags with el-icon slots
The setting / help / logout items in the avatar dropdown now render via
:icon=... (svg), while the English / 中文 items use emoji flags in a
bare <span>. The flag glyph rendered ~2px larger than the sibling svgs
and sat slightly high, so the two halves of the dropdown looked like
two different UI kits.

Switch .lang-flag to a 1em x 1em inline-flex box with matching
vertical-align so the flag occupies the same slot as an el-icon and
lines up on the text baseline. Spacing to the label is kept at 8px.
2026-05-02 00:37:16 +08:00
pnoker b5da1778b0 style(ui): polish header / toolbar / dialog cosmetics across the app
#1 header username color
  .user_name was rendered at #909399 (element-plus text-secondary) over
  a white background — basically ghosted. Lift to #303133 with a
  500 weight so it reads as a navigation item.

#2 avatar dropdown icons
  Register Setting / QuestionFilled / SwitchButton in the imports and
  pass them via :icon on the settings / help / logout el-dropdown-items,
  aligning them visually with the language flags above.

#3–5 ToolCard grid
  The filters slot was a naked inline form. Widths drifted per page
  (edit-form-default pinned to 250px, el-segmented auto-sized, some
  inputs uncapped). Replace the single-row flex with a wrapping 4-col
  grid: each form-item takes 25% min 220px, every common input surface
  (el-input / el-select / el-tree-select / el-date-editor / el-cascader
  / el-input-number / el-segmented / the edit-form-* helper classes)
  stretches to the cell. Footer now uses space-between with explicit
  gaps so the refresh / sort circles stop rubbing shoulders.

#6 body min-width
  #app had overflow: hidden, so when the viewport narrowed below the
  global body min-width (1280px) the header was visible but the
  absolutely-positioned body got clipped at the right edge. Drop the
  clip and give .body the same 1280px floor so both carry on the
  page-level horizontal scroll together.

#7 dialog vertical centre + grid form helper
  .things-dialog now vertically centres via an :has() rule on the
  el-overlay wrapper, and its max-height is bounded by the viewport
  with overflow-y:auto on the body — tall forms scroll inside the
  dialog instead of dropping past the bottom. Ship an opt-in helper:
  put things-dialog--wide + things-form-grid on any dialog/form and
  each direct el-form-item becomes a half-width cell. MenuEditForm is
  wired up as the reference implementation (parent + remark span both
  columns).
2026-05-02 00:27:42 +08:00
pnoker 0d942fb864 chore(i18n): use 'Api' casing instead of 'API' in English labels 2026-05-02 00:06:52 +08:00
pnoker e135156241 feat(settings): add user/role detail pages + fix breadcrumb depth & labels
Breadcrumb fixes:
- nameMap lacked settingsApi / settingsMenu and the *Detail keys, so the
  crumb fell back to the raw route name ("settingsApi", "settingsMenu").
  Add the missing i18n pointers.
- Detail routes (settingsUserDetail / settingsRoleDetail /
  settingsResourceDetail / settingsApiDetail / settingsMenuDetail) now
  get a mid-level crumb pointing at their list page, so the trail reads
  Home / Settings / Users / User Detail instead of flattening to
  Home / Settings / User Detail.

New user/role detail views:
- views/settings/user/detail/UserDetail.vue
- views/settings/role/detail/RoleDetail.vue
  Each is el-tabs + el-descriptions, loading via getUserById /
  getRoleById, mirroring the Api/Resource/Menu detail pages.
- Register settingsUserDetail / settingsRoleDetail in operate routes.
- User.vue and Role.vue operation columns gain a "Detail" button that
  pushes via router.push to the new detail routes.
- i18n zh/en add settings.user.detailTitle, settings.role.detailTitle,
  and nav.settings*Detail labels.
2026-05-02 00:02:08 +08:00
pnoker 183fe64e3c fix(layout): de-dupe breadcrumb when parent path matches current route
Clicking Settings from the avatar dropdown navigates to /settings/user.
Layout's breadcrumb computed pushed a parent crumb at /settings/user
and then pushed the current-page crumb at route.path (also
/settings/user), which gave el-breadcrumb two children with identical
:key and triggered

  [Vue warn]: Duplicate keys found during update:"/settings/user"

Skip the trailing current-page crumb whenever its path matches the
parent we already pushed.
2026-05-01 23:23:43 +08:00
pnoker b521270dae fix(deviceEdit): keep step 2 reachable when driver has no attributes
The driver-attribute step used v-if="driverAttributes.length > 0" on the
whole card, so devices whose driver exposes zero attributes rendered an
empty page with no Prev / Next buttons — users were trapped mid-wizard.

Always render step 2 once active === 1. If the driver has attributes,
show the config form; otherwise render <el-empty> with a
driverConfigEmpty hint. Navigation buttons are pulled out of the form
so Prev / Next stay visible in both branches; Reset shows only when
there's actually a form to reset.
2026-05-01 20:39:17 +08:00
pnoker bcec649d26 fix(pointValue): seed tinyArea chart with empty data before first render
The G2 chart was rendered immediately in onMounted while history() was
still in flight, so extractColumns ran on null and threw TypeError
("Cannot read properties of null (reading 'map')") on every Device
Detail page open. Give the mark an explicit empty array so the initial
render is a no-op; the async history response then calls changeData()
as before.
2026-05-01 20:25:52 +08:00
pnoker a10bd5bb88 fix(device): move v-loading off ElDialog root to silence directive warning
Vue 3 refuses to attach runtime directives to components whose root node
is a Teleport (ElDialog teleports to body). The import dialog had
v-loading on <el-dialog>, which Vue logged as:

  Runtime directive used on component with non-element root node. The
  directives will not function as intended. at <Teleport> at <ElDialog>
  at <DeviceImportForm>.

Drop it into the inner <el-form>, which renders a real <form> element.
Loading overlay still covers the form content, same UX.
2026-05-01 20:13:57 +08:00
pnoker fbed302127 chore(icons): register WalletFilled for Driver menu 2026-05-01 19:56:37 +08:00
pnoker fc01badd51 chore(icons): register Discount / Lock / MessageBox / TrendCharts
User-specified icon set for seeded menus. Add the four new glyphs to the
global registry so the updated seed rows render.
2026-05-01 19:50:21 +08:00
pnoker 4e66d033c8 refactor(router): return guard result instead of using deprecated next()
Vue Router 4 deprecated the next() callback in navigation guards and
warns loudly on every navigation. Switch the beforeEach guard to the
return-based API: `return true` to continue, `return { name: 'login' }`
to redirect. Drop the now-unused NavigationGuardNext import.
2026-05-01 19:41:18 +08:00
pnoker 013a8f4822 chore(icons): register Avatar / DataLine / Document / Share for seed menus
Expand the global icon registry so the updated menu seeds render with
semantically-matched filled icons:

  HomeFilled    (Home)       — filled house
  Connection    (Driver)     — connects to devices
  Document      (Profile)    — filled document / template
  Monitor       (Device)     — already filled
  DataLine      (PointValue) — real-time point values
  Setting       (Settings)   — already filled
  UserFilled    (User)       — filled person
  Avatar        (Role)       — headshot, distinct from user
  Management    (Resource)   — already filled
  Share         (Api)        — outbound API surface
  Menu          (Menu)       — already filled
2026-05-01 19:34:46 +08:00