20260810223432

This commit is contained in:
oneao committed 2026-08-10 22:34:33 +08:00
1 parent ed5ab3885d
commit 5adbf59425
364 files changed
+7559 -4358

No files matched your search

+422 -64
View File
@@ -1,80 +1,438 @@
<script setup lang="ts">
import { ref } from "vue"
import FmsSelect from "@/components/FmsSelect.vue"
import FmsAutoComplete from "@/components/FmsAutoComplete.vue"
import { computed, nextTick, ref, shallowRef } from "vue";
import { toast } from "vue-sonner";
import {
CheckCircle2,
Copy,
Database,
Pencil,
Plus,
RefreshCw,
Rows3,
Search,
Trash2,
X,
} from "@lucide/vue";
import FmsAutoComplete from "@/components/FmsAutoComplete.vue";
import FmsSelect from "@/components/FmsSelect.vue";
import FmsTable from "@/components/fms-table/FmsTable.vue";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
// FmsSelect demo
const localeCode = ref("")
const localeLabel = ref("")
const localeRecord = ref<Record<string, any> | null>(null)
type DemoRow = {
id: string;
code: string;
name: string;
owner: string;
status: number;
enabled: number;
updatedAt: string;
};
function onLocaleChange(record: Record<string, any> | null) {
localeRecord.value = record
const ROW_COUNT = 15;
const OWNER_OPTIONS = [
{ label: "林晓", value: "lin-xiao" },
{ label: "陈默", value: "chen-mo" },
{ label: "周宁", value: "zhou-ning" },
{ label: "方言", value: "fang-yan" },
];
const STATUS_OPTIONS = [
{ label: "启用", value: 1 },
{ label: "禁用", value: 0 },
];
function buildRows(): DemoRow[] {
return Array.from({ length: ROW_COUNT }, (_, index) => ({
id: `R${String(index + 1).padStart(4, "0")}`,
code: `FMS-${String(index + 1).padStart(4, "0")}`,
name: ["客户资料", "订单状态", "库存预警", "合同归档", "发票登记"][index % 5] + ` ${index + 1}`,
owner: OWNER_OPTIONS[index % OWNER_OPTIONS.length]!.value,
status: index % 7 === 0 ? 0 : 1,
enabled: index % 11 === 0 ? 0 : 1,
updatedAt: `2026-08-${String((index % 28) + 1).padStart(2, "0")} ${String((index % 10) + 8).padStart(2, "0")}:30`,
}));
}
// FmsAutoComplete demo
const autoValue = ref("")
const autoRecord = ref<Record<string, any> | null>(null)
const tableRef = ref<InstanceType<typeof FmsTable> | null>(null);
const dataSource = shallowRef<DemoRow[]>(buildRows());
const searchQuery = ref("");
const statusFilter = ref("all");
const selectedRowKeys = ref<string[]>([]);
const activeRowKey = ref<string | null>(null);
const nextRowNumber = ref(ROW_COUNT + 1);
function onAutoChange(record: Record<string, any> | null) {
autoRecord.value = record
const columns = shallowRef([
{ dataIndex: "code", title: "记录编号", width: 145, fixed: "left" },
{ dataIndex: "name", title: "项目名称", width: 220, editor: { type: "input" } },
{
dataIndex: "owner",
title: "负责人",
width: 130,
editor: { type: "select", options: OWNER_OPTIONS },
},
{
dataIndex: "status",
title: "状态",
width: 105,
editor: { type: "select", options: STATUS_OPTIONS },
},
{ dataIndex: "enabled", title: "启用", width: 82, editor: { type: "checkbox" } },
{ dataIndex: "updatedAt", title: "最近更新", width: 175 },
{
dataIndex: "actions",
key: "actions",
title: "操作",
width: 190,
actions: [
{ type: "edit", label: "编辑", icon: Pencil, onClick: (row: DemoRow) => editRow(row) },
{
type: "delete",
label: "删除",
icon: Trash2,
danger: true,
confirm: {
title: "删除这条记录?",
content: "删除后无法恢复,请确认操作。",
okType: "danger",
},
onClick: (row: DemoRow) => removeRow(row),
},
{
type: "more",
label: "更多",
items: [
{
key: "duplicate",
label: "复制记录",
icon: Copy,
onClick: (row: DemoRow) => duplicateRow(row),
},
{ key: "disable", label: "切换启用状态", onClick: (row: DemoRow) => toggleRow(row) },
],
},
],
},
]);
const filteredRows = computed(() => {
const keyword = searchQuery.value.trim().toLowerCase();
return dataSource.value.filter((row) => {
const matchesKeyword =
!keyword || `${row.code} ${row.name} ${row.owner}`.toLowerCase().includes(keyword);
const matchesStatus = statusFilter.value === "all" || String(row.status) === statusFilter.value;
return matchesKeyword && matchesStatus;
});
});
const rowSelection = computed(() => ({
selectedRowKeys: selectedRowKeys.value,
onChange: (keys: (string | number)[]) => {
selectedRowKeys.value = keys.map(String);
},
}));
const enabledCount = computed(() => dataSource.value.filter((row) => row.enabled === 1).length);
const selectedCount = computed(() => selectedRowKeys.value.length);
function syncRows() {
dataSource.value = [...dataSource.value];
}
function onTableChange(
row: DemoRow,
column: { title?: string; dataIndex?: string },
value: unknown,
) {
activeRowKey.value = row.id;
syncRows();
toast.success(`${column.title || column.dataIndex} 已更新`, {
description: `${row.code} · ${String(value)}`,
});
}
function onRowOrderChange(rows: DemoRow[]) {
dataSource.value = rows;
toast.success("行顺序已更新");
}
function regenerate() {
dataSource.value = buildRows();
selectedRowKeys.value = [];
activeRowKey.value = null;
nextRowNumber.value = ROW_COUNT + 1;
toast.success(`已生成 ${ROW_COUNT.toLocaleString()} 条演示数据`);
}
async function addRow() {
const rowNumber = nextRowNumber.value++;
const row: DemoRow = {
id: `R${String(rowNumber).padStart(4, "0")}`,
code: `FMS-${String(rowNumber).padStart(4, "0")}`,
name: `新建记录 ${rowNumber}`,
owner: OWNER_OPTIONS[0]!.value,
status: 1,
enabled: 1,
updatedAt: "2026-08-10 20:00",
};
dataSource.value = [...dataSource.value, row];
searchQuery.value = "";
statusFilter.value = "all";
activeRowKey.value = row.id;
await nextTick();
await tableRef.value?.editRow(row.id, "name");
toast.success(`已新增 ${row.code}`);
}
function editRow(row: DemoRow) {
activeRowKey.value = row.id;
tableRef.value?.editRow(row.id, "name", { scroll: false });
}
function removeRow(row: DemoRow) {
dataSource.value = dataSource.value.filter((item) => item.id !== row.id);
selectedRowKeys.value = selectedRowKeys.value.filter((key) => key !== row.id);
toast.success(`已删除 ${row.code}`);
}
function duplicateRow(row: DemoRow) {
const copyRow = {
...row,
id: `${row.id}-COPY`,
code: `${row.code}-COPY`,
name: `${row.name} 副本`,
};
dataSource.value = [copyRow, ...dataSource.value];
toast.success(`已复制 ${row.code}`);
}
function toggleRow(row: DemoRow) {
row.enabled = row.enabled === 1 ? 0 : 1;
syncRows();
toast.success(row.enabled === 1 ? "记录已启用" : "记录已停用");
}
function editFirstSelected() {
const firstKey = selectedRowKeys.value[0];
if (!firstKey) {
toast.info("请先选择一条记录");
return;
}
editRow(dataSource.value.find((row) => row.id === firstKey)!);
}
function clearSelection() {
selectedRowKeys.value = [];
tableRef.value?.clearSelectedArea();
}
// 远程控件 demo
const localeCode = ref("");
const localeLabel = ref("");
const localeRecord = ref<Record<string, unknown> | null>(null);
const autoValue = ref("");
const autoRecord = ref<Record<string, unknown> | null>(null);
</script>
<template>
<div class="flex h-full flex-col items-center gap-8 p-8">
<h1 class="text-2xl font-bold text-muted-foreground">Dashboard</h1>
<div class="grid w-full max-w-2xl gap-8">
<!-- FmsSelect demo -->
<div class="rounded-lg border p-6 space-y-3">
<h2 class="text-sm font-semibold">FmsSelect(多语言类型)</h2>
<p class="text-xs text-muted-foreground">
v-model + v-model:label 双绑定,支持远程搜索、滚动分页、清除、未匹配值回显
</p>
<div class="w-64">
<FmsSelect
v-model="localeCode"
v-model:label="localeLabel"
view-name="b_i18n_type"
label-key="b_name"
value-key="b_code"
order-by="b_code ASC"
:page-size="10"
allow-clear
placeholder="选择语言类型..."
@change="onLocaleChange"
/>
<main class="flex min-h-full flex-col gap-5 pb-2">
<header class="flex flex-wrap items-end justify-between gap-4 border-b pb-4">
<div class="min-w-0">
<div
class="mb-1 flex items-center gap-2 text-xs font-medium uppercase tracking-[0.16em] text-muted-foreground"
>
<Database class="size-3.5" />
FmsTable Playground
</div>
<div class="text-xs text-muted-foreground space-y-0.5">
<div>value(b_code):<code class="text-foreground">{{ localeCode || '-' }}</code></div>
<div>label(b_name):<code class="text-foreground">{{ localeLabel || '-' }}</code></div>
<div>record:<code class="text-foreground">{{ localeRecord || '-' }}</code></div>
<h1 class="text-2xl font-semibold tracking-tight">数据表格工作台</h1>
<p class="mt-1 max-w-2xl text-sm text-muted-foreground">
在真实数据场景下验证虚拟滚动、行选择、单元格编辑和行拖拽。
</p>
</div>
<div class="flex flex-wrap items-center gap-2">
<Button variant="outline" size="sm" @click="clearSelection">
<X data-icon="inline-start" />
清空选择
</Button>
<Button variant="outline" size="sm" :disabled="!selectedCount" @click="editFirstSelected">
<Pencil data-icon="inline-start" />
编辑选中
</Button>
<Button size="sm" @click="regenerate">
<RefreshCw data-icon="inline-start" />
重新生成
</Button>
</div>
</header>
<section class="grid grid-cols-2 gap-3 sm:grid-cols-4" aria-label="表格统计">
<Card class="gap-2 py-3">
<CardHeader class="flex items-center justify-between px-4 pb-0 pt-0"
><CardTitle class="text-xs font-medium text-muted-foreground">总记录</CardTitle
><Rows3 class="size-4 text-muted-foreground"
/></CardHeader>
<CardContent class="px-4"
><div class="text-xl font-semibold tabular-nums">
{{ dataSource.length.toLocaleString() }}
</div></CardContent
>
</Card>
<Card class="gap-2 py-3">
<CardHeader class="flex items-center justify-between px-4 pb-0 pt-0"
><CardTitle class="text-xs font-medium text-muted-foreground">当前显示</CardTitle
><Search class="size-4 text-muted-foreground"
/></CardHeader>
<CardContent class="px-4"
><div class="text-xl font-semibold tabular-nums">
{{ filteredRows.length.toLocaleString() }}
</div></CardContent
>
</Card>
<Card class="gap-2 py-3">
<CardHeader class="flex items-center justify-between px-4 pb-0 pt-0"
><CardTitle class="text-xs font-medium text-muted-foreground">已启用</CardTitle
><CheckCircle2 class="size-4 text-muted-foreground"
/></CardHeader>
<CardContent class="px-4"
><div class="text-xl font-semibold tabular-nums">
{{ enabledCount.toLocaleString() }}
</div></CardContent
>
</Card>
<Card class="gap-2 py-3">
<CardHeader class="flex items-center justify-between px-4 pb-0 pt-0"
><CardTitle class="text-xs font-medium text-muted-foreground">已选择</CardTitle
><Badge variant="secondary">{{ selectedCount }}</Badge></CardHeader
>
<CardContent class="px-4"
><div class="text-xl font-semibold tabular-nums">{{ selectedCount }}</div></CardContent
>
</Card>
</section>
<section class="flex min-h-0 flex-1 flex-col gap-3 rounded-lg border bg-card p-3 sm:p-4">
<div class="flex flex-wrap items-end justify-between gap-3">
<div>
<h2 class="text-base font-semibold">记录清单</h2>
<p class="mt-1 text-xs text-muted-foreground">
点击文本或下拉单元格开始编辑,复选框可直接切换。
</p>
</div>
<div class="flex flex-wrap items-end gap-2">
<Button size="sm" @click="addRow">
<Plus data-icon="inline-start" />
新增行
</Button>
<label class="grid gap-1 text-xs text-muted-foreground">
<span>搜索</span>
<Input
v-model="searchQuery"
class="h-8 w-44 text-xs"
placeholder="编号、名称或负责人"
/>
</label>
<label class="grid gap-1 text-xs text-muted-foreground">
<span>状态</span>
<Select v-model="statusFilter">
<SelectTrigger size="sm" class="h-8 w-28"
><SelectValue placeholder="全部状态"
/></SelectTrigger>
<SelectContent
><SelectGroup
><SelectItem value="all">全部状态</SelectItem
><SelectItem value="1">启用</SelectItem
><SelectItem value="0">禁用</SelectItem></SelectGroup
></SelectContent
>
</Select>
</label>
</div>
</div>
<!-- FmsAutoComplete demo -->
<div class="rounded-lg border p-6 space-y-3">
<h2 class="text-sm font-semibold">FmsAutoComplete(语言类型搜索)</h2>
<p class="text-xs text-muted-foreground">
单 v-model 绑定文本,回车/选中后拿到完整记录
</p>
<div class="w-64">
<FmsAutoComplete
v-model="autoValue"
view-name="b_i18n_type"
label-key="b_name"
value-key="b_code"
order-by="b_code ASC"
placeholder="输入语言关键字..."
@change="onAutoChange"
/>
</div>
<div class="text-xs text-muted-foreground space-y-0.5">
<div>v-model:<code class="text-foreground">{{ autoValue || '-' }}</code></div>
<div>record:<code class="text-foreground">{{ autoRecord || '-' }}</code></div>
</div>
<div class="min-h-[460px] flex-1">
<FmsTable
ref="tableRef"
v-model:columns="columns"
:data-source="filteredRows"
:row-selection="rowSelection"
:active-row-key="activeRowKey"
row-key="id"
row-draggable
show-overflow
@change="onTableChange"
@row-order-change="onRowOrderChange"
/>
</div>
</div>
</div>
</section>
<section class="grid gap-3 lg:grid-cols-2">
<Card class="gap-4 py-4">
<CardHeader class="px-4 pb-0 pt-0"
><CardTitle class="text-sm">FmsSelect 远程选择</CardTitle></CardHeader
>
<CardContent class="px-4">
<div class="grid gap-3 sm:grid-cols-[240px_1fr] sm:items-center">
<FmsSelect
v-model="localeCode"
v-model:label="localeLabel"
view-name="b_i18n_type"
label-key="b_name"
value-key="b_code"
order-by="b_code ASC"
:page-size="10"
allow-clear
placeholder="选择语言类型..."
@change="localeRecord = $event"
/>
<div class="grid gap-1 text-xs text-muted-foreground">
<span
>值:<code class="text-foreground">{{ localeCode || "-" }}</code></span
><span
>记录:<code class="text-foreground">{{
localeRecord ? JSON.stringify(localeRecord) : "-"
}}</code></span
>
</div>
</div>
</CardContent>
</Card>
<Card class="gap-4 py-4">
<CardHeader class="px-4 pb-0 pt-0"
><CardTitle class="text-sm">FmsAutoComplete 搜索</CardTitle></CardHeader
>
<CardContent class="px-4">
<div class="grid gap-3 sm:grid-cols-[240px_1fr] sm:items-center">
<FmsAutoComplete
v-model="autoValue"
view-name="b_i18n_type"
label-key="b_name"
value-key="b_code"
order-by="b_code ASC"
placeholder="输入语言关键字..."
@change="autoRecord = $event"
/>
<div class="grid gap-1 text-xs text-muted-foreground">
<span
>输入:<code class="text-foreground">{{ autoValue || "-" }}</code></span
><span
>记录:<code class="text-foreground">{{
autoRecord ? JSON.stringify(autoRecord) : "-"
}}</code></span
>
</div>
</div>
</CardContent>
</Card>
</section>
</main>
</template>