import { useEffect, useState } from "react"; import { Button, Dropdown, Input, Modal, Tabs, Toast, } from "@douyinfe/semi-ui-19"; import { IconCopy, IconDelete, IconEdit, IconMore, IconPlus, IconSave, IconList, } from "@douyinfe/semi-icons"; import { api, ApiError, post } from "../api"; import type { Submission } from "../types"; export type ViewContent = { name: string; filters: Record; columns: string[]; }; type View = { id: string; version: number; content: ViewContent }; type Editor = { mode: "create" | "rename" | "copy"; content: ViewContent; view?: View; }; // The API normalizes numbers and adds offset; neither changes the saved view. function filterSignature(filters: Record) { return JSON.stringify( Object.entries(filters) .filter( ([key, value]) => key !== "offset" && value !== "" && value != null, ) .map(([key, value]) => [key, String(value)]) .sort(([a], [b]) => a.localeCompare(b)), ); } export function SavedViews({ filters, columns, submission, submissionBlocked, selectedId, onSelect, onSubmissionChange, onRestore, suspended, onOverlay, }: { filters: Record; columns: string[]; submission: Submission; submissionBlocked: boolean; selectedId: string | null; onSelect: (id: string | null) => void; onSubmissionChange: (submission: string) => void; onRestore: (view: ViewContent) => void; suspended: boolean; onOverlay: () => void; }) { const [items, setItems] = useState([]); const [editor, setEditor] = useState(null); const [deleting, setDeleting] = useState(null); const [name, setName] = useState(""); const [busy, setBusy] = useState(false); const [revision, setRevision] = useState(0); const current = items.find((item) => item.id === selectedId); const modified = current && (filterSignature(filters) !== filterSignature(current.content.filters) || JSON.stringify(columns) !== JSON.stringify(current.content.columns)); useEffect(() => { const controller = new AbortController(); async function load() { const views: View[] = []; // Tabs replace remote search, so retain access to views beyond the first page. while (!controller.signal.aborted) { const result = await api<{ items: View[]; total: number }>( `/research/assets?kind=view&limit=100&offset=${views.length}`, { signal: controller.signal }, ); views.push(...result.items); if (views.length >= result.total || result.items.length === 0) break; } if (!controller.signal.aborted) setItems(views); } void load().catch((error: Error) => { if (!controller.signal.aborted) Toast.error(error.message); }); return () => controller.abort(); }, [revision]); function openEditor(mode: Editor["mode"], view?: View) { const content = view?.content ?? { name: "", filters, columns }; setEditor({ mode, content, view }); setName( mode === "copy" ? `${content.name} 副本`.slice(0, 200) : content.name, ); onOverlay(); } function reportError(error: unknown) { Toast.error((error as Error).message); if (error instanceof ApiError && error.status === 409) { setRevision((value) => value + 1); setEditor(null); setDeleting(null); } } async function save(content: ViewContent, old?: View, restore = false) { setBusy(true); try { const value = old ? await api(`/research/assets/${old.id}`, { method: "PUT", body: JSON.stringify({ kind: "view", version: old.version, content, }), }) : await post("/research/assets", { kind: "view", content }); setItems((previous) => old ? previous.map((item) => (item.id === value.id ? value : item)) : [...previous, value], ); if (restore) { onSelect(value.id); onRestore(value.content); } setEditor(null); Toast.success("筛选视图已保存"); } catch (error) { reportError(error); } finally { setBusy(false); } } async function remove() { if (!deleting) return; setBusy(true); try { await api(`/research/assets/${deleting.id}?version=${deleting.version}`, { method: "DELETE", }); setItems((previous) => previous.filter((item) => item.id !== deleting.id), ); if (selectedId === deleting.id) onSubmissionChange( submissionBlocked ? "SUBMISSION_BLOCKED" : submission, ); setDeleting(null); Toast.success("视图已删除"); } catch (error) { reportError(error); } finally { setBusy(false); } } return ( <>
{ const view = items.find((item) => item.id === key); if (view) { onSelect(view.id); onRestore(view.content); } else onSubmissionChange(key); }} tabList={[ { itemKey: "UNSUBMITTED", tab: "待提交", icon:
{ if (!busy) setEditor(null); }} confirmLoading={busy} okText="保存视图" okButtonProps={{ disabled: !name.trim(), "aria-label": "保存视图" }} onOk={() => { if (editor && name.trim()) void save( { ...editor.content, name: name.trim() }, editor.mode === "rename" ? editor.view : undefined, editor.mode !== "rename", ); }} > {editor?.mode !== "rename" && (

保存筛选条件、排序、每页条数和显示列,之后可从标签栏一键切换。

)}
{ if (!busy) setDeleting(null); }} confirmLoading={busy} okText="删除视图" okButtonProps={{ type: "danger", "aria-label": "删除视图" }} onOk={() => void remove()} >

删除“{deleting?.content.name}”后,该视图将从标签栏移除,Alpha 数据不受影响。

); }