change display layout to node editor

This commit is contained in:
2025-07-12 15:54:39 +09:00
parent 5804a6cbff
commit 84a2a50922
8 changed files with 368 additions and 107 deletions

View File

@@ -10,23 +10,10 @@
},
"pages": {
"screening": {
"title": "銘柄スクリーニング",
"rerun": "再実行",
"universe": {
"title": "ユニバース",
"desc": "スクリーニング対象の銘柄の条件を設定してください",
"type": {
"listed": "全上場銘柄"
"nodes": {
"fetch_ToshoListed": {
"title": "東証上場銘柄一覧"
}
},
"evaluation": {
"title": "銘柄評価",
"desc": "銘柄一覧テーブルに表示する列を設定してください",
"columnName": "カラム名"
},
"filterOut": {
"title": "選別",
"desc": "銘柄の選別条件を設定してください"
}
}
}

View File

@@ -1,96 +1,45 @@
import { ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { useCallback } from "react";
import { Background, Controls, ReactFlow, Node, useNodesState, useEdgesState, addEdge, Connection } from "@xyflow/react";
import * as node from "./node/";
function Screening() {
const {t} = useTranslation();
return (
<>
<h1>{t("pages.screening.title")}</h1>
<UniverseSpec/>
<EvaluationSpec/>
<FilterOutSpec/>
</>
);
}
export default Screening;
import "@xyflow/react/dist/style.css";
function UniverseSpec() {
const {t} = useTranslation();
return (
<FilterBox
title={t("pages.screening.universe.title")}
desc={t("pages.screening.universe.desc")}
>
<select>
<option>{t("pages.screening.universe.type.listed")}</option>
</select>
<label><input type="checkbox" />{t("terms.toshoGrowth")}</label>
<label><input type="checkbox" />{t("terms.toshoStandard")}</label>
<label><input type="checkbox" />{t("terms.toshoPrime")}</label>
</FilterBox>
);
}
import { History, Impl as HistoryImpl } from "./context/History";
function EvaluationSpec() {
const {t} = useTranslation();
return (
<FilterBox
title={t("pages.screening.evaluation.title")}
desc={t("pages.screening.evaluation.desc")}
>
<ol>
{
[...Array(3)].map((_,i)=>(
<li>
<input type="text" placeholder={t("pages.screening.evaluation.columnName")} />
=
<select>
<option>{t("terms.stock.volume")}</option>
<option>{t("terms.stock.price")}</option>
</select>
<a href="#">remove</a>
</li>
))
}
<li>
<a href="#">add</a>
</li>
</ol>
</FilterBox>
);
}
function FilterOutSpec() {
const {t} = useTranslation();
return (
<FilterBox
title={t("pages.screening.filterOut.title")}
desc={t("pages.screening.filterOut.desc")}
>
<textarea />
</FilterBox>
);
}
type FilterBoxProps = {
title: string,
desc: string,
children: ReactNode,
const nodeTypes = {
fetch_ToshoListed: node.fetch.ToshoListed,
};
function FilterBox({title, desc, children}: FilterBoxProps) {
const {t} = useTranslation();
const initialNodes: Node[] = [
{id: "n1", type: "fetch_ToshoListed", position: {x:0, y:0}, data: {prime: true, standard: true, growth: true}},
{id: "n2", position: {x:0, y:100}, data: {label: "hi"}},
{id: "n3", position: {x:0, y:200}, data: {label: "hello"}},
];
export default function Screening() {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const onConnect = useCallback(
(connection: Connection) => setEdges((eds) => addEdge(connection, eds)),
[]
);
return (
<div>
<div>
<div>
<h2>{title}</h2>
<p>{desc}</p>
</div>
<div>
<a href="#">{t("pages.screening.rerun")}</a>
</div>
<History.Provider value={new HistoryImpl(setNodes)}>
<div style={{ position: "absolute", left: 0, right: 0, top: 0, bottom: 0 }}>
<ReactFlow
nodeTypes={nodeTypes}
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
fitView
>
<Controls />
<Background />
</ReactFlow>
</div>
<div>{children}</div>
</div>
</History.Provider>
);
}

View File

@@ -0,0 +1,22 @@
import { Dispatch, SetStateAction, createContext } from "react";
import { Node, } from "@xyflow/react";
export interface Iface {
push(id: string, data: any): void,
};
export const History = createContext<Iface|undefined>(undefined);
export default History;
export class Impl implements Iface {
setNodes: Dispatch<SetStateAction<Node[]>>;
constructor(setNodes: Dispatch<SetStateAction<Node[]>>) {
this.setNodes = setNodes;
}
push(id: string, data: any): void {
this.setNodes(
(nds)=>nds.map((n)=>n.id == id? {...n, data: data}: n)
);
}
};

14
src/pages/node/Base.tsx Normal file
View File

@@ -0,0 +1,14 @@
import { ReactNode } from "react";
type Params = {
title: ReactNode,
children: ReactNode,
};
export default function Base(p: Params) {
return (
<div>
<div>{p.title}</div>
{p.children}
</div>
);
}

56
src/pages/node/fetch.tsx Normal file
View File

@@ -0,0 +1,56 @@
import { useContext } from "react";
import { useTranslation } from "react-i18next";
import { Handle, Node, NodeProps, Position } from "@xyflow/react";
import Base from "./Base";
import History from "../context/History";
export type ToshoListed = Node<{
prime: boolean,
standard: boolean,
growth: boolean,
}, "ToshoListed">;
export function ToshoListed({id, data}: NodeProps<ToshoListed>) {
const {t} = useTranslation();
const history = useContext(History);
if (history === undefined) {
throw new Error("no History context");
}
return (
<Base title={t("pages.screening.nodes.fetch_ToshoListed.title")}>
<div>
<div><label>
<input
type="checkbox"
name="prime"
checked={data.prime}
onChange={(e)=>history.push(id, {...data, prime: e.target.checked})}
/>
{t("terms.toshoPrime")}
</label></div>
<div><label>
<input
type="checkbox"
name="standard"
checked={data.standard}
onChange={(e)=>history.push(id, {...data, standard: e.target.checked})}
/>
{t("terms.toshoStandard")}
</label></div>
<div><label>
<input
type="checkbox"
name="growth"
checked={data.growth}
onChange={(e)=>history.push(id, {...data, growth: e.target.checked})}
/>
{t("terms.toshoGrowth")}
</label></div>
</div>
<Handle type="source" position={Position.Right}/>
</Base>
);
}

2
src/pages/node/index.tsx Normal file
View File

@@ -0,0 +1,2 @@
export { default as Base } from "./Base";
export * as fetch from "./fetch";