Skip to content

Commit

Permalink
feat(project): add scanning panel & logic (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
Moskize91 authored Nov 19, 2024
1 parent 3df9cc0 commit c4db437
Show file tree
Hide file tree
Showing 12 changed files with 337 additions and 31 deletions.
3 changes: 2 additions & 1 deletion browser/src/store/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "./scanner_store";
export * from "./scanner_store";
export * from "./scanning_store";
125 changes: 118 additions & 7 deletions browser/src/store/scanning_store.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
import { val, derive, combine, Val, ReadonlyVal } from "value-enhancer";
import { fetchJson, fetchJsonEvents, EventFetcher } from "../utils";

type Event = {
readonly kind: "scanning" | "completed" | "interrupted" | "heartbeat";
} | {
readonly kind: "scanCompleted";
readonly count: number;
} | {
readonly kind: "completeHandingFile";
readonly path: string;
} | {
readonly kind: "failure";
readonly error: string;
} | {
readonly kind: "startHandingFile";
readonly path: string;
}| {
readonly kind: "completeHandingFile";
readonly path: string;
} | {
readonly kind: "completeHandingPdfPage";
readonly index: number;
Expand All @@ -22,13 +20,71 @@ type Event = {
readonly kind: "completeIndexPdfPage";
readonly index: number;
readonly total: number;
} | {
readonly kind: "failure";
readonly error: string;
};

export type ScanningStore$ = {
readonly phase: ReadonlyVal<ScanningPhase>;
readonly scanCount: ReadonlyVal<number>;
readonly handlingFile: ReadonlyVal<HandingFile | null>;
readonly completedFiles: ReadonlyVal<readonly string[]>;
readonly error: ReadonlyVal<string | null>;
readonly isScanning: ReadonlyVal<boolean>;
};

export enum ScanningPhase {
Ready,
Scanning,
HandingFiles,
Completed,
Error,
}

export type HandingFile = {
readonly path: string;
readonly handlePdfPage?: {
readonly index: number;
readonly total: number;
};
readonly indexPdfPage?: {
readonly index: number;
readonly total: number;
};
};

export class ScanningStore {
public readonly $: ScanningStore$;

readonly #fetcher: EventFetcher<Event>;
readonly #phase$: Val<ScanningPhase> = val(ScanningPhase.Ready);
readonly #scanCount$: Val<number> = val(0);
readonly #handlingFile$: Val<HandingFile | null> = val<HandingFile | null>(null);
readonly #completedFiles$: Val<readonly string[]> = val<readonly string[]>([]);
readonly #error$: Val<string | null> = val<string | null>(null);

public constructor() {
this.#fetcher = fetchJsonEvents<Event>("/api/scanning");
this.$ = {
phase: derive(this.#phase$),
scanCount: derive(this.#scanCount$),
handlingFile: derive(this.#handlingFile$),
completedFiles: derive(this.#completedFiles$),
error: derive(this.#error$),
isScanning: derive(this.#phase$, phase => {
switch (phase) {
case ScanningPhase.Ready:
case ScanningPhase.Completed:
case ScanningPhase.Error: {
return false;
}
default: {
return true;
}
}
}),
};
}

public async scan(): Promise<void> {
Expand All @@ -45,7 +101,62 @@ export class ScanningStore {
if (!event) {
break;
}
console.log("event", event);
switch (event.kind) {
case "scanning": {
this.#phase$.set(ScanningPhase.Scanning);
this.#scanCount$.set(0);
this.#handlingFile$.set(null);
this.#completedFiles$.set([]);
this.#error$.set(null);
break;
}
case "scanCompleted": {
this.#phase$.set(ScanningPhase.HandingFiles);
this.#scanCount$.set(event.count);
break;
}
case "startHandingFile": {
this.#handlingFile$.set({ path: event.path });
break;
}
case "completeHandingFile": {
this.#handlingFile$.set(null);
this.#completedFiles$.set([
...this.#completedFiles$.value,
event.path,
]);
break;
}
case "completeHandingPdfPage": {
this.#handlingFile$.set({
...this.#handlingFile$.value!,
handlePdfPage: {
index: event.index,
total: event.total,
},
});
break;
}
case "completeIndexPdfPage": {
this.#handlingFile$.set({
...this.#handlingFile$.value!,
indexPdfPage: {
index: event.index,
total: event.total,
},
});
break;
}
case "completed": {
this.#phase$.set(ScanningPhase.Completed);
break;
}
case "failure": {
this.#phase$.set(ScanningPhase.Error);
this.#error$.set(event.error);
break;
}
}
}
}
}
29 changes: 29 additions & 0 deletions browser/src/views/ScannerPage.module.less
Original file line number Diff line number Diff line change
@@ -1,3 +1,32 @@
.root {
margin-bottom: 68px;
overflow-y: scroll;
}

.scan-button {
width: 128px;
}

.steps-bar {
margin-top: 75px;
margin-bottom: 28px;
}

.progress {
display: flex;
flex-direction: row;
align-items: center;
}

.progress-bar {
flex-basis: 340px;
flex-shrink: 1.0;
}

