修改素材云批量上传
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
#VITE_API_BASE=http://localhost:8000
|
VITE_API_BASE=http://192.168.120.17:8000
|
||||||
VITE_API_BASE=http://ceshi.apiforeign.minzhong.cn
|
# VITE_API_BASE=http://ceshi.apiforeign.minzhong.cn
|
||||||
VITE_USE_MOCK=false
|
VITE_USE_MOCK=false
|
||||||
# Encryption disabled for dev — enable in production
|
# Encryption disabled for dev — enable in production
|
||||||
VITE_ENCRYPTION_KEY=
|
VITE_ENCRYPTION_KEY=
|
||||||
@@ -3,8 +3,11 @@ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
|||||||
import { ConfigProvider, App as AntApp, Spin } from 'antd';
|
import { ConfigProvider, App as AntApp, Spin } from 'antd';
|
||||||
import zhCN from 'antd/locale/zh_CN';
|
import zhCN from 'antd/locale/zh_CN';
|
||||||
import AdminLayout from './pages/AdminLayout';
|
import AdminLayout from './pages/AdminLayout';
|
||||||
|
import AdminAuthoriz from './pages/AdminAuthoriz';
|
||||||
|
import AdminConsume from './pages/AdminConsume';
|
||||||
import AdminLoginPage from './pages/AdminLoginPage';
|
import AdminLoginPage from './pages/AdminLoginPage';
|
||||||
import AdminDashboard from './pages/AdminDashboard';
|
import AdminDashboard from './pages/AdminDashboard';
|
||||||
|
import AdminPlatform from './pages/AdminPlatform';
|
||||||
import AdminUsers from './pages/AdminUsers';
|
import AdminUsers from './pages/AdminUsers';
|
||||||
import AdminModels from './pages/AdminModels';
|
import AdminModels from './pages/AdminModels';
|
||||||
import AdminSettings from './pages/AdminSettings';
|
import AdminSettings from './pages/AdminSettings';
|
||||||
@@ -28,6 +31,7 @@ import AdminShotReplications from './pages/AdminShotReplications';
|
|||||||
import AdminShotTaskSetDetail from './pages/AdminShotTaskSetDetail';
|
import AdminShotTaskSetDetail from './pages/AdminShotTaskSetDetail';
|
||||||
import AdminReplicationProjectDetail from './pages/AdminReplicationProjectDetail';
|
import AdminReplicationProjectDetail from './pages/AdminReplicationProjectDetail';
|
||||||
import AdminVideoPromptSchemaConfig from './pages/AdminVideoPromptSchemaConfig';
|
import AdminVideoPromptSchemaConfig from './pages/AdminVideoPromptSchemaConfig';
|
||||||
|
|
||||||
import { useAdminStore } from './store';
|
import { useAdminStore } from './store';
|
||||||
|
|
||||||
const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
|
const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
|
||||||
@@ -96,6 +100,9 @@ const App = () => {
|
|||||||
<Route path="shot-replications" element={<AdminShotReplications />} />
|
<Route path="shot-replications" element={<AdminShotReplications />} />
|
||||||
<Route path="shot-replications/task-sets/:taskSetId" element={<AdminShotTaskSetDetail />} />
|
<Route path="shot-replications/task-sets/:taskSetId" element={<AdminShotTaskSetDetail />} />
|
||||||
<Route path="shot-replications/projects/:projectId" element={<AdminReplicationProjectDetail moduleType="shot_replicate" />} />
|
<Route path="shot-replications/projects/:projectId" element={<AdminReplicationProjectDetail moduleType="shot_replicate" />} />
|
||||||
|
<Route path="authoriza" element={<AdminAuthoriz />} />
|
||||||
|
<Route path="consume" element={<AdminConsume />} />
|
||||||
|
<Route path="platform" element={<AdminPlatform />} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@@ -540,3 +540,29 @@ export async function importVideoPromptSchemaConfig(payload: VideoPromptSchemaCo
|
|||||||
export async function previewVideoPromptSchemaConfig(payload: VideoPromptSchemaPreviewPayload): Promise<VideoPromptSchemaPreviewOut> {
|
export async function previewVideoPromptSchemaConfig(payload: VideoPromptSchemaPreviewPayload): Promise<VideoPromptSchemaPreviewOut> {
|
||||||
return api.post<VideoPromptSchemaPreviewOut>('/admin/video-prompt-schema-config/preview', payload);
|
return api.post<VideoPromptSchemaPreviewOut>('/admin/video-prompt-schema-config/preview', payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取授权链接
|
||||||
|
export interface RequestOAuthParams {
|
||||||
|
open_type: number;
|
||||||
|
}
|
||||||
|
export async function requestOAuth(params: RequestOAuthParams): Promise<any> {
|
||||||
|
return api.post(`/user-oauth/request_oauth`, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 授权列表
|
||||||
|
export interface OAuthListParams {
|
||||||
|
account_userid?: string;
|
||||||
|
open_type?: number;
|
||||||
|
account_id?: string;
|
||||||
|
page?: number;
|
||||||
|
page_size?: number;
|
||||||
|
}
|
||||||
|
export async function getOAuthList(params: OAuthListParams): Promise<any> {
|
||||||
|
const query = new URLSearchParams();
|
||||||
|
if (params.account_userid) query.set('account_userid', params.account_userid);
|
||||||
|
if (params.open_type !== undefined) query.set('open_type', String(params.open_type));
|
||||||
|
if (params.account_id) query.set('account_id', params.account_id);
|
||||||
|
if (params.page !== undefined) query.set('page', String(params.page));
|
||||||
|
if (params.page_size !== undefined) query.set('page_size', String(params.page_size));
|
||||||
|
return api.get(`/user-oauth/oauth_list?${query.toString()}`);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,362 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { Button, Card, Space, Table, Tag, Modal, Select, App, Input, Typography } from 'antd';
|
||||||
|
import { PlusOutlined, LockOutlined } from '@ant-design/icons';
|
||||||
|
import { getOAuthList, requestOAuth } from '../api';
|
||||||
|
|
||||||
|
const OPEN_TYPE_MAP: Record<number, string> = {
|
||||||
|
1: '千川',
|
||||||
|
2: '广告',
|
||||||
|
3: '本地推',
|
||||||
|
4: '星图',
|
||||||
|
5: '快手代理商',
|
||||||
|
6: '巨量星图',
|
||||||
|
7: '巨量服务单',
|
||||||
|
8: '腾讯服务单',
|
||||||
|
9: '腾讯营销K2',
|
||||||
|
10: '腾讯营销K3',
|
||||||
|
};
|
||||||
|
|
||||||
|
const PORT_TYPE_MAP: Record<number, string> = {
|
||||||
|
1: '巨量',
|
||||||
|
2: '磁力',
|
||||||
|
3: '巨量星图',
|
||||||
|
4: '服务单',
|
||||||
|
5: '腾讯',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 格式化时间 2026-06-12T03:47:28.542988Z -> 2026-06-12 03:47:28
|
||||||
|
const formatDateTime = (dateStr: string) => {
|
||||||
|
if (!dateStr) return '';
|
||||||
|
const date = new Date(dateStr);
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
const hours = String(date.getHours()).padStart(2, '0');
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||||
|
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface AuthorizationData {
|
||||||
|
id: string;
|
||||||
|
status: string;
|
||||||
|
description: string;
|
||||||
|
account_userid?: string;
|
||||||
|
open_type?: number;
|
||||||
|
account_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AuthorizationPage: React.FC = () => {
|
||||||
|
const { message } = App.useApp();
|
||||||
|
const [authorizations, setAuthorizations] = useState<AuthorizationData[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [listLoading, setListLoading] = useState(false);
|
||||||
|
const [showModal, setShowModal] = useState(false);
|
||||||
|
const [selectedOpenType, setSelectedOpenType] = useState<number | undefined>(undefined);
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [pageSize, setPageSize] = useState(10);
|
||||||
|
const [total, setTotal] = useState(0);
|
||||||
|
const [searchParams, setSearchParams] = useState({
|
||||||
|
account_userid: '',
|
||||||
|
open_type: undefined as number | undefined,
|
||||||
|
account_id: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadOAuthList();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadOAuthList = async (page = 1, pageSize = 10, params = searchParams) => {
|
||||||
|
setListLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await getOAuthList({
|
||||||
|
page,
|
||||||
|
page_size: pageSize,
|
||||||
|
account_userid: params.account_userid || undefined,
|
||||||
|
open_type: params.open_type,
|
||||||
|
account_id: params.account_id || undefined,
|
||||||
|
});
|
||||||
|
if (response) {
|
||||||
|
if (response.data) {
|
||||||
|
setAuthorizations(response.data.data || response.data);
|
||||||
|
}
|
||||||
|
if (response.pagination) {
|
||||||
|
setTotal(response.pagination.total || 0);
|
||||||
|
setCurrentPage(response.pagination.page || 1);
|
||||||
|
setPageSize(response.pagination.pageSize || 10);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setAuthorizations(response || []);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('获取授权列表失败');
|
||||||
|
} finally {
|
||||||
|
setListLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAuthorize = () => {
|
||||||
|
setShowModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirm = async () => {
|
||||||
|
if (!selectedOpenType) {
|
||||||
|
message.warning('请选择开户方式');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await requestOAuth({ open_type: selectedOpenType });
|
||||||
|
if (response.authUrl) {
|
||||||
|
window.open(response.authUrl, '_blank');
|
||||||
|
} else {
|
||||||
|
message.error('获取授权链接失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('请求授权失败');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
setShowModal(false);
|
||||||
|
setSelectedOpenType(undefined);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: 'ID',
|
||||||
|
dataIndex: 'id',
|
||||||
|
key: 'id',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '授权账户ID',
|
||||||
|
dataIndex: 'accountId',
|
||||||
|
key: 'accountId',
|
||||||
|
width: 160,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '授权账户名称',
|
||||||
|
dataIndex: 'accountName',
|
||||||
|
key: 'accountName',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '授权账户角色',
|
||||||
|
dataIndex: 'accountRole',
|
||||||
|
key: 'accountRole',
|
||||||
|
render: (role: string) => {
|
||||||
|
const roleMap: Record<string, string> = {
|
||||||
|
ADVERTISER: '客户',
|
||||||
|
CUSTOMER_ADMIN: '普通版工作台-管理员',
|
||||||
|
CUSTOMER_OPERATOR: '普通版工作台-协作者',
|
||||||
|
AGENT: '代理商',
|
||||||
|
CHILD_AGENT: '二级代理商',
|
||||||
|
PLATFORM_ROLE_STAR: '星图账户',
|
||||||
|
PLATFORM_ROLE_SHOP_ACCOUNT: '抖音店铺账户',
|
||||||
|
PLATFORM_ROLE_QIANCHUAN_AGENT: '千川代理商',
|
||||||
|
PLATFORM_ROLE_STAR_AGENT: '星图代理商',
|
||||||
|
PLATFORM_ROLE_AWEME: '抖音号',
|
||||||
|
PLATFORM_ROLE_STAR_MCN: '星图MCN机构',
|
||||||
|
PLATFORM_ROLE_STAR_ISV: '星图服务商',
|
||||||
|
AGENT_SYSTEM_ACCOUNT: '代理商系统账户',
|
||||||
|
PLATFORM_ROLE_LOCAL_AGENT: '本地推代理商',
|
||||||
|
PLATFORM_ROLE_YUNTU_BRAND_ISV_ADMIN: '云图品牌服务商管理员',
|
||||||
|
PLATFORM_ROLE_LIFE: '抖音来客账户',
|
||||||
|
PLATFORM_ROLE_ENTERPRISE_BP_ADMIN: '升级版工作台管理员',
|
||||||
|
PLATFORM_ROLE_ENTERPRISE_BP_OPERATOR: '升级版工作台协作者',
|
||||||
|
};
|
||||||
|
return roleMap[role] || role;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '授权账户用户ID',
|
||||||
|
dataIndex: 'accountUserid',
|
||||||
|
key: 'accountUserid',
|
||||||
|
render: (text: string) => <span style={{ color: text ? '#1e293b' : '#94a3b8' }}>{text || '-'}</span>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '授权账户用户名',
|
||||||
|
dataIndex: 'accountUsername',
|
||||||
|
key: 'accountUsername',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '授权应用ID',
|
||||||
|
dataIndex: 'appid',
|
||||||
|
key: 'appid',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '是否敏感物料授权',
|
||||||
|
dataIndex: 'materialAuthStatus',
|
||||||
|
key: 'materialAuthStatus',
|
||||||
|
width: 100,
|
||||||
|
render: (text: boolean) => (
|
||||||
|
<Tag color={text ? 'green' : 'default'}>
|
||||||
|
{text ? '是' : '否'}
|
||||||
|
</Tag>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '开户方式',
|
||||||
|
dataIndex: 'openType',
|
||||||
|
key: 'openType',
|
||||||
|
render: (text: number) => <span style={{ color: '#1e293b' }}>{OPEN_TYPE_MAP[text] || text}</span>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '平台端口',
|
||||||
|
dataIndex: 'portType',
|
||||||
|
key: 'portType',
|
||||||
|
ellipsis: true,
|
||||||
|
render: (text: number) => <span style={{ color: '#1e293b' }}>{PORT_TYPE_MAP[text] || text}</span>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '用户id',
|
||||||
|
dataIndex: 'userId',
|
||||||
|
key: 'userId',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '创建时间',
|
||||||
|
dataIndex: 'createdAt',
|
||||||
|
key: 'createdAt',
|
||||||
|
width: 160,
|
||||||
|
render: (text: string) => <span style={{ color: '#64748b' }}>{formatDateTime(text)}</span>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '更新时间',
|
||||||
|
dataIndex: 'updatedAt',
|
||||||
|
key: 'updatedAt',
|
||||||
|
width: 160,
|
||||||
|
render: (text: string) => <span style={{ color: '#64748b' }}>{formatDateTime(text)}</span>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
width: 120,
|
||||||
|
render: (_: unknown, record: AuthorizationData) => (
|
||||||
|
<Link to={`/consume?accountId=${record.id}`} style={{ color: '#6366f1' }}>
|
||||||
|
查看消耗
|
||||||
|
</Link>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const tableData = authorizations.map((item, index) => ({
|
||||||
|
...item,
|
||||||
|
index: index + 1,
|
||||||
|
key: item.id,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ minHeight: '94vh' }}>
|
||||||
|
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
|
||||||
|
<Space>
|
||||||
|
<LockOutlined style={{ fontSize: 18, color: '#6366f1' }} />
|
||||||
|
<Typography.Text strong style={{ fontSize: 16 }}>授权管理</Typography.Text>
|
||||||
|
</Space>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="medium"
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
onClick={handleAuthorize}
|
||||||
|
loading={loading}
|
||||||
|
style={{
|
||||||
|
borderRadius: 8,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
点击授权
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: 12, marginBottom: 16 }}>
|
||||||
|
<Input
|
||||||
|
placeholder="账号用户ID"
|
||||||
|
value={searchParams.account_userid}
|
||||||
|
onChange={(e) => setSearchParams(prev => ({ ...prev, account_userid: e.target.value }))}
|
||||||
|
style={{ width: 180 }}
|
||||||
|
onPressEnter={() => { setCurrentPage(1); loadOAuthList(1, pageSize); }}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
placeholder="开户方式"
|
||||||
|
value={searchParams.open_type}
|
||||||
|
onChange={(value) => setSearchParams(prev => ({ ...prev, open_type: value }))}
|
||||||
|
style={{ width: 140 }}
|
||||||
|
options={Object.entries(OPEN_TYPE_MAP).map(([key, value]) => ({
|
||||||
|
value: Number(key),
|
||||||
|
label: value,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="账号ID"
|
||||||
|
value={searchParams.account_id}
|
||||||
|
onChange={(e) => setSearchParams(prev => ({ ...prev, account_id: e.target.value }))}
|
||||||
|
style={{ width: 180 }}
|
||||||
|
onPressEnter={() => { setCurrentPage(1); loadOAuthList(1, pageSize); }}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
size="medium"
|
||||||
|
onClick={() => { setCurrentPage(1); loadOAuthList(1, pageSize); }}
|
||||||
|
>
|
||||||
|
搜索
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="medium"
|
||||||
|
onClick={() => {
|
||||||
|
setSearchParams({ account_userid: '', open_type: undefined, account_id: '' });
|
||||||
|
setCurrentPage(1);
|
||||||
|
loadOAuthList(1, pageSize);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
重置
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Table
|
||||||
|
dataSource={tableData}
|
||||||
|
columns={columns}
|
||||||
|
loading={listLoading}
|
||||||
|
rowKey="id"
|
||||||
|
bordered={false}
|
||||||
|
scroll={{ x: 'max-content' }}
|
||||||
|
pagination={{
|
||||||
|
current: currentPage,
|
||||||
|
pageSize: pageSize,
|
||||||
|
total,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showTotal: (t) => `共 ${t} 条记录`,
|
||||||
|
onChange: (page, size) => {
|
||||||
|
setCurrentPage(page);
|
||||||
|
setPageSize(size);
|
||||||
|
loadOAuthList(page, size);
|
||||||
|
},
|
||||||
|
size: 'small',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title="选择开户方式"
|
||||||
|
open={showModal}
|
||||||
|
onOk={handleConfirm}
|
||||||
|
onCancel={() => {
|
||||||
|
setShowModal(false);
|
||||||
|
setSelectedOpenType(undefined);
|
||||||
|
}}
|
||||||
|
okText="确认授权"
|
||||||
|
cancelText="取消"
|
||||||
|
confirmLoading={loading}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
placeholder="请选择开户方式"
|
||||||
|
value={selectedOpenType}
|
||||||
|
onChange={(value) => setSelectedOpenType(value)}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
options={Object.entries(OPEN_TYPE_MAP).map(([key, value]) => ({
|
||||||
|
value: Number(key),
|
||||||
|
label: value,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AuthorizationPage;
|
||||||
@@ -0,0 +1,267 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Card, Space, Table, Button, Typography, Modal, Checkbox, Input, message } from 'antd';
|
||||||
|
import { ArrowLeftOutlined, DollarOutlined, SettingOutlined } from '@ant-design/icons';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
// 表头字段模拟数据
|
||||||
|
const mockColumnsData = [
|
||||||
|
{ name: 'id', description: 'id' },
|
||||||
|
{ name: 'star_id', description: '星图ID' },
|
||||||
|
{ name: 'demand_id', description: '任务ID' },
|
||||||
|
{ name: 'demand_name', description: '任务名称' },
|
||||||
|
{ name: 'demander_name', description: '客户名称' },
|
||||||
|
{ name: 'brand', description: '品牌' },
|
||||||
|
{ name: 'max_publish_count', description: '最大发布数量' },
|
||||||
|
{ name: 'product_name', description: '产品名称' },
|
||||||
|
{ name: 'product_information', description: '产品信息' },
|
||||||
|
];
|
||||||
|
|
||||||
|
interface ConsumptionRecord {
|
||||||
|
id: string;
|
||||||
|
star_id?: string;
|
||||||
|
demand_id?: string;
|
||||||
|
demand_name?: string;
|
||||||
|
demander_name?: string;
|
||||||
|
brand?: string;
|
||||||
|
max_publish_count?: number;
|
||||||
|
product_name?: string;
|
||||||
|
product_information?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockConsumptionRecords: ConsumptionRecord[] = [
|
||||||
|
{ id: 'C001', star_id: 'ST001', demand_id: 'DM001', demand_name: '产品推广任务', demander_name: 'XX品牌方', brand: 'XX品牌', max_publish_count: 10, product_name: 'XX产品', product_information: '产品信息描述' },
|
||||||
|
{ id: 'C002', star_id: 'ST002', demand_id: 'DM002', demand_name: '新品发布任务', demander_name: 'YY品牌方', brand: 'YY品牌', max_publish_count: 5, product_name: 'YY产品', product_information: '新品信息描述' },
|
||||||
|
{ id: 'C003', star_id: 'ST003', demand_id: 'DM003', demand_name: '品牌宣传任务', demander_name: 'ZZ品牌方', brand: 'ZZ品牌', max_publish_count: 8, product_name: 'ZZ产品', product_information: '品牌信息描述' },
|
||||||
|
{ id: 'C004', star_id: 'ST001', demand_id: 'DM004', demand_name: '活动促销任务', demander_name: 'XX品牌方', brand: 'XX品牌', max_publish_count: 15, product_name: 'XX活动产品', product_information: '活动产品信息' },
|
||||||
|
{ id: 'C005', star_id: 'ST004', demand_id: 'DM005', demand_name: '节日营销任务', demander_name: 'AA品牌方', brand: 'AA品牌', max_publish_count: 20, product_name: 'AA节日产品', product_information: '节日产品信息' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const ConsumePage: React.FC = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [consumptionRecords, setConsumptionRecords] = useState<ConsumptionRecord[]>(mockConsumptionRecords);
|
||||||
|
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('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setConsumptionRecords(mockConsumptionRecords);
|
||||||
|
setLoading(false);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setColumnsData(mockColumnsData);
|
||||||
|
const saved = localStorage.getItem('consumeColumns');
|
||||||
|
if (saved) {
|
||||||
|
setSelectedColumns(JSON.parse(saved));
|
||||||
|
} else {
|
||||||
|
setSelectedColumns(mockColumnsData.map(item => item.name));
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const tableData = consumptionRecords.map((item, index) => ({
|
||||||
|
...item,
|
||||||
|
index: index + 1,
|
||||||
|
key: item.id,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
navigate('/authorization');
|
||||||
|
};
|
||||||
|
|
||||||
|
const dynamicColumns = columnsData
|
||||||
|
.filter(col => selectedColumns.includes(col.name))
|
||||||
|
.map(col => ({
|
||||||
|
title: col.description,
|
||||||
|
dataIndex: col.name,
|
||||||
|
key: col.name,
|
||||||
|
ellipsis: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ minHeight: '94vh' }}>
|
||||||
|
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
|
||||||
|
<Space>
|
||||||
|
<DollarOutlined style={{ fontSize: 18, color: '#6366f1' }} />
|
||||||
|
<Typography.Text strong style={{ fontSize: 16 }}>消耗记录</Typography.Text>
|
||||||
|
</Space>
|
||||||
|
<Space>
|
||||||
|
<Button icon={<SettingOutlined />} onClick={() => setShowModal(true)}>自定义表头</Button>
|
||||||
|
<Button icon={<ArrowLeftOutlined />} onClick={handleBack}>返回</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
<Table
|
||||||
|
dataSource={tableData}
|
||||||
|
columns={dynamicColumns}
|
||||||
|
loading={loading}
|
||||||
|
rowKey="id"
|
||||||
|
bordered={false}
|
||||||
|
scroll={{ x: 'max-content' }}
|
||||||
|
pagination={{
|
||||||
|
pageSize: 10,
|
||||||
|
total: consumptionRecords.length,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showTotal: (t) => `共 ${t} 条记录`,
|
||||||
|
size: 'small',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title="自定义表头"
|
||||||
|
open={showModal}
|
||||||
|
onOk={() => {
|
||||||
|
localStorage.setItem('consumeColumns', JSON.stringify(selectedColumns));
|
||||||
|
message.success('表头设置已保存');
|
||||||
|
setShowModal(false);
|
||||||
|
}}
|
||||||
|
onCancel={() => {
|
||||||
|
setShowModal(false);
|
||||||
|
setSearchText('');
|
||||||
|
}}
|
||||||
|
okText="确定"
|
||||||
|
cancelText="取消"
|
||||||
|
width={800}
|
||||||
|
bodyStyle={{ padding: 16 }}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', gap: 20, height: 400 }}>
|
||||||
|
<div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
|
||||||
|
<Input
|
||||||
|
placeholder="搜索指标..."
|
||||||
|
value={searchText}
|
||||||
|
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={() => {
|
||||||
|
message.info('更新表头字段');
|
||||||
|
setTimeout(() => {
|
||||||
|
setColumnsData(mockColumnsData);
|
||||||
|
}, 300);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
更新表头字段
|
||||||
|
</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;
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Button, Card, Space, Table, Tag, Typography, message, Modal, Input, Upload,
|
||||||
|
} from 'antd';
|
||||||
|
import {
|
||||||
|
HistoryOutlined, ReloadOutlined, PlusOutlined, UploadOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { getOperationLogs } from '../api';
|
||||||
|
import { formatDate } from '../utils/formatDate';
|
||||||
|
|
||||||
|
interface OperationLog {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
username: string;
|
||||||
|
action: string;
|
||||||
|
method: string;
|
||||||
|
path: string;
|
||||||
|
detail?: string;
|
||||||
|
ip?: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const METHOD_COLORS: Record<string, string> = { POST: 'green', PUT: 'blue', DELETE: 'red' };
|
||||||
|
|
||||||
|
const AdminPlatform: React.FC = () => {
|
||||||
|
const [logs, setLogs] = useState<OperationLog[]>([]);
|
||||||
|
const [total, setTotal] = useState(0);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [showModal, setShowModal] = useState(false);
|
||||||
|
const [formData, setFormData] = useState({ title: '', description: '', image: '' });
|
||||||
|
|
||||||
|
const load = async (p?: number) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await getOperationLogs(p || page);
|
||||||
|
setLogs(res.items || []);
|
||||||
|
setTotal(res.total || 0);
|
||||||
|
} catch {
|
||||||
|
message.error('加载平台管理失败');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => { load(); }, []);
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: '操作人', dataIndex: 'username', width: 120,
|
||||||
|
render: (v: string) => <Typography.Text strong>{v}</Typography.Text>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作', dataIndex: 'action', width: 160,
|
||||||
|
render: (v: string) => <Typography.Text>{v}</Typography.Text>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '方法', dataIndex: 'method', width: 80,
|
||||||
|
render: (v: string) => <Tag color={METHOD_COLORS[v] || 'default'}>{v}</Tag>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '路径', dataIndex: 'path', width: 220, ellipsis: true,
|
||||||
|
render: (v: string) => <Typography.Text type="secondary" style={{ fontSize: 12 }}>{v}</Typography.Text>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '时间', dataIndex: 'createdAt', width: 160,
|
||||||
|
render: (v: string) => <Typography.Text type="secondary" style={{ fontSize: 12 }}>{formatDate(v)}</Typography.Text>,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
|
||||||
|
<Space>
|
||||||
|
<HistoryOutlined style={{ fontSize: 18, color: '#6366f1' }} />
|
||||||
|
<Typography.Text strong style={{ fontSize: 16 }}>平台管理</Typography.Text>
|
||||||
|
</Space>
|
||||||
|
<Space>
|
||||||
|
<Button type="primary" icon={<PlusOutlined />} onClick={() => setShowModal(true)}>新增</Button>
|
||||||
|
<Button icon={<ReloadOutlined />} onClick={() => load()}>刷新</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
dataSource={logs}
|
||||||
|
rowKey="id"
|
||||||
|
loading={loading}
|
||||||
|
pagination={{
|
||||||
|
current: page,
|
||||||
|
pageSize: 20,
|
||||||
|
total,
|
||||||
|
showTotal: (t) => `共 ${t} 条记录`,
|
||||||
|
onChange: (p) => { setPage(p); load(p); },
|
||||||
|
}}
|
||||||
|
scroll={{ x: 800 }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title="新增平台"
|
||||||
|
open={showModal}
|
||||||
|
onOk={() => {
|
||||||
|
message.success('新增成功');
|
||||||
|
setShowModal(false);
|
||||||
|
setFormData({ title: '', description: '', image: '' });
|
||||||
|
load();
|
||||||
|
}}
|
||||||
|
onCancel={() => {
|
||||||
|
setShowModal(false);
|
||||||
|
setFormData({ title: '', description: '', image: '' });
|
||||||
|
}}
|
||||||
|
okText="确认"
|
||||||
|
cancelText="取消"
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||||
|
<div>
|
||||||
|
<Typography.Text strong style={{ marginBottom: 8, display: 'block' }}>标题</Typography.Text>
|
||||||
|
<Input
|
||||||
|
placeholder="请输入标题"
|
||||||
|
value={formData.title}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, title: e.target.value }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography.Text strong style={{ marginBottom: 8, display: 'block' }}>描述</Typography.Text>
|
||||||
|
<Input.TextArea
|
||||||
|
placeholder="请输入描述"
|
||||||
|
value={formData.description}
|
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography.Text strong style={{ marginBottom: 8, display: 'block' }}>封面图片</Typography.Text>
|
||||||
|
<Upload
|
||||||
|
action="/api/upload"
|
||||||
|
listType="picture-card"
|
||||||
|
onChange={(info) => {
|
||||||
|
if (info.file.status === 'done') {
|
||||||
|
setFormData(prev => ({ ...prev, image: info.file.response?.url || '' }));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<UploadOutlined style={{ fontSize: 24, color: '#999' }} />
|
||||||
|
<div style={{ marginTop: 8 }}>上传图片</div>
|
||||||
|
</div>
|
||||||
|
</Upload>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AdminPlatform;
|
||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
#VITE_API_BASE=http://localhost:8000
|
VITE_API_BASE=http://192.168.120.17:8000
|
||||||
VITE_API_BASE=http://ceshi.apiforeign.minzhong.cn
|
# VITE_API_BASE=http://ceshi.apiforeign.minzhong.cn
|
||||||
VITE_USE_MOCK=false
|
VITE_USE_MOCK=false
|
||||||
# Encryption disabled for dev — enable in production
|
# Encryption disabled for dev — enable in production
|
||||||
VITE_ENCRYPTION_KEY=
|
VITE_ENCRYPTION_KEY=
|
||||||
|
|||||||
@@ -577,4 +577,35 @@ export async function getArea(params?: GetAreaParams): Promise<any> {
|
|||||||
if (params?.parent_code) query.set('parent_code', params.parent_code);
|
if (params?.parent_code) query.set('parent_code', params.parent_code);
|
||||||
return api.get(`/pre-test-template/getArea?${query.toString()}`);
|
return api.get(`/pre-test-template/getArea?${query.toString()}`);
|
||||||
}
|
}
|
||||||
//
|
|
||||||
|
// ── Upload Material ───────────────────────────────────────
|
||||||
|
|
||||||
|
export interface AsyncBatchUploadTask {
|
||||||
|
advertiser_ids: string[];
|
||||||
|
resource_ids: string[];
|
||||||
|
oauth_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AsyncBatchUploadParams {
|
||||||
|
tasks: AsyncBatchUploadTask[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function asyncBatchUploadMaterial(params: AsyncBatchUploadParams): Promise<any> {
|
||||||
|
return api.post('/upload-material/async-batch-upload', params);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取上传历史
|
||||||
|
// /api/upload-material/upload-history
|
||||||
|
export interface UploadHistoryParams {
|
||||||
|
status?: string;
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
}
|
||||||
|
export async function getUploadHistory(params: UploadHistoryParams): Promise<any> {
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
|
if (params.status) searchParams.set('status', params.status);
|
||||||
|
if (params.page) searchParams.set('page', String(params.page));
|
||||||
|
if (params.pageSize) searchParams.set('page_size', String(params.pageSize));
|
||||||
|
const query = searchParams.toString();
|
||||||
|
return api.get(`/upload-material/upload-history${query ? `?${query}` : ''}`);
|
||||||
|
}
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ const AuthorizationPage: React.FC = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadOAuthList();
|
loadOAuthList();
|
||||||
handleCallback();
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadOAuthList = async (page = 1, pageSize = 10, params = searchParams) => {
|
const loadOAuthList = async (page = 1, pageSize = 10, params = searchParams) => {
|
||||||
@@ -97,22 +96,6 @@ const AuthorizationPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCallback = async () => {
|
|
||||||
const params = new URLSearchParams(window.location.search);
|
|
||||||
const authCode = params.get('auth_code');
|
|
||||||
const state = params.get('state');
|
|
||||||
if (authCode && state) {
|
|
||||||
try {
|
|
||||||
await juliang_callback({ auth_code: authCode, state });
|
|
||||||
message.success('授权成功');
|
|
||||||
window.history.replaceState({}, document.title, window.location.pathname);
|
|
||||||
loadOAuthList();
|
|
||||||
} catch (error) {
|
|
||||||
message.error('授权回调失败');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAuthorize = () => {
|
const handleAuthorize = () => {
|
||||||
setShowModal(true);
|
setShowModal(true);
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user