added ability to enable all gifts in one go

This commit is contained in:
onysd 2026-07-22 00:50:00 +03:00
parent 31b323e36a
commit 9844d78c5b
7 changed files with 122 additions and 15 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -4,8 +4,8 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>telesrv admin</title> <title>telesrv admin</title>
<script type="module" crossorigin src="/assets/index-DzNuxLKt.js"></script> <script type="module" crossorigin src="/assets/index-ybbM_ULl.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DHdrFM5j.css"> <link rel="stylesheet" crossorigin href="/assets/index-BVA68zgn.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View file

@ -304,6 +304,12 @@ const translations: Record<Language, Record<string, string>> = {
"gifts.replace": "New revision", "gifts.replace": "New revision",
"gifts.disable": "Disable", "gifts.disable": "Disable",
"gifts.enable": "Enable", "gifts.enable": "Enable",
"gifts.bulkSelected": "{count} selected",
"gifts.bulkSelectAll": "Select all visible gifts",
"gifts.bulkSelectOne": "Select gift {id}",
"gifts.bulkEnable": "Enable selected",
"gifts.bulkDisable": "Disable selected",
"gifts.bulkStatusFailed": "{failed} of {total} failed",
"gifts.empty": "No Star Gifts have been imported.", "gifts.empty": "No Star Gifts have been imported.",
"gifts.emptyHint": "Import the first animation above to build the gift catalog.", "gifts.emptyHint": "Import the first animation above to build the gift catalog.",
"gifts.validationReady": "Validation passed", "gifts.validationReady": "Validation passed",
@ -675,6 +681,12 @@ const translations: Record<Language, Record<string, string>> = {
"gifts.replace": "创建新版本", "gifts.replace": "创建新版本",
"gifts.disable": "停用", "gifts.disable": "停用",
"gifts.enable": "启用", "gifts.enable": "启用",
"gifts.bulkSelected": "已选择 {count} 个",
"gifts.bulkSelectAll": "选择所有可见礼物",
"gifts.bulkSelectOne": "选择礼物 {id}",
"gifts.bulkEnable": "启用所选",
"gifts.bulkDisable": "停用所选",
"gifts.bulkStatusFailed": "{total} 个中有 {failed} 个失败",
"gifts.empty": "尚未导入星星礼物。", "gifts.empty": "尚未导入星星礼物。",
"gifts.emptyHint": "从上方导入第一个动画,开始搭建礼物目录。", "gifts.emptyHint": "从上方导入第一个动画,开始搭建礼物目录。",
"gifts.validationReady": "校验已通过", "gifts.validationReady": "校验已通过",
@ -1046,6 +1058,12 @@ const translations: Record<Language, Record<string, string>> = {
"gifts.replace": "Новая версия", "gifts.replace": "Новая версия",
"gifts.disable": "Отключить", "gifts.disable": "Отключить",
"gifts.enable": "Включить", "gifts.enable": "Включить",
"gifts.bulkSelected": "Выбрано: {count}",
"gifts.bulkSelectAll": "Выбрать все видимые подарки",
"gifts.bulkSelectOne": "Выбрать подарок {id}",
"gifts.bulkEnable": "Включить выбранные",
"gifts.bulkDisable": "Отключить выбранные",
"gifts.bulkStatusFailed": "{failed} из {total} не выполнено",
"gifts.empty": "Звездные подарки еще не импортированы.", "gifts.empty": "Звездные подарки еще не импортированы.",
"gifts.emptyHint": "Импортируйте первую анимацию, чтобы начать наполнение каталога.", "gifts.emptyHint": "Импортируйте первую анимацию, чтобы начать наполнение каталога.",
"gifts.validationReady": "Проверка пройдена", "gifts.validationReady": "Проверка пройдена",

View file

@ -107,6 +107,10 @@ export function GiftsPage() {
const [busy, setBusy] = useState(false); const [busy, setBusy] = useState(false);
const [error, setError] = useState(""); const [error, setError] = useState("");
const [importError, setImportError] = useState(""); const [importError, setImportError] = useState("");
const [selected, setSelected] = useState<Set<string>>(new Set());
const [bulkReason, setBulkReason] = useState("");
const [bulkBusy, setBulkBusy] = useState(false);
const [bulkError, setBulkError] = useState("");
async function load() { async function load() {
setError(""); setError("");
@ -152,6 +156,61 @@ export function GiftsPage() {
); );
}, [gifts, query]); }, [gifts, query]);
const allVisibleSelected = visibleGifts.length > 0 && visibleGifts.every((gift) => selected.has(gift.GiftID));
function toggleSelected(giftID: string) {
setSelected((prev) => {
const next = new Set(prev);
if (next.has(giftID)) next.delete(giftID);
else next.add(giftID);
return next;
});
}
function toggleSelectAllVisible() {
setSelected((prev) => {
if (allVisibleSelected) {
const next = new Set(prev);
for (const gift of visibleGifts) next.delete(gift.GiftID);
return next;
}
const next = new Set(prev);
for (const gift of visibleGifts) next.add(gift.GiftID);
return next;
});
}
async function bulkSetEnabled(nextEnabled: boolean) {
if (!bulkReason.trim()) {
setBulkError(t("action.reasonRequired"));
return;
}
setBulkBusy(true);
setBulkError("");
const ids = Array.from(selected);
let failed = 0;
for (const id of ids) {
try {
await api.action("/api/actions/set-gift-enabled", {
gift_id: id,
enabled: nextEnabled,
reason: bulkReason.trim(),
confirm: true
});
} catch {
failed++;
}
}
setBulkBusy(false);
if (failed > 0) {
setBulkError(t("gifts.bulkStatusFailed", { failed, total: ids.length }));
} else {
setSelected(new Set());
setBulkReason("");
}
await load();
}
function uploadForm(confirm: boolean, commandID = "") { function uploadForm(confirm: boolean, commandID = "") {
if (!file) throw new Error(t("gifts.fileRequired")); if (!file) throw new Error(t("gifts.fileRequired"));
if (!reason.trim()) throw new Error(t("action.reasonRequired")); if (!reason.trim()) throw new Error(t("action.reasonRequired"));
@ -249,12 +308,25 @@ export function GiftsPage() {
<span className="gift-list-summary">{t("gifts.listSummary", { shown: visibleGifts.length, total: gifts.length })}</span> <span className="gift-list-summary">{t("gifts.listSummary", { shown: visibleGifts.length, total: gifts.length })}</span>
</div> </div>
</QueryPanel> </QueryPanel>
{selected.size > 0 && <div className="gift-bulk-toolbar">
<span className="gift-bulk-count">{t("gifts.bulkSelected", { count: selected.size })}</span>
<label className="gift-reason-field gift-bulk-reason"><span>{t("gifts.reason")}</span><input value={bulkReason} placeholder={t("gifts.reasonPlaceholder")} onChange={(e) => setBulkReason(e.target.value)} /></label>
<button className="btn" type="button" onClick={() => bulkSetEnabled(true)} disabled={bulkBusy}>
{bulkBusy ? <Loader2 className="spin" size={14} /> : <CheckCircle2 size={14} />} {t("gifts.bulkEnable")}
</button>
<button className="btn" type="button" onClick={() => bulkSetEnabled(false)} disabled={bulkBusy}>
{bulkBusy ? <Loader2 className="spin" size={14} /> : <Pause size={14} />} {t("gifts.bulkDisable")}
</button>
<button className="btn" type="button" onClick={() => { setSelected(new Set()); setBulkError(""); }} disabled={bulkBusy}>{t("common.close")}</button>
{bulkError && <span className="gift-bulk-error">{bulkError}</span>}
</div>}
<div className="table-wrap gift-table-wrap"> <div className="table-wrap gift-table-wrap">
<table className="data-table gift-table"> <table className="data-table gift-table">
<thead><tr><th>{t("gifts.animation")}</th><th>{t("gifts.idRevision")}</th><th>{t("gifts.title")}</th><th>{t("gifts.price")}</th><th>{t("gifts.source")}</th><th>{t("gifts.received")}</th><th>{t("common.status")}</th><th>{t("common.updatedAt")}</th><th>{t("common.actions")}</th></tr></thead> <thead><tr><th className="gift-select-col"><input type="checkbox" checked={allVisibleSelected} onChange={toggleSelectAllVisible} aria-label={t("gifts.bulkSelectAll")} /></th><th>{t("gifts.animation")}</th><th>{t("gifts.idRevision")}</th><th>{t("gifts.title")}</th><th>{t("gifts.price")}</th><th>{t("gifts.source")}</th><th>{t("gifts.received")}</th><th>{t("common.status")}</th><th>{t("common.updatedAt")}</th><th>{t("common.actions")}</th></tr></thead>
<tbody> <tbody>
{visibleGifts.map((gift) => ( {visibleGifts.map((gift) => (
<tr className={gift.Enabled ? "" : "gift-row-disabled"} key={gift.GiftID}> <tr className={gift.Enabled ? "" : "gift-row-disabled"} key={gift.GiftID}>
<td className="gift-select-col"><input type="checkbox" checked={selected.has(gift.GiftID)} onChange={() => toggleSelected(gift.GiftID)} aria-label={t("gifts.bulkSelectOne", { id: gift.GiftID })} /></td>
<td><LottiePreview giftID={gift.GiftID} revision={gift.Revision} compact /></td> <td><LottiePreview giftID={gift.GiftID} revision={gift.Revision} compact /></td>
<td className="mono">{gift.GiftID} / {gift.Revision}</td> <td className="mono">{gift.GiftID} / {gift.Revision}</td>
<td><strong className="gift-table-title">{gift.Title || `Gift #${gift.GiftID}`}</strong><span className="gift-sort-order">{t("gifts.sortOrder")}: {gift.SortOrder}</span></td> <td><strong className="gift-table-title">{gift.Title || `Gift #${gift.GiftID}`}</strong><span className="gift-sort-order">{t("gifts.sortOrder")}: {gift.SortOrder}</span></td>
@ -266,7 +338,7 @@ export function GiftsPage() {
<td><div className="gift-table-actions"><button className="btn compact-btn collectible-button" type="button" onClick={() => setCollectibleGift(gift)}><Gem size={13} />{t("collectibles.manage")}</button><button className="btn compact-btn" type="button" onClick={() => startRevision(gift)}>{t("gifts.replace")}</button><ActionButton compact tone="neutral" label={gift.Enabled ? t("gifts.disable") : t("gifts.enable")} path="/api/actions/set-gift-enabled" payload={() => ({ gift_id: gift.GiftID, enabled: !gift.Enabled })} onDone={() => void load()} /></div></td> <td><div className="gift-table-actions"><button className="btn compact-btn collectible-button" type="button" onClick={() => setCollectibleGift(gift)}><Gem size={13} />{t("collectibles.manage")}</button><button className="btn compact-btn" type="button" onClick={() => startRevision(gift)}>{t("gifts.replace")}</button><ActionButton compact tone="neutral" label={gift.Enabled ? t("gifts.disable") : t("gifts.enable")} path="/api/actions/set-gift-enabled" payload={() => ({ gift_id: gift.GiftID, enabled: !gift.Enabled })} onDone={() => void load()} /></div></td>
</tr> </tr>
))} ))}
{visibleGifts.length === 0 && <EmptyRow colSpan={9} />} {visibleGifts.length === 0 && <EmptyRow colSpan={10} />}
</tbody> </tbody>
</table> </table>
</div> </div>

View file

@ -408,8 +408,25 @@
.gift-table-wrap { background: #ffffff; } .gift-table-wrap { background: #ffffff; }
.gift-table { min-width: 1080px; } .gift-table { min-width: 1080px; }
.gift-table th:first-child { width: 74px; } .gift-table th:nth-child(2) { width: 74px; }
.gift-table td { vertical-align: middle; } .gift-table td { vertical-align: middle; }
.gift-select-col { width: 34px; text-align: center; }
.gift-select-col input { width: 15px; height: 15px; }
.gift-bulk-toolbar {
display: flex;
align-items: center;
gap: 10px;
padding: 9px 12px;
margin-bottom: 10px;
background: #f3f8f7;
border: 1px solid var(--line);
border-radius: 9px;
}
.gift-bulk-count { color: var(--text); font-size: 12px; font-weight: 700; white-space: nowrap; }
.gift-bulk-reason { flex: 1; min-width: 160px; }
.gift-bulk-reason input { height: 34px; }
.gift-bulk-error { color: #b42318; font-size: 11px; font-weight: 700; }
.gift-animation-shell.compact { width: 56px; min-height: 56px; overflow: hidden; border: 1px solid var(--line); border-radius: 9px; } .gift-animation-shell.compact { width: 56px; min-height: 56px; overflow: hidden; border: 1px solid var(--line); border-radius: 9px; }
.gift-animation-shell.compact .gift-animation { width: 54px; height: 54px; } .gift-animation-shell.compact .gift-animation { width: 54px; height: 54px; }
.gift-animation-shell.compact .gift-play { right: 3px; bottom: 3px; width: 20px; height: 20px; } .gift-animation-shell.compact .gift-play { right: 3px; bottom: 3px; width: 20px; height: 20px; }