feat(ui06): B3d governance + B3e closing — page headings, stat cards, info strip, embed slots

- 治理页通用层(ui06.scss):.ui06-page 工作区 / .ui06-page-heading 27px 页头 / .ui06-stats
  三列统计卡 / .ui06-info-strip 提示条 / .ui06-card-title 17px 卡头
- 10 账号:页头(查看同步入口)+ 来源提示条;分页/同步档案列/G4 功能零改动
- 11 同步:页头(手动补拉上移);状态卡/历史/失败明细抽屉保留
- 12 用量:页头(阈值设置上移)+ 统计卡对齐示例(数字 27px + 注脚 10px)
- 13 展示位:页头 + 卡片标题(配置对话框沿用 UI-04 真实组件)
- 17-19 异常页:沿用 ArtException(大数字+文案+回退,语义等同示例;登记偏差)
- i18n zh/en;typecheck/lint/vitest/build 全绿

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-23 07:04:18 +08:00
parent 352d7405c2
commit 9471562f67
28 changed files with 254 additions and 51 deletions
@@ -111,3 +111,114 @@ body {
outline: 3px solid #9fc7ff;
outline-offset: 2px;
}
/* ══════════ 治理页通用(示例 .workspace / .page-heading / .stats / .stat / .info-strip) ══════════ */
.ui06-page {
max-width: 1660px;
padding: 28px 30px 44px;
margin: auto;
}
.ui06-page-heading {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
margin-bottom: 22px;
h1 {
font-size: 27px;
letter-spacing: -0.7px;
line-height: 1.4;
}
> div > p {
margin-top: 6px;
font-size: 12px;
color: #7b899f;
}
.actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
}
}
/* 三列统计卡(示例 .stats / .stat) */
.ui06-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 18px;
margin-bottom: 22px;
.ui06-stat {
padding: 21px;
background: #ffffff;
border: 1px solid #e5eaf1;
border-radius: 12px;
p {
font-size: 11px;
color: #8797ac;
}
strong {
display: block;
margin: 9px 0 4px;
font-size: 27px;
font-weight: 650;
letter-spacing: -0.6px;
.unit {
font-size: 17px;
font-weight: 600;
}
}
small {
font-size: 10px;
color: #97a4b5;
}
}
}
/* 提示条(示例 .info-strip) */
.ui06-info-strip {
display: flex;
gap: 9px;
align-items: center;
padding: 12px 16px;
margin-bottom: 20px;
font-size: 12px;
color: #6480a8;
background: #edf4fe;
border: 1px solid #e2edfc;
border-radius: 9px;
}
@media (max-width: 900px) {
.ui06-stats {
grid-template-columns: 1fr;
gap: 10px;
}
.ui06-page {
padding: 20px 14px 40px;
}
.ui06-page-heading {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
}
/* 卡片头标题(示例 .card-head h2 17px) */
.ui06-card-title {
font-size: 17px;
font-weight: 600;
line-height: 1.4;
}
+20 -3
View File
@@ -586,7 +586,10 @@
"totals": {
"photoCount": "Total Photos",
"totalBytes": "Total Storage",
"thresholds": "Alert Thresholds"
"thresholds": "Alert Thresholds",
"photoCountNote": "Aggregate from usage response",
"totalBytesNote": "Originals + viewing variants",
"thresholdsNote": "An alert threshold, not a hard storage quota"
},
"albums": {
"title": "Album Usage",
@@ -621,6 +624,10 @@
"kind_storage": "Storage",
"kind_perAlbum": "Per album",
"empty": "No alerts"
},
"page": {
"title": "Storage & usage",
"subtitle": "Understand retained assets and act before thresholds are exceeded."
}
},
"usersManage": {
@@ -649,7 +656,11 @@
"actionResetPassword": "Reset Password",
"actionActivate": "Activate",
"actionDeactivate": "Deactivate",
"actionUnlock": "Unlock"
"actionUnlock": "Unlock",
"subtitle": "Provisioned company accounts, credential state and local access controls.",
"viewSync": "View user sync",
"tableTitle": "Accounts",
"sourceHint": "Users are pulled from the company directory on a schedule; no public sign-up. Source state and local lock are independent."
},
"filter": {
"queryPlaceholder": "Account/name prefix",
@@ -770,6 +781,10 @@
"colCode": "Code",
"colDetail": "Detail",
"colCreatedAt": "Recorded At"
},
"page": {
"title": "User sync",
"subtitle": "Review scheduled directory pulls and resolve failed records."
}
},
"embedSlotsManage": {
@@ -790,7 +805,9 @@
"unbound": "Unbound",
"actionEdit": "Edit",
"actionBind": "Bind Album",
"actionCode": "Embed Code"
"actionCode": "Embed Code",
"subtitle": "Manage floating photo widgets: album binding, toggles and embed code.",
"tableTitle": "Slots"
},
"form": {
"nameLabel": "Name",
+20 -3
View File
@@ -586,7 +586,10 @@
"totals": {
"photoCount": "照片总数",
"totalBytes": "存储总量",
"thresholds": "告警阈值"
"thresholds": "告警阈值",
"photoCountNote": "用量接口汇总",
"totalBytesNote": "原图 + 浏览变体",
"thresholdsNote": "告警阈值,并非存储硬配额"
},
"albums": {
"title": "相册用量",
@@ -621,6 +624,10 @@
"kind_storage": "全库存储",
"kind_perAlbum": "单相册",
"empty": "暂无告警"
},
"page": {
"title": "存储与用量",
"subtitle": "查看长期保留的照片资产与告警阈值。"
}
},
"usersManage": {
@@ -649,7 +656,11 @@
"actionResetPassword": "重置密码",
"actionActivate": "激活",
"actionDeactivate": "停用",
"actionUnlock": "解锁"
"actionUnlock": "解锁",
"subtitle": "统一查看企业来源状态、登录凭据与本地账号治理。",
"viewSync": "查看用户同步",
"tableTitle": "账号列表",
"sourceHint": "用户信息来自企业接口定期拉取,不开放注册;来源状态与本地锁定相互独立。"
},
"filter": {
"queryPlaceholder": "账号/姓名前缀",
@@ -770,6 +781,10 @@
"colCode": "分类码",
"colDetail": "详情",
"colCreatedAt": "记录时间"
},
"page": {
"title": "用户同步",
"subtitle": "查看企业用户接口的定时拉取结果,定位失败记录。"
}
},
"embedSlotsManage": {
@@ -790,7 +805,9 @@
"unbound": "未绑定",
"actionEdit": "编辑",
"actionBind": "绑定相册",
"actionCode": "嵌入代码"
"actionCode": "嵌入代码",
"subtitle": "管理官网悬浮照片展示位:绑定相册、启停与接入代码。",
"tableTitle": "展示位列表"
},
"form": {
"nameLabel": "名称",
@@ -3,14 +3,24 @@
文案)+ 嵌入代码卡。启停与任何管理面变更一样递增 configVersion——toast
携带新版本号提示(嵌入端轮询发现版本升即整体重置渲染)。 -->
<template>
<div class="p-5">
<div class="ui06-page">
<!-- 页头(示例 13 .page-heading) -->
<div class="ui06-page-heading">
<div>
<h1>{{ $t('embedSlotsManage.list.title') }}</h1>
<p>{{ $t('embedSlotsManage.list.subtitle') }}</p>
</div>
<div class="actions">
<ElButton type="primary" data-test="slot-create-btn" @click="openCreate">
{{ $t('embedSlotsManage.list.create') }}
</ElButton>
</div>
</div>
<ElCard data-test="embed-slots-card">
<template #header>
<div class="flex items-center justify-between">
<span>{{ $t('embedSlotsManage.list.title') }}</span>
<ElButton type="primary" data-test="slot-create-btn" @click="openCreate">
{{ $t('embedSlotsManage.list.create') }}
</ElButton>
<span class="ui06-card-title">{{ $t('embedSlotsManage.list.tableTitle') }}</span>
</div>
</template>
+33 -27
View File
@@ -2,24 +2,37 @@
相册用量表(title/photoCount/originalBytes/variantBytes/totalBytes
人类可读)+ 阈值设置对话框 + 告警列表(未解决红/已解决灰,含信息与时间)。 -->
<template>
<div class="p-5">
<!-- ── 合计头卡 ── -->
<div class="grid gap-4 mb-4 sm:grid-cols-3" data-test="usage-totals">
<ElCard shadow="never">
<div class="text-gray-500 text-sm">{{ $t('usage.totals.photoCount') }}</div>
<div class="text-2xl font-medium mt-1" data-test="usage-total-photos">
{{ summary?.totals.photoCount ?? '-' }}
</div>
</ElCard>
<ElCard shadow="never">
<div class="text-gray-500 text-sm">{{ $t('usage.totals.totalBytes') }}</div>
<div class="text-2xl font-medium mt-1" data-test="usage-total-bytes">
<div class="ui06-page">
<!-- 页头(示例 12 .page-heading:标题 + 副文 + 阈值设置入口上移) -->
<div class="ui06-page-heading">
<div>
<h1>{{ $t('usage.page.title') }}</h1>
<p>{{ $t('usage.page.subtitle') }}</p>
</div>
<div class="actions">
<ElButton plain data-test="usage-thresholds-btn" @click="thresholdsVisible = true">
{{ $t('usage.thresholds.entry') }}
</ElButton>
</div>
</div>
<!-- ── 合计头卡(示例 .stats / .stat 三列统计) ── -->
<div class="ui06-stats" data-test="usage-totals">
<div class="ui06-stat">
<p>{{ $t('usage.totals.photoCount') }}</p>
<strong data-test="usage-total-photos">{{ summary?.totals.photoCount ?? '-' }}</strong>
<small>{{ $t('usage.totals.photoCountNote') }}</small>
</div>
<div class="ui06-stat">
<p>{{ $t('usage.totals.totalBytes') }}</p>
<strong data-test="usage-total-bytes">
{{ summary ? formatBytes(summary.totals.totalBytes) : '-' }}
</div>
</ElCard>
<ElCard shadow="never">
<div class="text-gray-500 text-sm">{{ $t('usage.totals.thresholds') }}</div>
<div class="mt-1 flex flex-wrap gap-2" data-test="usage-threshold-tags">
</strong>
<small>{{ $t('usage.totals.totalBytesNote') }}</small>
</div>
<div class="ui06-stat">
<p>{{ $t('usage.totals.thresholds') }}</p>
<div class="mt-2 flex flex-wrap gap-2" data-test="usage-threshold-tags">
<ElTag size="small" :type="summary?.thresholds.storageBytes ? 'success' : 'info'">
{{ $t('usage.thresholds.storageShort') }}
{{
@@ -37,22 +50,15 @@
}}
</ElTag>
</div>
</ElCard>
<small>{{ $t('usage.totals.thresholdsNote') }}</small>
</div>
</div>
<!-- ── 相册用量 + 告警 ── -->
<ElCard data-test="usage-albums-card">
<template #header>
<div class="flex items-center justify-between">
<span>{{ $t('usage.albums.title') }}</span>
<ElButton
type="primary"
plain
data-test="usage-thresholds-btn"
@click="thresholdsVisible = true"
>
{{ $t('usage.thresholds.entry') }}
</ElButton>
<span class="ui06-card-title">{{ $t('usage.albums.title') }}</span>
</div>
</template>
+21 -11
View File
@@ -2,21 +2,31 @@
运行历史(游标累积)、失败明细抽屉(点行打开)。页面存续期间 10s 自动
刷新状态(onUnmounted 清理);手动补拉 202 受理后轮询至批次离开 running。 -->
<template>
<div class="p-5">
<div class="ui06-page">
<!-- 页头(示例 11 .page-heading:标题 + 副文 + 手动补拉入口上移) -->
<div class="ui06-page-heading">
<div>
<h1>{{ $t('userSync.page.title') }}</h1>
<p>{{ $t('userSync.page.subtitle') }}</p>
</div>
<div class="actions">
<ElButton
type="primary"
:loading="triggering"
:disabled="status?.state === 'running'"
data-test="sync-trigger-btn"
@click="triggerSync"
>
{{ $t('userSync.status.trigger') }}
</ElButton>
</div>
</div>
<!-- ── 状态卡 ── -->
<ElCard data-test="sync-status-card" class="mb-4">
<template #header>
<div class="flex items-center justify-between">
<span>{{ $t('userSync.status.title') }}</span>
<ElButton
type="primary"
:loading="triggering"
:disabled="status?.state === 'running'"
data-test="sync-trigger-btn"
@click="triggerSync"
>
{{ $t('userSync.status.trigger') }}
</ElButton>
<span class="ui06-card-title">{{ $t('userSync.status.title') }}</span>
</div>
</template>
+24 -2
View File
@@ -3,11 +3,30 @@
公司)展示 + 行内治理操作(编辑/重置密码/激活/停用/解锁)。自我保护双
保险:UI 对自身行禁用角色/锁定编辑,服务端 400 仍会经 http 层本地化弹出。 -->
<template>
<div class="p-5">
<div class="ui06-page">
<!-- 页头(示例 10 .page-heading:标题 + 副文 + 查看同步入口) -->
<div class="ui06-page-heading">
<div>
<h1>{{ $t('usersManage.list.title') }}</h1>
<p>{{ $t('usersManage.list.subtitle') }}</p>
</div>
<div class="actions">
<ElButton plain data-test="users-goto-sync" @click="router.push('/system/user-sync')">
{{ $t('usersManage.list.viewSync') }}
</ElButton>
</div>
</div>
<!-- 来源提示条(示例 .info-strip:来源状态与本地锁定相互独立) -->
<div class="ui06-info-strip">
<ElIcon><InfoFilled /></ElIcon>
<span>{{ $t('usersManage.list.sourceHint') }}</span>
</div>
<ElCard data-test="users-card">
<template #header>
<div class="flex items-center justify-between">
<span>{{ $t('usersManage.list.title') }}</span>
<span class="ui06-card-title">{{ $t('usersManage.list.tableTitle') }}</span>
<!-- 过滤栏:前缀查询(account/displayName)+ 治理状态单选 -->
<div class="flex flex-wrap items-center gap-2">
<ElInput
@@ -196,8 +215,10 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { ElMessage, ElMessageBox } from 'element-plus'
import { InfoFilled } from '@element-plus/icons-vue'
import UserEditDialog from './UserEditDialog.vue'
import ResetPasswordDialog from './ResetPasswordDialog.vue'
import {
@@ -213,6 +234,7 @@
defineOptions({ name: 'SystemUsers' })
const { t } = useI18n()
const router = useRouter()
const userStore = useUserStore()
// ── 过滤(查询即整页重载:游标键集依赖过滤条件稳定) ──