Files
new-api-hunter/web/src/hooks/redemptions/useRedemptionsData.jsx
CaIon a78c1c9be9 fix(i18n): prioritize user settings over Accept-Language header
The i18n middleware runs before UserAuth, so user settings weren't
available when language was detected. Now GetLangFromContext checks
user settings first (set by UserAuth) before falling back to the
language set by middleware or Accept-Language header.
2026-02-05 00:37:18 +08:00

361 lines
9.2 KiB
JavaScript

/*
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { useState, useEffect } from 'react';
import { API, showError, showSuccess, copy } from '../../helpers';
import { ITEMS_PER_PAGE } from '../../constants';
import {
REDEMPTION_ACTIONS,
REDEMPTION_STATUS,
} from '../../constants/redemption.constants';
import { Modal } from '@douyinfe/semi-ui';
import { useTranslation } from 'react-i18next';
import { useTableCompactMode } from '../common/useTableCompactMode';
export const useRedemptionsData = () => {
const { t } = useTranslation();
// Basic state
const [redemptions, setRedemptions] = useState([]);
const [loading, setLoading] = useState(true);
const [searching, setSearching] = useState(false);
const [activePage, setActivePage] = useState(1);
const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
const [tokenCount, setTokenCount] = useState(0);
const [selectedKeys, setSelectedKeys] = useState([]);
// Edit state
const [editingRedemption, setEditingRedemption] = useState({
id: undefined,
});
const [showEdit, setShowEdit] = useState(false);
// Form API
const [formApi, setFormApi] = useState(null);
// UI state
const [compactMode, setCompactMode] = useTableCompactMode('redemptions');
// Form state
const formInitValues = {
searchKeyword: '',
};
// Get form values
const getFormValues = () => {
const formValues = formApi ? formApi.getValues() : {};
return {
searchKeyword: formValues.searchKeyword || '',
};
};
// Set redemption data format
const setRedemptionFormat = (redemptions) => {
setRedemptions(redemptions);
};
// Load redemption list
const loadRedemptions = async (page = 1, pageSize) => {
setLoading(true);
try {
const res = await API.get(
`/api/redemption/?p=${page}&page_size=${pageSize}`,
);
const { success, message, data } = res.data;
if (success) {
const newPageData = data.items;
setActivePage(data.page <= 0 ? 1 : data.page);
setTokenCount(data.total);
setRedemptionFormat(newPageData);
} else {
showError(message);
}
} catch (error) {
showError(error.message);
}
setLoading(false);
};
// Search redemption codes
const searchRedemptions = async () => {
const { searchKeyword } = getFormValues();
if (searchKeyword === '') {
await loadRedemptions(1, pageSize);
return;
}
setSearching(true);
try {
const res = await API.get(
`/api/redemption/search?keyword=${searchKeyword}&p=1&page_size=${pageSize}`,
);
const { success, message, data } = res.data;
if (success) {
const newPageData = data.items;
setActivePage(data.page || 1);
setTokenCount(data.total);
setRedemptionFormat(newPageData);
} else {
showError(message);
}
} catch (error) {
showError(error.message);
}
setSearching(false);
};
// Manage redemption codes (CRUD operations)
const manageRedemption = async (id, action, record) => {
setLoading(true);
let data = { id };
let res;
try {
switch (action) {
case REDEMPTION_ACTIONS.DELETE:
res = await API.delete(`/api/redemption/${id}/`);
break;
case REDEMPTION_ACTIONS.ENABLE:
data.status = REDEMPTION_STATUS.UNUSED;
res = await API.put('/api/redemption/?status_only=true', data);
break;
case REDEMPTION_ACTIONS.DISABLE:
data.status = REDEMPTION_STATUS.DISABLED;
res = await API.put('/api/redemption/?status_only=true', data);
break;
default:
throw new Error('Unknown operation type');
}
const { success, message } = res.data;
if (success) {
showSuccess(t('操作成功完成!'));
let redemption = res.data.data;
let newRedemptions = [...redemptions];
if (action !== REDEMPTION_ACTIONS.DELETE) {
record.status = redemption.status;
}
setRedemptions(newRedemptions);
} else {
showError(message);
}
} catch (error) {
showError(error.message);
}
setLoading(false);
};
// Refresh data
const refresh = async (page = activePage) => {
const { searchKeyword } = getFormValues();
if (searchKeyword === '') {
await loadRedemptions(page, pageSize);
} else {
await searchRedemptions();
}
};
// Handle page change
const handlePageChange = (page) => {
setActivePage(page);
const { searchKeyword } = getFormValues();
if (searchKeyword === '') {
loadRedemptions(page, pageSize);
} else {
searchRedemptions();
}
};
// Handle page size change
const handlePageSizeChange = (size) => {
setPageSize(size);
setActivePage(1);
const { searchKeyword } = getFormValues();
if (searchKeyword === '') {
loadRedemptions(1, size);
} else {
searchRedemptions();
}
};
// Row selection configuration
const rowSelection = {
onSelect: (record, selected) => {},
onSelectAll: (selected, selectedRows) => {},
onChange: (selectedRowKeys, selectedRows) => {
setSelectedKeys(selectedRows);
},
};
// Row style handling - using isExpired function
const handleRow = (record, index) => {
// Local isExpired function
const isExpired = (rec) => {
return (
rec.status === REDEMPTION_STATUS.UNUSED &&
rec.expired_time !== 0 &&
rec.expired_time < Math.floor(Date.now() / 1000)
);
};
if (record.status !== REDEMPTION_STATUS.UNUSED || isExpired(record)) {
return {
style: {
background: 'var(--semi-color-disabled-border)',
},
};
} else {
return {};
}
};
// Copy text
const copyText = async (text) => {
if (await copy(text)) {
showSuccess('已复制到剪贴板!');
} else {
Modal.error({
title: '无法复制到剪贴板,请手动复制',
content: text,
size: 'large',
});
}
};
// Batch copy redemption codes
const batchCopyRedemptions = async () => {
if (selectedKeys.length === 0) {
showError(t('请至少选择一个兑换码!'));
return;
}
let keys = '';
for (let i = 0; i < selectedKeys.length; i++) {
keys += selectedKeys[i].name + ' ' + selectedKeys[i].key + '\n';
}
await copyText(keys);
};
// Batch delete redemption codes (clear invalid)
const batchDeleteRedemptions = async () => {
Modal.confirm({
title: t('确定清除所有失效兑换码?'),
content: t('将删除已使用、已禁用及过期的兑换码,此操作不可撤销。'),
onOk: async () => {
setLoading(true);
const res = await API.delete('/api/redemption/invalid');
const { success, message, data } = res.data;
if (success) {
showSuccess(t('已删除 {{count}} 条失效兑换码', { count: data }));
await refresh();
} else {
showError(message);
}
setLoading(false);
},
});
};
// Close edit modal
const closeEdit = () => {
setShowEdit(false);
setTimeout(() => {
setEditingRedemption({
id: undefined,
});
}, 500);
};
// Remove record (for UI update after deletion)
const removeRecord = (key) => {
let newDataSource = [...redemptions];
if (key != null) {
let idx = newDataSource.findIndex((data) => data.key === key);
if (idx > -1) {
newDataSource.splice(idx, 1);
setRedemptions(newDataSource);
}
}
};
// Initialize data loading
useEffect(() => {
loadRedemptions(1, pageSize)
.then()
.catch((reason) => {
showError(reason);
});
}, [pageSize]);
return {
// Data state
redemptions,
loading,
searching,
activePage,
pageSize,
tokenCount,
selectedKeys,
// Edit state
editingRedemption,
showEdit,
// Form state
formApi,
formInitValues,
// UI state
compactMode,
setCompactMode,
// Data operations
loadRedemptions,
searchRedemptions,
manageRedemption,
refresh,
copyText,
removeRecord,
// State updates
setActivePage,
setPageSize,
setSelectedKeys,
setEditingRedemption,
setShowEdit,
setFormApi,
setLoading,
// Event handlers
handlePageChange,
handlePageSizeChange,
rowSelection,
handleRow,
closeEdit,
getFormValues,
// Batch operations
batchCopyRedemptions,
batchDeleteRedemptions,
// Translation function
t,
};
};