feat(config): 重构配置模块导航与版本切换,新增版本删除能力
This commit is contained in:
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import {
|
||||
deleteOddsVersion,
|
||||
getAdminPlayTypes,
|
||||
getOddsVersion,
|
||||
getOddsVersions,
|
||||
@@ -23,21 +24,7 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "@/components/ui/sheet";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { ConfigVersionSwitcher } from "@/modules/config/config-version-switcher";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
|
||||
import { LotteryApiBizError } from "@/types/api/errors";
|
||||
@@ -91,7 +78,6 @@ export function OddsConfigDocScreen() {
|
||||
|
||||
const [rollbackOpen, setRollbackOpen] = useState(false);
|
||||
const [rollbackTarget, setRollbackTarget] = useState<ConfigVersionSummary | null>(null);
|
||||
const [historyOpen, setHistoryOpen] = useState(false);
|
||||
|
||||
const refreshTypes = useCallback(async () => {
|
||||
setLoadingTypes(true);
|
||||
@@ -332,6 +318,22 @@ export function OddsConfigDocScreen() {
|
||||
|
||||
const activeHead = list.find((x) => x.status === "active");
|
||||
|
||||
async function handleDeleteVersion(row: ConfigVersionSummary) {
|
||||
try {
|
||||
await deleteOddsVersion(row.id);
|
||||
toast.success("已删除该版本");
|
||||
await refreshList();
|
||||
} catch (e) {
|
||||
toast.error(e instanceof LotteryApiBizError ? e.message : "删除失败");
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
function requestRollback(row: ConfigVersionSummary) {
|
||||
setRollbackTarget(row);
|
||||
setRollbackOpen(true);
|
||||
}
|
||||
|
||||
const catTabs: { id: CatTab; label: string }[] = [
|
||||
{ id: "all", label: "全部" },
|
||||
{ id: "d4", label: "4D" },
|
||||
@@ -340,8 +342,6 @@ export function OddsConfigDocScreen() {
|
||||
{ id: "jackpot", label: "Jackpot" },
|
||||
];
|
||||
|
||||
const sortedHistory = useMemo(() => [...list].sort((a, b) => b.id - a.id), [list]);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="space-y-1">
|
||||
@@ -354,7 +354,6 @@ export function OddsConfigDocScreen() {
|
||||
<Button
|
||||
key={t.id}
|
||||
type="button"
|
||||
size="sm"
|
||||
variant={catTab === t.id ? "default" : "outline"}
|
||||
className={cn(catTab === t.id && "shadow-sm")}
|
||||
onClick={() => {
|
||||
@@ -377,7 +376,6 @@ export function OddsConfigDocScreen() {
|
||||
<Button
|
||||
key={t.play_code}
|
||||
type="button"
|
||||
size="sm"
|
||||
variant={resolvedPlayCode === t.play_code ? "secondary" : "outline"}
|
||||
onClick={() => setPlayCode(t.play_code)}
|
||||
>
|
||||
@@ -388,17 +386,28 @@ export function OddsConfigDocScreen() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ConfigVersionSwitcher
|
||||
versions={list}
|
||||
selectedId={selectedId}
|
||||
onSelectedIdChange={setSelectedId}
|
||||
loading={loadingList}
|
||||
sheetTitle="赔率配置版本"
|
||||
sheetDescription="选择版本在本页查看;非草稿版本可回滚为新建草稿。"
|
||||
onDeleteVersion={handleDeleteVersion}
|
||||
onRollbackVersion={requestRollback}
|
||||
rollbackBusy={saving}
|
||||
/>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
disabled={loadingList}
|
||||
onClick={() => void refreshList()}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
<Button type="button" size="sm" onClick={() => void handleNewDraft()} disabled={saving}>
|
||||
<Button type="button" onClick={() => void handleNewDraft()} disabled={saving}>
|
||||
新建草稿
|
||||
</Button>
|
||||
</div>
|
||||
@@ -483,59 +492,6 @@ export function OddsConfigDocScreen() {
|
||||
) : null}
|
||||
|
||||
<div className="flex flex-wrap gap-2 pt-2">
|
||||
<Button type="button" variant="outline" size="sm" onClick={() => setHistoryOpen(true)}>
|
||||
查看历史版本
|
||||
</Button>
|
||||
<Sheet open={historyOpen} onOpenChange={setHistoryOpen}>
|
||||
<SheetContent side="right" className="sm:max-w-lg flex flex-col">
|
||||
<SheetHeader>
|
||||
<SheetTitle>赔率版本历史</SheetTitle>
|
||||
<SheetDescription>选择一条历史版本执行回滚(克隆为新草稿)。</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className="flex-1 overflow-auto rounded-md border mt-4">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>版本</TableHead>
|
||||
<TableHead>状态</TableHead>
|
||||
<TableHead>时间</TableHead>
|
||||
<TableHead className="w-[100px]">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{sortedHistory.map((v) => (
|
||||
<TableRow key={v.id}>
|
||||
<TableCell className="font-mono text-xs">v{v.version_no}</TableCell>
|
||||
<TableCell className="text-xs">
|
||||
{v.status === "active" ? "生效" : v.status === "draft" ? "草稿" : "归档"}
|
||||
</TableCell>
|
||||
<TableCell className="text-xs text-muted-foreground whitespace-nowrap">
|
||||
{v.updated_at ? formatDt(v.updated_at) : "—"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-xs"
|
||||
disabled={saving || v.status === "draft"}
|
||||
onClick={() => {
|
||||
setRollbackTarget(v);
|
||||
setRollbackOpen(true);
|
||||
setHistoryOpen(false);
|
||||
}}
|
||||
>
|
||||
回滚
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
<Button type="button" onClick={() => void handleSave()} disabled={!isDraft || saving || loadingDetail}>
|
||||
保存
|
||||
</Button>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import {
|
||||
deletePlayConfigVersion,
|
||||
getAdminPlayTypes,
|
||||
getPlayConfigVersion,
|
||||
getPlayConfigVersions,
|
||||
@@ -33,6 +34,7 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { ConfigVersionSwitcher } from "@/modules/config/config-version-switcher";
|
||||
import { LotteryApiBizError } from "@/types/api/errors";
|
||||
import type {
|
||||
AdminPlayTypeRow,
|
||||
@@ -41,6 +43,59 @@ import type {
|
||||
PlayConfigVersionDetail,
|
||||
} from "@/types/api/admin-config";
|
||||
|
||||
const DEFAULT_PLAY_MIN_BET = 100;
|
||||
const DEFAULT_PLAY_MAX_BET = 500_000_000;
|
||||
|
||||
type PlayConfigSaveItemPayload = {
|
||||
play_code: string;
|
||||
is_enabled: boolean;
|
||||
min_bet_amount: number;
|
||||
max_bet_amount: number;
|
||||
display_order: number;
|
||||
rule_text_zh: string | null;
|
||||
rule_text_en: string | null;
|
||||
rule_text_ne: string | null;
|
||||
extra_config_json: unknown;
|
||||
};
|
||||
|
||||
/** 与「玩法目录」对齐的完整列表,避免保存草稿时用残缺数组覆盖后端导致其它玩法配置被删。 */
|
||||
function buildPlayConfigSavePayload(
|
||||
typeRows: AdminPlayTypeRow[],
|
||||
draftRows: PlayConfigItemRow[],
|
||||
): PlayConfigSaveItemPayload[] {
|
||||
const byCode = new Map(draftRows.map((r) => [r.play_code, r]));
|
||||
const sorted = [...typeRows].sort(
|
||||
(a, b) => a.sort_order - b.sort_order || a.play_code.localeCompare(b.play_code),
|
||||
);
|
||||
return sorted.map((t) => {
|
||||
const row = byCode.get(t.play_code);
|
||||
if (row) {
|
||||
return {
|
||||
play_code: row.play_code,
|
||||
is_enabled: row.is_enabled,
|
||||
min_bet_amount: row.min_bet_amount,
|
||||
max_bet_amount: row.max_bet_amount,
|
||||
display_order: row.display_order,
|
||||
rule_text_zh: row.rule_text_zh,
|
||||
rule_text_en: row.rule_text_en,
|
||||
rule_text_ne: row.rule_text_ne,
|
||||
extra_config_json: row.extra_config_json,
|
||||
};
|
||||
}
|
||||
return {
|
||||
play_code: t.play_code,
|
||||
is_enabled: t.is_enabled,
|
||||
min_bet_amount: DEFAULT_PLAY_MIN_BET,
|
||||
max_bet_amount: DEFAULT_PLAY_MAX_BET,
|
||||
display_order: t.sort_order,
|
||||
rule_text_zh: null,
|
||||
rule_text_en: null,
|
||||
rule_text_ne: null,
|
||||
extra_config_json: null,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function PlayConfigDocScreen() {
|
||||
const [types, setTypes] = useState<AdminPlayTypeRow[]>([]);
|
||||
const [list, setList] = useState<ConfigVersionSummary[]>([]);
|
||||
@@ -199,6 +254,7 @@ export function PlayConfigDocScreen() {
|
||||
setSaving(true);
|
||||
try {
|
||||
const updated = await patchAdminPlayType(play_code, { is_enabled: next });
|
||||
const typesForPayload = types.map((r) => (r.play_code === updated.play_code ? updated : r));
|
||||
setTypes((prev) =>
|
||||
[...prev.map((r) => (r.play_code === updated.play_code ? updated : r))].sort(
|
||||
(a, b) => a.sort_order - b.sort_order || a.play_code.localeCompare(b.play_code),
|
||||
@@ -207,20 +263,11 @@ export function PlayConfigDocScreen() {
|
||||
if (isDraft) {
|
||||
updateConfigRow(play_code, { is_enabled: next });
|
||||
const idx = draftRowIndex(play_code);
|
||||
const nextRows = draftRows.map((r, i) =>
|
||||
i === idx ? { ...r, is_enabled: next } : r,
|
||||
);
|
||||
const payload = nextRows.map((r) => ({
|
||||
play_code: r.play_code,
|
||||
is_enabled: r.is_enabled,
|
||||
min_bet_amount: r.min_bet_amount,
|
||||
max_bet_amount: r.max_bet_amount,
|
||||
display_order: r.display_order,
|
||||
rule_text_zh: r.rule_text_zh,
|
||||
rule_text_en: r.rule_text_en,
|
||||
rule_text_ne: r.rule_text_ne,
|
||||
extra_config_json: r.extra_config_json,
|
||||
}));
|
||||
const rowsForMerge =
|
||||
idx >= 0
|
||||
? draftRows.map((r, i) => (i === idx ? { ...r, is_enabled: next } : r))
|
||||
: draftRows;
|
||||
const payload = buildPlayConfigSavePayload(typesForPayload, rowsForMerge);
|
||||
const d = await putPlayConfigItems(detail.id, payload);
|
||||
setDetail(d);
|
||||
setDraftRows(d.items.map((it) => ({ ...it })));
|
||||
@@ -239,7 +286,8 @@ export function PlayConfigDocScreen() {
|
||||
if (!detail || !isDraft) {
|
||||
return;
|
||||
}
|
||||
for (const r of draftRows) {
|
||||
const payload = buildPlayConfigSavePayload(types, draftRows);
|
||||
for (const r of payload) {
|
||||
if (r.min_bet_amount > r.max_bet_amount) {
|
||||
toast.error(`${r.play_code}: 最小额不能大于最大额`);
|
||||
return;
|
||||
@@ -247,17 +295,6 @@ export function PlayConfigDocScreen() {
|
||||
}
|
||||
setSaving(true);
|
||||
try {
|
||||
const payload = draftRows.map((r) => ({
|
||||
play_code: r.play_code,
|
||||
is_enabled: r.is_enabled,
|
||||
min_bet_amount: r.min_bet_amount,
|
||||
max_bet_amount: r.max_bet_amount,
|
||||
display_order: r.display_order,
|
||||
rule_text_zh: r.rule_text_zh,
|
||||
rule_text_en: r.rule_text_en,
|
||||
rule_text_ne: r.rule_text_ne,
|
||||
extra_config_json: r.extra_config_json,
|
||||
}));
|
||||
const d = await putPlayConfigItems(detail.id, payload);
|
||||
setDetail(d);
|
||||
setDraftRows(d.items.map((it) => ({ ...it })));
|
||||
@@ -328,20 +365,50 @@ export function PlayConfigDocScreen() {
|
||||
|
||||
const activeHead = list.find((x) => x.status === "active");
|
||||
|
||||
async function handleDeleteVersion(row: ConfigVersionSummary) {
|
||||
try {
|
||||
await deletePlayConfigVersion(row.id);
|
||||
toast.success("已删除该版本");
|
||||
await refreshList();
|
||||
} catch (e) {
|
||||
toast.error(e instanceof LotteryApiBizError ? e.message : "删除失败");
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="space-y-1">
|
||||
<CardTitle className="text-lg">玩法配置</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="rounded-lg border border-sky-200 bg-sky-50 px-3 py-2.5 text-sm text-sky-950 dark:border-sky-900/60 dark:bg-sky-950/40 dark:text-sky-50">
|
||||
<p className="font-medium">玩家端如何生效</p>
|
||||
<p className="mt-1 text-xs leading-relaxed opacity-90">
|
||||
只有状态为「生效中」的版本会进入{" "}
|
||||
<span className="font-mono text-[11px]">GET /api/v1/play/effective</span>{" "}
|
||||
;草稿需先「保存草稿」再点「启用为当前版本」。保存时会按左侧玩法目录<strong>自动补全</strong>
|
||||
缺失的配置行,避免误删其它玩法。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ConfigVersionSwitcher
|
||||
versions={list}
|
||||
selectedId={selectedId}
|
||||
onSelectedIdChange={setSelectedId}
|
||||
loading={loadingList}
|
||||
sheetTitle="玩法配置版本"
|
||||
onDeleteVersion={handleDeleteVersion}
|
||||
/>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Button type="button" variant="secondary" size="sm" onClick={() => void refreshList()} disabled={loadingList}>
|
||||
<Button type="button" variant="secondary" onClick={() => void refreshList()} disabled={loadingList}>
|
||||
刷新版本
|
||||
</Button>
|
||||
<Button type="button" variant="secondary" size="sm" onClick={() => void refreshTypes()} disabled={loadingTypes}>
|
||||
<Button type="button" variant="secondary" onClick={() => void refreshTypes()} disabled={loadingTypes}>
|
||||
刷新目录
|
||||
</Button>
|
||||
<Button type="button" size="sm" onClick={() => void handleNewDraft()} disabled={saving}>
|
||||
<Button type="button" onClick={() => void handleNewDraft()} disabled={saving}>
|
||||
新建草稿
|
||||
</Button>
|
||||
<Button type="button" onClick={() => void handleSaveDraft()} disabled={!isDraft || saving || loadingDetail}>
|
||||
@@ -390,7 +457,7 @@ export function PlayConfigDocScreen() {
|
||||
<TableHead className="w-[100px]">分类</TableHead>
|
||||
<TableHead className="w-[88px] text-center">状态</TableHead>
|
||||
<TableHead className="min-w-[120px]">显示名称</TableHead>
|
||||
<TableHead className="w-[88px]">排序</TableHead>
|
||||
<TableHead className="w-[120px]">排序</TableHead>
|
||||
<TableHead className="w-[110px]">最小下注</TableHead>
|
||||
<TableHead className="w-[110px]">最大下注</TableHead>
|
||||
<TableHead className="w-[140px]">操作</TableHead>
|
||||
@@ -426,10 +493,10 @@ export function PlayConfigDocScreen() {
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TableCell className="w-[120px]">
|
||||
<Input
|
||||
type="number"
|
||||
className="h-8 font-mono tabular-nums"
|
||||
className="h-8 w-full font-mono tabular-nums text-right"
|
||||
defaultValue={t.sort_order}
|
||||
key={`${t.play_code}-so-${t.updated_at}`}
|
||||
disabled={saving}
|
||||
@@ -479,7 +546,6 @@ export function PlayConfigDocScreen() {
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={!item || !isDraft || saving}
|
||||
onClick={() => openRuleEditor(t.play_code)}
|
||||
>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import {
|
||||
deleteOddsVersion,
|
||||
getAdminPlayTypes,
|
||||
getOddsVersion,
|
||||
getOddsVersions,
|
||||
@@ -16,6 +17,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { ConfigVersionSwitcher } from "@/modules/config/config-version-switcher";
|
||||
import { useAdminDateTimeFormatter } from "@/hooks/use-admin-datetime-formatter";
|
||||
import { LotteryApiBizError } from "@/types/api/errors";
|
||||
import type {
|
||||
@@ -242,17 +244,38 @@ export function RebateConfigDocScreen() {
|
||||
|
||||
const activeHead = listRows.find((x) => x.status === "active");
|
||||
|
||||
async function handleDeleteVersion(row: ConfigVersionSummary) {
|
||||
try {
|
||||
await deleteOddsVersion(row.id);
|
||||
toast.success("已删除该版本");
|
||||
await refreshList();
|
||||
} catch (e) {
|
||||
toast.error(e instanceof LotteryApiBizError ? e.message : "删除失败");
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="space-y-1">
|
||||
<CardTitle className="text-lg">佣金 / 回水配置</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6 max-w-xl">
|
||||
<ConfigVersionSwitcher
|
||||
versions={listRows}
|
||||
selectedId={selectedId}
|
||||
onSelectedIdChange={setSelectedId}
|
||||
loading={loading}
|
||||
sheetTitle="回水配置版本"
|
||||
sheetDescription="回水写入赔率版本草稿;选择与赔率配置共用同一套版本。"
|
||||
onDeleteVersion={handleDeleteVersion}
|
||||
/>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button type="button" variant="secondary" size="sm" onClick={() => void refreshList()} disabled={loading}>
|
||||
<Button type="button" variant="secondary" onClick={() => void refreshList()} disabled={loading}>
|
||||
刷新
|
||||
</Button>
|
||||
<Button type="button" size="sm" onClick={() => void handleNewDraft()} disabled={saving}>
|
||||
<Button type="button" onClick={() => void handleNewDraft()} disabled={saving}>
|
||||
新建草稿
|
||||
</Button>
|
||||
<Button type="button" onClick={() => void handleSave()} disabled={!isDraft || saving || loadingDetail}>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import {
|
||||
deleteRiskCapVersion,
|
||||
getRiskCapVersion,
|
||||
getRiskCapVersions,
|
||||
postRiskCapVersion,
|
||||
@@ -22,13 +23,7 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { ConfigVersionSwitcher } from "@/modules/config/config-version-switcher";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -272,8 +267,6 @@ export function RiskCapDocScreen() {
|
||||
toast.message("已写入本地草稿,记得保存草稿");
|
||||
}
|
||||
|
||||
const sortedList = useMemo(() => [...list].sort((a, b) => b.id - a.id), [list]);
|
||||
|
||||
const occFiltered = useMemo(() => {
|
||||
const q = occSearch.trim();
|
||||
if (!q) {
|
||||
@@ -282,6 +275,17 @@ export function RiskCapDocScreen() {
|
||||
return draftRows.filter((r) => r.normalized_number.includes(q));
|
||||
}, [draftRows, occSearch]);
|
||||
|
||||
async function handleDeleteVersion(row: ConfigVersionSummary) {
|
||||
try {
|
||||
await deleteRiskCapVersion(row.id);
|
||||
toast.success("已删除该版本");
|
||||
await refreshList();
|
||||
} catch (e) {
|
||||
toast.error(e instanceof LotteryApiBizError ? e.message : "删除失败");
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="space-y-1">
|
||||
@@ -296,35 +300,20 @@ export function RiskCapDocScreen() {
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-8">
|
||||
<ConfigVersionSwitcher
|
||||
versions={list}
|
||||
selectedId={selectedId}
|
||||
onSelectedIdChange={setSelectedId}
|
||||
loading={loadingList}
|
||||
sheetTitle="风控封顶版本"
|
||||
onDeleteVersion={handleDeleteVersion}
|
||||
/>
|
||||
|
||||
<div className="flex flex-wrap items-end gap-4">
|
||||
<div className="grid gap-2 min-w-[260px]">
|
||||
<Label>配置版本</Label>
|
||||
<Select
|
||||
value={selectedId}
|
||||
onValueChange={(v) => setSelectedId(v ?? "")}
|
||||
disabled={loadingList || sortedList.length === 0}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={loadingList ? "加载中…" : "选择版本"} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sortedList.map((v) => (
|
||||
<SelectItem key={v.id} value={String(v.id)}>
|
||||
#{v.id} · v{v.version_no} ·{" "}
|
||||
{v.status === "active"
|
||||
? "生效中"
|
||||
: v.status === "draft"
|
||||
? "草稿"
|
||||
: "已归档"}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Button type="button" variant="secondary" size="sm" onClick={() => void refreshList()}>
|
||||
<Button type="button" variant="secondary" onClick={() => void refreshList()}>
|
||||
刷新
|
||||
</Button>
|
||||
<Button type="button" size="sm" onClick={() => void handleNewDraft()} disabled={saving}>
|
||||
<Button type="button" onClick={() => void handleNewDraft()} disabled={saving}>
|
||||
新建草稿
|
||||
</Button>
|
||||
<Button type="button" onClick={() => void handleSave()} disabled={!isDraft || saving || loadingDetail}>
|
||||
@@ -381,7 +370,6 @@ export function RiskCapDocScreen() {
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={!isDraft || saving}
|
||||
onClick={() => setDraftRows((prev) => [...prev, newRow()])}
|
||||
>
|
||||
@@ -442,7 +430,6 @@ export function RiskCapDocScreen() {
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-destructive"
|
||||
disabled={!isDraft || saving || draftRows.length <= 1}
|
||||
onClick={() => removeRow(idx)}
|
||||
@@ -474,13 +461,12 @@ export function RiskCapDocScreen() {
|
||||
onChange={(e) => setOccSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button type="button" variant="outline" size="sm" onClick={() => toast.message("售罄 / 高风险筛选待接入")}>
|
||||
<Button type="button" variant="outline" onClick={() => toast.message("售罄 / 高风险筛选待接入")}>
|
||||
筛选预设…
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => toast.message("导出 CSV 待接入")}
|
||||
>
|
||||
导出 CSV
|
||||
@@ -507,7 +493,7 @@ export function RiskCapDocScreen() {
|
||||
<TableCell className="text-right text-muted-foreground">—</TableCell>
|
||||
<TableCell className="text-center text-muted-foreground">—</TableCell>
|
||||
<TableCell>
|
||||
<Button type="button" variant="ghost" size="sm" disabled>
|
||||
<Button type="button" variant="ghost" disabled>
|
||||
关闭
|
||||
</Button>
|
||||
</TableCell>
|
||||
|
||||
Reference in New Issue
Block a user