20260819173221
This commit is contained in:
1 parent
1ffc8e1ac9
commit
a0a4273f4c
326 files changed
+301646
-2314
No files matched your search
@@ -1,7 +1,10 @@
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { LayoutDashboard } from '@lucide/vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import Button from '@/components/ui/button/button.vue'
|
||||
import Input from '@/components/ui/input/input.vue'
|
||||
import FmsTable from '@/components/fms-table/FmsTable.vue'
|
||||
|
||||
defineOptions({ name: 'Dashboard' })
|
||||
|
||||
@@ -11,21 +14,133 @@ const router = useRouter()
|
||||
function goDetail(id) {
|
||||
router.push(`/detail/${id}`)
|
||||
}
|
||||
|
||||
/* ---------- 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: '待审' },
|
||||
]
|
||||
// 额外可编辑文本列数量,与下方业务列合计 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' },
|
||||
})
|
||||
}
|
||||
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,
|
||||
}
|
||||
for (let j = 1; j <= EXTRA_FIELD_COUNT; j++) row[`f${j}`] = `${i + 1}-${j}`
|
||||
rows.push(row)
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
const DEFAULT_ROW_COUNT = 5000
|
||||
const columns = ref(makeColumns())
|
||||
const rowCount = ref(DEFAULT_ROW_COUNT)
|
||||
const dataSource = ref(makeRows(DEFAULT_ROW_COUNT))
|
||||
const editCount = ref(0)
|
||||
|
||||
// 修改行数后重新生成全新数据(行对象全部重建,fms-table 编辑状态随之自然重置)
|
||||
function regenerate() {
|
||||
const count = Math.max(
|
||||
1,
|
||||
Math.min(100000, Math.floor(Number(rowCount.value)) || DEFAULT_ROW_COUNT),
|
||||
)
|
||||
rowCount.value = count
|
||||
dataSource.value = makeRows(count)
|
||||
editCount.value = 0
|
||||
}
|
||||
|
||||
// fms-table 编辑提交时回传 (row, col, newValue),这里统计编辑次数
|
||||
function onCellChange() {
|
||||
editCount.value++
|
||||
}
|
||||
|
||||
const totalCells = computed(() => columns.value.length * dataSource.value.length)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dashboard-page">
|
||||
<div class="dashboard-icon" aria-hidden="true">
|
||||
<LayoutDashboard />
|
||||
</div>
|
||||
<h1 class="dashboard-title">工作台</h1>
|
||||
<p class="dashboard-desc">布局壳与业务页面已就绪,点击下方按钮进入详情页验证多参数缓存。</p>
|
||||
<header class="dashboard-header">
|
||||
<div class="dashboard-heading">
|
||||
<div class="dashboard-icon" aria-hidden="true">
|
||||
<LayoutDashboard />
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="dashboard-title">工作台 · fms-table Demo</h1>
|
||||
<p class="dashboard-desc">
|
||||
20 列单行编辑示例:单击单元格进入编辑,Enter 提交,Esc 取消,方向键在单元格间移动。默认
|
||||
5000 行(虚拟滚动),可修改行数后「重新生成」。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-actions">
|
||||
<Button type="primary" @click="goDetail(1)">打开详情 #1</Button>
|
||||
<Button type="primary" @click="goDetail(2)">打开详情 #2</Button>
|
||||
<Button type="primary" @click="goDetail(3)">打开详情 #3</Button>
|
||||
</div>
|
||||
<div class="dashboard-actions">
|
||||
<Button type="primary" @click="goDetail(1)">打开详情 #1</Button>
|
||||
<Button type="primary" @click="goDetail(2)">打开详情 #2</Button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="demo-card">
|
||||
<div class="demo-toolbar">
|
||||
<label class="toolbar-field">
|
||||
<span class="toolbar-label">行数</span>
|
||||
<Input v-model="rowCount" type="number" class="toolbar-input" />
|
||||
</label>
|
||||
<Button type="primary" @click="regenerate">重新生成</Button>
|
||||
<span class="toolbar-meta">
|
||||
{{ dataSource.length.toLocaleString() }} 行 × {{ columns.length }} 列(内置序号列不计入),
|
||||
共 {{ totalCells.toLocaleString() }} 个单元格 · 已编辑 {{ editCount }} 次
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="demo-table">
|
||||
<FmsTable :columns="columns" :data-source="dataSource" row-key="id" @change="onCellChange" />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -33,48 +148,106 @@ function goDetail(id) {
|
||||
.dashboard-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
min-height: 100%;
|
||||
padding: 24px;
|
||||
/* 页面高度 = fms-content 可视内容高度(100vh - 顶栏40 - 布局gap8 - 页签40 - 内容padding 12+16),
|
||||
让滚动条落在表格内部而不是整个页面滚动 */
|
||||
height: calc(100vh - 116px);
|
||||
color: var(--fms-text);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.dashboard-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.dashboard-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 12px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
flex: none;
|
||||
border-radius: 10px;
|
||||
background: color-mix(in srgb, var(--fms-primary) 10%, var(--fms-card));
|
||||
color: var(--fms-primary);
|
||||
|
||||
:deep(svg) {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-title {
|
||||
margin: 0;
|
||||
font-size: 22px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dashboard-desc {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
margin: 2px 0 0;
|
||||
font-size: 13px;
|
||||
color: var(--fms-text-secondary);
|
||||
}
|
||||
|
||||
.dashboard-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* ---- demo 卡片:工具栏 + 表格 ---- */
|
||||
.demo-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--fms-border);
|
||||
border-radius: 8px;
|
||||
background: var(--fms-card);
|
||||
}
|
||||
|
||||
.demo-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 8px;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--fms-border);
|
||||
}
|
||||
|
||||
.toolbar-field {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.toolbar-label {
|
||||
font-size: 13px;
|
||||
color: var(--fms-text-secondary);
|
||||
}
|
||||
|
||||
.toolbar-input {
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.toolbar-meta {
|
||||
margin-left: auto;
|
||||
font-size: 12px;
|
||||
color: var(--fms-text-secondary);
|
||||
}
|
||||
|
||||
.demo-table {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -9,7 +9,6 @@ import Button from '@/components/ui/button/button.vue'
|
||||
import Message from '@/components/ui/message/message-manager.js'
|
||||
import { loginApi } from '@/services/api'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { usePermissionStore } from '@/stores/permissions'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
@@ -26,35 +25,21 @@ const loginHint = computed(() => {
|
||||
/* ---------- 表单 ---------- */
|
||||
const formModel = reactive({
|
||||
orgId: '',
|
||||
userId: '',
|
||||
account: '',
|
||||
password: '',
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
orgId: [{ required: true, message: '请输入机构码' }],
|
||||
userId: [{ required: true, message: '请输入账号' }],
|
||||
account: [{ required: true, message: '请输入账号' }],
|
||||
}
|
||||
|
||||
const submitting = ref(false)
|
||||
|
||||
const LAST_LOGIN_KEY = 'fms-last-login'
|
||||
|
||||
onMounted(() => {
|
||||
// 从独立存储读取上次登录信息回填,与认证状态分离
|
||||
let lastOrgId = ''
|
||||
let lastAccount = ''
|
||||
try {
|
||||
const raw = localStorage.getItem(LAST_LOGIN_KEY)
|
||||
if (raw) {
|
||||
const last = JSON.parse(raw)
|
||||
lastOrgId = last.orgId || ''
|
||||
lastAccount = last.account || ''
|
||||
}
|
||||
} catch {
|
||||
// 解析失败不影响登录
|
||||
}
|
||||
formModel.orgId = lastOrgId
|
||||
formModel.userId = lastAccount
|
||||
// 回填上次登录信息:由 persist 插件在启动时恢复,不手动碰 localStorage
|
||||
formModel.orgId = authStore.userInfo.orgId || ''
|
||||
formModel.account = authStore.userInfo.account || ''
|
||||
formModel.password = ''
|
||||
// 聚焦机构码输入框(id 落在 Input 根元素上,需定位内部 input)
|
||||
document.querySelector('#orgId input')?.focus()
|
||||
@@ -64,13 +49,14 @@ onMounted(() => {
|
||||
async function onSubmit(values) {
|
||||
submitting.value = true
|
||||
try {
|
||||
const res = await loginApi(values.orgId, values.userId, values.password ?? '')
|
||||
const res = await loginApi(values.orgId, values.account, values.password ?? '')
|
||||
authStore.setSession(
|
||||
{ token: res.data.token, user: { ...res.data.user, account: values.userId } },
|
||||
{ token: res.data.token, user: { ...res.data.user } },
|
||||
res.data.orgid ?? values.orgId,
|
||||
)
|
||||
// 登录成功后、进入布局前先把菜单/权限数据拉齐,避免 /dashboard 挂载时菜单异步“跳出”
|
||||
await usePermissionStore().load().catch(() => {})
|
||||
// 记住本次登录(机构码 + 账号),供下次回填;统一由 persist 插件持久化
|
||||
authStore.saveLastLogin(values.orgId, values.account)
|
||||
// 权限数据由路由守卫在跳转 /dashboard 时统一加载,无需在此预加载
|
||||
Message.success('登录成功')
|
||||
router.replace(String(route.query.redirect || '/dashboard'))
|
||||
} catch (e) {
|
||||
@@ -122,10 +108,11 @@ function onFailed() {
|
||||
</Input>
|
||||
</FormItem>
|
||||
|
||||
<FormItem name="userId" label="账号">
|
||||
<FormItem name="account" label="账号">
|
||||
<Input
|
||||
id="userId"
|
||||
v-model="formModel.userId"
|
||||
id="account"
|
||||
v-model="formModel.account"
|
||||
autocomplete="username"
|
||||
placeholder="请输入账号"
|
||||
:disabled="submitting"
|
||||
>
|
||||
@@ -138,6 +125,7 @@ function onFailed() {
|
||||
id="password"
|
||||
v-model="formModel.password"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
placeholder="请输入密码"
|
||||
:disabled="submitting"
|
||||
>
|
||||
|
||||
Reference in new issue
Block a user