feat: localmpr viewer
This commit is contained in:
parent
3077c78c76
commit
6df7442b9b
|
@ -8,4 +8,9 @@
|
||||||
- 换版本重新测量
|
- 换版本重新测量
|
||||||
|
|
||||||
|
|
||||||
pnpm config set virtual-store-dir-max-length 70
|
pnpm config set virtual-store-dir-max-length 70
|
||||||
|
|
||||||
|
|
||||||
|
## 待解决
|
||||||
|
|
||||||
|
- dicom导入后,本地appData建立一个拷贝区域,复制dicom原片,满足二次导出,防止原片的路径修改、移动硬盘被拔电源
|
|
@ -72,6 +72,10 @@ function createWindow() {
|
||||||
python_process = spawn(path.join(process.env.VITE_PUBLIC!, "main.exe"));
|
python_process = spawn(path.join(process.env.VITE_PUBLIC!, "main.exe"));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if (platform !== "macos") {
|
||||||
|
python_process = spawn(path.join(process.env.VITE_PUBLIC!, "main.exe"));
|
||||||
|
}
|
||||||
|
|
||||||
win.loadFile(path.join(RENDERER_DIST, "index.html")).then(() => {
|
win.loadFile(path.join(RENDERER_DIST, "index.html")).then(() => {
|
||||||
if (process.argv.length >= 2) {
|
if (process.argv.length >= 2) {
|
||||||
const folderPath = process.argv[2];
|
const folderPath = process.argv[2];
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { useEffect } from "react";
|
||||||
import { Models } from "./pages/Models";
|
import { Models } from "./pages/Models";
|
||||||
import { Tools } from "./pages/Tools";
|
import { Tools } from "./pages/Tools";
|
||||||
import { Datasource } from "./pages/Datasource";
|
import { Datasource } from "./pages/Datasource";
|
||||||
|
import { Viewer } from "./pages/Viewer";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const theme = document.querySelector("html")!.getAttribute("theme") as
|
const theme = document.querySelector("html")!.getAttribute("theme") as
|
||||||
|
@ -33,6 +34,7 @@ function App() {
|
||||||
<Route path="datasource" element={<Datasource />} />
|
<Route path="datasource" element={<Datasource />} />
|
||||||
<Route path="tools" element={<Tools />} />
|
<Route path="tools" element={<Tools />} />
|
||||||
<Route path="setting" element={<Setting />} />
|
<Route path="setting" element={<Setting />} />
|
||||||
|
<Route path="viewer" element={<Viewer />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
</Router>
|
</Router>
|
||||||
|
|
|
@ -90,7 +90,7 @@ export const MenuBar = () => {
|
||||||
toast({
|
toast({
|
||||||
variant: "default",
|
variant: "default",
|
||||||
title: "完成",
|
title: "完成",
|
||||||
description: `本次操作共导入${structDicom.length}组序列数据,耗时:${(
|
description: `导入${structDicom.length}组序列数据,耗时:${(
|
||||||
scanDuration / 1000
|
scanDuration / 1000
|
||||||
).toFixed(2)} s`,
|
).toFixed(2)} s`,
|
||||||
action: (
|
action: (
|
||||||
|
|
|
@ -71,185 +71,6 @@ const columnsAlias: { [K in keyof Partial<Series> as string]: string } = {
|
||||||
AcquisitionDate: "采集日期",
|
AcquisitionDate: "采集日期",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const columns: ColumnDef<Series>[] = [
|
|
||||||
{
|
|
||||||
id: "select",
|
|
||||||
header: ({ table }) => (
|
|
||||||
<Checkbox
|
|
||||||
checked={
|
|
||||||
table.getIsAllPageRowsSelected() ||
|
|
||||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
|
||||||
}
|
|
||||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
|
||||||
aria-label="Select all"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<Checkbox
|
|
||||||
checked={row.getIsSelected()}
|
|
||||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
|
||||||
aria-label="Select row"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
enableSorting: false,
|
|
||||||
enableHiding: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "PatientName",
|
|
||||||
accessorKey: "PatientName",
|
|
||||||
header: columnsAlias["PatientName"],
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className="capitalize">{row.getValue("PatientName")}</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "PatientAge",
|
|
||||||
accessorKey: "PatientAge",
|
|
||||||
header: columnsAlias["PatientAge"],
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className="capitalize">{row.getValue("PatientAge")}</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "PatientSex",
|
|
||||||
accessorKey: "PatientSex",
|
|
||||||
header: columnsAlias["PatientSex"],
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className="capitalize">{row.getValue("PatientSex")}</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "SeriesInstanceUID",
|
|
||||||
accessorKey: "SeriesInstanceUID",
|
|
||||||
header: columnsAlias["SeriesInstanceUID"],
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className="capitalize">{row.getValue("SeriesInstanceUID")}</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "filePaths",
|
|
||||||
accessorKey: "filePaths",
|
|
||||||
header: columnsAlias["filePaths"],
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className="capitalize">
|
|
||||||
{(row.getValue("filePaths") as string[]).length}
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "AcquisitionDate",
|
|
||||||
accessorKey: "AcquisitionDate",
|
|
||||||
header: ({ column }) => {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center">
|
|
||||||
{columnsAlias["AcquisitionDate"]}
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="h-7 w-7 ml-2"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
|
||||||
>
|
|
||||||
<ArrowUpDown className="h-3.5 w-3.5" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
cell: ({ row }) =>
|
|
||||||
row.getValue("AcquisitionDate") && (
|
|
||||||
<div className="capitalize">
|
|
||||||
<span>{row.getValue("AcquisitionDate")}</span>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
id: "createTime",
|
|
||||||
accessorKey: "createTime",
|
|
||||||
header: ({ column }) => {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center">
|
|
||||||
{columnsAlias["createTime"]}
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="h-7 w-7 ml-2"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
|
||||||
>
|
|
||||||
<ArrowUpDown className="h-3.5 w-3.5" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
cell: ({ row }) =>
|
|
||||||
row.getValue("createTime") && (
|
|
||||||
<div className="capitalize">
|
|
||||||
<span>{dayjs(row.getValue("createTime")).format("YYYYMMDD")}</span>
|
|
||||||
<Badge className="ml-2 text-xs font-normal">
|
|
||||||
{timeAgo(row.getValue("createTime"))}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "updateTime",
|
|
||||||
accessorKey: "updateTime",
|
|
||||||
header: ({ column }) => {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center">
|
|
||||||
{columnsAlias["updateTime"]}
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="h-7 w-7 ml-2"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
|
||||||
>
|
|
||||||
<ArrowUpDown className="h-3.5 w-3.5" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
cell: ({ row }) =>
|
|
||||||
row.getValue("updateTime") && (
|
|
||||||
<div className="capitalize">
|
|
||||||
<span>{dayjs(row.getValue("updateTime")).format("YYYYMMDD")}</span>
|
|
||||||
<Badge className="ml-2 text-xs font-normal">
|
|
||||||
{timeAgo(row.getValue("updateTime"))}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "actions",
|
|
||||||
header: "操作",
|
|
||||||
enableHiding: false,
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const payment = row.original;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
|
||||||
<span className="sr-only">Open menu</span>
|
|
||||||
<MoreHorizontal 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>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
interface SeriesTableProps {
|
interface SeriesTableProps {
|
||||||
data: Series[];
|
data: Series[];
|
||||||
}
|
}
|
||||||
|
@ -265,6 +86,183 @@ export function SeriesTable(props: SeriesTableProps) {
|
||||||
const [rowSelection, setRowSelection] = useState({});
|
const [rowSelection, setRowSelection] = useState({});
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const columns: ColumnDef<Series>[] = [
|
||||||
|
{
|
||||||
|
id: "select",
|
||||||
|
header: ({ table }) => (
|
||||||
|
<Checkbox
|
||||||
|
checked={
|
||||||
|
table.getIsAllPageRowsSelected() ||
|
||||||
|
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||||
|
}
|
||||||
|
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||||
|
aria-label="Select all"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Checkbox
|
||||||
|
checked={row.getIsSelected()}
|
||||||
|
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||||
|
aria-label="Select row"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
enableSorting: false,
|
||||||
|
enableHiding: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "PatientName",
|
||||||
|
accessorKey: "PatientName",
|
||||||
|
header: columnsAlias["PatientName"],
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="capitalize">{row.getValue("PatientName")}</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "PatientAge",
|
||||||
|
accessorKey: "PatientAge",
|
||||||
|
header: columnsAlias["PatientAge"],
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="capitalize">{row.getValue("PatientAge")}</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "PatientSex",
|
||||||
|
accessorKey: "PatientSex",
|
||||||
|
header: columnsAlias["PatientSex"],
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="capitalize">{row.getValue("PatientSex")}</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "SeriesInstanceUID",
|
||||||
|
accessorKey: "SeriesInstanceUID",
|
||||||
|
header: columnsAlias["SeriesInstanceUID"],
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="capitalize">{row.getValue("SeriesInstanceUID")}</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "filePaths",
|
||||||
|
accessorKey: "filePaths",
|
||||||
|
header: columnsAlias["filePaths"],
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="capitalize">
|
||||||
|
{(row.getValue("filePaths") as string[]).length}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "AcquisitionDate",
|
||||||
|
accessorKey: "AcquisitionDate",
|
||||||
|
header: ({ column }) => {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center">
|
||||||
|
{columnsAlias["AcquisitionDate"]}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="h-7 w-7 ml-2"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||||
|
>
|
||||||
|
<ArrowUpDown className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
cell: ({ row }) =>
|
||||||
|
row.getValue("AcquisitionDate") && (
|
||||||
|
<div className="capitalize">
|
||||||
|
<span>{row.getValue("AcquisitionDate")}</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: "createTime",
|
||||||
|
accessorKey: "createTime",
|
||||||
|
header: ({ column }) => {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center">
|
||||||
|
{columnsAlias["createTime"]}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="h-7 w-7 ml-2"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||||
|
>
|
||||||
|
<ArrowUpDown className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
cell: ({ row }) =>
|
||||||
|
row.getValue("createTime") && (
|
||||||
|
<div className="capitalize">
|
||||||
|
<span>{dayjs(row.getValue("createTime")).format("YYYYMMDD")}</span>
|
||||||
|
<Badge className="ml-2 text-xs font-normal">
|
||||||
|
{timeAgo(row.getValue("createTime"))}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "updateTime",
|
||||||
|
accessorKey: "updateTime",
|
||||||
|
header: ({ column }) => {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center">
|
||||||
|
{columnsAlias["updateTime"]}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="h-7 w-7 ml-2"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||||
|
>
|
||||||
|
<ArrowUpDown className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
cell: ({ row }) =>
|
||||||
|
row.getValue("updateTime") && (
|
||||||
|
<div className="capitalize">
|
||||||
|
<span>{dayjs(row.getValue("updateTime")).format("YYYYMMDD")}</span>
|
||||||
|
<Badge className="ml-2 text-xs font-normal">
|
||||||
|
{timeAgo(row.getValue("updateTime"))}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "actions",
|
||||||
|
header: "操作",
|
||||||
|
enableHiding: false,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const dicom = row.original;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||||
|
<span className="sr-only">Open menu</span>
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuLabel>操作</DropdownMenuLabel>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => handle2Viewer(dicom)}
|
||||||
|
>阅片</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem>移除</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem>重新测量</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data: props.data,
|
data: props.data,
|
||||||
columns,
|
columns,
|
||||||
|
@ -295,6 +293,11 @@ export function SeriesTable(props: SeriesTableProps) {
|
||||||
navigate("/", { state: { selectDicoms } });
|
navigate("/", { state: { selectDicoms } });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handle2Viewer = (dicom) => {
|
||||||
|
const { SeriesInstanceUID } = dicom
|
||||||
|
navigate(`/viewer?SeriesInstanceUID=${SeriesInstanceUID}`)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full flex flex-col">
|
<div className="w-full h-full flex flex-col">
|
||||||
<div className="flex-shrink-0 flex items-center p-4">
|
<div className="flex-shrink-0 flex items-center p-4">
|
||||||
|
@ -387,9 +390,9 @@ export function SeriesTable(props: SeriesTableProps) {
|
||||||
{header.isPlaceholder
|
{header.isPlaceholder
|
||||||
? null
|
? null
|
||||||
: flexRender(
|
: flexRender(
|
||||||
header.column.columnDef.header,
|
header.column.columnDef.header,
|
||||||
header.getContext()
|
header.getContext()
|
||||||
)}
|
)}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
12
apps/desktop/src/pages/Viewer/index.tsx
Normal file
12
apps/desktop/src/pages/Viewer/index.tsx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { useLocation } from "react-router-dom"
|
||||||
|
|
||||||
|
export const Viewer = () => {
|
||||||
|
const location = useLocation()
|
||||||
|
const queryParams = new URLSearchParams(location.search);
|
||||||
|
const SeriesInstanceUID = queryParams.get('SeriesInstanceUID'); // 获取URL参数
|
||||||
|
|
||||||
|
return <div>
|
||||||
|
{SeriesInstanceUID}
|
||||||
|
<p>找到一个本地dicom的mpr方案</p>
|
||||||
|
</div>
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user