435 lines
14 KiB
Vue
435 lines
14 KiB
Vue
<script setup>
|
||
import { computed, ref } from 'vue'
|
||
import { ChevronDown, ChevronUp, Download, Plus, RefreshCw, Search, Trash2, Upload } from '@lucide/vue'
|
||
import { Button, Dropdown, Input, Pagination, RangePicker, Select } from '@/components/ui'
|
||
import FmsTable from '@/components/fms-table/FmsTable.vue'
|
||
|
||
defineOptions({ name: 'Dashboard' })
|
||
|
||
/* ---------- fms-table 大数据量 + 单行编辑 demo ---------- */
|
||
const TYPE_OPTIONS = [
|
||
{ value: 'task', label: '任务' },
|
||
{ value: 'job', label: '作业' },
|
||
{ value: 'flow', label: '流程' },
|
||
{ value: 'alert', label: '告警' },
|
||
]
|
||
const STATUS_OPTIONS = [
|
||
{ value: 'active', label: '启用' },
|
||
{ value: 'disabled', label: '禁用' },
|
||
{ value: 'pending', label: '待审' },
|
||
]
|
||
// 低频筛选字段选项(模拟多字段场景)
|
||
const PRIORITY_OPTIONS = [
|
||
{ value: 'high', label: '高' },
|
||
{ value: 'mid', label: '中' },
|
||
{ value: 'low', label: '低' },
|
||
]
|
||
const SOURCE_OPTIONS = [
|
||
{ value: 'manual', label: '手工录入' },
|
||
{ value: 'import', label: '导入' },
|
||
{ value: 'api', label: 'API' },
|
||
]
|
||
const ENABLED_OPTIONS = [
|
||
{ value: '1', label: '是' },
|
||
{ value: '0', label: '否' },
|
||
]
|
||
// 额外可编辑文本列数量,与下方业务列合计 20 列
|
||
const EXTRA_FIELD_COUNT = 14
|
||
|
||
function makeColumns() {
|
||
const columns = [
|
||
{ title: 'ID', dataIndex: 'id', width: 70, editor: { type: 'input', inputType: 'number' } },
|
||
{ title: '名称', dataIndex: 'name', width: 120, editor: { type: 'input' } },
|
||
{
|
||
title: '类型',
|
||
dataIndex: 'type',
|
||
width: 90,
|
||
editor: { type: 'select', options: TYPE_OPTIONS },
|
||
},
|
||
{
|
||
title: '状态',
|
||
dataIndex: 'status',
|
||
width: 90,
|
||
editor: { type: 'select', options: STATUS_OPTIONS },
|
||
},
|
||
{ title: '启用', dataIndex: 'enabled', width: 64, editor: { type: 'checkbox' } },
|
||
{ title: '评分', dataIndex: 'score', width: 80, editor: { type: 'input', inputType: 'number' } },
|
||
]
|
||
for (let i = 1; i <= EXTRA_FIELD_COUNT; i++) {
|
||
columns.push({
|
||
title: `字段 ${i}`,
|
||
dataIndex: `f${i}`,
|
||
width: 80,
|
||
editor: { type: 'input' },
|
||
})
|
||
}
|
||
// 纯展示列(无 editor、无 customCell),走库原生文本渲染:
|
||
// money 千分位 / datetime 日期时间 / date 日期 / percent 百分比
|
||
columns.push(
|
||
{ title: '金额', dataIndex: 'amount', width: 120 },
|
||
{ title: '创建时间', dataIndex: 'createdAt', width: 170 },
|
||
{ title: '到期日', dataIndex: 'expireDate', width: 110 },
|
||
{ title: '完成率', dataIndex: 'progress', width: 90 },
|
||
)
|
||
return columns
|
||
}
|
||
function makeRows(count) {
|
||
const rows = []
|
||
for (let i = 0; i < count; i++) {
|
||
const row = {
|
||
id: i + 1,
|
||
name: `任务-${String(i + 1).padStart(5, '0')}`,
|
||
type: TYPE_OPTIONS[i % TYPE_OPTIONS.length].value,
|
||
status: STATUS_OPTIONS[i % STATUS_OPTIONS.length].value,
|
||
enabled: i % 2,
|
||
score: Math.round(Math.random() * 1000) / 10,
|
||
amount: Math.round(Math.random() * 10000000) / 100,
|
||
createdAt: new Date(Date.now() - (count - i) * 3600_000).toISOString(),
|
||
expireDate: new Date(Date.now() + ((i % 365) + 1) * 86400_000).toISOString(),
|
||
progress: Math.round(Math.random() * 100) / 100,
|
||
}
|
||
for (let j = 1; j <= EXTRA_FIELD_COUNT; j++) row[`f${j}`] = `${i + 1}-${j}`
|
||
rows.push(row)
|
||
}
|
||
return rows
|
||
}
|
||
|
||
// 行级特殊样式:根据业务字段返回行 class(经 $attrs 透传给 stk-table 的 rowClassName)。
|
||
// 返回的 class 在下方 <style scoped> 中通过 :deep() 定义字体/背景颜色。
|
||
function rowClassName(row) {
|
||
if (row.type === 'alert') return 'fms-row-alert'
|
||
if (row.status === 'pending') return 'fms-row-pending'
|
||
if (row.status === 'disabled') return 'fms-row-disabled'
|
||
return ''
|
||
}
|
||
|
||
/* ---------- 表格数据:默认 1w 行(虚拟滚动) ---------- */
|
||
const DEFAULT_ROW_COUNT = 10000
|
||
const columns = ref(makeColumns())
|
||
const dataSource = ref(makeRows(DEFAULT_ROW_COUNT))
|
||
|
||
/* ---------- 搜索表单 + 操作工具栏(仅界面骨架,查询/操作逻辑后续接入) ---------- */
|
||
const searchForm = ref({
|
||
name: '',
|
||
type: undefined,
|
||
status: undefined,
|
||
range: null,
|
||
owner: undefined,
|
||
priority: undefined,
|
||
source: undefined,
|
||
enabled: undefined,
|
||
minScore: null,
|
||
})
|
||
|
||
// 搜索字段描述:顺序即表单字段顺序(前 FIELDS_PER_ROW 个为常驻行)
|
||
const searchFields = [
|
||
{ key: 'name', label: '名称 / ID', type: 'input', placeholder: '模糊搜索', submitIcon: true },
|
||
{ key: 'type', label: '类型', type: 'select', options: TYPE_OPTIONS },
|
||
{ key: 'status', label: '状态', type: 'select', options: STATUS_OPTIONS },
|
||
{ key: 'range', label: '创建时间', type: 'range' },
|
||
{ key: 'owner', label: '负责人', type: 'input' },
|
||
{ key: 'priority', label: '优先级', type: 'select', options: PRIORITY_OPTIONS },
|
||
{ key: 'source', label: '来源', type: 'select', options: SOURCE_OPTIONS },
|
||
{ key: 'enabled', label: '启用', type: 'select', options: ENABLED_OPTIONS },
|
||
{ key: 'minScore', label: '最低评分', type: 'input-number' },
|
||
]
|
||
|
||
/* 每行固定 4 个字段格;字段更多时低频字段另起整行(展开/收起) */
|
||
const FIELDS_PER_ROW = 4
|
||
const searchExpanded = ref(false)
|
||
const fieldsCollapsible = computed(() => searchFields.length > FIELDS_PER_ROW)
|
||
const firstRowFields = computed(() => searchFields.slice(0, FIELDS_PER_ROW))
|
||
const moreRowFields = computed(() => searchFields.slice(FIELDS_PER_ROW))
|
||
const moreCount = computed(() => moreRowFields.value.length)
|
||
|
||
function toggleSearchExpanded() {
|
||
searchExpanded.value = !searchExpanded.value
|
||
if (!searchExpanded.value) {
|
||
// 收起时清空被隐藏的低频字段,保证「看到的条件 = 生效的条件」
|
||
for (const field of moreRowFields.value) searchForm.value[field.key] = null
|
||
}
|
||
}
|
||
|
||
const moreItems = [
|
||
{ key: 'export', label: '导出数据', icon: Download },
|
||
{ key: 'import', label: '导入数据', icon: Upload },
|
||
{ key: 'refresh', label: '刷新', icon: RefreshCw },
|
||
{ key: 'divider-1', divider: true },
|
||
{ key: 'batch-delete', label: '批量删除', icon: Trash2, danger: true },
|
||
]
|
||
|
||
// TODO: 接入真实搜索 / 更多操作 / 新增逻辑
|
||
function onMoreAction() {}
|
||
function onSearch() {}
|
||
function onReset() {
|
||
searchForm.value = {
|
||
name: '',
|
||
type: undefined,
|
||
status: undefined,
|
||
range: null,
|
||
owner: undefined,
|
||
priority: undefined,
|
||
source: undefined,
|
||
enabled: undefined,
|
||
minScore: null,
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div class="dashboard-page">
|
||
<!-- 搜索卡(独立白卡):每行固定 4 个字段格;字段更多时低频字段另起整行(展开/收起),
|
||
操作区固定末行。「看到的条件 = 生效的条件」,收起时清空隐藏字段 -->
|
||
<form class="search-card" @submit.prevent="onSearch">
|
||
<div class="search-row">
|
||
<div v-for="field in firstRowFields" :key="field.key" class="search-field">
|
||
<label class="search-label">{{ field.label }}</label>
|
||
<Input
|
||
v-if="field.type === 'input' || field.type === 'input-number'"
|
||
v-model="searchForm[field.key]"
|
||
:input-type="field.type === 'input-number' ? 'number' : 'text'"
|
||
:placeholder="field.placeholder"
|
||
allow-clear
|
||
>
|
||
<template v-if="field.submitIcon" #suffix>
|
||
<button type="submit" class="search-submit" aria-label="查询">
|
||
<Search :size="14" aria-hidden="true" />
|
||
</button>
|
||
</template>
|
||
</Input>
|
||
<Select
|
||
v-else-if="field.type === 'select'"
|
||
v-model="searchForm[field.key]"
|
||
:options="field.options"
|
||
clearable
|
||
/>
|
||
<RangePicker v-else-if="field.type === 'range'" v-model="searchForm[field.key]" />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 低频字段行:字段总数超过每行格数时按开关显隐 -->
|
||
<div v-if="fieldsCollapsible && searchExpanded" class="search-row">
|
||
<div v-for="field in moreRowFields" :key="field.key" class="search-field">
|
||
<label class="search-label">{{ field.label }}</label>
|
||
<Input
|
||
v-if="field.type === 'input' || field.type === 'input-number'"
|
||
v-model="searchForm[field.key]"
|
||
:input-type="field.type === 'input-number' ? 'number' : 'text'"
|
||
:placeholder="field.placeholder"
|
||
allow-clear
|
||
/>
|
||
<Select
|
||
v-else-if="field.type === 'select'"
|
||
v-model="searchForm[field.key]"
|
||
:options="field.options"
|
||
clearable
|
||
/>
|
||
<RangePicker v-else-if="field.type === 'range'" v-model="searchForm[field.key]" />
|
||
</div>
|
||
</div>
|
||
|
||
<div class="search-actions">
|
||
<Button type="primary" @click="onSearch">查询</Button>
|
||
<Button type="secondary" @click="onReset">重置</Button>
|
||
<Button v-if="fieldsCollapsible" type="text" @click="toggleSearchExpanded">
|
||
{{ searchExpanded ? '收起' : `展开 ${moreCount} 项` }}
|
||
<component :is="searchExpanded ? ChevronUp : ChevronDown" :size="14" aria-hidden="true" />
|
||
</Button>
|
||
</div>
|
||
</form>
|
||
|
||
<!-- 列表卡:操作工具栏 + 表格 + 分页 -->
|
||
<section class="demo-card">
|
||
<!-- 操作工具栏:主操作靠左,批量/更多操作靠右 -->
|
||
<div class="demo-toolbar">
|
||
<div class="toolbar-left">
|
||
<Button type="primary">
|
||
<Plus :size="14" aria-hidden="true" />
|
||
新增
|
||
</Button>
|
||
</div>
|
||
|
||
<div class="toolbar-right">
|
||
<Dropdown :items="moreItems" @click="onMoreAction">
|
||
<template #trigger>
|
||
<Button type="outline">
|
||
更多操作
|
||
<ChevronDown :size="14" aria-hidden="true" />
|
||
</Button>
|
||
</template>
|
||
</Dropdown>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="demo-table">
|
||
<FmsTable :columns="columns" :data-source="dataSource" row-key="id" :bordered="false" />
|
||
</div>
|
||
|
||
<!-- 底部分页:仅 UI 骨架,分页逻辑后续接入 -->
|
||
<div class="demo-pagination">
|
||
<Pagination :total="dataSource.length" show-total show-size-changer />
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.dashboard-page {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
/* 纯 flex 链撑满内容区可视高度(.fms-content / .fms-page-wrap 均已 flex:1 + min-height:0),
|
||
让滚动条落在表格内部而不是整个页面滚动 */
|
||
flex: 1;
|
||
min-height: 0;
|
||
color: var(--fms-text);
|
||
}
|
||
|
||
/* ---- 列表卡:操作工具栏 + 表格 + 分页(灰底画布上的白卡,靠阴影分层,无边框线) ---- */
|
||
.demo-card {
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-height: 0;
|
||
flex: 1;
|
||
overflow: hidden;
|
||
border-radius: 8px;
|
||
background: var(--fms-card);
|
||
box-shadow: var(--fms-shadow);
|
||
}
|
||
|
||
/* ---- 操作工具栏:主操作靠左,更多操作靠右(白底,靠 padding 与上下分区) ---- */
|
||
.demo-toolbar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
/* 换行时整组掉下去,避免左右两组元素交错堆叠 */
|
||
flex-wrap: wrap;
|
||
padding: 10px 12px;
|
||
}
|
||
|
||
/* 组内不换行:窄屏由外层整组换行兜底 */
|
||
.toolbar-left,
|
||
.toolbar-right {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
/* ---- 搜索卡(独立白卡):行堆叠,每行固定 4 个等宽字段格。
|
||
* 格内 label 行内居左、控件占满剩余宽度。
|
||
* 回车提交:表单 submit + 首字段尾部的内嵌查询按钮。 ---- */
|
||
.search-card {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
padding: 12px;
|
||
border-radius: 8px;
|
||
background: var(--fms-card);
|
||
box-shadow: var(--fms-shadow);
|
||
}
|
||
|
||
.search-row {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
column-gap: 16px;
|
||
row-gap: 10px;
|
||
}
|
||
|
||
.search-field {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
/* 控件自适应内容宽度,不撑满整行 */
|
||
flex: 0 1 auto;
|
||
min-width: 0;
|
||
}
|
||
|
||
.search-label {
|
||
font-size: 12px;
|
||
color: var(--fms-text-secondary);
|
||
/* 多语言下 label 可能很长:不强制单行,允许换行,防溢出 */
|
||
white-space: normal;
|
||
word-break: break-word;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
/* 控件按内容自适应宽度(Input / Select / RangePicker 根节点),带保底宽度 */
|
||
.search-field :deep(.input-wrapper),
|
||
.search-field :deep(.select),
|
||
.search-field :deep(.fms-range-picker) {
|
||
width: auto;
|
||
min-width: 160px;
|
||
max-width: 260px;
|
||
}
|
||
|
||
/* 内嵌在首字段尾部的小查询按钮(原生 button,避免触发表单内嵌套问题) */
|
||
.search-submit {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border: none;
|
||
background: none;
|
||
padding: 2px;
|
||
color: var(--fms-text-secondary);
|
||
cursor: pointer;
|
||
}
|
||
|
||
.search-submit:hover {
|
||
color: var(--fms-primary);
|
||
}
|
||
|
||
.search-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
/* 右对齐:按钮落在表单末行右下角,与字段的阅读终点衔接 */
|
||
margin-left: auto;
|
||
}
|
||
|
||
.demo-table {
|
||
flex: 1;
|
||
min-height: 0;
|
||
/* 表头文字对齐 12px 轴线:4px 容器 padding + 表格单元格自带 8px 水平 padding */
|
||
padding: 0 4px;
|
||
|
||
/* 行级特殊样式:rowClassName 返回的 class 落在 stk-table 的 tr 上,
|
||
这里通过 :deep() 跨过组件边界设置字体颜色与背景色 */
|
||
:deep(.fms-row-alert) {
|
||
color: var(--fms-danger);
|
||
|
||
td {
|
||
background: color-mix(in srgb, var(--fms-danger) 8%, var(--fms-card));
|
||
}
|
||
}
|
||
|
||
:deep(.fms-row-pending) {
|
||
color: var(--fms-warning);
|
||
|
||
td {
|
||
background: color-mix(in srgb, var(--fms-warning) 8%, var(--fms-card));
|
||
}
|
||
}
|
||
|
||
:deep(.fms-row-disabled) {
|
||
color: var(--fms-disabled-text);
|
||
|
||
td {
|
||
background: var(--fms-background);
|
||
}
|
||
}
|
||
}
|
||
|
||
/* ---- 底部分页条:白底,靠 padding 与表格分区(列表卡整体纯白);
|
||
* 总数靠左,分页靠右 ---- */
|
||
.demo-pagination {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
padding: 10px 12px;
|
||
}
|
||
</style>
|