feat(dashboard): 工作台前端四区块与 ECharts 图表
- /dashboard 由 Bento 占位升级:KPI 四卡(本月绩效/应付/核算人数/团队数+月结状态)、近 6 个月绩效折线与回款柱状、待办/预警列表与跳转、最近动态时间线(按模块筛选)、常用操作 - ECharts 5.6 按需引入(core + Bar/Line/Grid/Tooltip + SVGRenderer);textStyle.fontFamily 显式设为思源黑体栈(Windows 默认雅黑,字体商用合规红线) - 路由 allowedRoles admin/finance/leader,与后端 require_roles 双层 - 新增依赖 echarts@5.6.0 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
1 parent
6ff202d637
commit
294d99a0d3
6 files changed
+641
-209
No files matched your search
@@ -1,133 +1,372 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import * as echarts from 'echarts/core'
|
||||
import { BarChart, LineChart } from 'echarts/charts'
|
||||
import { GridComponent, TooltipComponent } from 'echarts/components'
|
||||
import { SVGRenderer } from 'echarts/renderers'
|
||||
import type { ECharts, EChartsCoreOption } from 'echarts/core'
|
||||
|
||||
import * as businessRoleApi from '@/api/businessRoles'
|
||||
import * as paramsApi from '@/api/params'
|
||||
import * as dashboardApi from '@/api/dashboard'
|
||||
import type { DashboardData, TrendPoint } from '@/api/dashboard'
|
||||
import { handledError } from '@/utils/error'
|
||||
|
||||
echarts.use([BarChart, LineChart, GridComponent, TooltipComponent, SVGRenderer])
|
||||
|
||||
/**
|
||||
* 工作台(M4 ⑧):经营绩效总览 / 待办事项 / 风险预警 / 最近动态。
|
||||
*
|
||||
* - 四区块由一次聚合请求返回(后端实时派生,见 dashboard_service 注释)
|
||||
* - 总览图表严格按设计预览:SVG 渲染、无网格线、靛蓝渐变;
|
||||
* **必须显式设置 fontFamily**——ECharts 在 Windows 默认微软雅黑(字体合规红线)
|
||||
* - ⑥ T+ 同步未建:同步失败待办/同步预警显示「模块未启用」,不显示 0 条
|
||||
*/
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
/**
|
||||
* 首页仪表盘(Bento Grid,2026-10-12 视觉方向)。
|
||||
*
|
||||
* M1 阶段:计提参数与岗位配置已就绪(真实数据);绩效/回款/待办等业务卡
|
||||
* 展示空态 —— 业务数据管理上线后接入真实统计(图表用 ECharts 渲染)。
|
||||
* 空态文案遵循全局规范:空态是邀请,说明「这里将出现什么、怎么让它出现」。
|
||||
*/
|
||||
const roleCount = ref<number | null>(null)
|
||||
const paramCount = ref<number | null>(null)
|
||||
const latestVersion = ref<string | null>(null)
|
||||
const CHART_FONT =
|
||||
"'Noto Sans SC','Source Han Sans SC','HarmonyOS Sans SC','PingFang SC',sans-serif"
|
||||
|
||||
const SETTLEMENT_STATUS_LABELS: Record<number, string> = {
|
||||
1: '草稿',
|
||||
2: '待审批',
|
||||
3: '已锁定',
|
||||
4: '已驳回',
|
||||
}
|
||||
const ORDER_TYPE_LABELS: Record<number, string> = {
|
||||
1: '二次分配方案',
|
||||
2: '月度结算',
|
||||
3: '调整单',
|
||||
}
|
||||
|
||||
const month = ref(currentMonth())
|
||||
const data = ref<DashboardData | null>(null)
|
||||
const loading = ref(false)
|
||||
|
||||
const perfChartRef = ref<HTMLDivElement | null>(null)
|
||||
const receiptChartRef = ref<HTMLDivElement | null>(null)
|
||||
let perfChart: ECharts | null = null
|
||||
let receiptChart: ECharts | null = null
|
||||
|
||||
function currentMonth(): string {
|
||||
const now = new Date()
|
||||
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
async function load(): Promise<void> {
|
||||
loading.value = true
|
||||
try {
|
||||
data.value = await dashboardApi.getDashboard({ month: month.value })
|
||||
await nextTick()
|
||||
renderCharts()
|
||||
} catch (error) {
|
||||
handledError(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function renderCharts(): void {
|
||||
const trend = data.value?.overview.trend ?? []
|
||||
if (perfChartRef.value) {
|
||||
perfChart ??= echarts.init(perfChartRef.value, undefined, { renderer: 'svg' })
|
||||
perfChart.setOption(performanceOption(trend))
|
||||
}
|
||||
if (receiptChartRef.value) {
|
||||
receiptChart ??= echarts.init(receiptChartRef.value, undefined, { renderer: 'svg' })
|
||||
receiptChart.setOption(receiptOption(trend))
|
||||
}
|
||||
}
|
||||
|
||||
function performanceOption(trend: TrendPoint[]): EChartsCoreOption {
|
||||
return {
|
||||
textStyle: { fontFamily: CHART_FONT },
|
||||
grid: { left: 8, right: 12, top: 18, bottom: 4, containLabel: true },
|
||||
tooltip: { trigger: 'axis', valueFormatter: (value: unknown) => Number(value).toFixed(2) },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: trend.map((item) => item.month.slice(5)),
|
||||
axisLine: { lineStyle: { color: 'rgba(28,35,51,.15)' } },
|
||||
axisTick: { show: false },
|
||||
axisLabel: { fontFamily: CHART_FONT, color: '#9aa3b2' },
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
splitLine: { show: false },
|
||||
axisLabel: { fontFamily: CHART_FONT, color: '#9aa3b2' },
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '绩效',
|
||||
type: 'line',
|
||||
smooth: 0.4,
|
||||
symbol: 'none',
|
||||
data: trend.map((item) => Number(item.performance_total ?? 0)),
|
||||
lineStyle: { color: '#2e4ba8', width: 2 },
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(46,75,168,.22)' },
|
||||
{ offset: 1, color: 'rgba(46,75,168,0)' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
function receiptOption(trend: TrendPoint[]): EChartsCoreOption {
|
||||
return {
|
||||
textStyle: { fontFamily: CHART_FONT },
|
||||
grid: { left: 8, right: 12, top: 18, bottom: 4, containLabel: true },
|
||||
tooltip: { trigger: 'axis', valueFormatter: (value: unknown) => Number(value).toFixed(2) },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: trend.map((item) => item.month.slice(5)),
|
||||
axisLine: { lineStyle: { color: 'rgba(28,35,51,.15)' } },
|
||||
axisTick: { show: false },
|
||||
axisLabel: { fontFamily: CHART_FONT, color: '#9aa3b2' },
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
splitLine: { show: false },
|
||||
axisLabel: { fontFamily: CHART_FONT, color: '#9aa3b2' },
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '回款',
|
||||
type: 'bar',
|
||||
data: trend.map((item) => Number(item.receipt_total ?? 0)),
|
||||
barMaxWidth: 22,
|
||||
itemStyle: { color: '#2e4ba8', borderRadius: [4, 4, 0, 0] },
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
function handleResize(): void {
|
||||
perfChart?.resize()
|
||||
receiptChart?.resize()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 三项统计各自容错:一张卡失败不影响其余卡片
|
||||
void (async () => {
|
||||
try {
|
||||
const page = await businessRoleApi.listBusinessRoles({ pageNum: 1, pageSize: 1 })
|
||||
roleCount.value = page.total
|
||||
} catch (error) {
|
||||
handledError(error)
|
||||
}
|
||||
})()
|
||||
void (async () => {
|
||||
try {
|
||||
const catalog = await paramsApi.getCatalog()
|
||||
paramCount.value = catalog.length
|
||||
} catch (error) {
|
||||
handledError(error)
|
||||
}
|
||||
})()
|
||||
void (async () => {
|
||||
try {
|
||||
const page = await paramsApi.listBatches({ pageNum: 1, pageSize: 1 })
|
||||
latestVersion.value = page.list[0]?.version_no ?? null
|
||||
} catch (error) {
|
||||
handledError(error)
|
||||
}
|
||||
})()
|
||||
void load()
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
perfChart?.dispose()
|
||||
receiptChart?.dispose()
|
||||
})
|
||||
|
||||
const activityModule = ref('')
|
||||
const activityModules = computed(() => {
|
||||
const modules = new Set((data.value?.activities ?? []).map((item) => item.module))
|
||||
return [...modules]
|
||||
})
|
||||
const filteredActivities = computed(() =>
|
||||
(data.value?.activities ?? []).filter(
|
||||
(item) => !activityModule.value || item.module === activityModule.value,
|
||||
),
|
||||
)
|
||||
|
||||
function go(path: string | null): void {
|
||||
if (path) void router.push(path)
|
||||
}
|
||||
|
||||
function formatTime(value: string): string {
|
||||
return value.replace('T', ' ').slice(0, 16)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div v-loading="loading">
|
||||
<div class="page-head">
|
||||
<h1 class="page-title">仪表盘</h1>
|
||||
<p class="page-desc">系统概览 · 计提参数与岗位配置就绪,业务数据管理上线后接入核算统计</p>
|
||||
<h1 class="page-title">工作台</h1>
|
||||
<p class="page-desc">经营绩效总览 · 待办与风险集中处置 · 最近动态</p>
|
||||
<span class="waterline"></span>
|
||||
</div>
|
||||
|
||||
<div class="toolbar">
|
||||
<el-date-picker
|
||||
v-model="month"
|
||||
type="month"
|
||||
value-format="YYYY-MM"
|
||||
:clearable="false"
|
||||
style="width: 140px"
|
||||
@change="load"
|
||||
/>
|
||||
<el-button type="primary" :loading="loading" @click="load">刷新</el-button>
|
||||
<span class="toolbar-note">{{ data?.overview.note ?? '' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="bento">
|
||||
<!-- 绩效总额:业务上线前的空态 -->
|
||||
<div class="bento-card b-2x2">
|
||||
<!-- 经营绩效总览:KPI -->
|
||||
<div class="bento-card">
|
||||
<div class="b-eyebrow">本月绩效总额</div>
|
||||
<div class="b-empty">
|
||||
<div class="b-empty-title">暂无核算数据</div>
|
||||
<div class="b-empty-desc">回款录入并核算后,这里将显示本月绩效总额与趋势</div>
|
||||
<div class="b-num">{{ data?.overview.performance_total ?? '—' }}</div>
|
||||
</div>
|
||||
<div class="bento-card">
|
||||
<div class="b-eyebrow">本月应付绩效</div>
|
||||
<div class="b-num">{{ data?.overview.payable_total ?? '—' }}</div>
|
||||
</div>
|
||||
<div class="bento-card">
|
||||
<div class="b-eyebrow">核算人数</div>
|
||||
<div class="b-num">{{ data?.overview.person_count ?? '—' }}<small>人</small></div>
|
||||
</div>
|
||||
<div class="bento-card">
|
||||
<div class="b-eyebrow">营销团队</div>
|
||||
<div class="b-num">{{ data?.overview.team_count ?? '—' }}<small>个</small></div>
|
||||
<div class="b-sub">
|
||||
月结:
|
||||
{{
|
||||
data?.overview.settlement.exists
|
||||
? SETTLEMENT_STATUS_LABELS[data.overview.settlement.status ?? 0] ?? '—'
|
||||
: '未生成'
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 回款趋势:业务上线前的空态 -->
|
||||
<!-- 经营绩效总览:图表 -->
|
||||
<div class="bento-card b-2x2">
|
||||
<div class="b-eyebrow">近 6 个月绩效</div>
|
||||
<div ref="perfChartRef" class="chart"></div>
|
||||
</div>
|
||||
<div class="bento-card b-2x2">
|
||||
<div class="b-eyebrow">近 6 个月回款</div>
|
||||
<div class="b-empty">
|
||||
<div class="b-empty-title">暂无回款数据</div>
|
||||
<div class="b-empty-desc">业务数据管理上线后,此处展示按月回款趋势</div>
|
||||
<div ref="receiptChartRef" class="chart"></div>
|
||||
</div>
|
||||
|
||||
<!-- 待办事项 -->
|
||||
<div class="bento-card b-2x2">
|
||||
<div class="b-eyebrow">
|
||||
待办事项
|
||||
<span class="b-count">{{ data?.todos.pending_approval_total ?? 0 }}</span>
|
||||
</div>
|
||||
<div v-if="data?.todos.pending_approvals.length" class="todo-list">
|
||||
<button
|
||||
v-for="item in data.todos.pending_approvals"
|
||||
:key="item.order_id"
|
||||
class="todo-row"
|
||||
type="button"
|
||||
@click="go('/system/approvals')"
|
||||
>
|
||||
<span class="todo-title">
|
||||
{{ ORDER_TYPE_LABELS[item.order_type] ?? '审批' }} · {{ item.order_no }}
|
||||
</span>
|
||||
<span class="todo-meta">{{ item.submitter_name ?? '—' }} · {{ formatTime(item.create_time) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="b-empty-mini">无待审批事项</div>
|
||||
|
||||
<div class="b-eyebrow b-eyebrow-gap">
|
||||
回款未指定批次
|
||||
<span class="b-count">{{ data?.todos.unassigned_receipt_total ?? 0 }}</span>
|
||||
</div>
|
||||
<div v-if="data?.todos.unassigned_receipts.length" class="todo-list">
|
||||
<button
|
||||
v-for="item in data.todos.unassigned_receipts"
|
||||
:key="item.receipt_id"
|
||||
class="todo-row"
|
||||
type="button"
|
||||
@click="go('/business/receipts')"
|
||||
>
|
||||
<span class="todo-title">{{ item.receipt_batch_no }} · {{ item.amount }}</span>
|
||||
<span class="todo-meta">业务 {{ item.business_no ?? item.business_id }} · {{ item.receipt_date }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="b-empty-mini">无未指定批次的回款</div>
|
||||
|
||||
<div class="b-eyebrow b-eyebrow-gap">T+ 同步失败</div>
|
||||
<div class="b-empty-mini">
|
||||
{{ data?.todos.sync_available ? `${data.todos.sync_failure_total} 条待处置` : '同步模块未启用(待 T+ 对接)' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 待办 -->
|
||||
<div class="bento-card b-2x1">
|
||||
<div class="b-eyebrow">待办</div>
|
||||
<div class="b-empty-mini">业务数据管理上线后,待核算回款会出现在这里</div>
|
||||
<!-- 风险预警 -->
|
||||
<div class="bento-card b-2x2">
|
||||
<div class="b-eyebrow">
|
||||
风险预警
|
||||
<span class="b-count">{{ data?.warnings.items.length ?? 0 }}</span>
|
||||
</div>
|
||||
<div v-if="data && !data.warnings.enabled" class="b-empty-mini">
|
||||
预警已关闭(参数 warning.enabled=0)
|
||||
</div>
|
||||
<div v-else-if="data?.warnings.items.length" class="warning-list">
|
||||
<button
|
||||
v-for="item in data.warnings.items"
|
||||
:key="`${item.code}-${item.object_id}`"
|
||||
class="warning-row"
|
||||
type="button"
|
||||
@click="go(item.link)"
|
||||
>
|
||||
<el-tag
|
||||
size="small"
|
||||
:type="item.level === 'danger' ? 'danger' : 'warning'"
|
||||
disable-transitions
|
||||
>
|
||||
{{ item.level === 'danger' ? '资金' : '时效' }}
|
||||
</el-tag>
|
||||
<span class="warning-title">{{ item.title }}</span>
|
||||
<span class="warning-detail">{{ item.detail }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="b-empty-mini">
|
||||
暂无预警(回款逾期 {{ data?.warnings.receipt_overdue_days ?? 90 }} 天 /
|
||||
审批超时 {{ data?.warnings.approval_timeout_days ?? 3 }} 天)
|
||||
</div>
|
||||
<div class="b-eyebrow b-eyebrow-gap">同步异常预警</div>
|
||||
<div class="b-empty-mini">
|
||||
{{ data?.warnings.sync_available ? '已接入' : '同步模块未启用(待 T+ 对接)' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 营销人员:人员管理上线后接入 -->
|
||||
<div class="bento-card">
|
||||
<div class="b-eyebrow">营销人员</div>
|
||||
<div class="b-empty-mini">人员管理上线后同步</div>
|
||||
</div>
|
||||
|
||||
<!-- 岗位角色:真实数据 -->
|
||||
<div class="bento-card">
|
||||
<div class="b-eyebrow">岗位角色</div>
|
||||
<div class="b-num">{{ roleCount ?? '—' }}<small>个</small></div>
|
||||
<button class="b-link" type="button" @click="router.push('/system/business-roles')">
|
||||
管理 ›
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 计提参数:真实数据 -->
|
||||
<div class="bento-card">
|
||||
<div class="b-eyebrow">计提参数</div>
|
||||
<div class="b-num">{{ paramCount ?? '—' }}<small>项</small></div>
|
||||
<button class="b-link" type="button" @click="router.push('/system/params')">
|
||||
维护 ›
|
||||
</button>
|
||||
<!-- 最近动态 -->
|
||||
<div class="bento-card b-full">
|
||||
<div class="b-eyebrow">
|
||||
最近动态
|
||||
<el-select
|
||||
v-model="activityModule"
|
||||
size="small"
|
||||
clearable
|
||||
placeholder="全部模块"
|
||||
style="width: 140px; margin-left: 12px"
|
||||
>
|
||||
<el-option v-for="item in activityModules" :key="item" :label="item" :value="item" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div v-if="filteredActivities.length" class="activity-list">
|
||||
<div v-for="item in filteredActivities" :key="item.id" class="activity-row">
|
||||
<span class="activity-time">{{ formatTime(item.create_time) }}</span>
|
||||
<el-tag size="small" :type="item.success === 1 ? 'info' : 'danger'" disable-transitions>
|
||||
{{ item.module }} · {{ item.action }}
|
||||
</el-tag>
|
||||
<span class="activity-summary">{{ item.summary }}</span>
|
||||
<span class="activity-user">{{ item.username ?? '—' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="b-empty-mini">暂无动态</div>
|
||||
</div>
|
||||
|
||||
<!-- 常用操作 -->
|
||||
<div class="bento-card b-2x1">
|
||||
<div class="bento-card b-full">
|
||||
<div class="b-eyebrow">常用操作</div>
|
||||
<div class="b-links">
|
||||
<button class="b-op" type="button" @click="router.push('/system/params')">
|
||||
计提参数
|
||||
<button class="b-op" type="button" @click="go('/business/receipts')">录入回款</button>
|
||||
<button class="b-op" type="button" @click="go('/calc/ledger')">绩效台账</button>
|
||||
<button class="b-op" type="button" @click="go('/settlements')">月度结算</button>
|
||||
<button class="b-op" type="button" @click="go('/reports/personal-detail')">
|
||||
报表统计
|
||||
</button>
|
||||
<button class="b-op" type="button" @click="router.push('/system/business-roles')">
|
||||
岗位角色
|
||||
</button>
|
||||
<button class="b-op" type="button" @click="router.push('/system/logs')">
|
||||
操作日志
|
||||
</button>
|
||||
<span class="b-op b-op-disabled" title="业务数据管理上线后开放">录入回款</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- T+ 同步 -->
|
||||
<div class="bento-card">
|
||||
<div class="b-eyebrow">T+ 同步</div>
|
||||
<div class="b-state">待接入</div>
|
||||
<div class="b-sub">每周一次 · 手动触发</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -139,56 +378,62 @@ onMounted(() => {
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--ys-ink);
|
||||
}
|
||||
|
||||
.page-desc {
|
||||
margin: 4px 0 0;
|
||||
font-size: 12px;
|
||||
margin: 6px 0 0;
|
||||
font-size: 13px;
|
||||
color: var(--ys-ink-3);
|
||||
}
|
||||
|
||||
.waterline {
|
||||
display: block;
|
||||
width: 56px;
|
||||
width: 46px;
|
||||
height: 2px;
|
||||
margin-top: 10px;
|
||||
border-radius: 1px;
|
||||
margin-top: 12px;
|
||||
background: linear-gradient(90deg, var(--ys-indigo), rgba(46, 75, 168, 0));
|
||||
}
|
||||
|
||||
/* Bento Grid:4 列基格,卡片按重要性混排大小 */
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.toolbar-note {
|
||||
font-size: 12px;
|
||||
color: var(--ys-ink-3);
|
||||
}
|
||||
|
||||
.bento {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
grid-auto-rows: 102px;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.bento-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 17px 20px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
padding: 16px 18px;
|
||||
background: linear-gradient(180deg, #ffffff, #fcfcfe);
|
||||
border: 1px solid var(--ys-line);
|
||||
border-radius: 10px;
|
||||
box-shadow:
|
||||
0 1px 2px rgba(28, 35, 51, 0.04),
|
||||
0 3px 10px rgba(28, 35, 51, 0.03);
|
||||
transition:
|
||||
transform 180ms cubic-bezier(0.25, 0.1, 0.25, 1),
|
||||
box-shadow 180ms cubic-bezier(0.25, 0.1, 0.25, 1);
|
||||
0 10px 24px -18px rgba(28, 35, 51, 0.18);
|
||||
transition: transform 0.18s ease, box-shadow 0.18s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bento-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow:
|
||||
0 1px 2px rgba(28, 35, 51, 0.05),
|
||||
0 6px 18px rgba(28, 35, 51, 0.07);
|
||||
0 2px 4px rgba(28, 35, 51, 0.05),
|
||||
0 16px 30px -18px rgba(28, 35, 51, 0.22);
|
||||
}
|
||||
|
||||
.b-2x2 {
|
||||
@@ -196,149 +441,190 @@ onMounted(() => {
|
||||
grid-row: span 2;
|
||||
}
|
||||
|
||||
.b-2x1 {
|
||||
grid-column: span 2;
|
||||
.b-full {
|
||||
grid-column: span 4;
|
||||
}
|
||||
|
||||
.b-eyebrow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--ys-ink-2);
|
||||
color: var(--ys-ink-3);
|
||||
}
|
||||
|
||||
.b-eyebrow-gap {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.b-count {
|
||||
min-width: 20px;
|
||||
padding: 0 6px;
|
||||
font-size: 11px;
|
||||
line-height: 18px;
|
||||
text-align: center;
|
||||
color: var(--ys-indigo);
|
||||
background: var(--ys-indigo-wash);
|
||||
border-radius: 9px;
|
||||
}
|
||||
|
||||
.b-num {
|
||||
margin-top: auto;
|
||||
font-size: 24px;
|
||||
margin-top: 10px;
|
||||
font-size: 26px;
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 1.2;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--ys-ink);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.b-num small {
|
||||
margin-left: 3px;
|
||||
margin-left: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: var(--ys-ink-3);
|
||||
}
|
||||
|
||||
.b-sub {
|
||||
margin-top: 3px;
|
||||
font-size: 11px;
|
||||
color: var(--ys-ink-3);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.b-state {
|
||||
margin-top: auto;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--ys-ink-2);
|
||||
}
|
||||
|
||||
.b-empty {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.b-empty-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--ys-ink-2);
|
||||
}
|
||||
|
||||
.b-empty-desc {
|
||||
max-width: 260px;
|
||||
margin-top: 6px;
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--ys-ink-3);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.b-empty-mini {
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--ys-ink-3);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.chart {
|
||||
height: 150px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.todo-list,
|
||||
.warning-list {
|
||||
max-height: 178px;
|
||||
margin-top: 8px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.todo-row,
|
||||
.warning-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
width: 100%;
|
||||
padding: 6px 4px;
|
||||
border: none;
|
||||
border-bottom: 1px dashed var(--ys-line);
|
||||
background: none;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.todo-row:hover,
|
||||
.warning-row:hover {
|
||||
background: rgba(46, 75, 168, 0.04);
|
||||
}
|
||||
|
||||
.todo-title {
|
||||
font-size: 13px;
|
||||
color: var(--ys-ink);
|
||||
}
|
||||
|
||||
.todo-meta {
|
||||
font-size: 11px;
|
||||
color: var(--ys-ink-3);
|
||||
}
|
||||
|
||||
.warning-row {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-template-rows: auto auto;
|
||||
column-gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.warning-title {
|
||||
font-size: 13px;
|
||||
color: var(--ys-ink);
|
||||
}
|
||||
|
||||
.warning-detail {
|
||||
grid-column: 2;
|
||||
font-size: 11px;
|
||||
color: var(--ys-ink-3);
|
||||
}
|
||||
|
||||
.activity-list {
|
||||
max-height: 150px;
|
||||
margin-top: 8px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.activity-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 5px 0;
|
||||
border-bottom: 1px dashed var(--ys-line);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.activity-time {
|
||||
width: 118px;
|
||||
color: var(--ys-ink-3);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.activity-summary {
|
||||
flex: 1;
|
||||
color: var(--ys-ink-2);
|
||||
}
|
||||
|
||||
.activity-user {
|
||||
color: var(--ys-ink-3);
|
||||
}
|
||||
|
||||
.b-links {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.b-link {
|
||||
padding: 0;
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--ys-indigo);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.b-link:hover {
|
||||
color: var(--ys-indigo-deep);
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.b-op {
|
||||
height: 30px;
|
||||
padding: 0 13px;
|
||||
font-size: 12px;
|
||||
font-family: inherit;
|
||||
color: var(--ys-ink);
|
||||
cursor: pointer;
|
||||
background: linear-gradient(180deg, #ffffff, #fbfcfe);
|
||||
border: 1px solid var(--ys-line);
|
||||
border-radius: 7px;
|
||||
transition:
|
||||
color 150ms cubic-bezier(0.25, 0.1, 0.25, 1),
|
||||
border-color 150ms cubic-bezier(0.25, 0.1, 0.25, 1);
|
||||
}
|
||||
|
||||
button.b-op:hover {
|
||||
padding: 6px 14px;
|
||||
font-size: 13px;
|
||||
color: var(--ys-indigo);
|
||||
border-color: rgba(46, 75, 168, 0.45);
|
||||
background: var(--ys-indigo-wash);
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.b-op-disabled {
|
||||
color: var(--ys-ink-3);
|
||||
cursor: not-allowed;
|
||||
.b-op:hover {
|
||||
border-color: var(--ys-indigo);
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.bento {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.b-full {
|
||||
grid-column: span 2;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.bento {
|
||||
grid-template-columns: 1fr;
|
||||
grid-auto-rows: auto;
|
||||
}
|
||||
|
||||
.bento-card {
|
||||
min-height: 90px;
|
||||
}
|
||||
|
||||
.b-2x2,
|
||||
.b-2x1 {
|
||||
.b-full {
|
||||
grid-column: span 1;
|
||||
grid-row: span 1;
|
||||
min-height: 130px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,5 +632,9 @@ button.b-op:hover {
|
||||
.bento-card {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.bento-card:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user