improve(ui):优化管理后台的表格显示效果

1. 表头横向滚动条变小
2. 表格占位图片垂直居中
3. 移除表格底部分割线
This commit is contained in:
qian committed 2026-09-15 00:20:19 +08:00
1 parent 2de068633c
commit c9cdfd7138
18 files changed
+300 -34

No files matched your search

+48 -3
View File
@@ -242,6 +242,17 @@ body {
color: #64748b;
}
/* 表格横向滚动条细化:Arco 默认轨道 15px、滑块 9px,列多时悬停表头会出现粗滚动条遮住表头文字。
压成 3px 细线贴在底部,仅作「可横向滚动」的提示;拖拽/点击滚动同步逻辑不变,表头表体通用 */
.arco-table .arco-scrollbar-track-direction-horizontal {
height: 3px;
}
.arco-table .arco-scrollbar-thumb-direction-horizontal .arco-scrollbar-thumb-bar {
height: 3px;
margin: 0;
}
.arco-table-td {
border-bottom: 1px solid var(--ta-border-light);
}
@@ -259,6 +270,40 @@ body {
vertical-align: middle;
}
/* page-table 布局下表体被拉伸为滚动容器,固定 240px 的空态行会顶在表体上方。
空数据时把表体区域(含固定列触发的表头/表体分离结构:Scrollbar 根 → arco-table-body →
table)逐层拉满表头以下的剩余高度,再让空态行撑满表格,「暂无数据」即在整个表体
可视区内垂直居中,表头行高度不变 */
.page-table .arco-table-content:has(.arco-table-tr-empty) {
display: flex;
flex-direction: column;
}
/* 表体区域的 Scrollbar 根(表头是第一个子节点,取 last-child 只拉伸表体) */
.page-table .arco-table-content:has(.arco-table-tr-empty) > .arco-scrollbar:last-child {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
/* 表体滚动容器(Arco 把 body 类挂在 scrollbar-container 上) */
.page-table .arco-table-content:has(.arco-table-tr-empty) .arco-table-body {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.page-table .arco-table-content:has(.arco-table-tr-empty) .arco-table-body .arco-table-element {
flex: 1;
}
/* 空态行撑满拉高后的表格(此表只剩该行,无表头行分高) */
.page-table .arco-table-content:has(.arco-table-tr-empty) .arco-table-tr-empty {
height: 100%;
}
/* Card */
.arco-card {
border-radius: var(--ta-radius);
@@ -464,16 +509,16 @@ body {
z-index: 12;
}
/* 右下角分页栏 */
/* 右下角分页栏:表格底线(末行 td 的 border-bottom)即分隔线,
footer 不再画 border-top / 留 margin-top,避免表格底部出现两条临近分割线,
仅用 padding-top 与表格保持间距 */
.page-table .table-footer {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 12px;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--ta-border-light);
}
/* 统计总数从表格左上角移到底部后:footer 内存在统计栏时两端对齐(统计居左、分页居右),
-2
View File
@@ -692,9 +692,7 @@ const { exporting, exportFile } = useExport({
align-items: center;
justify-content: space-between;
gap: 12px;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--ta-border-light);
}
/* 统计栏移入底部后,去掉全局样式的下边距,与分页栏垂直居中 */
.table-footer :deep(.table-stats) {
+10 -2
View File
@@ -326,12 +326,13 @@ onMounted(() => {
:on-before-ok="handleSaveModels"
:ok-loading="modelsLoading"
>
<ASpin :loading="modelsLoading">
<ASpin :loading="modelsLoading" class="models-transfer-wrap">
<ATransfer
v-model="selectedModelIds"
:data="transferOptions"
:title="['可选模型', '已选模型']"
searchable
class="models-transfer"
/>
</ASpin>
</AModal>
@@ -347,7 +348,6 @@ onMounted(() => {
align-items: center;
justify-content: space-between;
gap: 12px;
margin-top: 16px;
}
/* 统计栏移入底部后,去掉全局样式的下边距,与分页栏垂直居中 */
.table-footer :deep(.table-stats) {
@@ -356,4 +356,12 @@ onMounted(() => {
.mb-4 {
margin-bottom: 16px;
}
/* 模型穿梭框:弹窗内水平居中,两侧列表加高 */
.models-transfer-wrap {
display: flex;
justify-content: center;
}
.models-transfer :deep(.arco-transfer-view) {
height: 420px;
}
</style>
-2
View File
@@ -1010,9 +1010,7 @@ function resetImport() {
align-items: center;
justify-content: space-between;
gap: 12px;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--ta-border-light);
}
/* 统计栏移入底部后,去掉全局样式的下边距,与分页栏垂直居中 */
.table-footer :deep(.table-stats) {
-2
View File
@@ -418,9 +418,7 @@ onMounted(() => {
align-items: center;
justify-content: space-between;
gap: 12px;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--ta-border-light);
}
/* 统计栏移入底部后,去掉全局样式的下边距,与分页栏垂直居中 */
.table-footer :deep(.table-stats) {
@@ -138,8 +138,6 @@ onMounted(fetchData)
.table-footer {
display: flex;
justify-content: flex-end;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--ta-border-light);
}
</style>
@@ -312,9 +312,7 @@ onMounted(() => {
.table-footer {
display: flex;
justify-content: flex-end;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--color-border-light, #e5e6eb);
}
.email-body {
background: var(--color-fill-2, #f7f8fa);
@@ -367,8 +367,6 @@ const { exporting, exportFile } = useExport({
.table-footer {
display: flex;
justify-content: flex-end;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--ta-border-light);
}
</style>
@@ -549,9 +549,7 @@ const columns: TableColumnData[] = [
.table-footer {
display: flex;
justify-content: flex-end;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--ta-border-light);
}
.perm-toolbar {
@@ -462,9 +462,7 @@ onMounted(() => {
align-items: center;
justify-content: space-between;
gap: 12px;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--color-border-light, #e5e6eb);
}
/* 统计栏移入底部后,去掉全局样式的下边距,与分页栏垂直居中 */
.table-footer :deep(.table-stats) {
-1
View File
@@ -615,7 +615,6 @@ onMounted(() => {
.table-footer {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
.retention-pop {
width: 260px;
@@ -398,8 +398,6 @@ onMounted(() => {
.table-footer {
display: flex;
justify-content: flex-end;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--ta-border-light);
}
</style>
-2
View File
@@ -532,9 +532,7 @@ const { exporting, exportFile } = useExport({
.table-footer {
display: flex;
justify-content: flex-end;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--ta-border-light);
}
.role-empty-hint {
@@ -477,8 +477,6 @@ const { exporting, exportFile } = useExport({
.table-footer {
display: flex;
justify-content: flex-end;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--ta-border-light);
}
</style>
@@ -492,8 +492,6 @@ const { exporting, exportFile } = useExport({
.table-footer {
display: flex;
justify-content: flex-end;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--ta-border-light);
}
</style>
@@ -1551,9 +1551,7 @@ const { exporting, exportFile } = useExport({
align-items: center;
justify-content: space-between;
gap: 16px;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--color-border-light, #e5e6eb);
}
.table-footer-left {
@@ -7,9 +7,7 @@
align-items: center;
justify-content: space-between;
gap: 12px;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--ta-border-light);
}
.table-footer .table-stats {
margin-bottom: 0;
@@ -0,0 +1,242 @@
<script setup lang="ts">
import { computed } from 'vue'
import Icon from '@/components/common/Icon.vue'
// 卡片式异步任务加载占位:按成品形状(图片 1:1 / 视频 16:9)占位的动效卡片,
// 中央进度环展示百分比(无进度数值时转为旋转弧的不确定态),
// 底层为浅色渐变底 + 双色光斑漂移 + 斜向扫光,视觉对齐 OpenAI / Gemini 生成中的占位效果。
const props = withDefaults(
defineProps<{
kind?: 'image' | 'video'
progress?: string
label?: string
sublabel?: string
}>(),
{
kind: 'image',
progress: '',
label: '生成中...',
sublabel: '',
},
)
// 从进度文本解析 0-100 数值(上游协议为 "45%" 形态);解析不到时走不确定态
const pct = computed<number | null>(() => {
const m = /(\d+(?:\.\d+)?)/.exec(props.progress)
if (!m) return null
const n = Number(m[1])
if (!Number.isFinite(n)) return null
return Math.min(100, Math.max(0, Math.round(n)))
})
// 进度环几何参数:viewBox 100x100,r=42
const RADIUS = 42
const CIRCUMFERENCE = 2 * Math.PI * RADIUS
const dashOffset = computed(() => CIRCUMFERENCE * (1 - (pct.value ?? 0) / 100))
// 渐变 id 按实例自增,避免同页多个卡片时 url(#id) 引用错乱
let seq = 0
const gradId = `gen-grad-${++seq}`
</script>
<template>
<div class="gen-card animate-scale-in" :class="kind === 'video' ? 'gen-card--video' : 'gen-card--image'">
<!-- 底层:浅色渐变底 + 光斑漂移 + 斜向扫光 -->
<div class="gen-card__bg"></div>
<div class="gen-blob gen-blob--a"></div>
<div class="gen-blob gen-blob--b"></div>
<div class="gen-sheen"></div>
<!-- 中央:进度环 + 状态文案 -->
<div class="gen-center">
<div class="gen-ring">
<svg viewBox="0 0 100 100" class="gen-ring__svg">
<defs>
<linearGradient :id="gradId" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#2dd4bf" />
<stop offset="100%" stop-color="#0d9488" />
</linearGradient>
</defs>
<!-- 轨道 -->
<circle cx="50" cy="50" :r="RADIUS" fill="none" stroke="rgba(148, 163, 184, 0.25)" stroke-width="6" />
<!-- 不确定态:旋转短弧 -->
<circle
v-if="pct === null"
class="gen-ring__indeterminate"
cx="50" cy="50" :r="RADIUS"
fill="none" :stroke="`url(#${gradId})`"
stroke-width="6" stroke-linecap="round"
:stroke-dasharray="`${CIRCUMFERENCE * 0.24} ${CIRCUMFERENCE}`"
/>
<!-- 确定态:按进度绘制弧长 -->
<circle
v-else
class="gen-ring__progress"
cx="50" cy="50" :r="RADIUS"
fill="none" :stroke="`url(#${gradId})`"
stroke-width="6" stroke-linecap="round"
:stroke-dasharray="CIRCUMFERENCE"
:stroke-dashoffset="dashOffset"
/>
</svg>
<div class="gen-ring__text">
<span v-if="pct !== null" class="gen-pct">{{ pct }}<small>%</small></span>
<Icon v-else name="hourglass" size="lg" class="animate-pulse text-primary-500" />
</div>
</div>
<p class="gen-label">{{ label }}</p>
<p v-if="sublabel" class="gen-sublabel">{{ sublabel }}</p>
</div>
</div>
</template>
<style scoped>
/* 卡片容器:按成品形态占位 */
.gen-card {
position: relative;
overflow: hidden;
border-radius: 16px;
border: 1px solid #99f6e4;
box-shadow: 0 10px 34px rgba(91, 104, 151, 0.08);
}
.gen-card--image {
aspect-ratio: 1 / 1;
max-width: 360px;
margin-inline: auto;
}
.gen-card--video {
aspect-ratio: 16 / 9;
}
/* 底层浅色渐变 */
.gen-card__bg {
position: absolute;
inset: 0;
background: linear-gradient(135deg, #f0fdfa 0%, #f8fafc 45%, #eff6ff 100%);
}
/* 双色光斑缓慢漂移(青 + 靛),叠加出极光质感 */
.gen-blob {
position: absolute;
width: 60%;
aspect-ratio: 1 / 1;
border-radius: 50%;
filter: blur(42px);
pointer-events: none;
}
.gen-blob--a {
left: -12%;
top: -16%;
background: radial-gradient(circle, rgba(45, 212, 191, 0.4), transparent 70%);
animation: gen-drift-a 7s ease-in-out infinite alternate;
}
.gen-blob--b {
right: -14%;
bottom: -20%;
background: radial-gradient(circle, rgba(129, 140, 248, 0.32), transparent 70%);
animation: gen-drift-b 9s ease-in-out infinite alternate;
}
@keyframes gen-drift-a {
from { transform: translate(0, 0) scale(1); }
to { transform: translate(22%, 16%) scale(1.18); }
}
@keyframes gen-drift-b {
from { transform: translate(0, 0) scale(1.12); }
to { transform: translate(-20%, -14%) scale(0.94); }
}
/* 斜向扫光带,周期性掠过卡片 */
.gen-sheen {
position: absolute;
top: -12%;
bottom: -12%;
width: 42%;
background: linear-gradient(105deg, transparent, rgba(255, 255, 255, 0.55), transparent);
transform: skewX(-16deg) translateX(-130%);
animation: gen-sweep 2.8s ease-in-out infinite;
pointer-events: none;
}
@keyframes gen-sweep {
0% { transform: skewX(-16deg) translateX(-130%); }
55%, 100% { transform: skewX(-16deg) translateX(330%); }
}
/* 中央内容 */
.gen-center {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 14px;
}
/* 进度环:白色圆盘承托,保证文字与光斑叠加时的可读性 */
.gen-ring {
position: relative;
width: 104px;
height: 104px;
background: rgba(255, 255, 255, 0.8);
border: 1px solid rgba(255, 255, 255, 0.9);
border-radius: 50%;
box-shadow: 0 8px 24px rgba(76, 91, 142, 0.12);
}
.gen-ring__svg {
position: absolute;
inset: 6px;
}
.gen-ring__indeterminate {
transform-box: view-box;
transform-origin: center;
animation: gen-spin 1.1s linear infinite;
}
.gen-ring__progress {
transition: stroke-dashoffset 0.6s ease;
}
@keyframes gen-spin {
to { transform: rotate(360deg); }
}
.gen-ring__text {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
}
.gen-pct {
font-size: 26px;
font-weight: 700;
color: #0f766e;
font-variant-numeric: tabular-nums;
line-height: 1;
}
.gen-pct small {
font-size: 14px;
font-weight: 600;
margin-left: 1px;
}
.gen-label {
font-size: 14px;
font-weight: 500;
color: #334155;
}
.gen-sublabel {
font-size: 12px;
color: #94a3b8;
max-width: 80%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 弱动效偏好:静止装饰动画,仅保留进度过渡 */
@media (prefers-reduced-motion: reduce) {
.gen-blob--a,
.gen-blob--b,
.gen-sheen,
.gen-ring__indeterminate {
animation: none;
}
}
</style>