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.
361 lines
9.2 KiB
JavaScript
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,
|
|
};
|
|
};
|