feat: 导入数据前端列表显示
This commit is contained in:
parent
d4a43e211a
commit
40717541d4
|
@ -1,24 +1,38 @@
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { JSONFilePreset } from "lowdb/node";
|
import { JSONFilePreset } from "lowdb/node";
|
||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
|
import { Low } from "node_modules/lowdb/lib/core/Low";
|
||||||
|
import { StructuredMetadata } from "./dicom";
|
||||||
|
|
||||||
const initDb = async () => {
|
interface ICreateDatabase {
|
||||||
// Read or create db.json
|
name: string;
|
||||||
const defaultData = { posts: [] };
|
}
|
||||||
const db = await JSONFilePreset(
|
|
||||||
path.join(app.getPath("userData"), "db.json"),
|
|
||||||
defaultData
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update db.json
|
type SeriesTableType = StructuredMetadata & {
|
||||||
await db.update(({ posts }) => posts.push("hello world"));
|
createTime?: number;
|
||||||
|
updateTime?: number;
|
||||||
// Alternatively you can call db.write() explicitely later
|
|
||||||
// to write to db.json
|
|
||||||
db.data.posts.push("hello world");
|
|
||||||
await db.write();
|
|
||||||
|
|
||||||
console.log(db);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
initDb();
|
interface DbTable {
|
||||||
|
series: SeriesTableType[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultData: DbTable = { series: [] };
|
||||||
|
export let db: Low<{ series: SeriesTableType[] }>;
|
||||||
|
|
||||||
|
export const createDatabase = async (config: ICreateDatabase) => {
|
||||||
|
const { name } = config;
|
||||||
|
db = await JSONFilePreset(
|
||||||
|
path.join(app.getPath("userData"), name),
|
||||||
|
defaultData
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update db.json
|
||||||
|
// await db.update(({ posts }) => posts.push("hello world"));
|
||||||
|
|
||||||
|
// Alternatively you can call db.write() explicitely later to write to db.json
|
||||||
|
// db.data.posts.push("hello world");
|
||||||
|
// await db.write();
|
||||||
|
|
||||||
|
// console.log(db);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import * as dicomParser from "dicom-parser";
|
import * as dicomParser from "dicom-parser";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import crypto from 'crypto';
|
import crypto from "crypto";
|
||||||
|
|
||||||
export interface StructuredData {
|
export interface StructuredData {
|
||||||
[SeriesInstanceUID: string]: ExtractMetadata[];
|
[SeriesInstanceUID: string]: ExtractMetadata[];
|
||||||
|
@ -13,6 +13,8 @@ export interface ExtractMetadata {
|
||||||
SeriesInstanceUID?: string;
|
SeriesInstanceUID?: string;
|
||||||
PatientName?: string;
|
PatientName?: string;
|
||||||
pixelData?: Uint16Array;
|
pixelData?: Uint16Array;
|
||||||
|
PatientSex: string;
|
||||||
|
PatientAge: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -77,6 +79,8 @@ export const parseDICOMFile = async (
|
||||||
const StudyInstanceUID = dataSet.string("x0020000d");
|
const StudyInstanceUID = dataSet.string("x0020000d");
|
||||||
const SeriesInstanceUID = dataSet.string("x0020000e");
|
const SeriesInstanceUID = dataSet.string("x0020000e");
|
||||||
const PatientName = dataSet.string("x00100030");
|
const PatientName = dataSet.string("x00100030");
|
||||||
|
const PatientSex = dataSet.string("x00100040") ?? "";
|
||||||
|
const PatientAge = dataSet.string("x00101010") ?? "";
|
||||||
const pixelDataElement = dataSet.elements.x7fe00010;
|
const pixelDataElement = dataSet.elements.x7fe00010;
|
||||||
const pixelData = new Uint16Array(
|
const pixelData = new Uint16Array(
|
||||||
dataSet.byteArray.buffer,
|
dataSet.byteArray.buffer,
|
||||||
|
@ -89,6 +93,8 @@ export const parseDICOMFile = async (
|
||||||
StudyInstanceUID,
|
StudyInstanceUID,
|
||||||
SeriesInstanceUID,
|
SeriesInstanceUID,
|
||||||
PatientName,
|
PatientName,
|
||||||
|
PatientSex,
|
||||||
|
PatientAge,
|
||||||
// pixelData,
|
// pixelData,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -129,34 +135,37 @@ export interface StructuredMetadata {
|
||||||
StudyInstanceUID?: string;
|
StudyInstanceUID?: string;
|
||||||
SeriesInstanceUID?: string;
|
SeriesInstanceUID?: string;
|
||||||
PatientName?: string;
|
PatientName?: string;
|
||||||
fileHash: string[]
|
fileHash: string[];
|
||||||
|
PatientSex: string;
|
||||||
|
PatientAge: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScanProgress {
|
export interface ScanProgress {
|
||||||
percentage: number
|
percentage: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算文件的哈希值
|
// 计算文件的哈希值
|
||||||
async function calculateFileHash(filePath: string): Promise<string> {
|
async function calculateFileHash(filePath: string): Promise<string> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const hash = crypto.createHash('sha256');
|
const hash = crypto.createHash("sha256");
|
||||||
const stream = fs.createReadStream(filePath);
|
const stream = fs.createReadStream(filePath);
|
||||||
|
|
||||||
stream.on('error', reject);
|
stream.on("error", reject);
|
||||||
stream.pipe(hash).on('finish', () => resolve(hash.digest('hex')));
|
stream.pipe(hash).on("finish", () => resolve(hash.digest("hex")));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 序列级别
|
||||||
|
export const keyProp = "SeriesInstanceUID";
|
||||||
|
|
||||||
export const structureMetadata = async (
|
export const structureMetadata = async (
|
||||||
data: ExtractMetadata[],
|
data: ExtractMetadata[],
|
||||||
progressCallback?: (progress: ScanProgress) => void
|
progressCallback?: (progress: ScanProgress) => void
|
||||||
): Promise<StructuredMetadata[]> => {
|
): Promise<StructuredMetadata[]> => {
|
||||||
const result: StructuredMetadata[] = [];
|
const result: StructuredMetadata[] = [];
|
||||||
// 序列级别
|
|
||||||
const keyProp = 'SeriesInstanceUID'
|
|
||||||
|
|
||||||
for (let i = 0; i < data.length; i++) {
|
for (let i = 0; i < data.length; i++) {
|
||||||
const item = data[i]
|
const item = data[i];
|
||||||
const existItem = result.find((i) => i[keyProp] === item[keyProp]);
|
const existItem = result.find((i) => i[keyProp] === item[keyProp]);
|
||||||
const hash = await calculateFileHash(item.filePath);
|
const hash = await calculateFileHash(item.filePath);
|
||||||
|
|
||||||
|
@ -164,7 +173,7 @@ export const structureMetadata = async (
|
||||||
// 如果找到了相同的条目,合并 filePath
|
// 如果找到了相同的条目,合并 filePath
|
||||||
if (!existItem.fileHash.includes(hash)) {
|
if (!existItem.fileHash.includes(hash)) {
|
||||||
existItem.filePaths.push(item.filePath);
|
existItem.filePaths.push(item.filePath);
|
||||||
existItem.fileHash.push(hash)
|
existItem.fileHash.push(hash);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 如果没有找到,创建一个新的条目
|
// 如果没有找到,创建一个新的条目
|
||||||
|
@ -174,10 +183,12 @@ export const structureMetadata = async (
|
||||||
StudyInstanceUID: item.StudyInstanceUID,
|
StudyInstanceUID: item.StudyInstanceUID,
|
||||||
SeriesInstanceUID: item.SeriesInstanceUID,
|
SeriesInstanceUID: item.SeriesInstanceUID,
|
||||||
PatientName: item.PatientName,
|
PatientName: item.PatientName,
|
||||||
|
PatientAge: item.PatientAge,
|
||||||
|
PatientSex: item.PatientSex,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const progress: ScanProgress = {
|
const progress: ScanProgress = {
|
||||||
percentage: ((i + 1) / data.length) * 100
|
percentage: ((i + 1) / data.length) * 100,
|
||||||
};
|
};
|
||||||
progressCallback?.(progress);
|
progressCallback?.(progress);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,14 @@ import { dialog, ipcMain } from "electron";
|
||||||
import os from "os";
|
import os from "os";
|
||||||
import {
|
import {
|
||||||
findDcmFiles,
|
findDcmFiles,
|
||||||
|
keyProp,
|
||||||
processFilesInBatches,
|
processFilesInBatches,
|
||||||
|
StructuredMetadata,
|
||||||
structureMetadata,
|
structureMetadata,
|
||||||
} from "./core/dicom";
|
} from "./core/dicom";
|
||||||
import { EVENT_PARSE_DICOM } from "./ipcEvent";
|
import { EVENT_PARSE_DICOM } from "./ipcEvent";
|
||||||
import PythonManager from "./core/PythonManager";
|
import PythonManager from "./core/PythonManager";
|
||||||
|
import { db } from "./core/db";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 渲染进程和主进程的事件调度
|
* 渲染进程和主进程的事件调度
|
||||||
|
@ -60,8 +63,25 @@ const registerIpcMainHandlers = (
|
||||||
const structDicom = await structureMetadata(items, (progress) => {
|
const structDicom = await structureMetadata(items, (progress) => {
|
||||||
event.reply("scan-progress", progress);
|
event.reply("scan-progress", progress);
|
||||||
});
|
});
|
||||||
|
// 存数据库
|
||||||
|
const changeTime = Date.now();
|
||||||
|
for (const item of structDicom) {
|
||||||
|
const existSeries = db.data.series.find(
|
||||||
|
(i) => i[keyProp] === item[keyProp]
|
||||||
|
);
|
||||||
|
existSeries
|
||||||
|
? Object.assign(existSeries, item, { updateTime: changeTime })
|
||||||
|
: db.data.series.push({ ...item, createTime: changeTime });
|
||||||
|
await db.write();
|
||||||
|
}
|
||||||
event.reply("scan-progress-done", structDicom);
|
event.reply("scan-progress-done", structDicom);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.on("db:series:select", async (event, data) => {
|
||||||
|
await db.read();
|
||||||
|
const seriesList = db.data.series;
|
||||||
|
event.reply("db:series:select:response", seriesList);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export default registerIpcMainHandlers;
|
export default registerIpcMainHandlers;
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { fileURLToPath } from "node:url";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import registerIpcMainHandlers from "./ipcMainHandlers";
|
import registerIpcMainHandlers from "./ipcMainHandlers";
|
||||||
import PythonManager from "./core/PythonManager";
|
import PythonManager from "./core/PythonManager";
|
||||||
|
import { createDatabase } from "./core/db";
|
||||||
|
|
||||||
const require = createRequire(import.meta.url);
|
const require = createRequire(import.meta.url);
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
@ -133,6 +134,8 @@ app.whenReady().then(() => {
|
||||||
createWindow();
|
createWindow();
|
||||||
createTray();
|
createTray();
|
||||||
registerGlobalShortcuts();
|
registerGlobalShortcuts();
|
||||||
|
createDatabase({ name: "cvpilot.json" });
|
||||||
|
console.log(path.join(app.getPath("userData")));
|
||||||
|
|
||||||
// 设置 Dock 图标
|
// 设置 Dock 图标
|
||||||
if (process.platform === "darwin") {
|
if (process.platform === "darwin") {
|
||||||
|
|
|
@ -1,11 +1,4 @@
|
||||||
"use client";
|
import { ChevronDownIcon } from "@radix-ui/react-icons";
|
||||||
|
|
||||||
import * as React from "react";
|
|
||||||
import {
|
|
||||||
CaretSortIcon,
|
|
||||||
ChevronDownIcon,
|
|
||||||
DotsHorizontalIcon,
|
|
||||||
} from "@radix-ui/react-icons";
|
|
||||||
import {
|
import {
|
||||||
ColumnDef,
|
ColumnDef,
|
||||||
ColumnFiltersState,
|
ColumnFiltersState,
|
||||||
|
@ -25,9 +18,6 @@ import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuCheckboxItem,
|
DropdownMenuCheckboxItem,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
@ -39,48 +29,22 @@ import {
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
|
||||||
const data: Payment[] = [
|
export type Series = {
|
||||||
{
|
|
||||||
id: "m5gr84i9",
|
|
||||||
amount: 316,
|
|
||||||
status: "success",
|
|
||||||
email: "ken99@yahoo.com",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "3u1reuv4",
|
|
||||||
amount: 242,
|
|
||||||
status: "success",
|
|
||||||
email: "Abe45@gmail.com",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "derv1ws0",
|
|
||||||
amount: 837,
|
|
||||||
status: "processing",
|
|
||||||
email: "Monserrat44@gmail.com",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "5kma53ae",
|
|
||||||
amount: 874,
|
|
||||||
status: "success",
|
|
||||||
email: "Silas22@gmail.com",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "bhqecj4p",
|
|
||||||
amount: 721,
|
|
||||||
status: "failed",
|
|
||||||
email: "carmella@hotmail.com",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export type Payment = {
|
|
||||||
id: string;
|
id: string;
|
||||||
amount: number;
|
StudyInstanceUID?: string;
|
||||||
status: "pending" | "processing" | "success" | "failed";
|
SeriesInstanceUID?: string;
|
||||||
email: string;
|
PatientName?: string;
|
||||||
|
PatientAge: string;
|
||||||
|
PatientSex: string;
|
||||||
|
createTime?: number;
|
||||||
|
updateTime?: number;
|
||||||
|
filePaths: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const columns: ColumnDef<Payment>[] = [
|
export const columns: ColumnDef<Series>[] = [
|
||||||
{
|
{
|
||||||
id: "select",
|
id: "select",
|
||||||
header: ({ table }) => (
|
header: ({ table }) => (
|
||||||
|
@ -104,81 +68,82 @@ export const columns: ColumnDef<Payment>[] = [
|
||||||
enableHiding: false,
|
enableHiding: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "status",
|
accessorKey: "PatientName",
|
||||||
header: "Status",
|
header: "姓名",
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className="capitalize">{row.getValue("status")}</div>
|
<div className="capitalize">{row.getValue("PatientName")}</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "email",
|
accessorKey: "PatientAge",
|
||||||
header: ({ column }) => {
|
header: "年龄",
|
||||||
return (
|
cell: ({ row }) => (
|
||||||
<Button
|
<div className="capitalize">{row.getValue("PatientAge")}</div>
|
||||||
variant="ghost"
|
),
|
||||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
|
||||||
>
|
|
||||||
Email
|
|
||||||
<CaretSortIcon className="ml-2 h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
cell: ({ row }) => <div className="lowercase">{row.getValue("email")}</div>,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "amount",
|
accessorKey: "PatientSex",
|
||||||
header: () => <div className="text-right">Amount</div>,
|
header: "性别",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => (
|
||||||
const amount = parseFloat(row.getValue("amount"));
|
<div className="capitalize">{row.getValue("PatientSex")}</div>
|
||||||
|
),
|
||||||
// Format the amount as a dollar amount
|
|
||||||
const formatted = new Intl.NumberFormat("en-US", {
|
|
||||||
style: "currency",
|
|
||||||
currency: "USD",
|
|
||||||
}).format(amount);
|
|
||||||
|
|
||||||
return <div className="text-right font-medium">{formatted}</div>;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "actions",
|
accessorKey: "SeriesInstanceUID",
|
||||||
enableHiding: false,
|
header: "序列UID",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => (
|
||||||
const payment = row.original;
|
<div className="capitalize">{row.getValue("SeriesInstanceUID")}</div>
|
||||||
|
),
|
||||||
return (
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
|
||||||
<span className="sr-only">Open menu</span>
|
|
||||||
<DotsHorizontalIcon className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end">
|
|
||||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => navigator.clipboard.writeText(payment.id)}
|
|
||||||
>
|
|
||||||
Copy payment ID
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<DropdownMenuItem>View customer</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem>View payment details</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "filePaths",
|
||||||
|
header: "层数",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="capitalize">
|
||||||
|
{(row.getValue("filePaths") as string[]).length}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "createTime",
|
||||||
|
header: "创建时间",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="capitalize">
|
||||||
|
{new Date(row.getValue("createTime")).toLocaleString()}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "updateTime",
|
||||||
|
header: "修改时间",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="capitalize">
|
||||||
|
{new Date(row.getValue("updateTime")).toLocaleString()}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function SeriesTable() {
|
export function SeriesTable() {
|
||||||
const [sorting, setSorting] = React.useState<SortingState>([]);
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
const [data, setData] = useState<Series[]>([]);
|
||||||
[]
|
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||||
);
|
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({
|
||||||
const [columnVisibility, setColumnVisibility] =
|
SeriesInstanceUID: false,
|
||||||
React.useState<VisibilityState>({});
|
updateTime: false,
|
||||||
const [rowSelection, setRowSelection] = React.useState({});
|
});
|
||||||
|
const [rowSelection, setRowSelection] = useState({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.ipcRenderer.send("db:series:select");
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.ipcRenderer.on("db:series:select:response", (event, data) => {
|
||||||
|
console.log(data);
|
||||||
|
setData(data);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data,
|
data,
|
||||||
|
@ -200,20 +165,22 @@ export function SeriesTable() {
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full h-full flex flex-col">
|
||||||
<div className="flex items-center py-4">
|
<div className="flex-shrink-0 flex items-center p-4">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Filter emails..."
|
placeholder="筛选姓名"
|
||||||
value={(table.getColumn("email")?.getFilterValue() as string) ?? ""}
|
value={
|
||||||
|
(table.getColumn("PatientName")?.getFilterValue() as string) ?? ""
|
||||||
|
}
|
||||||
onChange={(event) =>
|
onChange={(event) =>
|
||||||
table.getColumn("email")?.setFilterValue(event.target.value)
|
table.getColumn("PatientName")?.setFilterValue(event.target.value)
|
||||||
}
|
}
|
||||||
className="max-w-sm"
|
className="max-w-sm"
|
||||||
/>
|
/>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="outline" className="ml-auto">
|
<Button variant="outline" size="sm" className="ml-auto">
|
||||||
Columns <ChevronDownIcon className="ml-2 h-4 w-4" />
|
显示 <ChevronDownIcon className="ml-2 h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
|
@ -230,13 +197,14 @@ export function SeriesTable() {
|
||||||
column.toggleVisibility(!!value)
|
column.toggleVisibility(!!value)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{column.id}
|
{column.columnDef.header}
|
||||||
</DropdownMenuCheckboxItem>
|
</DropdownMenuCheckboxItem>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
|
<ScrollArea className="h-full w-full flex-grow pl-4 pr-4">
|
||||||
<div className="rounded-md border">
|
<div className="rounded-md border">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
|
@ -287,30 +255,7 @@ export function SeriesTable() {
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-end space-x-2 py-4">
|
</ScrollArea>
|
||||||
<div className="flex-1 text-sm text-muted-foreground">
|
|
||||||
{table.getFilteredSelectedRowModel().rows.length} of{" "}
|
|
||||||
{table.getFilteredRowModel().rows.length} row(s) selected.
|
|
||||||
</div>
|
|
||||||
<div className="space-x-2">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => table.previousPage()}
|
|
||||||
disabled={!table.getCanPreviousPage()}
|
|
||||||
>
|
|
||||||
Previous
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => table.nextPage()}
|
|
||||||
disabled={!table.getCanNextPage()}
|
|
||||||
>
|
|
||||||
Next
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,8 @@ import { ToastAction } from "@/components/ui/toast";
|
||||||
import { useToast } from "@/components/ui/use-toast";
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
import { RocketIcon } from "@radix-ui/react-icons";
|
import { RocketIcon } from "@radix-ui/react-icons";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { SeriesTable } from "./SeriesTable";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
interface ScanProgress {
|
interface ScanProgress {
|
||||||
percentage: number;
|
percentage: number;
|
||||||
|
@ -43,7 +45,7 @@ export const Datasource = () => {
|
||||||
setImportDialogVisible(true);
|
setImportDialogVisible(true);
|
||||||
setStartTime(Date.now());
|
setStartTime(Date.now());
|
||||||
});
|
});
|
||||||
}, [progress?.percentage, toast]);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleScanFinished = (event, data) => {
|
const handleScanFinished = (event, data) => {
|
||||||
|
@ -63,8 +65,9 @@ export const Datasource = () => {
|
||||||
toast({
|
toast({
|
||||||
variant: "default",
|
variant: "default",
|
||||||
title: "完成",
|
title: "完成",
|
||||||
description: `本次操作共导入${data.length}组序列数据,耗时:${timeDuration } s`,
|
description: `本次操作共导入${data.length}组序列数据,耗时:${timeDuration} s`,
|
||||||
action: <ToastAction altText="启动AI测量">启动AI测量</ToastAction>,
|
action: <ToastAction altText="启动AI测量">启动AI测量</ToastAction>,
|
||||||
|
duration: 30 * 1000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -86,7 +89,15 @@ export const Datasource = () => {
|
||||||
}, [importDialogVisible]);
|
}, [importDialogVisible]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4">
|
<motion.div
|
||||||
|
className="h-full"
|
||||||
|
initial={{ y: 10, opacity: 0 }}
|
||||||
|
animate={{ y: 0, opacity: 1 }}
|
||||||
|
exit={{ y: 0, opacity: 0 }}
|
||||||
|
transition={{ duration: 0.25 }}
|
||||||
|
>
|
||||||
|
<SeriesTable />
|
||||||
|
{/* 导入数据dialog */}
|
||||||
<Dialog open={importDialogVisible} onOpenChange={setImportDialogVisible}>
|
<Dialog open={importDialogVisible} onOpenChange={setImportDialogVisible}>
|
||||||
<DialogContent className="sm:max-w-md">
|
<DialogContent className="sm:max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
|
@ -106,6 +117,6 @@ export const Datasource = () => {
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -251,9 +251,9 @@ export function ModelTable() {
|
||||||
<div className="flex-shrink-0 flex items-center justify-between p-4">
|
<div className="flex-shrink-0 flex items-center justify-between p-4">
|
||||||
<Input
|
<Input
|
||||||
placeholder="根据名称查询模型"
|
placeholder="根据名称查询模型"
|
||||||
value={(table.getColumn("email")?.getFilterValue() as string) ?? ""}
|
value={(table.getColumn("modelname")?.getFilterValue() as string) ?? ""}
|
||||||
onChange={(event) =>
|
onChange={(event) =>
|
||||||
table.getColumn("email")?.setFilterValue(event.target.value)
|
table.getColumn("modelname")?.setFilterValue(event.target.value)
|
||||||
}
|
}
|
||||||
className="max-w-sm"
|
className="max-w-sm"
|
||||||
/>
|
/>
|
||||||
|
@ -266,7 +266,7 @@ export function ModelTable() {
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ScrollArea className="flex-grow pl-4 pr-4">
|
<ScrollArea className="h-full w-full flex-grow pl-4 pr-4">
|
||||||
<div className="rounded-md border">
|
<div className="rounded-md border">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { ModelTable } from "./ModelTable";
|
||||||
export const Models = () => {
|
export const Models = () => {
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
|
className="h-full"
|
||||||
initial={{ y: 10, opacity: 0 }}
|
initial={{ y: 10, opacity: 0 }}
|
||||||
animate={{ y: 0, opacity: 1 }}
|
animate={{ y: 0, opacity: 1 }}
|
||||||
exit={{ y: 0, opacity: 0 }}
|
exit={{ y: 0, opacity: 0 }}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user