feat: persist feature research and versioned alpha assessments

This commit is contained in:
yuxuanhui
2026-09-08 21:52:46 +08:00
parent f89ae211d2
commit eb4850a003
32 changed files with 2003 additions and 20 deletions
+113
View File
@@ -0,0 +1,113 @@
import { useEffect, useState } from "react";
import { Button, Input, Toast } from "@douyinfe/semi-ui-19";
import { api, post } from "../api";
import { ResearchSelect } from "./ResearchSelect";
export type ViewContent = {
name: string;
filters: Record<string, unknown>;
columns: string[];
};
type View = { id: string; version: number; content: ViewContent };
export function SavedViews({
filters,
columns,
onRestore,
}: {
filters: Record<string, unknown>;
columns: string[];
onRestore: (view: ViewContent) => void;
}) {
const [items, setItems] = useState<View[]>([]);
const [name, setName] = useState("");
const [selected, setSelected] = useState<string>();
const [busy, setBusy] = useState(false);
const [q, setQ] = useState("");
const [version, setVersion] = useState(0);
useEffect(() => {
const controller = new AbortController();
api<{ items: View[] }>(
`/research/assets?kind=view&q=${encodeURIComponent(q)}&limit=100`,
{ signal: controller.signal },
)
.then((r) => setItems(r.items))
.catch((e) => {
if (!controller.signal.aborted) Toast.error(e.message);
});
return () => controller.abort();
}, [q, version]);
async function save(replace: boolean) {
setBusy(true);
try {
const old = items.find((i) => i.id === selected);
if (replace && !old) return;
const content = {
name: name.trim() || old?.content.name || "研究队列",
filters: Object.fromEntries(
Object.entries(filters).filter(
([, v]) => v !== "" && v !== null && v !== undefined,
),
),
columns,
};
const value = replace
? await api<View>(`/research/assets/${old!.id}`, {
method: "PUT",
body: JSON.stringify({
kind: "view",
version: old!.version,
content,
}),
})
: await post<View>("/research/assets", { kind: "view", content });
setSelected(value.id);
setName(value.content.name);
setVersion((n) => n + 1);
Toast.success("筛选视图已保存");
} catch (e) {
Toast.error((e as Error).message);
} finally {
setBusy(false);
}
}
return (
<div className="research-saved-views inline-actions">
<ResearchSelect
label="保存的 Alpha 视图"
placeholder="搜索并恢复研究队列"
filter
remote
onSearch={setQ}
value={selected}
optionList={items.map((i) => ({
value: i.id,
label: `${i.content.name} · v${i.version}`,
}))}
onChange={(v) => {
const item = items.find((i) => i.id === v);
if (item) {
setSelected(item.id);
setName(item.content.name);
onRestore(item.content);
}
}}
/>
<Input
aria-label="视图名称"
placeholder="视图名称"
value={name}
onChange={setName}
style={{ width: 160 }}
/>
<Button size="small" disabled={busy} onClick={() => void save(false)}>
保存为新视图
</Button>
<Button
size="small"
disabled={busy || !selected}
onClick={() => void save(true)}
>
更新当前视图
</Button>
</div>
);
}