1. Split monolithic `ChannelsTable` (2200+ LOC) into focused components
• `channels/index.jsx` – composition entry
• `ChannelsTable.jsx` – pure `<Table>` rendering
• `ChannelsActions.jsx` – bulk & settings toolbar
• `ChannelsFilters.jsx` – search / create / column-settings form
• `ChannelsTabs.jsx` – type tabs
• `ChannelsColumnDefs.js` – column definitions & render helpers
• `modals/` – BatchTag, ColumnSelector, ModelTest modals
2. Extract domain hook
• Moved `useChannelsData.js` → `src/hooks/channels/useChannelsData.js`
– centralises state, API calls, pagination, filters, batch ops
– now exports `setActivePage`, fixing tab / status switch errors
3. Update wiring
• All sub-components consume data via `useChannelsData` props
• Adjusted import paths after hook relocation
4. Clean legacy file
• Legacy `components/table/ChannelsTable.js` now re-exports new module
5. Bug fixes
• Tab switching, status filter & tag aggregation restored
• Column selector & batch actions operate via unified hook
This commit completes the first phase of modularising the Channels feature, laying groundwork for consistent, maintainable table architecture across the app.
306 lines
9.6 KiB
JavaScript
306 lines
9.6 KiB
JavaScript
import React, { useEffect, useState, useRef } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import {
|
|
API,
|
|
downloadTextAsFile,
|
|
showError,
|
|
showSuccess,
|
|
renderQuota,
|
|
renderQuotaWithPrompt,
|
|
} from '../../helpers';
|
|
import { useIsMobile } from '../../hooks/common/useIsMobile.js';
|
|
import {
|
|
Button,
|
|
Modal,
|
|
SideSheet,
|
|
Space,
|
|
Spin,
|
|
Typography,
|
|
Card,
|
|
Tag,
|
|
Form,
|
|
Avatar,
|
|
Row,
|
|
Col,
|
|
} from '@douyinfe/semi-ui';
|
|
import {
|
|
IconCreditCard,
|
|
IconSave,
|
|
IconClose,
|
|
IconGift,
|
|
} from '@douyinfe/semi-icons';
|
|
|
|
const { Text, Title } = Typography;
|
|
|
|
const EditRedemption = (props) => {
|
|
const { t } = useTranslation();
|
|
const isEdit = props.editingRedemption.id !== undefined;
|
|
const [loading, setLoading] = useState(isEdit);
|
|
const isMobile = useIsMobile();
|
|
const formApiRef = useRef(null);
|
|
|
|
const getInitValues = () => ({
|
|
name: '',
|
|
quota: 100000,
|
|
count: 1,
|
|
expired_time: null,
|
|
});
|
|
|
|
const handleCancel = () => {
|
|
props.handleClose();
|
|
};
|
|
|
|
const loadRedemption = async () => {
|
|
setLoading(true);
|
|
let res = await API.get(`/api/redemption/${props.editingRedemption.id}`);
|
|
const { success, message, data } = res.data;
|
|
if (success) {
|
|
if (data.expired_time === 0) {
|
|
data.expired_time = null;
|
|
} else {
|
|
data.expired_time = new Date(data.expired_time * 1000);
|
|
}
|
|
formApiRef.current?.setValues({ ...getInitValues(), ...data });
|
|
} else {
|
|
showError(message);
|
|
}
|
|
setLoading(false);
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (formApiRef.current) {
|
|
if (isEdit) {
|
|
loadRedemption();
|
|
} else {
|
|
formApiRef.current.setValues(getInitValues());
|
|
}
|
|
}
|
|
}, [props.editingRedemption.id]);
|
|
|
|
const submit = async (values) => {
|
|
let name = values.name;
|
|
if (!isEdit && (!name || name === '')) {
|
|
name = renderQuota(values.quota);
|
|
}
|
|
setLoading(true);
|
|
let localInputs = { ...values };
|
|
localInputs.count = parseInt(localInputs.count) || 0;
|
|
localInputs.quota = parseInt(localInputs.quota) || 0;
|
|
localInputs.name = name;
|
|
if (!localInputs.expired_time) {
|
|
localInputs.expired_time = 0;
|
|
} else {
|
|
localInputs.expired_time = Math.floor(localInputs.expired_time.getTime() / 1000);
|
|
}
|
|
let res;
|
|
if (isEdit) {
|
|
res = await API.put(`/api/redemption/`, {
|
|
...localInputs,
|
|
id: parseInt(props.editingRedemption.id),
|
|
});
|
|
} else {
|
|
res = await API.post(`/api/redemption/`, {
|
|
...localInputs,
|
|
});
|
|
}
|
|
const { success, message, data } = res.data;
|
|
if (success) {
|
|
if (isEdit) {
|
|
showSuccess(t('兑换码更新成功!'));
|
|
props.refresh();
|
|
props.handleClose();
|
|
} else {
|
|
showSuccess(t('兑换码创建成功!'));
|
|
props.refresh();
|
|
formApiRef.current?.setValues(getInitValues());
|
|
props.handleClose();
|
|
}
|
|
} else {
|
|
showError(message);
|
|
}
|
|
if (!isEdit && data) {
|
|
let text = '';
|
|
for (let i = 0; i < data.length; i++) {
|
|
text += data[i] + '\n';
|
|
}
|
|
Modal.confirm({
|
|
title: t('兑换码创建成功'),
|
|
content: (
|
|
<div>
|
|
<p>{t('兑换码创建成功,是否下载兑换码?')}</p>
|
|
<p>{t('兑换码将以文本文件的形式下载,文件名为兑换码的名称。')}</p>
|
|
</div>
|
|
),
|
|
onOk: () => {
|
|
downloadTextAsFile(text, `${localInputs.name}.txt`);
|
|
},
|
|
});
|
|
}
|
|
setLoading(false);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<SideSheet
|
|
placement={isEdit ? 'right' : 'left'}
|
|
title={
|
|
<Space>
|
|
{isEdit ?
|
|
<Tag color="blue" shape="circle">{t('更新')}</Tag> :
|
|
<Tag color="green" shape="circle">{t('新建')}</Tag>
|
|
}
|
|
<Title heading={4} className="m-0">
|
|
{isEdit ? t('更新兑换码信息') : t('创建新的兑换码')}
|
|
</Title>
|
|
</Space>
|
|
}
|
|
bodyStyle={{ padding: '0' }}
|
|
visible={props.visiable}
|
|
width={isMobile ? '100%' : 600}
|
|
footer={
|
|
<div className="flex justify-end bg-white">
|
|
<Space>
|
|
<Button
|
|
theme="solid"
|
|
onClick={() => formApiRef.current?.submitForm()}
|
|
icon={<IconSave />}
|
|
loading={loading}
|
|
>
|
|
{t('提交')}
|
|
</Button>
|
|
<Button
|
|
theme="light"
|
|
type="primary"
|
|
onClick={handleCancel}
|
|
icon={<IconClose />}
|
|
>
|
|
{t('取消')}
|
|
</Button>
|
|
</Space>
|
|
</div>
|
|
}
|
|
closeIcon={null}
|
|
onCancel={() => handleCancel()}
|
|
>
|
|
<Spin spinning={loading}>
|
|
<Form
|
|
initValues={getInitValues()}
|
|
getFormApi={(api) => formApiRef.current = api}
|
|
onSubmit={submit}
|
|
>
|
|
{({ values }) => (
|
|
<div className="p-2">
|
|
<Card className="!rounded-2xl shadow-sm border-0 mb-6">
|
|
{/* Header: Basic Info */}
|
|
<div className="flex items-center mb-2">
|
|
<Avatar size="small" color="blue" className="mr-2 shadow-md">
|
|
<IconGift size={16} />
|
|
</Avatar>
|
|
<div>
|
|
<Text className="text-lg font-medium">{t('基本信息')}</Text>
|
|
<div className="text-xs text-gray-600">{t('设置兑换码的基本信息')}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Row gutter={12}>
|
|
<Col span={24}>
|
|
<Form.Input
|
|
field='name'
|
|
label={t('名称')}
|
|
placeholder={t('请输入名称')}
|
|
style={{ width: '100%' }}
|
|
rules={!isEdit ? [] : [{ required: true, message: t('请输入名称') }]}
|
|
showClear
|
|
/>
|
|
</Col>
|
|
<Col span={24}>
|
|
<Form.DatePicker
|
|
field='expired_time'
|
|
label={t('过期时间')}
|
|
type='dateTime'
|
|
placeholder={t('选择过期时间(可选,留空为永久)')}
|
|
style={{ width: '100%' }}
|
|
showClear
|
|
/>
|
|
</Col>
|
|
</Row>
|
|
</Card>
|
|
|
|
<Card className="!rounded-2xl shadow-sm border-0">
|
|
{/* Header: Quota Settings */}
|
|
<div className="flex items-center mb-2">
|
|
<Avatar size="small" color="green" className="mr-2 shadow-md">
|
|
<IconCreditCard size={16} />
|
|
</Avatar>
|
|
<div>
|
|
<Text className="text-lg font-medium">{t('额度设置')}</Text>
|
|
<div className="text-xs text-gray-600">{t('设置兑换码的额度和数量')}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Row gutter={12}>
|
|
<Col span={12}>
|
|
<Form.AutoComplete
|
|
field='quota'
|
|
label={t('额度')}
|
|
placeholder={t('请输入额度')}
|
|
style={{ width: '100%' }}
|
|
type='number'
|
|
rules={[
|
|
{ required: true, message: t('请输入额度') },
|
|
{
|
|
validator: (rule, v) => {
|
|
const num = parseInt(v, 10);
|
|
return num > 0
|
|
? Promise.resolve()
|
|
: Promise.reject(t('额度必须大于0'));
|
|
},
|
|
},
|
|
]}
|
|
extraText={renderQuotaWithPrompt(Number(values.quota) || 0)}
|
|
data={[
|
|
{ value: 500000, label: '1$' },
|
|
{ value: 5000000, label: '10$' },
|
|
{ value: 25000000, label: '50$' },
|
|
{ value: 50000000, label: '100$' },
|
|
{ value: 250000000, label: '500$' },
|
|
{ value: 500000000, label: '1000$' },
|
|
]}
|
|
showClear
|
|
/>
|
|
</Col>
|
|
{!isEdit && (
|
|
<Col span={12}>
|
|
<Form.InputNumber
|
|
field='count'
|
|
label={t('生成数量')}
|
|
min={1}
|
|
rules={[
|
|
{ required: true, message: t('请输入生成数量') },
|
|
{
|
|
validator: (rule, v) => {
|
|
const num = parseInt(v, 10);
|
|
return num > 0
|
|
? Promise.resolve()
|
|
: Promise.reject(t('生成数量必须大于0'));
|
|
},
|
|
},
|
|
]}
|
|
style={{ width: '100%' }}
|
|
showClear
|
|
/>
|
|
</Col>
|
|
)}
|
|
</Row>
|
|
</Card>
|
|
</div>
|
|
)}
|
|
</Form>
|
|
</Spin>
|
|
</SideSheet>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default EditRedemption;
|