el-tabs renders flat by default, expecting to live inside a card.
Since Overview is a board with tabs at the top level, the header was
floating on the page background and the full-width nav underline
doubled up with the cards below. Give the header a bg surface +
radius + padding and hide the nav-wrap::after line so the active-tab
underline stands alone.
Also: lint-driven attribute reordering across the same file (imports,
el-button / stat-card / el-tab-pane props) — pure formatting.
SlaBadge is rendered conditionally between the banner row and the
stat strip. Neither neighbour sets a top margin, so the badge was
sitting flush against the stats and reading as a banner appendage
instead of its own strip. Give it the page's 8px rhythm.
Second half of the shared-infra refactor. Points every consumer at the
freshly-extracted modules, deleting ~400 lines of duplicated boilerplate
across home and event-overview.
- api/dashboard.ts: re-import all payload interfaces from
config/entity/dashboard; endpoints now share AlertSource / TimeRangeParams
/ Granularity / TopDimension unions.
- home/index.ts: statsTimeseries call switches from legacy rangeHours:24
to rangeKey:'24h' — server prefers rangeKey anyway.
- SlaBadge / TopologySankey: types now come from config/entity; SlaBadge
adopts useAsyncLoader.
- 9 event-overview cards (AgingBacklog, AlertCorrelation, ChangeImpact,
FlappingSources, MttaMttr, PeerDeviation, PointCoverageGap,
ProtocolHealth, SilentSources): use useAsyncLoader instead of ad-hoc
loading flag + try/catch, useEntityNames instead of per-card resolveNames
+ nameMap, jumpToEntity / jumpToSourceEvents instead of inline
router.push, formatDateTime / formatMs / humanDuration instead of
per-card time helpers, resolveDashboardColour / DASHBOARD_PALETTE
instead of hard-coded hex, and the @clickable-rows SCSS mixin instead
of repeated :deep(.el-table__row){cursor:pointer}.
- Every card also exposes refresh() via defineExpose so parent panels
can drive a coordinated reload.
Every dashboard / event-overview card had been re-declaring the same
loading flag + try/catch, its own resolveNames + nameMap, its own
formatTime, and its own router.push handler. Consolidated those seams
into reusable modules so future cards (and brand tweaks) are a one-line
change instead of a 30-file sweep.
- config/entity/dashboard.ts: payload interfaces + shared primitives
(AlertSource, RangeKey, TimeRangeParams) — interface-only module per
the verbatimModuleSyntax convention.
- composables/useAsyncLoader: folds loading flag + global-error swallow
into one run() helper.
- composables/useEntityNames: process-wide reactive cache with
inflight dedup for device/driver/profile/point name lookups.
- utils/time: formatDateTime / formatClock / formatShortDateTime /
formatMs / humanDuration — normalised zh-CN, 24-hour.
- utils/jump: jumpToEntity / jumpToSourceEvents for the consistent
detail-page + event-page routing convention.
- config/constant/palette.ts + styles/palette.scss: driver/device/
profile/point tone tokens (TS for JS consumers, SCSS for components).
Overview is now tabbed (situation / noise / availability / sla), synced
to ?tab= so SLA badge deeplinks land correctly. Situation keeps the
existing quick-actions + stat cards + charts + heatmap layout; the
three new tabs host:
- Noise: FlappingSources, PeerDeviation, AlertCorrelation
- Availability: SilentSources, PointCoverageGap, ProtocolHealth
- SLA: AgingBacklog, MttaMttr, ChangeImpact
Adds the 9 matching dashboard API endpoints and i18n strings.
- SLA badge above row 1 surfaces unack >24h + silent-device counts and
deeplinks to the matching event-overview tab; hides when clean.
- New bottom row renders a Driver → Device → Profile → Point Sankey
over the /dashboard/topology endpoint. Row 2/3 swapped so realtime
sits above analytics.
- Add @antv/g6 dependency for the Sankey layout, plus home.topology /
home.sla i18n strings (zh + en).
- Created a modular `DashboardCard` component to standardize card design and functionality across the application.
- Updated `AlertStormSources`, `ActivityHeatmap`, `LatencyChart`, `AlertList`, `LiveDataFeed`, and `AnalyticsTabs` to use `DashboardCard`, reducing duplicated styles and logic.
- Improved maintainability and consistency by replacing individual card implementations with the unified component.
The previous fix (body as flex column + el-scrollbar flex:1) didn't
actually hold the 300px baseline. el-scrollbar is a simulated
scrollbar whose height="100%" passes through multiple internal
wrappers; in our flex sizing chain it silently degraded to auto,
leaving content free to push the card past its min-height.
RecentUnconfirmed on the event-overview page has never had this
problem — it puts `overflow: auto` directly on the card body and
renders the list straight into it, no el-scrollbar. Copy that exact
pattern here:
- AlertList: remove <el-scrollbar>, render the timeline groups
directly into the body, body gains `overflow: auto`. Native
browser overflow respects the flex:1 + min-height:0 sizing and
stops exactly at the card's computed height.
- LiveDataFeed: replace <el-scrollbar> with a plain <div class=
"live-feed__scroll"> so the footer can stay pinned at the bottom
of the body (can't put overflow directly on the body or the
footer would scroll too). The div carries flex:1 + min-height:0
+ overflow:auto.
Row 3 now honours the 300px baseline on all three cards, and
el-row's stretch no longer lifts the whole row to match a runaway
sibling.
Root cause of row-3 cards rendering at 800+px despite min-height:300:
el-card__body was flex-child (flex:1, min-height:0) but NOT itself
display:flex. The inner <el-scrollbar height="100%"> therefore had
no concrete parent height to resolve against — height:100% degraded
to auto, and the 20-row timeline / 10-row alarm list grew the body
as far as its content wanted. el-row's align-items:stretch then
lifted TrendChart alongside them, so all three looked "way too
tall" together.
Fix is narrow: make the body itself a flex column and give each
el-scrollbar a class that claims flex:1 + min-height:0. Now the
scrollbar has a finite height (= body height = card height − header),
content scrolls inside it, and the card stays at 300.
Chart cards (TrendChart etc.) already had display:flex on the body
from the earlier refactor, so they're unaffected.
The 4:1:1 row (TrendChart / LiveFeed / Alerts) pairs a wide 12/24
chart with two 6/24 side columns (~320px wide each on 1280 screens).
At min-height 360 the two narrow cards rendered ~320 × 360 — visibly
taller than they are wide, and el-row's align-items:stretch meant
the whole row couldn't go below whatever the tallest sibling claimed.
Drop the family baseline to 300. Widths stay the same but aspect
ratios read better:
TrendChart ~640 × 300 (2:1, good for 30-day x-axis)
LiveFeed ~320 × 300 (~1:1, no longer tall)
Alerts ~320 × 300 (same)
Row 4 cards ~630 × 300 (2:1, latency + activity heatmap)
Event diag ~640 × 300 (2:1)
Content still fits: 300 − 48 header ≈ 250 in chart canvas / scroll
area, enough for 4-5 timeline items, a 30-day sparkline, or a 7×24
heatmap. Flex:1 + min-height:0 on the inner slot means each widget
still expands if its row stretches beyond 300 (which won't happen
on its own anymore, but remains safe when external code pins a row
height).
440 was inherited from LiveDataFeed's old min-height and left every
card feeling oversized — two-row grids pushed Data Ingestion Trend
taller than the useful chart content inside it. Rebase the family
baseline to 360, which matches the median of the previous per-card
heights (TrendChart 340, AlertList 370, the event-overview chart row
340, diag cards 320) without anything actually shrinking below its
content.
All twelve dashboard cards (home row 3/4 + event overview chart +
diag) now share min-height: 360. Chart/scrollbar/list slots still
take flex:1 inside the flex column so content expands to whatever
the row stretches them to — no card ever drops below 360, but rows
that set a larger stretched height (when one sibling card demands it)
still line up.
The home page and event-overview page together had four different card
heights floating around (440 / 340 / 320 / 280 / 240), making the grid
feel bumpy as rows didn't line up. Standardise on one baseline so
every dashboard card reads as part of the same family.
Every chart / list card now follows the same skeleton:
min-height: 440px
height: 100%
display: flex; flex-direction: column
::v-deep(.el-card__body) { flex: 1; min-height: 0; flex column }
chart / scrollbar / list slot: flex: 1; min-height: 0
G2 Chart constructor: autoFit: true (no explicit height)
Cards converted:
Home row 4: LatencyChart, ActivityHeatmap (was 280 → 440)
Event overview row 2: EventTrendChart, TopSourcesChart (was 340 → 440)
Event overview diag grid: AlertActivityHeatmap, AlertTypePie,
AlertStormSources, RecentUnconfirmed (was 240/320 → 440)
Overview.vue's grid-item min-height overrides (340 / 320) are now
redundant — dropped so the height lives entirely on the card. An
AlertTypePie empty state and AlertStormSources/RecentUnconfirmed
list slots pick up flex:1 so empty / short content still fills the
card instead of leaving a gap.
Each widget was sizing itself independently:
TrendChart: fixed 280px canvas + header ≈ 340px total
LiveDataFeed: min-height 440px + scrollbar height fixed 360px
AlertList: height 100% + scrollbar prop default 320px ≈ 370px
In a single flex row el-col does stretch children to the tallest col,
but the stretched space was being ignored by each card's internals,
so the three still rendered at different heights.
Unify:
- All three cards set min-height: 440px + display:flex column.
- el-card__body becomes flex:1 with min-height:0 so overflow children
(chart / scrollbar) collapse properly into the available space.
- TrendChart canvas is flex:1 instead of a fixed 280px, and Chart()
loses its height option so autoFit owns both axes.
- LiveDataFeed / AlertList el-scrollbars switch to height="100%" so
they fill whatever the body gives them; AlertList drops its now-
redundant scrollHeight prop.
The 4:1:1 split squeezed the side columns to 4/24 (≈ 200px on 1280
screens), where timestamps and message text in Recent Alarms /
LiveFeed rows started wrapping awkwardly. Loosen to 12/6/6 so the
two side widgets each get ~300px of breathing room while TrendChart
still claims the dominant half.
Collapse the previous two-row layout (TrendChart alone on its own
full-width row, LiveFeed + Alerts split 12:12 above) into a single
row at 4:1:1 — TrendChart keeps 16/24 of the width for readable
30-day axis, LiveFeed and Alerts take 4/24 each as narrow side
columns. Saves a full vertical slot on the home page without losing
any of the three widgets.
Narrow breakpoints (md/sm/xs) still stack each col to full width so
the 4/24 side columns don't squeeze below usable size.
Rework the middle of the home page into 12/12 columns so related
widgets read as equal siblings instead of a dominant-chart +
side-rail pattern:
- AnalyticsTabs takes a new `group` prop (structural | top). Each
instance renders only its three tabs — structural breakdowns
(protocol / deviceStatus / profile) on one card, top-N activity
(topDriver / topPoint / topDevice) on the other. Default activeTab
within each group is its first entry.
- Row 2: structural analytics + top-N analytics, 12 + 12.
- Row 3: LiveDataFeed + AlertList, 12 + 12 — the two "live now"
widgets pair more naturally than the old trend+alert combo did.
- Row 4: TrendChart claims its own full-width row. A 30-day time
series benefits from the extra x-axis room, and nothing else
competes for attention alongside it.
- Row 5 (latency + activity heatmap) unchanged.
When the caption row was added between the tab bar and the chart,
body height shrank from 360 → 340, but Chart constructor still
specified height: 360 explicitly — G2's autoFit only adopts container
width in that case, keeping the 360 height as-is. The chart then
painted 20px below the visible area, clipping the x-axis labels and
last row of ticks.
Remove the height option so autoFit sizes both axes from the
container's clientHeight. The .analytics-tabs__chart div is
height:100% inside the 340px body, so every chart now fits exactly.
Both tab groups now follow the same entity precedence so the strip
reads consistently left-to-right:
Structural: Drivers by Protocol → Device Enable Status → Devices by Profile
Top-N: Most Active Drivers → Most Active Points → Most Active Devices
Driver-level views lead each group (drivers are the entry point for
diagnosing most DC3 fleets — "which protocols are we running" / "which
drivers are busiest" is the first question operators ask). Default
active tab switches from deviceStatus → protocol to match.
The "Drivers by Protocol" tab was sourced from byType, which is
client vs server mode — not what the label promises. Switch to the
new byService bucket the manager backend now emits, and strip the
dc3-driver- prefix from service names so the chart axis reads
modbus-tcp / mqtt / opc-ua / ... instead of the verbose form.
Caption updated to match: "grouped by service_name (modbus-tcp /
mqtt / opc-ua, …)" so operators know exactly which column of the
driver table backs the visualisation.
Per-tab icons (PieChart / Connection / Collection / Trophy×3) added
visual noise without carrying information the labels don't already
convey. The six-tab strip now reads calmer as plain text, and the
caption row below still tells operators exactly what each chart
measures.
Tab labels and captions are unchanged — only the icon column is gone.
The six analytics tabs were named by "what" (Device Status, Protocols,
TOP Devices, …) but never said "by which dimension" or "ranked how",
so operators couldn't tell what the chart was measuring. Rework along
three axes:
1. Tab labels now carry the subject AND the dimension:
Device Status → Device Enable Status
Protocols → Drivers by Protocol
Profiles → Devices by Profile (TOP 10)
TOP Devices → Most Active Devices
(plus TOP Points / Drivers analogously)
2. A caption line between the tab bar and the chart spells out the
exact bucketing or ranking rule. Structural tabs get a fixed
phrase; top-N tabs interpolate the active range segmented value so
"ranked by write volume over Last 7 days" stays accurate when the
user flips the control.
3. Tab icons split the grid into two visual groups:
- Structural (deviceStatus / protocol / profile) → PieChart /
Connection / Collection — each hints at the chart shape
- Top-N (topDevice / topPoint / topDriver) → all share Trophy so
the "ranking" concept reads at a glance; the subject is
disambiguated by the label text.
Body height dropped 360 → 340 to leave room for the caption row; card
min-height stays 440 so the whole strip lines up with LiveDataFeed
next door.
Element Plus ships <el-main> with overflow:auto — it's meant for the
"aside doesn't scroll, main scrolls" layout. In our stack the outer
Layout already wraps router-view in an el-scrollbar, so main's auto
overflow is redundant at best. At worst, flex-row sub-pixel rounding
lets main believe its content is 0.5px taller than itself and pops a
ghost scrollbar, which then eats wheel events over every settings
card. Reset main to overflow:visible so it never owns a scroll axis
of its own.
Drop the Settings-scoped <el-backtop target=".settings-main"> along
with it: with main no longer scrollable, that backtop had nothing
valid to scroll anyway — the Layout-level backtop on
.body .el-scrollbar__wrap already covers every settings sub-page.
The inner <el-scrollbar :max-height="320px"> turned RecentUnconfirmed
into an independent scroll container. Whenever the cursor hovered the
card and the list actually exceeded 320px (≥ 4-5 items with messages),
wheel events were consumed by the inner wrapper and the outer Layout
scrollbar couldn't advance — the page felt frozen until the mouse
moved onto a chart card.
Two-pronged fix:
1. Remove the el-scrollbar entirely so the timeline flows naturally
inside the card. No nested scrollable means wheel events always
reach the Layout scrollbar.
2. Drop alertPage size from 10 → 5 so five timeline items fit inside
the diag-grid's existing min-height: 320px without overflowing.
Five is enough for "recent unconfirmed at a glance"; operators who
need more follow the Unconfirmed quick-action chip into the event
list page (already the affordance this card links toward).
All home cards now carry a second data point in their subtitle, mirroring
the new event-overview rhythm where no primary number stands alone:
- Driver / Device cards → "{n} cumulative alarms" — surfaces fleet
health at a glance next to the fleet size.
- Point card → "across {n} profiles" — shows how the points are
organised (via the already-loaded profileCount).
- Today's data card → unchanged ("Total {n}"), that subtitle was
already doing the right job.
- Today's driver/device alarm cards → always show unconfirmed count
(including 0) so card heights stay uniform across the grid instead
of jumping when the backlog clears.
- Titles on the total/unconfirmed cards now carry an explicit 累计 /
Total prefix so "Driver unconfirmed" is no longer ambiguous with the
today cards — it reads as the cumulative backlog up to now, not the
portion from today.
- Subtitles are unified so every card shows a second data point tied
to the primary metric:
- Total cards → how many of that total are still unhandled
(e.g. "未确认 12/340 · 3%")
- Unconfirmed cards → how large the backlog is against cumulative
(e.g. "累计 340 条 · 占 3%")
- Today cards → today's open count pulled from the already-returned
todayDriverUnconfirmed / todayDeviceUnconfirmed fields
The bare "→ open driver events" navigation hint is gone; a click
anywhere on the card already handles that.
Drops three i18n keys (unconfirmedRatio, goToDriver, goToDevice) that
nothing references after this change.
Two things were keeping the trend arrow + delta from showing up the way
Home's cards do:
1. sparkTrend returned null whenever the previous bucket was 0, which
in a quiet event stream meant most cards had no indicator at all.
Switch to Home's diff-style trend ("+5" / "-3" / "0") — we always
have something to show as long as there are two buckets.
2. All six cards were sharing one sparkline pulled from alertStats'
sparkline24h — and that series is the aggregate hourly count, not
per-source. Driver and device cards ended up with identical shapes.
Pull alertTrend(7) for per-day driver/device buckets and feed
total/unconfirmed cards from whichever source they represent, so
each card has its own shape and day-over-day delta. Today cards
keep the 24h hourly bucket since "today" is intra-day.
Event overview rework based on three separate asks — landed in one
commit because they all touch the same page:
- Reorder stat cards driver-first (driver-total → device-total →
driver-unconfirmed → device-unconfirmed), matching the way tenants
actually read alerts: driver-lane issues are the more common
starting point for diagnosis.
- Add two "Today" cards (today-driver, today-device) sourced from
alertStats.todayDriverAlarms / todayDeviceAlarms — backend already
surfaces these for Home's today widgets, so no extra round-trip.
Grid widens from 4 → 6 columns; breakpoint shifts to 3 cols at
≤1280px so cards don't squeeze below usable width.
- Quick-actions chips: drop "Last 24h" and surface "Today / 7d / 30d"
(mapping to rangeKey values EventTable already handles). Thin
vertical divider between confirmation-state chips (All, Unconfirmed)
and time-range chips so the two groups read as groups.
- EventTrendChart and TopSourcesChart headers get a circular refresh
button with its own loading spinner, consistent with how
RecentUnconfirmed already does it.
Two UX fixes noticed while exercising the new settings forms:
1. ResourceEditForm's Parent Resource tree-select mixed all seven
resource types into one flat tree — you couldn't tell MENU from
API from DATA while picking a parent. Regroup the same way the
role-assign dialog does: one disabled group node per type, real
resources nested underneath. Group nodes can't be picked (prefixed
id, disabled flag surfaced via `props.disabled`) so the model only
ever carries a real resource id. Default-expanded so everything
shows on first click rather than hiding behind type folders.
2. RoleAssignResources' trees had `check-strictly` so ticking a parent
didn't tick its children — surprised users who expected
"grant the whole folder" to Just Work. Drop the flag. el-tree's
default cascade matches the mental model: ticking a parent selects
all descendants, and `getCheckedKeys` returns them all on submit.
Two resolved readability issues on the settings detail pages:
- ResourceDetail now mirrors UserDetail/RoleDetail with two extra
tabs: "Assigned Roles" (backed by the new listRoleByResourceId
backend endpoint) and "Child Resources" (walks the resource tree to
locate the current node and renders its children as a tree-table).
Loads lazily on first open with ?active= URL persistence, same as
the other detail pages.
- MenuDetail's parentMenuId used to render a raw id. Resolve it via
menuStore.findById (cached tree already loaded by Layout) so the
descriptions row shows "Settings" instead of "1047". onMounted
primes the tree in case the page is reached via a bookmark that
skips Layout's initial fetch.
Two fixes to the Resource page's readability:
1. Parent Resource was a free-text id input; reuse the existing
/resource/tree endpoint to feed an el-tree-select with a synthetic
Root node (top-level becomes a real choice). Same pattern as
Role/Menu parent pickers.
2. Entity column rendered as a raw id for API and MENU rows because
resolveEntityNames only covered DRIVER/DEVICE/POINT/PROFILE. Pull
the API list (capped 1000, one-shot) into a local id→apiName map
and resolve MENU rows via the already-cached menuStore (add a
findById getter). Column header renames from "Entity ID" to
"Entity" now that the raw id is gone in favor of the name.
menuStore.fetchTree is idempotent so we await it before name
resolution — Layout usually primes it on startup, but a hard reload
onto /settings/resource needs it before MENU entries render.
APIs are registered by each service at startup; editing them from the
UI would drift from the real routes the next time anything restarts.
Keep the list page read-only: no Add button, no row-level Edit or
Delete — just Detail.
ApiEditForm and its index.ts are deleted (nothing references them).
The tool bar loses its `#actions` slot (+ Plus icon import) and the
`@add` emit; the action column trims from 240px → 120px now that it
only carries one button.
The right-hand "Assigned Resources" table re-stated information already
visible on the left: checkboxes on the tree show what's selected, and
each tab label carries (selected/total). Rendering both made the dialog
wider than it needed to be and forced mental reconciliation between
two views of the same state.
Drop the pane. Dialog narrows to 640px, the tree stretches full-width,
and a terse "{count} selected" label in the header gives the global
count that used to justify the right pane.
Removed the now-unused assignedList / removeOne / typeTagType helpers
and the type-tag column — the per-tab label already shows the type
context.
One giant tree mixing MENU/API/DATA/DEVICE/... was visually noisy — you
couldn't tell at a glance which branch was which type, and cross-type
parent/child links (e.g. a DATA node under a MENU parent) made the
hierarchy misleading.
Left pane now shows a tab per resource type, each hosting its own
el-tree with only that type's nodes. Parent/child links are rebuilt
per-type so a DATA node whose source-tree parent is a MENU becomes a
root inside the DATA tab. Each tab label carries `(selected/total)`
so you can see at a glance where picks are sitting without flipping
through.
One el-tree per type with v-show + a ref map — each tree owns its own
checked state, onCheckChange merges across all trees into selectedIds.
The right pane now also carries a resourceTypeFlag tag on each row so
the type context survives even after the user moves off the tab.
Default tab lands on MENU when present, else first available.
The role-resource transfer inherited the same problem the user-role
transfer had — flat columns, single-line labels, no horizontal scroll
— but it was also semantically wrong: resources form a tree and
el-transfer collapsed that structure into a flat list. Rebuild around
the actual shape of the data.
- Left pane: el-tree fed from /resource/tree, show-checkbox with
check-strictly (a parent grant does not imply granting children —
pick exactly what you mean). Has its own search box via filter-node.
- Right pane: el-table driven by a flattened node map from the tree,
one row per selected resource with name/code/type and a per-row
Remove button. Matches the visual rhythm of the new
UserAssignRoles dual-table layout.
emit('save', roleId, addIds, removeBindIds, done) stays intact, so
Role.vue's onAssignResources needs no changes. assign/index.ts
deleted — the component is now single-file <script setup>.
Mirror UserDetail's pattern: 'detail' tab keeps the descriptions pane,
'user' tab shows assigned users via the new listUserByRoleId endpoint,
'resource' tab shows granted resources via the already-existing
listResourceByRoleId. Each tab loads on first open and writes
`?active=` to the URL so a reload stays on the selected tab.
Note RoleDetail.parentRoleId still displays as a raw id — the parent
role name would need a separate hop to render. Keep for a later pass;
not blocking this UX improvement.
Parent Role was a free-text id input — unusable since nobody knows the
numeric ids. Swap it for el-tree-select sourced from the new
POST /role/tree endpoint, rendered the same way MenuEditForm does its
parent picker.
- Synthesize a "Root" node (id=0) so top-level roles stay pickable.
- Load the tree alongside the paged list (initial + after
add/update/delete) so edits see fresh parent choices.
- RoleEditForm now takes a treeData prop and resets parentRoleId to 0
(not empty string) so el-tree-select's controlled value is valid
from the start.
Mirror the DriverDetail / DeviceDetail pattern: keep the existing
descriptions pane on the 'detail' tab, add 'role' and 'resource' tabs
for the relations that the user is reached through. Each tab loads on
first open (rolesLoaded / resourcesLoaded guards) and writes
`?active=` to the URL so a reload stays on the selected tab.
Roles come from listRoleByUserId (already existed). Resources come from
the new listResourceByUserId backend endpoint — user → role →
resource is two hops, doing it in one call is cleaner than fanning out
per-role on the client.
el-transfer wedged every role onto a single truncated line inside a
cramped two-column layout that wouldn't scroll horizontally, so you
couldn't actually read the role name + code + remark together. Drop it
for two independent el-tables with their own search boxes, multi-select
checkboxes, and a center column of Assign/Remove buttons.
Assigned roles land in the right pane seeded from listRoleByUserId; the
left pane holds everything else (All - Assigned). Moving rows between
panes is purely client-side state; the save() diff + emit signature
against the parent are unchanged, so onAssignRoles keeps working with
no changes.
assign/index.ts is deleted — the component is now a single-file
<script setup>.
Pair with the backend change that starts populating creatorName/operatorName.
Two new columns land between the enable-tag and the time columns, with
a '-' fallback so legacy rows (written before the backend change) don't
show as blanks.
- global popconfirm styling: min-width 240px on main, vertical spacing
between title and actions, button gap — fixes the cramped "delete?"
bubble that felt like it was squeezed into a corner.
- Layout avatar dropdown: remove the standalone "Language" caption so
the EN/中 segmented control sits cleanly on its own, stretched full
width of the menu row. Unused layout.language i18n key deleted.
Restore the inner el-scrollbar but use max-height so when the list is
short the wrapper collapses to fit content and wheel events bubble to
the outer el-main; once rows exceed the cap the scrollbar takes over.