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.
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.
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
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.
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.
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.
- 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
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.
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.
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"
"最新采集数据" -> "实时位号数据流"
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.
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.
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.
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.
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.
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.
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.
- 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.*.
- 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.
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.
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.
- 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.
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.
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.
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.
.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.
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.
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.
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.
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.
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".
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.
#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).
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.
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.
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.
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.
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.
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.