Files
workspace/code/fms/fms-vue/src/views/dashboard/index.vue
T
2026-09-03 17:30:39 +08:00

435 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>