.progress-label {
color: #8C8C8C;
flex-basis: 48px;
font-size: 12px;
margin-top: 18px;
margin-bottom: 18px;
}
161 changes: 156 additions & 5 deletions browser/src/views/ScannerPage.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React from "react";
import styles from "./ScannerPage.module.less";

import { Skeleton, Button, Divider, Typography } from "antd";
import { ScanOutlined } from "@ant-design/icons";
import { Skeleton, Result, Steps, List, Button, Divider, Progress, Typography } from "antd";
import { ScanOutlined, ProfileTwoTone, SyncOutlined, FilePdfTwoTone } from "@ant-design/icons";
import { val } from "value-enhancer";
import { useVal } from "use-value-enhancer";
import { ScannerStore } from "../store";
import { ScannerStore, ScanningStore, ScanningPhase } from "../store";
import { Sources } from "./Sources";

const { Title, Paragraph } = Typography;
Expand Down Expand Up @@ -33,10 +33,11 @@ export const ScannerPage: React.FC = () => {
return <Skeleton active />;
}
return (
<div>
<div className={styles.root}>
<Sources store={store} />
<Divider />
<Scanner store={store} />
<ScanningPanel store={store.scanningStore} />
</div>
);
};
Expand All @@ -47,6 +48,7 @@ type ScannerProps = {

const Scanner: React.FC<ScannerProps> = ({ store }) => {
const scanningStore = store.scanningStore;
const isScanning = useVal(scanningStore.$.isScanning);
const onClickScan = React.useCallback(
() => scanningStore.scan(),
[scanningStore],
Expand All @@ -63,9 +65,158 @@ const Scanner: React.FC<ScannerProps> = ({ store }) => {
shape="round"
size="large"
className={styles["scan-button"]}
disabled={isScanning}
loading={isScanning}
icon={<ScanOutlined />}
onClick={onClickScan} >
扫 描
</Button>
</>;
}
}

type ScanningPanelProps = {
readonly store: ScanningStore;
};

const ScanningPanel: React.FC<ScanningPanelProps> = ({ store }) => {
type Record = {
readonly icon: React.ReactNode;
readonly title: string;
readonly content: string;
readonly loading: boolean;
};
const records: Record[] = [];
const phase = useVal(store.$.phase);
const scanCount = useVal(store.$.scanCount);
const completedFiles = useVal(store.$.completedFiles);
const handlingFile = useVal(store.$.handlingFile);
const error = useVal(store.$.error);

let currentIndex: number;
let status: "wait" | "process" | "finish" | "error" = "process";

switch (phase) {
case ScanningPhase.Ready: {
return null;
}
case ScanningPhase.Scanning: {
currentIndex = 0;
break;
}
case ScanningPhase.HandingFiles: {
currentIndex = 1;
break;
}
case ScanningPhase.Completed: {
currentIndex = 2;
status = "finish";
break;
}
case ScanningPhase.Error: {
currentIndex = 2;
status = "error";
break;
}
}
if (phase === ScanningPhase.Scanning) {
records.push({
icon: <ProfileTwoTone />,
title: "扫描文件",
content: `正在扫描文件的更新……`,
loading: true,
});
} else {
records.push({
icon: <ProfileTwoTone />,
title: "扫描文件",
content: `扫描完成,发现 ${scanCount} 个文件有更新`,
loading: false,
});
}
for (const file of completedFiles) {
records.push({
icon: <FilePdfTwoTone />,
title: "录入 PDF 文件",
content: file,
loading: false,
});
}
if (handlingFile) {
records.push({
icon: <FilePdfTwoTone />,
title: "录入 PDF 文件",
content: handlingFile.path,
loading: true,
});
}
return <>
<Steps
className={styles["steps-bar"]}
current={currentIndex}
status={status}
items={[
{ title: "扫描" },
{ title: "处理文件" },
{ title: phase === ScanningPhase.Error ? "错误" : "完成" },
]}
/>
<List
itemLayout="horizontal"
dataSource={records}
renderItem={item => (
<List.Item>
<List.Item.Meta
avatar={item.icon}
title={item.title}
description={item.content}
/>
{item.loading && (
<SyncOutlined spin />
)}
</List.Item>
)}
/>
<ProgressBar
name="解析"
pdfPage={handlingFile?.handlePdfPage} />
<ProgressBar
name="索引"
pdfPage={handlingFile?.indexPdfPage} />
{error && (
<Result
status="error"
title="扫描失败"
subTitle={error}
/>
)}
</>;
};

type ProgressBarProps = {
readonly name: string;
readonly pdfPage?: {
readonly index: number;
readonly total: number;
};
};

const ProgressBar: React.FC<ProgressBarProps> = ({ name, pdfPage }) => {
if (!pdfPage) {
return null;
}
const { index, total } = pdfPage;
const percent = Math.floor(Math.min(index / total, 1.0) * 100);
const status = percent === 100 ? "success" : "active";
return (
<div className={styles["progress"]}>
<label className={styles["progress-label"]}>
{name}
</label>
<Progress
className={styles["progress-bar"]}
percent={percent}
size="small"
status={status} />
</div>
);
};
Loading

0 comments on commit c4db437

Please sign in to comment.