| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589 |
- /*
- 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 React, { useState, useEffect } from 'react';
- import { useTranslation } from 'react-i18next';
- import {
- Modal,
- Button,
- Table,
- Tag,
- Typography,
- Space,
- Tooltip,
- Popconfirm,
- Empty,
- Spin,
- Select,
- Row,
- Col,
- Badge,
- Progress,
- Card
- } from '@douyinfe/semi-ui';
- import { IllustrationNoResult, IllustrationNoResultDark } from '@douyinfe/semi-illustrations';
- import { API, showError, showSuccess, timestamp2string } from '../../../../helpers';
- const { Text } = Typography;
- const MultiKeyManageModal = ({
- visible,
- onCancel,
- channel,
- onRefresh
- }) => {
- const { t } = useTranslation();
- const [loading, setLoading] = useState(false);
- const [keyStatusList, setKeyStatusList] = useState([]);
- const [operationLoading, setOperationLoading] = useState({});
- // Pagination states
- const [currentPage, setCurrentPage] = useState(1);
- const [pageSize, setPageSize] = useState(10);
- const [total, setTotal] = useState(0);
- const [totalPages, setTotalPages] = useState(0);
- // Statistics states
- const [enabledCount, setEnabledCount] = useState(0);
- const [manualDisabledCount, setManualDisabledCount] = useState(0);
- const [autoDisabledCount, setAutoDisabledCount] = useState(0);
- // Filter states
- const [statusFilter, setStatusFilter] = useState(null); // null=all, 1=enabled, 2=manual_disabled, 3=auto_disabled
- // Load key status data
- const loadKeyStatus = async (page = currentPage, size = pageSize, status = statusFilter) => {
- if (!channel?.id) return;
- setLoading(true);
- try {
- const requestData = {
- channel_id: channel.id,
- action: 'get_key_status',
- page: page,
- page_size: size
- };
- // Add status filter if specified
- if (status !== null) {
- requestData.status = status;
- }
- const res = await API.post('/api/channel/multi_key/manage', requestData);
- if (res.data.success) {
- const data = res.data.data;
- setKeyStatusList(data.keys || []);
- setTotal(data.total || 0);
- setCurrentPage(data.page || 1);
- setPageSize(data.page_size || 10);
- setTotalPages(data.total_pages || 0);
- // Update statistics (these are always the overall statistics)
- setEnabledCount(data.enabled_count || 0);
- setManualDisabledCount(data.manual_disabled_count || 0);
- setAutoDisabledCount(data.auto_disabled_count || 0);
- } else {
- showError(res.data.message);
- }
- } catch (error) {
- console.error(error);
- showError(t('获取密钥状态失败'));
- } finally {
- setLoading(false);
- }
- };
- // Disable a specific key
- const handleDisableKey = async (keyIndex) => {
- const operationId = `disable_${keyIndex}`;
- setOperationLoading(prev => ({ ...prev, [operationId]: true }));
- try {
- const res = await API.post('/api/channel/multi_key/manage', {
- channel_id: channel.id,
- action: 'disable_key',
- key_index: keyIndex
- });
- if (res.data.success) {
- showSuccess(t('密钥已禁用'));
- await loadKeyStatus(currentPage, pageSize); // Reload current page
- onRefresh && onRefresh(); // Refresh parent component
- } else {
- showError(res.data.message);
- }
- } catch (error) {
- showError(t('禁用密钥失败'));
- } finally {
- setOperationLoading(prev => ({ ...prev, [operationId]: false }));
- }
- };
- // Enable a specific key
- const handleEnableKey = async (keyIndex) => {
- const operationId = `enable_${keyIndex}`;
- setOperationLoading(prev => ({ ...prev, [operationId]: true }));
- try {
- const res = await API.post('/api/channel/multi_key/manage', {
- channel_id: channel.id,
- action: 'enable_key',
- key_index: keyIndex
- });
- if (res.data.success) {
- showSuccess(t('密钥已启用'));
- await loadKeyStatus(currentPage, pageSize); // Reload current page
- onRefresh && onRefresh(); // Refresh parent component
- } else {
- showError(res.data.message);
- }
- } catch (error) {
- showError(t('启用密钥失败'));
- } finally {
- setOperationLoading(prev => ({ ...prev, [operationId]: false }));
- }
- };
- // Enable all disabled keys
- const handleEnableAll = async () => {
- setOperationLoading(prev => ({ ...prev, enable_all: true }));
- try {
- const res = await API.post('/api/channel/multi_key/manage', {
- channel_id: channel.id,
- action: 'enable_all_keys'
- });
- if (res.data.success) {
- showSuccess(res.data.message || t('已启用所有密钥'));
- // Reset to first page after bulk operation
- setCurrentPage(1);
- await loadKeyStatus(1, pageSize);
- onRefresh && onRefresh(); // Refresh parent component
- } else {
- showError(res.data.message);
- }
- } catch (error) {
- showError(t('启用所有密钥失败'));
- } finally {
- setOperationLoading(prev => ({ ...prev, enable_all: false }));
- }
- };
- // Disable all enabled keys
- const handleDisableAll = async () => {
- setOperationLoading(prev => ({ ...prev, disable_all: true }));
- try {
- const res = await API.post('/api/channel/multi_key/manage', {
- channel_id: channel.id,
- action: 'disable_all_keys'
- });
- if (res.data.success) {
- showSuccess(res.data.message || t('已禁用所有密钥'));
- // Reset to first page after bulk operation
- setCurrentPage(1);
- await loadKeyStatus(1, pageSize);
- onRefresh && onRefresh(); // Refresh parent component
- } else {
- showError(res.data.message);
- }
- } catch (error) {
- showError(t('禁用所有密钥失败'));
- } finally {
- setOperationLoading(prev => ({ ...prev, disable_all: false }));
- }
- };
- // Delete all disabled keys
- const handleDeleteDisabledKeys = async () => {
- setOperationLoading(prev => ({ ...prev, delete_disabled: true }));
- try {
- const res = await API.post('/api/channel/multi_key/manage', {
- channel_id: channel.id,
- action: 'delete_disabled_keys'
- });
- if (res.data.success) {
- showSuccess(res.data.message);
- // Reset to first page after deletion as data structure might change
- setCurrentPage(1);
- await loadKeyStatus(1, pageSize);
- onRefresh && onRefresh(); // Refresh parent component
- } else {
- showError(res.data.message);
- }
- } catch (error) {
- showError(t('删除禁用密钥失败'));
- } finally {
- setOperationLoading(prev => ({ ...prev, delete_disabled: false }));
- }
- };
- // Handle page change
- const handlePageChange = (page) => {
- setCurrentPage(page);
- loadKeyStatus(page, pageSize);
- };
- // Handle page size change
- const handlePageSizeChange = (size) => {
- setPageSize(size);
- setCurrentPage(1); // Reset to first page
- loadKeyStatus(1, size);
- };
- // Handle status filter change
- const handleStatusFilterChange = (status) => {
- setStatusFilter(status);
- setCurrentPage(1); // Reset to first page when filter changes
- loadKeyStatus(1, pageSize, status);
- };
- // Effect to load data when modal opens
- useEffect(() => {
- if (visible && channel?.id) {
- setCurrentPage(1); // Reset to first page when opening
- loadKeyStatus(1, pageSize);
- }
- }, [visible, channel?.id]);
- // Reset pagination when modal closes
- useEffect(() => {
- if (!visible) {
- setCurrentPage(1);
- setKeyStatusList([]);
- setTotal(0);
- setTotalPages(0);
- setEnabledCount(0);
- setManualDisabledCount(0);
- setAutoDisabledCount(0);
- setStatusFilter(null); // Reset filter
- }
- }, [visible]);
- // Percentages for progress display
- const enabledPercent = total > 0 ? Math.round((enabledCount / total) * 100) : 0;
- const manualDisabledPercent = total > 0 ? Math.round((manualDisabledCount / total) * 100) : 0;
- const autoDisabledPercent = total > 0 ? Math.round((autoDisabledCount / total) * 100) : 0;
- // 取消饼图:不再需要图表数据与配置
- // Get status tag component
- const renderStatusTag = (status) => {
- switch (status) {
- case 1:
- return <Tag color='green' shape='circle' size='small'>{t('已启用')}</Tag>;
- case 2:
- return <Tag color='red' shape='circle' size='small'>{t('已禁用')}</Tag>;
- case 3:
- return <Tag color='orange' shape='circle' size='small'>{t('自动禁用')}</Tag>;
- default:
- return <Tag color='grey' shape='circle' size='small'>{t('未知状态')}</Tag>;
- }
- };
- // Table columns definition
- const columns = [
- {
- title: t('索引'),
- dataIndex: 'index',
- render: (text) => `#${text}`,
- },
- // {
- // title: t('密钥预览'),
- // dataIndex: 'key_preview',
- // render: (text) => (
- // <Text code style={{ fontSize: '12px' }}>
- // {text}
- // </Text>
- // ),
- // },
- {
- title: t('状态'),
- dataIndex: 'status',
- render: (status) => renderStatusTag(status),
- },
- {
- title: t('禁用原因'),
- dataIndex: 'reason',
- render: (reason, record) => {
- if (record.status === 1 || !reason) {
- return <Text type='quaternary'>-</Text>;
- }
- return (
- <Tooltip content={reason}>
- <Text style={{ maxWidth: '200px', display: 'block' }} ellipsis>
- {reason}
- </Text>
- </Tooltip>
- );
- },
- },
- {
- title: t('禁用时间'),
- dataIndex: 'disabled_time',
- render: (time, record) => {
- if (record.status === 1 || !time) {
- return <Text type='quaternary'>-</Text>;
- }
- return (
- <Tooltip content={timestamp2string(time)}>
- <Text style={{ fontSize: '12px' }}>
- {timestamp2string(time)}
- </Text>
- </Tooltip>
- );
- },
- },
- {
- title: t('操作'),
- key: 'action',
- fixed: 'right',
- width: 100,
- render: (_, record) => (
- <Space>
- {record.status === 1 ? (
- <Button
- type='danger'
- size='small'
- loading={operationLoading[`disable_${record.index}`]}
- onClick={() => handleDisableKey(record.index)}
- >
- {t('禁用')}
- </Button>
- ) : (
- <Button
- type='primary'
- size='small'
- loading={operationLoading[`enable_${record.index}`]}
- onClick={() => handleEnableKey(record.index)}
- >
- {t('启用')}
- </Button>
- )}
- </Space>
- ),
- },
- ];
- return (
- <Modal
- title={
- <Space>
- <Text>{t('多密钥管理')}</Text>
- {channel?.name && (
- <Tag size='small' shape='circle' color='white'>{channel.name}</Tag>
- )}
- <Tag size='small' shape='circle' color='white'>
- {t('总密钥数')}: {total}
- </Tag>
- {channel?.channel_info?.multi_key_mode && (
- <Tag size='small' shape='circle' color='white'>
- {channel.channel_info.multi_key_mode === 'random' ? t('随机模式') : t('轮询模式')}
- </Tag>
- )}
- </Space>
- }
- visible={visible}
- onCancel={onCancel}
- width={900}
- footer={null}
- >
- <div className="flex flex-col mb-5">
- {/* Stats & Mode */}
- <div
- className="rounded-xl p-4 mb-3"
- style={{
- background: 'var(--semi-color-bg-1)',
- border: '1px solid var(--semi-color-border)'
- }}
- >
- <Row gutter={16} align="middle">
- <Col span={8}>
- <div style={{ background: 'var(--semi-color-bg-0)', border: '1px solid var(--semi-color-border)', borderRadius: 12, padding: 12 }}>
- <div className="flex items-center gap-2 mb-2">
- <Badge dot type='success' />
- <Text type='tertiary'>{t('已启用')}</Text>
- </div>
- <div className="flex items-end gap-2 mb-2">
- <Text style={{ fontSize: 18, fontWeight: 700, color: '#22c55e' }}>{enabledCount}</Text>
- <Text style={{ fontSize: 18, color: 'var(--semi-color-text-2)' }}>/ {total}</Text>
- </div>
- <Progress percent={enabledPercent} showInfo={false} size="small" stroke="#22c55e" style={{ height: 6, borderRadius: 999 }} />
- </div>
- </Col>
- <Col span={8}>
- <div style={{ background: 'var(--semi-color-bg-0)', border: '1px solid var(--semi-color-border)', borderRadius: 12, padding: 12 }}>
- <div className="flex items-center gap-2 mb-2">
- <Badge dot type='danger' />
- <Text type='tertiary'>{t('手动禁用')}</Text>
- </div>
- <div className="flex items-end gap-2 mb-2">
- <Text style={{ fontSize: 18, fontWeight: 700, color: '#ef4444' }}>{manualDisabledCount}</Text>
- <Text style={{ fontSize: 18, color: 'var(--semi-color-text-2)' }}>/ {total}</Text>
- </div>
- <Progress percent={manualDisabledPercent} showInfo={false} size="small" stroke="#ef4444" style={{ height: 6, borderRadius: 999 }} />
- </div>
- </Col>
- <Col span={8}>
- <div style={{ background: 'var(--semi-color-bg-0)', border: '1px solid var(--semi-color-border)', borderRadius: 12, padding: 12 }}>
- <div className="flex items-center gap-2 mb-2">
- <Badge dot type='warning' />
- <Text type='tertiary'>{t('自动禁用')}</Text>
- </div>
- <div className="flex items-end gap-2 mb-2">
- <Text style={{ fontSize: 18, fontWeight: 700, color: '#f59e0b' }}>{autoDisabledCount}</Text>
- <Text style={{ fontSize: 18, color: 'var(--semi-color-text-2)' }}>/ {total}</Text>
- </div>
- <Progress percent={autoDisabledPercent} showInfo={false} size="small" stroke="#f59e0b" style={{ height: 6, borderRadius: 999 }} />
- </div>
- </Col>
- </Row>
- </div>
- {/* Table */}
- <div className="flex-1 flex flex-col min-h-0">
- <Spin spinning={loading}>
- <Card className='!rounded-xl'>
- <Table
- title={() => (
- <Row gutter={12} style={{ width: '100%' }}>
- <Col span={14}>
- <Row gutter={12} style={{ alignItems: 'center' }}>
- <Col>
- <Select
- value={statusFilter}
- onChange={handleStatusFilterChange}
- size='small'
- placeholder={t('全部状态')}
- >
- <Select.Option value={null}>{t('全部状态')}</Select.Option>
- <Select.Option value={1}>{t('已启用')}</Select.Option>
- <Select.Option value={2}>{t('手动禁用')}</Select.Option>
- <Select.Option value={3}>{t('自动禁用')}</Select.Option>
- </Select>
- </Col>
- </Row>
- </Col>
- <Col span={10} style={{ display: 'flex', justifyContent: 'flex-end' }}>
- <Space>
- <Button
- size='small'
- type='tertiary'
- onClick={() => loadKeyStatus(currentPage, pageSize)}
- loading={loading}
- >
- {t('刷新')}
- </Button>
- {(manualDisabledCount + autoDisabledCount) > 0 && (
- <Popconfirm
- title={t('确定要启用所有密钥吗?')}
- onConfirm={handleEnableAll}
- position={'topRight'}
- >
- <Button
- size='small'
- type='primary'
- loading={operationLoading.enable_all}
- >
- {t('启用全部')}
- </Button>
- </Popconfirm>
- )}
- {enabledCount > 0 && (
- <Popconfirm
- title={t('确定要禁用所有的密钥吗?')}
- onConfirm={handleDisableAll}
- okType={'danger'}
- position={'topRight'}
- >
- <Button
- size='small'
- type='danger'
- loading={operationLoading.disable_all}
- >
- {t('禁用全部')}
- </Button>
- </Popconfirm>
- )}
- <Popconfirm
- title={t('确定要删除所有已自动禁用的密钥吗?')}
- content={t('此操作不可撤销,将永久删除已自动禁用的密钥')}
- onConfirm={handleDeleteDisabledKeys}
- okType={'danger'}
- position={'topRight'}
- >
- <Button
- size='small'
- type='warning'
- loading={operationLoading.delete_disabled}
- >
- {t('删除自动禁用密钥')}
- </Button>
- </Popconfirm>
- </Space>
- </Col>
- </Row>
- )}
- columns={columns}
- dataSource={keyStatusList}
- pagination={{
- currentPage: currentPage,
- pageSize: pageSize,
- total: total,
- showSizeChanger: true,
- showQuickJumper: true,
- pageSizeOpts: [10, 20, 50, 100],
- onChange: (page, size) => {
- setCurrentPage(page);
- loadKeyStatus(page, size);
- },
- onShowSizeChange: (current, size) => {
- setCurrentPage(1);
- handlePageSizeChange(size);
- }
- }}
- size='small'
- bordered={false}
- rowKey='index'
- scroll={{ x: 'max-content' }}
- empty={
- <Empty
- image={<IllustrationNoResult style={{ width: 140, height: 140 }} />}
- darkModeImage={<IllustrationNoResultDark style={{ width: 140, height: 140 }} />}
- title={t('暂无密钥数据')}
- description={t('请检查渠道配置或刷新重试')}
- style={{ padding: 30 }}
- />
- }
- />
- </Card>
- </Spin>
- </div>
- </div>
- </Modal>
- );
- };
- export default MultiKeyManageModal;
|