Files
video-gen/video-gen-admin/src/pages/AdminConsume.tsx
T
2026-06-25 17:07:32 +08:00

339 lines
13 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { Card, Space, Table, Button, Typography, Modal, Checkbox, Input, message, DatePicker } from 'antd';
import { ArrowLeftOutlined, DollarOutlined, SettingOutlined, SearchOutlined, ReloadOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import { getFields, getMaterialConsumpList } from '../api';
import dayjs from 'dayjs';
const { RangePicker } = DatePicker;
const toCamelCase = (str: string) => str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
interface ConsumptionRecord {
id: string;
advertiser_id?: string;
user_id?: string;
consume_date?: string;
[key: string]: any;
}
const ConsumePage: React.FC = () => {
const navigate = useNavigate();
const [consumptionRecords, setConsumptionRecords] = useState<ConsumptionRecord[]>([]);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false);
const [showModal, setShowModal] = useState(false);
const [columnsData, setColumnsData] = useState<{ name: string; description: string }[]>([]);
const [selectedColumns, setSelectedColumns] = useState<string[]>([]);
const [searchText, setSearchText] = useState('');
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [advertiserId, setAdvertiserId] = useState('');
const [userId, setUserId] = useState('');
const [consumeDateRange, setConsumeDateRange] = useState<[string, string] | undefined>();
useEffect(() => {
loadColumns();
loadData();
}, [page, pageSize, consumeDateRange]);
const loadData = async () => {
setLoading(true);
console.log(consumeDateRange);
try {
const res = await getMaterialConsumpList({
advertiser_id: advertiserId || undefined,
user_id: userId || undefined,
consume_date: consumeDateRange,
page,
page_size: pageSize,
});
setConsumptionRecords(res.data || []);
setTotal(res.pagination?.total || 0);
} catch (e: any) {
message.error('加载数据失败');
} finally {
setLoading(false);
}
};
const loadColumns = async () => {
try {
const res = await getFields();
const fields = res.data || [];
const formattedFields = fields.map((item: { field: string; description: string }) => ({
name: toCamelCase(item.field),
description: item.description,
}));
setColumnsData(formattedFields);
const saved = localStorage.getItem('consumeColumns');
if (saved) {
setSelectedColumns(JSON.parse(saved));
} else {
setSelectedColumns(formattedFields.map((item: { name: string }) => item.name));
}
} catch (e: any) {
message.error('加载表头字段失败');
}
};
const tableData = consumptionRecords.map((item, index) => ({
...item,
index: index + 1,
key: item.id,
}));
const handleBack = () => {
navigate('/authorization');
};
const handleSearch = () => {
setPage(1);
loadData();
};
const handleReset = () => {
setAdvertiserId('');
setUserId('');
setConsumeDateRange(undefined);
setPage(1);
loadData();
};
const dynamicColumns = selectedColumns
.map(colName => columnsData.find(col => col.name === colName))
.filter((col): col is { name: string; description: string } => !!col)
.map(col => ({
title: col.description,
dataIndex: col.name,
key: col.name,
ellipsis: true,
}));
return (
<div style={{ minHeight: '94vh' }}>
<Card variant="outlined" style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
<Space>
{/*<Button type="text" icon={<ArrowLeftOutlined />} onClick={handleBack} />
<Button icon={<ArrowLeftOutlined />} onClick={handleBack}>返回</Button> */}
<DollarOutlined style={{ fontSize: 18, color: '#6366f1' }} />
<Typography.Text strong style={{ fontSize: 16 }}>消耗记录</Typography.Text>
</Space>
<Space>
<Button icon={<SettingOutlined />} onClick={() => setShowModal(true)}>自定义表头</Button>
</Space>
</div>
<div style={{ marginBottom: 16 }}>
<Space wrap>
<Input
placeholder="广告主ID"
value={advertiserId}
onChange={(e) => setAdvertiserId(e.target.value)}
style={{ width: 200 }}
/>
<Input
placeholder="用户ID"
value={userId}
onChange={(e) => setUserId(e.target.value)}
style={{ width: 200 }}
/>
<RangePicker
value={consumeDateRange ? [dayjs(consumeDateRange[0]), dayjs(consumeDateRange[1])] : undefined}
onChange={(dates) => {
if (dates && dates[0] && dates[1]) {
setConsumeDateRange([dates[0].format('YYYY-MM-DD'), dates[1].format('YYYY-MM-DD')]);
} else {
setConsumeDateRange(undefined);
}
}}
/>
<Button type="primary" icon={<SearchOutlined />} onClick={handleSearch}>搜索</Button>
<Button icon={<ReloadOutlined />} onClick={handleReset}>重置</Button>
</Space>
</div>
<Table
dataSource={tableData}
columns={dynamicColumns}
loading={loading}
rowKey="id"
bordered={false}
scroll={{ x: 'max-content' }}
pagination={{
current: page,
pageSize,
total,
showSizeChanger: true,
showTotal: (t) => `共 ${t} 条记录`,
size: 'small',
onChange: (p, ps) => {
setPage(p);
setPageSize(ps);
},
}}
/>
</Card>
<Modal
title="自定义表头"
open={showModal}
onOk={() => {
localStorage.setItem('consumeColumns', JSON.stringify(selectedColumns));
message.success('表头设置已保存');
setShowModal(false);
}}
onCancel={() => {
setShowModal(false);
setSearchText('');
}}
okText="确定"
cancelText="取消"
width={800}
>
<div style={{ display: 'flex', gap: 20, height: 400 }}>
<div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
<Input
placeholder="搜索指标..."
value={searchText}
allowClear={true}
onChange={(e) => setSearchText(e.target.value)}
style={{ marginBottom: 8 }}
/>
<Typography.Text type="secondary" style={{ fontSize: 12, marginBottom: 12, display: 'block' }}>
输入指标名称进行搜索
</Typography.Text>
<div style={{ flex: 1, overflowY: 'auto', border: '1px solid #f0f0f5', borderRadius: 8, padding: 12 }}>
<div style={{ marginBottom: 16 }}>
<Typography.Text strong style={{ fontSize: 14, color: '#1e293b' }}>任务详情指标</Typography.Text>
</div>
<Checkbox
checked={selectedColumns.length === columnsData.length && columnsData.length > 0}
indeterminate={selectedColumns.length > 0 && selectedColumns.length < columnsData.length}
onChange={(e) => {
if (e.target.checked) {
setSelectedColumns(columnsData.map(item => item.name));
} else {
setSelectedColumns([]);
}
}}
style={{ marginBottom: 12 }}
>
全选
</Checkbox>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
{columnsData
.filter(item =>
item.description.toLowerCase().includes(searchText.toLowerCase()) ||
item.name.toLowerCase().includes(searchText.toLowerCase())
)
.map(item => (
<Checkbox
key={item.name}
checked={selectedColumns.includes(item.name)}
onChange={(e) => {
if (e.target.checked) {
setSelectedColumns(prev => [...prev, item.name]);
} else {
setSelectedColumns(prev => prev.filter(col => col !== item.name));
}
}}
>
{item.description}
</Checkbox>
))}
</div>
</div>
</div>
<div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
<Typography.Text strong style={{ fontSize: 14, color: '#1e293b' }}>
已添加({selectedColumns.length})
</Typography.Text>
<Space>
<Button
size="small"
onClick={() => setSelectedColumns([])}
disabled={selectedColumns.length === 0}
>
清空
</Button>
<Button
size="small"
onClick={async () => {
try {
const res = await getFields();
const fields = res.data || [];
const formattedFields = fields.map((item: { field: string; description: string }) => ({
name: toCamelCase(item.field),
description: item.description,
}));
setColumnsData(formattedFields);
setSelectedColumns(formattedFields.map((item: { name: string }) => item.name));
localStorage.removeItem('consumeColumns');
message.success('表头字段已更新');
} catch (e: any) {
message.error('更新表头字段失败');
}
}}
>
更新表头字段
</Button>
</Space>
</div>
<div style={{ flex: 1, overflowY: 'auto', border: '1px solid #f0f0f5', borderRadius: 8, padding: 12 }}>
{selectedColumns.length === 0 ? (
<Typography.Text type="secondary" style={{ fontSize: 14, textAlign: 'center', display: 'block', padding: '40px 0' }}>
暂无已选指标
</Typography.Text>
) : (
<div>
{selectedColumns.map((colName, index) => {
const col = columnsData.find(c => c.name === colName);
return (
<div
key={colName}
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '8px 12px',
borderBottom: '1px solid #f0f0f5',
cursor: 'move',
userSelect: 'none',
}}
draggable
onDragStart={(e) => {
e.dataTransfer.setData('index', String(index));
}}
onDragOver={(e) => {
e.preventDefault();
}}
onDrop={(e) => {
const fromIndex = parseInt(e.dataTransfer.getData('index'));
const toIndex = index;
if (fromIndex !== toIndex) {
const newSelected = [...selectedColumns];
const [removed] = newSelected.splice(fromIndex, 1);
newSelected.splice(toIndex, 0, removed);
setSelectedColumns(newSelected);
}
}}
>
<Typography.Text>{col?.description || colName}</Typography.Text>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{index + 1}
</Typography.Text>
</div>
);
})}
</div>
)}
</div>
</div>
</div>
</Modal>
</div>
);
};
export default ConsumePage;