mirror of
https://github.com/songquanpeng/one-api.git
synced 2025-11-18 14:13:43 +08:00
@@ -31,6 +31,7 @@
|
||||
"react-device-detect": "^2.2.3",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-perfect-scrollbar": "^1.5.8",
|
||||
"react-qrcode-logo": "^3.0.0",
|
||||
"react-redux": "^9.1.0",
|
||||
"react-router": "6.21.3",
|
||||
"react-router-dom": "6.21.3",
|
||||
@@ -82,5 +83,6 @@
|
||||
"immutable": "^4.3.5",
|
||||
"prettier": "^3.2.4",
|
||||
"sass": "^1.70.0"
|
||||
}
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
}
|
||||
|
||||
BIN
web/public/ali_pay.png
Normal file
BIN
web/public/ali_pay.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.7 KiB |
5
web/public/logo-loading-white.svg
Normal file
5
web/public/logo-loading-white.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg id="eKwXi4fbWI71" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 590 360" shape-rendering="geometricPrecision" text-rendering="geometricPrecision">
|
||||
<style><![CDATA[
|
||||
#eKwXi4fbWI73_to {animation: eKwXi4fbWI73_to__to 3000ms linear infinite normal forwards}@keyframes eKwXi4fbWI73_to__to { 0% {transform: translate(386.852639px,-200.193104px)} 16.666667% {transform: translate(386.848452px,-187.188246px)} 33.333333% {transform: translate(386.848452px,-200.188246px)} 50% {transform: translate(386.848452px,-187.188246px)} 66.666667% {transform: translate(386.848452px,-200.188246px)} 83.333333% {transform: translate(386.848452px,-187.188246px)} 100% {transform: translate(386.848452px,-200.188246px)}}
|
||||
]]></style>
|
||||
<g><g id="eKwXi4fbWI73_to" transform="translate(386.852639,-200.193104)"><ellipse rx="28.47" ry="28.14" transform="rotate(103.599994) translate(287.6,33.87)" fill="#fff"/></g><path d="M232.92,128.89c3.78,27.29-1.81,55.44-17.71,78.09-.610963.870887-.634672,2.024751-.06,2.92c1.24,1.92,2.96,5.05,5.56,4.94q5.25-.22,10.79.11c.672283.037376,1.196382.596709,1.19,1.27l-.4,42.53c-.005493.719585-.590394,1.300021-1.31,1.3q-16.77-.09-36.53.01-2.25.02-3.71-1.56-16.02-17.28-31.98-35.32c-5.13-5.8-10.18-11.16-14.86-17.59-.234342-.319666-.313973-.727248-.217488-1.113186s.359185-.710636.717488-.886814q12.88-6.32,22.13-17.12q18.18-21.23,15.08-48.84-2.66-23.7-22.4-40.46-23.43-19.9-54.88-13.86c-4.1.79-7.83,2.5-11.72,4.12q-11.86,4.94-20.59,14.64c-14.25,15.81-20.07,36.4-15.05,57.16q4.99,20.63,22.86,35.71c10.45,8.81,23.7,13.12,37.26,14.18q1.47.11,3.6,2.65c11.68,13.89,24.48,27.72,35.94,41.96.089095.111267.117261.259455.0752.39565s-.148879.242697-.2852.28435q-22.51,7.27-47.37,5.37-19.4-1.47-39.74-11.22-18.27-8.75-30.59-21.28Q20.06,208.3,10.7,183.71q-10.8-28.4-4.93-58.67c1.59-8.17,4.03-17,7.42-24.61Q18.27,89.05,24.8,79.79Q50.21,43.76,93.25,33.66q32.42-7.61,64.23,3.92q25.31,9.17,43.2,27.31c16.85,17.09,28.91,40.01,32.24,64Z" transform="translate(-.326548 38.749742)" fill="#fff"/><path d="M499.47,180.61c6.45,13.53,16.44,21.75,31.96,22q11.94.19,22.17-5.36q2.21-1.2,3.93.69q12.56,13.78,24.89,28.47q1.21,1.44,1.44,3.13c.049759.339937-.087897.680254-.36.89-1.62,1.23-3.33,2.71-5.03,3.69Q549.1,251.13,516,245.43c-20.61-3.55-39.05-15.24-51.47-32.51q-6.4-8.89-9.91-17.08c-2.62-6.12-4.73-13.3-5.41-20.08q-3.96-39.88,22.94-67.74c9.48-9.81,21.15-16.67,34.39-19.49c16.54-3.53,34.64-1.83,48.77,7.1q13.92,8.79,21.13,20.4q11.07,17.84,10.48,38.92c-.02.94-.21,1.81-.85,2.54q-7.73,8.77-18.71,20.16c-1.28,1.32-2.61,2.26-4.51,2.23q-24.45-.37-51.64-.41-5.03,0-10.84-.22c-.334463-.009876-.650686.153565-.834487.431309s-.208627.629665-.065513.928691Zm1.12-37.17q-.55,1.19-.63,2.34-.08,1.01.94,1.03q19.01.25,36.98.01.5,0,.94-.22.57-.28.44-.9-2.34-11.6-14.11-15.25-3.59-1.11-6.44-.57-13.07,2.5-18.12,13.56Z" transform="translate(-.326548 38.749742)" fill="#fff"/><path d="M312.3,100.22c-.001444.196624.116164.373518.298977.44969s.395635.036957.541023-.09969q2.76-2.64,5.82-4.31q8.45-4.62,16.71-6.57c15.81-3.72,33.58-3.2,48.2,3.95q24.49,11.98,35.05,35.76c4.66,10.5,5.44,22.96,5.5,35.35q.21,49.99-.12,88-.03,3.06-.08,6.16c-.010966.725105-.604834,1.305577-1.33,1.3q-20.22-.18-40.18-.23-3.64-.01-8.13-.44c-.537084-.051394-.94781-.505354-.95-1.05q.02-45.49-.22-92.99c-.03-6.25-1.21-13.88-5.05-18.95q-5.33-7.03-12.32-10.18c-10.99-4.93-24.52-1.84-33.13,6.37q-10.01,9.53-10.07,23.76-.11,25.46-.1,48.98c0,3.52-.06,8.31-1.1,11.68-4.37,14.04-17.31,19.5-31.04,16.77-8.22-1.64-15.07-7.75-17.62-15.62q-1.45-4.49-1.42-10.2.3-64.69.1-129.86c0-.124652.049518-.244198.13766-.33234s.207688-.13766.33234-.13766l48.46-.35c.852152-.005489,1.548978.682403,1.56,1.54l.15,11.25Z" transform="translate(-.326548 38.749742)" fill="#fff"/></g></svg>
|
||||
|
After Width: | Height: | Size: 3.7 KiB |
5
web/public/logo-loading.svg
Normal file
5
web/public/logo-loading.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg id="eKwXi4fbWI71" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 590 360" shape-rendering="geometricPrecision" text-rendering="geometricPrecision">
|
||||
<style><![CDATA[
|
||||
#eKwXi4fbWI73_to {animation: eKwXi4fbWI73_to__to 3000ms linear infinite normal forwards}@keyframes eKwXi4fbWI73_to__to { 0% {transform: translate(386.852639px,-200.193104px)} 16.666667% {transform: translate(386.848452px,-187.188246px)} 33.333333% {transform: translate(386.848452px,-200.188246px)} 50% {transform: translate(386.848452px,-187.188246px)} 66.666667% {transform: translate(386.848452px,-200.188246px)} 83.333333% {transform: translate(386.848452px,-187.188246px)} 100% {transform: translate(386.848452px,-200.188246px)}}
|
||||
]]></style>
|
||||
<g><g id="eKwXi4fbWI73_to" transform="translate(386.852639,-200.193104)"><ellipse rx="28.47" ry="28.14" transform="rotate(103.599994) translate(287.6,33.87)" fill="#0d161f"/></g><path d="M232.92,128.89c3.78,27.29-1.81,55.44-17.71,78.09-.610963.870887-.634672,2.024751-.06,2.92c1.24,1.92,2.96,5.05,5.56,4.94q5.25-.22,10.79.11c.672283.037376,1.196382.596709,1.19,1.27l-.4,42.53c-.005493.719585-.590394,1.300021-1.31,1.3q-16.77-.09-36.53.01-2.25.02-3.71-1.56-16.02-17.28-31.98-35.32c-5.13-5.8-10.18-11.16-14.86-17.59-.234342-.319666-.313973-.727248-.217488-1.113186s.359185-.710636.717488-.886814q12.88-6.32,22.13-17.12q18.18-21.23,15.08-48.84-2.66-23.7-22.4-40.46-23.43-19.9-54.88-13.86c-4.1.79-7.83,2.5-11.72,4.12q-11.86,4.94-20.59,14.64c-14.25,15.81-20.07,36.4-15.05,57.16q4.99,20.63,22.86,35.71c10.45,8.81,23.7,13.12,37.26,14.18q1.47.11,3.6,2.65c11.68,13.89,24.48,27.72,35.94,41.96.089095.111267.117261.259455.0752.39565s-.148879.242697-.2852.28435q-22.51,7.27-47.37,5.37-19.4-1.47-39.74-11.22-18.27-8.75-30.59-21.28Q20.06,208.3,10.7,183.71q-10.8-28.4-4.93-58.67c1.59-8.17,4.03-17,7.42-24.61Q18.27,89.05,24.8,79.79Q50.21,43.76,93.25,33.66q32.42-7.61,64.23,3.92q25.31,9.17,43.2,27.31c16.85,17.09,28.91,40.01,32.24,64Z" transform="translate(-.326548 38.749742)" fill="#0d161f"/><path d="M499.47,180.61c6.45,13.53,16.44,21.75,31.96,22q11.94.19,22.17-5.36q2.21-1.2,3.93.69q12.56,13.78,24.89,28.47q1.21,1.44,1.44,3.13c.049759.339937-.087897.680254-.36.89-1.62,1.23-3.33,2.71-5.03,3.69Q549.1,251.13,516,245.43c-20.61-3.55-39.05-15.24-51.47-32.51q-6.4-8.89-9.91-17.08c-2.62-6.12-4.73-13.3-5.41-20.08q-3.96-39.88,22.94-67.74c9.48-9.81,21.15-16.67,34.39-19.49c16.54-3.53,34.64-1.83,48.77,7.1q13.92,8.79,21.13,20.4q11.07,17.84,10.48,38.92c-.02.94-.21,1.81-.85,2.54q-7.73,8.77-18.71,20.16c-1.28,1.32-2.61,2.26-4.51,2.23q-24.45-.37-51.64-.41-5.03,0-10.84-.22c-.334463-.009876-.650686.153565-.834487.431309s-.208627.629665-.065513.928691Zm1.12-37.17q-.55,1.19-.63,2.34-.08,1.01.94,1.03q19.01.25,36.98.01.5,0,.94-.22.57-.28.44-.9-2.34-11.6-14.11-15.25-3.59-1.11-6.44-.57-13.07,2.5-18.12,13.56Z" transform="translate(-.326548 38.749742)" fill="#0d161f"/><path d="M312.3,100.22c-.001444.196624.116164.373518.298977.44969s.395635.036957.541023-.09969q2.76-2.64,5.82-4.31q8.45-4.62,16.71-6.57c15.81-3.72,33.58-3.2,48.2,3.95q24.49,11.98,35.05,35.76c4.66,10.5,5.44,22.96,5.5,35.35q.21,49.99-.12,88-.03,3.06-.08,6.16c-.010966.725105-.604834,1.305577-1.33,1.3q-20.22-.18-40.18-.23-3.64-.01-8.13-.44c-.537084-.051394-.94781-.505354-.95-1.05q.02-45.49-.22-92.99c-.03-6.25-1.21-13.88-5.05-18.95q-5.33-7.03-12.32-10.18c-10.99-4.93-24.52-1.84-33.13,6.37q-10.01,9.53-10.07,23.76-.11,25.46-.1,48.98c0,3.52-.06,8.31-1.1,11.68-4.37,14.04-17.31,19.5-31.04,16.77-8.22-1.64-15.07-7.75-17.62-15.62q-1.45-4.49-1.42-10.2.3-64.69.1-129.86c0-.124652.049518-.244198.13766-.33234s.207688-.13766.33234-.13766l48.46-.35c.852152-.005489,1.548978.682403,1.56,1.54l.15,11.25Z" transform="translate(-.326548 38.749742)" fill="#0d161f"/></g></svg>
|
||||
|
After Width: | Height: | Size: 3.7 KiB |
BIN
web/public/wechat_pay.png
Normal file
BIN
web/public/wechat_pay.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.0 KiB |
1
web/src/assets/images/success.svg
Normal file
1
web/src/assets/images/success.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1717527029040" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6841" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M874.119618 149.859922A510.816461 510.816461 0 0 0 511.997 0.00208a509.910462 509.910462 0 0 0-362.119618 149.857842c-199.817789 199.679789-199.817789 524.581447 0 724.260236a509.969462 509.969462 0 0 0 362.119618 149.857842A508.872463 508.872463 0 0 0 874.119618 874.120158c199.836789-199.679789 199.836789-524.581447 0-724.260236zM814.94268 378.210681L470.999043 744.132295a15.359984 15.359984 0 0 1-5.887994 4.095996c-1.751998 1.180999-2.913997 2.362998-5.276994 2.913997a34.499964 34.499964 0 0 1-13.469986 2.914997 45.547952 45.547952 0 0 1-12.897986-2.303998l-4.095996-2.363997a45.291952 45.291952 0 0 1-7.009992-4.095996l-196.902793-193.789796a34.126964 34.126964 0 0 1-10.555989-25.186973c0-9.37399 3.583996-18.74698 9.98399-25.186974a36.429962 36.429962 0 0 1 50.372947 0l169.98382 167.423824L763.389735 330.220732a37.059961 37.059961 0 0 1 50.371947-1.732998 33.647965 33.647965 0 0 1 11.165988 25.186973 35.544963 35.544963 0 0 1-9.98399 24.575974v-0.04z m0 0" fill="#52C41A" p-id="6842"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -26,7 +26,7 @@ const config = {
|
||||
lark_login: false,
|
||||
lark_client_id: '',
|
||||
telegram_bot: '',
|
||||
isLoading: true, // 添加加载状态
|
||||
isLoading: true // 添加加载状态
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -14,7 +14,8 @@ import {
|
||||
IconReceipt2,
|
||||
IconBrush,
|
||||
IconBrandGithubCopilot,
|
||||
IconBallFootball
|
||||
IconBallFootball,
|
||||
IconBrandPaypal
|
||||
} from '@tabler/icons-react';
|
||||
|
||||
// constant
|
||||
@@ -33,7 +34,8 @@ const icons = {
|
||||
IconReceipt2,
|
||||
IconBrush,
|
||||
IconBrandGithubCopilot,
|
||||
IconBallFootball
|
||||
IconBallFootball,
|
||||
IconBrandPaypal
|
||||
};
|
||||
|
||||
// ==============================|| DASHBOARD MENU ITEMS ||============================== //
|
||||
@@ -154,6 +156,15 @@ const panel = {
|
||||
breadcrumbs: false,
|
||||
isAdmin: false
|
||||
},
|
||||
{
|
||||
id: 'payment',
|
||||
title: '支付',
|
||||
type: 'item',
|
||||
url: '/panel/payment',
|
||||
icon: icons.IconBrandPaypal,
|
||||
breadcrumbs: false,
|
||||
isAdmin: true
|
||||
},
|
||||
{
|
||||
id: 'setting',
|
||||
title: '设置',
|
||||
|
||||
@@ -19,6 +19,7 @@ const Pricing = Loadable(lazy(() => import('views/Pricing')));
|
||||
const Midjourney = Loadable(lazy(() => import('views/Midjourney')));
|
||||
const ModelPrice = Loadable(lazy(() => import('views/ModelPrice')));
|
||||
const Playground = Loadable(lazy(() => import('views/Playground')));
|
||||
const Payment = Loadable(lazy(() => import('views/Payment')));
|
||||
|
||||
// dashboard routing
|
||||
const Dashboard = Loadable(lazy(() => import('views/Dashboard')));
|
||||
@@ -96,6 +97,10 @@ const MainRoutes = {
|
||||
{
|
||||
path: 'playground',
|
||||
element: <Playground />
|
||||
},
|
||||
{
|
||||
path: 'payment',
|
||||
element: <Payment />
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ const siteInfoReducer = (state = initialState, action) => {
|
||||
return {
|
||||
...state,
|
||||
...action.payload,
|
||||
isLoading: false, // 添加加载状态
|
||||
isLoading: false // 添加加载状态
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
|
||||
294
web/src/views/Payment/Gateway.js
Normal file
294
web/src/views/Payment/Gateway.js
Normal file
@@ -0,0 +1,294 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { showError, trims, showSuccess } from 'utils/common';
|
||||
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableContainer from '@mui/material/TableContainer';
|
||||
import PerfectScrollbar from 'react-perfect-scrollbar';
|
||||
import TablePagination from '@mui/material/TablePagination';
|
||||
import LinearProgress from '@mui/material/LinearProgress';
|
||||
import ButtonGroup from '@mui/material/ButtonGroup';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
|
||||
import { Button, Card, Stack, Container, Typography, Box } from '@mui/material';
|
||||
import PaymentTableRow from './component/TableRow';
|
||||
import KeywordTableHead from 'ui-component/TableHead';
|
||||
import TableToolBar from './component/TableToolBar';
|
||||
import EditeModal from './component/EditModal';
|
||||
import { API } from 'utils/api';
|
||||
import { ITEMS_PER_PAGE } from 'constants';
|
||||
import { IconRefresh, IconSearch, IconPlus } from '@tabler/icons-react';
|
||||
|
||||
export default function Gateway() {
|
||||
const originalKeyword = {
|
||||
p: 0,
|
||||
type: '',
|
||||
name: '',
|
||||
uuid: '',
|
||||
currency: ''
|
||||
};
|
||||
|
||||
const [page, setPage] = useState(0);
|
||||
const [order, setOrder] = useState('desc');
|
||||
const [orderBy, setOrderBy] = useState('created_at');
|
||||
const [rowsPerPage, setRowsPerPage] = useState(ITEMS_PER_PAGE);
|
||||
const [listCount, setListCount] = useState(0);
|
||||
const [searching, setSearching] = useState(false);
|
||||
const [toolBarValue, setToolBarValue] = useState(originalKeyword);
|
||||
const [searchKeyword, setSearchKeyword] = useState(originalKeyword);
|
||||
const [refreshFlag, setRefreshFlag] = useState(false);
|
||||
const [openModal, setOpenModal] = useState(false);
|
||||
const [editPaymentId, setEditPaymentId] = useState(0);
|
||||
|
||||
const [payment, setPayment] = useState([]);
|
||||
|
||||
const handleSort = (event, id) => {
|
||||
const isAsc = orderBy === id && order === 'asc';
|
||||
if (id !== '') {
|
||||
setOrder(isAsc ? 'desc' : 'asc');
|
||||
setOrderBy(id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChangePage = (event, newPage) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const handleOpenModal = (channelId) => {
|
||||
setEditPaymentId(channelId);
|
||||
setOpenModal(true);
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setOpenModal(false);
|
||||
setEditPaymentId(0);
|
||||
};
|
||||
|
||||
const handleOkModal = (status) => {
|
||||
if (status === true) {
|
||||
handleCloseModal();
|
||||
handleRefresh();
|
||||
}
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (event) => {
|
||||
setPage(0);
|
||||
setRowsPerPage(parseInt(event.target.value, 10));
|
||||
};
|
||||
|
||||
const search = async () => {
|
||||
setPage(0);
|
||||
setSearchKeyword(toolBarValue);
|
||||
};
|
||||
|
||||
const handleToolBarValue = (event) => {
|
||||
setToolBarValue({ ...toolBarValue, [event.target.name]: event.target.value });
|
||||
};
|
||||
|
||||
const managePayment = async (id, action, value) => {
|
||||
const url = '/api/payment/';
|
||||
let data = { id };
|
||||
let res;
|
||||
|
||||
try {
|
||||
switch (action) {
|
||||
case 'delete':
|
||||
res = await API.delete(url + id);
|
||||
break;
|
||||
case 'status':
|
||||
res = await API.put(url, {
|
||||
...data,
|
||||
enable: value
|
||||
});
|
||||
break;
|
||||
case 'sort':
|
||||
res = await API.put(url, {
|
||||
...data,
|
||||
sort: value
|
||||
});
|
||||
break;
|
||||
}
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
showSuccess('操作成功完成!');
|
||||
await handleRefresh();
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const fetchData = useCallback(async (page, rowsPerPage, keyword, order, orderBy) => {
|
||||
setSearching(true);
|
||||
keyword = trims(keyword);
|
||||
try {
|
||||
if (orderBy) {
|
||||
orderBy = order === 'desc' ? '-' + orderBy : orderBy;
|
||||
}
|
||||
const res = await API.get('/api/payment/', {
|
||||
params: {
|
||||
page: page + 1,
|
||||
size: rowsPerPage,
|
||||
order: orderBy,
|
||||
...keyword
|
||||
}
|
||||
});
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
setListCount(data.total_count);
|
||||
setPayment(data.data);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
setSearching(false);
|
||||
}, []);
|
||||
|
||||
// 处理刷新
|
||||
const handleRefresh = async () => {
|
||||
setOrderBy('created_at');
|
||||
setOrder('desc');
|
||||
setToolBarValue(originalKeyword);
|
||||
setSearchKeyword(originalKeyword);
|
||||
setRefreshFlag(!refreshFlag);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData(page, rowsPerPage, searchKeyword, order, orderBy);
|
||||
}, [page, rowsPerPage, searchKeyword, order, orderBy, fetchData, refreshFlag]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack direction="row" alignItems="center" justifyContent="space-between" mb={5}>
|
||||
<Typography variant="h4">支付网关</Typography>
|
||||
<Button variant="contained" color="primary" startIcon={<IconPlus />} onClick={() => handleOpenModal(0)}>
|
||||
新建支付
|
||||
</Button>
|
||||
</Stack>
|
||||
<Card>
|
||||
<Box component="form" noValidate>
|
||||
<TableToolBar filterName={toolBarValue} handleFilterName={handleToolBarValue} />
|
||||
</Box>
|
||||
<Toolbar
|
||||
sx={{
|
||||
textAlign: 'right',
|
||||
height: 50,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
p: (theme) => theme.spacing(0, 1, 0, 3)
|
||||
}}
|
||||
>
|
||||
<Container>
|
||||
<ButtonGroup variant="outlined" aria-label="outlined small primary button group">
|
||||
<Button onClick={handleRefresh} startIcon={<IconRefresh width={'18px'} />}>
|
||||
刷新/清除搜索条件
|
||||
</Button>
|
||||
|
||||
<Button onClick={search} startIcon={<IconSearch width={'18px'} />}>
|
||||
搜索
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Container>
|
||||
</Toolbar>
|
||||
{searching && <LinearProgress />}
|
||||
<PerfectScrollbar component="div">
|
||||
<TableContainer sx={{ overflow: 'unset' }}>
|
||||
<Table sx={{ minWidth: 800 }}>
|
||||
<KeywordTableHead
|
||||
order={order}
|
||||
orderBy={orderBy}
|
||||
onRequestSort={handleSort}
|
||||
headLabel={[
|
||||
{
|
||||
id: 'id',
|
||||
label: 'ID',
|
||||
disableSort: false
|
||||
},
|
||||
{
|
||||
id: 'uuid',
|
||||
label: 'UUID',
|
||||
disableSort: false
|
||||
},
|
||||
{
|
||||
id: 'name',
|
||||
label: '名称',
|
||||
disableSort: true
|
||||
},
|
||||
{
|
||||
id: 'type',
|
||||
label: '类型',
|
||||
disableSort: false
|
||||
},
|
||||
{
|
||||
id: 'icon',
|
||||
label: '图标',
|
||||
disableSort: true
|
||||
},
|
||||
{
|
||||
id: 'fixed_fee',
|
||||
label: '固定手续费',
|
||||
disableSort: true
|
||||
},
|
||||
{
|
||||
id: 'percent_fee',
|
||||
label: '百分比手续费',
|
||||
disableSort: true
|
||||
},
|
||||
{
|
||||
id: 'sort',
|
||||
label: '排序',
|
||||
disableSort: false
|
||||
},
|
||||
{
|
||||
id: 'enable',
|
||||
label: '启用',
|
||||
disableSort: false
|
||||
},
|
||||
{
|
||||
id: 'created_at',
|
||||
label: '创建时间',
|
||||
disableSort: false
|
||||
},
|
||||
{
|
||||
id: 'action',
|
||||
label: '操作',
|
||||
disableSort: true
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<TableBody>
|
||||
{payment.map((row, index) => (
|
||||
<PaymentTableRow
|
||||
item={row}
|
||||
key={`${row.id}_${index}`}
|
||||
managePayment={managePayment}
|
||||
handleOpenModal={handleOpenModal}
|
||||
setModalPaymentId={setEditPaymentId}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</PerfectScrollbar>
|
||||
<TablePagination
|
||||
page={page}
|
||||
component="div"
|
||||
count={listCount}
|
||||
rowsPerPage={rowsPerPage}
|
||||
onPageChange={handleChangePage}
|
||||
rowsPerPageOptions={[10, 25, 30]}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||
showFirstButton
|
||||
showLastButton
|
||||
/>
|
||||
<EditeModal open={openModal} onCancel={handleCloseModal} onOk={handleOkModal} paymentId={editPaymentId} />
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
220
web/src/views/Payment/Order.js
Normal file
220
web/src/views/Payment/Order.js
Normal file
@@ -0,0 +1,220 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { showError, trims } from 'utils/common';
|
||||
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableContainer from '@mui/material/TableContainer';
|
||||
import PerfectScrollbar from 'react-perfect-scrollbar';
|
||||
import TablePagination from '@mui/material/TablePagination';
|
||||
import LinearProgress from '@mui/material/LinearProgress';
|
||||
import ButtonGroup from '@mui/material/ButtonGroup';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
|
||||
import { Button, Card, Stack, Container, Typography, Box } from '@mui/material';
|
||||
import LogTableRow from './component/OrderTableRow';
|
||||
import KeywordTableHead from 'ui-component/TableHead';
|
||||
import TableToolBar from './component/OrderTableToolBar';
|
||||
import { API } from 'utils/api';
|
||||
import { ITEMS_PER_PAGE } from 'constants';
|
||||
import { IconRefresh, IconSearch } from '@tabler/icons-react';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
export default function Order() {
|
||||
const originalKeyword = {
|
||||
p: 0,
|
||||
user_id: '',
|
||||
trade_no: '',
|
||||
status: '',
|
||||
gateway_no: '',
|
||||
start_timestamp: 0,
|
||||
end_timestamp: dayjs().unix() + 3600
|
||||
};
|
||||
|
||||
const [page, setPage] = useState(0);
|
||||
const [order, setOrder] = useState('desc');
|
||||
const [orderBy, setOrderBy] = useState('created_at');
|
||||
const [rowsPerPage, setRowsPerPage] = useState(ITEMS_PER_PAGE);
|
||||
const [listCount, setListCount] = useState(0);
|
||||
const [searching, setSearching] = useState(false);
|
||||
const [toolBarValue, setToolBarValue] = useState(originalKeyword);
|
||||
const [searchKeyword, setSearchKeyword] = useState(originalKeyword);
|
||||
const [refreshFlag, setRefreshFlag] = useState(false);
|
||||
|
||||
const [orderList, setOrderList] = useState([]);
|
||||
|
||||
const handleSort = (event, id) => {
|
||||
const isAsc = orderBy === id && order === 'asc';
|
||||
if (id !== '') {
|
||||
setOrder(isAsc ? 'desc' : 'asc');
|
||||
setOrderBy(id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChangePage = (event, newPage) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const handleChangeRowsPerPage = (event) => {
|
||||
setPage(0);
|
||||
setRowsPerPage(parseInt(event.target.value, 10));
|
||||
};
|
||||
|
||||
const searchLogs = async () => {
|
||||
setPage(0);
|
||||
setSearchKeyword(toolBarValue);
|
||||
};
|
||||
|
||||
const handleToolBarValue = (event) => {
|
||||
setToolBarValue({ ...toolBarValue, [event.target.name]: event.target.value });
|
||||
};
|
||||
|
||||
const fetchData = useCallback(async (page, rowsPerPage, keyword, order, orderBy) => {
|
||||
setSearching(true);
|
||||
keyword = trims(keyword);
|
||||
try {
|
||||
if (orderBy) {
|
||||
orderBy = order === 'desc' ? '-' + orderBy : orderBy;
|
||||
}
|
||||
const res = await API.get('/api/payment/order', {
|
||||
params: {
|
||||
page: page + 1,
|
||||
size: rowsPerPage,
|
||||
order: orderBy,
|
||||
...keyword
|
||||
}
|
||||
});
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
setListCount(data.total_count);
|
||||
setOrderList(data.data);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
setSearching(false);
|
||||
}, []);
|
||||
|
||||
// 处理刷新
|
||||
const handleRefresh = async () => {
|
||||
setOrderBy('created_at');
|
||||
setOrder('desc');
|
||||
setToolBarValue(originalKeyword);
|
||||
setSearchKeyword(originalKeyword);
|
||||
setRefreshFlag(!refreshFlag);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData(page, rowsPerPage, searchKeyword, order, orderBy);
|
||||
}, [page, rowsPerPage, searchKeyword, order, orderBy, fetchData, refreshFlag]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack direction="row" alignItems="center" justifyContent="space-between" mb={5}>
|
||||
<Typography variant="h4">日志</Typography>
|
||||
</Stack>
|
||||
<Card>
|
||||
<Box component="form" noValidate>
|
||||
<TableToolBar filterName={toolBarValue} handleFilterName={handleToolBarValue} />
|
||||
</Box>
|
||||
<Toolbar
|
||||
sx={{
|
||||
textAlign: 'right',
|
||||
height: 50,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
p: (theme) => theme.spacing(0, 1, 0, 3)
|
||||
}}
|
||||
>
|
||||
<Container>
|
||||
<ButtonGroup variant="outlined" aria-label="outlined small primary button group">
|
||||
<Button onClick={handleRefresh} startIcon={<IconRefresh width={'18px'} />}>
|
||||
刷新/清除搜索条件
|
||||
</Button>
|
||||
|
||||
<Button onClick={searchLogs} startIcon={<IconSearch width={'18px'} />}>
|
||||
搜索
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Container>
|
||||
</Toolbar>
|
||||
{searching && <LinearProgress />}
|
||||
<PerfectScrollbar component="div">
|
||||
<TableContainer sx={{ overflow: 'unset' }}>
|
||||
<Table sx={{ minWidth: 800 }}>
|
||||
<KeywordTableHead
|
||||
order={order}
|
||||
orderBy={orderBy}
|
||||
onRequestSort={handleSort}
|
||||
headLabel={[
|
||||
{
|
||||
id: 'created_at',
|
||||
label: '时间',
|
||||
disableSort: false
|
||||
},
|
||||
{
|
||||
id: 'user_id',
|
||||
label: '用户',
|
||||
disableSort: false
|
||||
},
|
||||
{
|
||||
id: 'trade_no',
|
||||
label: '订单号',
|
||||
disableSort: true
|
||||
},
|
||||
{
|
||||
id: 'gateway_no',
|
||||
label: '网关订单号',
|
||||
disableSort: true
|
||||
},
|
||||
{
|
||||
id: 'amount',
|
||||
label: '充值金额',
|
||||
disableSort: true
|
||||
},
|
||||
{
|
||||
id: 'fee',
|
||||
label: '手续费',
|
||||
disableSort: true
|
||||
},
|
||||
{
|
||||
id: 'order_amount',
|
||||
label: '实际支付金额',
|
||||
disableSort: true
|
||||
},
|
||||
{
|
||||
id: 'quota',
|
||||
label: '到帐点数',
|
||||
disableSort: true
|
||||
},
|
||||
{
|
||||
id: 'status',
|
||||
label: '状态',
|
||||
disableSort: true
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<TableBody>
|
||||
{orderList.map((row, index) => (
|
||||
<LogTableRow item={row} key={`${row.id}_${index}`} />
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</PerfectScrollbar>
|
||||
<TablePagination
|
||||
page={page}
|
||||
component="div"
|
||||
count={listCount}
|
||||
rowsPerPage={rowsPerPage}
|
||||
onPageChange={handleChangePage}
|
||||
rowsPerPageOptions={[10, 25, 30]}
|
||||
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||
showFirstButton
|
||||
showLastButton
|
||||
/>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
359
web/src/views/Payment/component/EditModal.js
Normal file
359
web/src/views/Payment/component/EditModal.js
Normal file
@@ -0,0 +1,359 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import * as Yup from 'yup';
|
||||
import { Formik } from 'formik';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
Divider,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
OutlinedInput,
|
||||
TextField,
|
||||
Select,
|
||||
MenuItem,
|
||||
FormHelperText
|
||||
} from '@mui/material';
|
||||
|
||||
import { showSuccess, showError, trims } from 'utils/common';
|
||||
import { API } from 'utils/api';
|
||||
import { PaymentType, CurrencyType, PaymentConfig } from '../type/Config';
|
||||
|
||||
const validationSchema = Yup.object().shape({
|
||||
is_edit: Yup.boolean(),
|
||||
name: Yup.string().required('名称 不能为空'),
|
||||
icon: Yup.string().required('图标 不能为空'),
|
||||
fixed_fee: Yup.number().min(0, '固定手续费 不能小于 0'),
|
||||
percent_fee: Yup.number().min(0, '百分比手续费 不能小于 0'),
|
||||
currency: Yup.string().required('货币 不能为空')
|
||||
});
|
||||
|
||||
const originInputs = {
|
||||
is_edit: false,
|
||||
type: 'epay',
|
||||
uuid: '',
|
||||
name: '',
|
||||
icon: '',
|
||||
notify_domain: '',
|
||||
fixed_fee: 0,
|
||||
percent_fee: 0,
|
||||
currency: 'CNY',
|
||||
config: {},
|
||||
sort: 0,
|
||||
enable: true
|
||||
};
|
||||
|
||||
const EditModal = ({ open, paymentId, onCancel, onOk }) => {
|
||||
const theme = useTheme();
|
||||
const [inputs, setInputs] = useState(originInputs);
|
||||
|
||||
const submit = async (values, { setErrors, setStatus, setSubmitting }) => {
|
||||
setSubmitting(true);
|
||||
|
||||
let config = JSON.stringify(values.config);
|
||||
let res;
|
||||
values = trims(values);
|
||||
try {
|
||||
if (values.is_edit) {
|
||||
res = await API.put(`/api/payment/`, { ...values, id: parseInt(paymentId), config });
|
||||
} else {
|
||||
res = await API.post(`/api/payment/`, { ...values, config });
|
||||
}
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
if (values.is_edit) {
|
||||
showSuccess('更新成功!');
|
||||
} else {
|
||||
showSuccess('创建成功!');
|
||||
}
|
||||
setSubmitting(false);
|
||||
setStatus({ success: true });
|
||||
onOk(true);
|
||||
} else {
|
||||
showError(message);
|
||||
setErrors({ submit: message });
|
||||
}
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const loadPayment = async () => {
|
||||
try {
|
||||
let res = await API.get(`/api/payment/${paymentId}`);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
data.is_edit = true;
|
||||
data.config = JSON.parse(data.config);
|
||||
setInputs(data);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (paymentId) {
|
||||
loadPayment().then();
|
||||
} else {
|
||||
setInputs(originInputs);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [paymentId]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onCancel} fullWidth maxWidth={'md'}>
|
||||
<DialogTitle sx={{ margin: '0px', fontWeight: 700, lineHeight: '1.55556', padding: '24px', fontSize: '1.125rem' }}>
|
||||
{paymentId ? '编辑支付' : '新建支付'}
|
||||
</DialogTitle>
|
||||
<Divider />
|
||||
<DialogContent>
|
||||
<Formik initialValues={inputs} enableReinitialize validationSchema={validationSchema} onSubmit={submit}>
|
||||
{({ errors, handleBlur, handleChange, handleSubmit, touched, values, isSubmitting }) => (
|
||||
<form noValidate onSubmit={handleSubmit}>
|
||||
<FormControl fullWidth error={Boolean(touched.type && errors.type)} sx={{ ...theme.typography.otherInput }}>
|
||||
<InputLabel htmlFor="channel-type-label">类型</InputLabel>
|
||||
<Select
|
||||
id="channel-type-label"
|
||||
label="类型"
|
||||
value={values.type}
|
||||
name="type"
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
MenuProps={{
|
||||
PaperProps: {
|
||||
style: {
|
||||
maxHeight: 200
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{Object.entries(PaymentType).map(([value, text]) => (
|
||||
<MenuItem key={value} value={value}>
|
||||
{text}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
{touched.type && errors.type ? (
|
||||
<FormHelperText error id="helper-tex-channel-type-label">
|
||||
{errors.type}
|
||||
</FormHelperText>
|
||||
) : (
|
||||
<FormHelperText id="helper-tex-channel-type-label"> 支付类型 </FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
|
||||
<FormControl fullWidth error={Boolean(touched.name && errors.name)} sx={{ ...theme.typography.otherInput }}>
|
||||
<InputLabel htmlFor="channel-name-label">名称</InputLabel>
|
||||
<OutlinedInput
|
||||
id="channel-name-label"
|
||||
label="名称"
|
||||
type="text"
|
||||
value={values.name}
|
||||
name="name"
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
inputProps={{ autoComplete: 'name' }}
|
||||
aria-describedby="helper-text-channel-name-label"
|
||||
/>
|
||||
{touched.name && errors.name && (
|
||||
<FormHelperText error id="helper-tex-channel-name-label">
|
||||
{errors.name}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
|
||||
<FormControl fullWidth error={Boolean(touched.icon && errors.icon)} sx={{ ...theme.typography.otherInput }}>
|
||||
<InputLabel htmlFor="channel-icon-label">图标</InputLabel>
|
||||
<OutlinedInput
|
||||
id="channel-icon-label"
|
||||
label="图标"
|
||||
type="text"
|
||||
value={values.icon}
|
||||
name="icon"
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
inputProps={{ autoComplete: 'icon' }}
|
||||
aria-describedby="helper-text-channel-icon-label"
|
||||
/>
|
||||
{touched.icon && errors.icon && (
|
||||
<FormHelperText error id="helper-tex-channel-icon-label">
|
||||
{errors.icon}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
|
||||
<FormControl fullWidth error={Boolean(touched.notify_domain && errors.notify_domain)} sx={{ ...theme.typography.otherInput }}>
|
||||
<InputLabel htmlFor="channel-notify_domain-label">回调域名</InputLabel>
|
||||
<OutlinedInput
|
||||
id="channel-notify_domain-label"
|
||||
label="回调域名"
|
||||
type="text"
|
||||
value={values.notify_domain}
|
||||
name="notify_domain"
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
inputProps={{ autoComplete: 'notify_domain' }}
|
||||
aria-describedby="helper-text-channel-notify_domain-label"
|
||||
/>
|
||||
{touched.notify_domain && errors.notify_domain ? (
|
||||
<FormHelperText error id="helper-tex-notify_domain-label">
|
||||
{errors.notify_domain}
|
||||
</FormHelperText>
|
||||
) : (
|
||||
<FormHelperText id="helper-tex-notify_domain-label"> 支付回调的域名,除非你自行配置过,否则保持为空 </FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
|
||||
<FormControl fullWidth error={Boolean(touched.fixed_fee && errors.fixed_fee)} sx={{ ...theme.typography.otherInput }}>
|
||||
<InputLabel htmlFor="channel-fixed_fee-label">固定手续费</InputLabel>
|
||||
<OutlinedInput
|
||||
id="channel-fixed_fee-label"
|
||||
label="固定手续费"
|
||||
type="number"
|
||||
value={values.fixed_fee}
|
||||
name="fixed_fee"
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
inputProps={{ autoComplete: 'fixed_fee' }}
|
||||
aria-describedby="helper-text-channel-fixed_fee-label"
|
||||
/>
|
||||
{touched.fixed_fee && errors.fixed_fee ? (
|
||||
<FormHelperText error id="helper-tex-fixed_fee-label">
|
||||
{errors.fixed_fee}
|
||||
</FormHelperText>
|
||||
) : (
|
||||
<FormHelperText id="helper-tex-fixed_fee-label"> 每次支付收取固定的手续费,单位 美元 </FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
|
||||
<FormControl fullWidth error={Boolean(touched.percent_fee && errors.percent_fee)} sx={{ ...theme.typography.otherInput }}>
|
||||
<InputLabel htmlFor="channel-percent_fee-label">百分比手续费</InputLabel>
|
||||
<OutlinedInput
|
||||
id="channel-percent_fee-label"
|
||||
label="百分比手续费"
|
||||
type="number"
|
||||
value={values.percent_fee}
|
||||
name="percent_fee"
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
inputProps={{ autoComplete: 'percent_fee' }}
|
||||
aria-describedby="helper-text-channel-percent_fee-label"
|
||||
/>
|
||||
{touched.percent_fee && errors.percent_fee ? (
|
||||
<FormHelperText error id="helper-tex-percent_fee-label">
|
||||
{errors.percent_fee}
|
||||
</FormHelperText>
|
||||
) : (
|
||||
<FormHelperText id="helper-tex-percent_fee-label"> 每次支付按百分比收取手续费,如果为5%,请填写 0.05 </FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
|
||||
<FormControl fullWidth error={Boolean(touched.currency && errors.currency)} sx={{ ...theme.typography.otherInput }}>
|
||||
<InputLabel htmlFor="channel-currency-label">网关货币类型</InputLabel>
|
||||
<Select
|
||||
id="channel-currency-label"
|
||||
label="网关货币类型"
|
||||
value={values.currency}
|
||||
name="currency"
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
MenuProps={{
|
||||
PaperProps: {
|
||||
style: {
|
||||
maxHeight: 200
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{Object.entries(CurrencyType).map(([value, text]) => (
|
||||
<MenuItem key={value} value={value}>
|
||||
{text}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
{touched.currency && errors.currency ? (
|
||||
<FormHelperText error id="helper-tex-channel-currency-label">
|
||||
{errors.currency}
|
||||
</FormHelperText>
|
||||
) : (
|
||||
<FormHelperText id="helper-tex-channel-currency-label"> 该网关是收取什么货币的,请查询对应网关文档 </FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
|
||||
{PaymentConfig[values.type] &&
|
||||
Object.keys(PaymentConfig[values.type]).map((configKey) => {
|
||||
const param = PaymentConfig[values.type][configKey];
|
||||
const name = `config.${configKey}`;
|
||||
return param.type === 'select' ? (
|
||||
<FormControl key={name} fullWidth>
|
||||
<InputLabel htmlFor="channel-currency-label">{param.name}</InputLabel>
|
||||
<Select
|
||||
label={param.name}
|
||||
value={values.config?.[configKey] || ''}
|
||||
key={name}
|
||||
name={name}
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
MenuProps={{
|
||||
PaperProps: {
|
||||
style: {
|
||||
maxHeight: 200
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{Object.values(param.options).map((option) => {
|
||||
return (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
{option.name}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
<FormHelperText id="helper-tex-channel-currency-label"> {param.description} </FormHelperText>
|
||||
</FormControl>
|
||||
) : (
|
||||
<FormControl key={name} fullWidth sx={{ ...theme.typography.otherInput }}>
|
||||
<TextField
|
||||
multiline
|
||||
key={name}
|
||||
name={name}
|
||||
value={values.config?.[configKey] || ''}
|
||||
label={param.name}
|
||||
placeholder={param.description}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<FormHelperText id="helper-tex-channel-key-label"> {param.description} </FormHelperText>
|
||||
</FormControl>
|
||||
);
|
||||
})}
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={onCancel}>取消</Button>
|
||||
<Button disableElevation disabled={isSubmitting} type="submit" variant="contained" color="primary">
|
||||
提交
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</form>
|
||||
)}
|
||||
</Formik>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditModal;
|
||||
|
||||
EditModal.propTypes = {
|
||||
open: PropTypes.bool,
|
||||
paymentId: PropTypes.number,
|
||||
onCancel: PropTypes.func,
|
||||
onOk: PropTypes.func
|
||||
};
|
||||
45
web/src/views/Payment/component/OrderTableRow.js
Normal file
45
web/src/views/Payment/component/OrderTableRow.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { TableRow, TableCell } from '@mui/material';
|
||||
|
||||
import { timestamp2string } from 'utils/common';
|
||||
import Label from 'ui-component/Label';
|
||||
|
||||
const StatusType = {
|
||||
pending: { name: '待支付', value: 'pending', color: 'primary' },
|
||||
success: { name: '支付成功', value: 'success', color: 'success' },
|
||||
failed: { name: '支付失败', value: 'failed', color: 'error' },
|
||||
closed: { name: '已关闭', value: 'closed', color: 'default' }
|
||||
};
|
||||
|
||||
function statusLabel(status) {
|
||||
let statusOption = StatusType[status];
|
||||
|
||||
return <Label color={statusOption?.color || 'secondary'}> {statusOption?.name || '未知'} </Label>;
|
||||
}
|
||||
|
||||
export { StatusType };
|
||||
|
||||
export default function OrderTableRow({ item }) {
|
||||
return (
|
||||
<>
|
||||
<TableRow tabIndex={item.id}>
|
||||
<TableCell>{timestamp2string(item.created_at)}</TableCell>
|
||||
<TableCell>{item.user_id}</TableCell>
|
||||
<TableCell>{item.trade_no}</TableCell>
|
||||
<TableCell>{item.gateway_no}</TableCell>
|
||||
<TableCell>${item.amount}</TableCell>
|
||||
<TableCell>${item.fee}</TableCell>
|
||||
<TableCell>
|
||||
{item.order_amount} {item.order_currency}
|
||||
</TableCell>
|
||||
<TableCell>{item.quota}</TableCell>
|
||||
<TableCell>{statusLabel(item.status)}</TableCell>
|
||||
</TableRow>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
OrderTableRow.propTypes = {
|
||||
item: PropTypes.object
|
||||
};
|
||||
138
web/src/views/Payment/component/OrderTableToolBar.js
Normal file
138
web/src/views/Payment/component/OrderTableToolBar.js
Normal file
@@ -0,0 +1,138 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { OutlinedInput, Stack, FormControl, InputLabel, Select, MenuItem } from '@mui/material';
|
||||
import { LocalizationProvider, DateTimePicker } from '@mui/x-date-pickers';
|
||||
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
||||
import dayjs from 'dayjs';
|
||||
import { StatusType } from './OrderTableRow';
|
||||
require('dayjs/locale/zh-cn');
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export default function OrderTableToolBar({ filterName, handleFilterName }) {
|
||||
return (
|
||||
<>
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={{ xs: 3, sm: 2, md: 4 }} padding={'24px'} paddingBottom={'0px'}>
|
||||
<FormControl>
|
||||
<InputLabel htmlFor="channel-user_id-label">用户ID</InputLabel>
|
||||
<OutlinedInput
|
||||
id="user_id"
|
||||
name="user_id"
|
||||
sx={{
|
||||
minWidth: '100%'
|
||||
}}
|
||||
label="用户ID"
|
||||
value={filterName.user_id}
|
||||
onChange={handleFilterName}
|
||||
placeholder="用户ID"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<InputLabel htmlFor="channel-trade_no-label">订单号</InputLabel>
|
||||
<OutlinedInput
|
||||
id="trade_no"
|
||||
name="trade_no"
|
||||
sx={{
|
||||
minWidth: '100%'
|
||||
}}
|
||||
label="订单号"
|
||||
value={filterName.trade_no}
|
||||
onChange={handleFilterName}
|
||||
placeholder="订单号"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<InputLabel htmlFor="channel-gateway_no-label">网关订单号</InputLabel>
|
||||
<OutlinedInput
|
||||
id="gateway_no"
|
||||
name="gateway_no"
|
||||
sx={{
|
||||
minWidth: '100%'
|
||||
}}
|
||||
label="网关订单号"
|
||||
value={filterName.gateway_no}
|
||||
onChange={handleFilterName}
|
||||
placeholder="网关订单号"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale={'zh-cn'}>
|
||||
<DateTimePicker
|
||||
label="起始时间"
|
||||
ampm={false}
|
||||
name="start_timestamp"
|
||||
value={filterName.start_timestamp === 0 ? null : dayjs.unix(filterName.start_timestamp)}
|
||||
onChange={(value) => {
|
||||
if (value === null) {
|
||||
handleFilterName({ target: { name: 'start_timestamp', value: 0 } });
|
||||
return;
|
||||
}
|
||||
handleFilterName({ target: { name: 'start_timestamp', value: value.unix() } });
|
||||
}}
|
||||
slotProps={{
|
||||
actionBar: {
|
||||
actions: ['clear', 'today', 'accept']
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</LocalizationProvider>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale={'zh-cn'}>
|
||||
<DateTimePicker
|
||||
label="结束时间"
|
||||
name="end_timestamp"
|
||||
ampm={false}
|
||||
value={filterName.end_timestamp === 0 ? null : dayjs.unix(filterName.end_timestamp)}
|
||||
onChange={(value) => {
|
||||
if (value === null) {
|
||||
handleFilterName({ target: { name: 'end_timestamp', value: 0 } });
|
||||
return;
|
||||
}
|
||||
handleFilterName({ target: { name: 'end_timestamp', value: value.unix() } });
|
||||
}}
|
||||
slotProps={{
|
||||
actionBar: {
|
||||
actions: ['clear', 'today', 'accept']
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</LocalizationProvider>
|
||||
</FormControl>
|
||||
<FormControl sx={{ minWidth: '22%' }}>
|
||||
<InputLabel htmlFor="channel-status-label">状态</InputLabel>
|
||||
<Select
|
||||
id="channel-type-label"
|
||||
label="状态"
|
||||
value={filterName.status}
|
||||
name="status"
|
||||
onChange={handleFilterName}
|
||||
sx={{
|
||||
minWidth: '100%'
|
||||
}}
|
||||
MenuProps={{
|
||||
PaperProps: {
|
||||
style: {
|
||||
maxHeight: 200
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{Object.values(StatusType).map((option) => {
|
||||
return (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
{option.name}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
OrderTableToolBar.propTypes = {
|
||||
filterName: PropTypes.object,
|
||||
handleFilterName: PropTypes.func
|
||||
};
|
||||
153
web/src/views/Payment/component/TableRow.js
Normal file
153
web/src/views/Payment/component/TableRow.js
Normal file
@@ -0,0 +1,153 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { useState } from 'react';
|
||||
|
||||
import {
|
||||
TableRow,
|
||||
TableCell,
|
||||
Popover,
|
||||
MenuItem,
|
||||
IconButton,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogActions,
|
||||
Button,
|
||||
TextField
|
||||
} from '@mui/material';
|
||||
|
||||
import { IconDotsVertical, IconEdit, IconTrash } from '@tabler/icons-react';
|
||||
import { timestamp2string, showError } from 'utils/common';
|
||||
import { PaymentType } from '../type/Config';
|
||||
import TableSwitch from 'ui-component/Switch';
|
||||
|
||||
export default function PaymentTableRow({ item, managePayment, handleOpenModal, setModalPaymentId }) {
|
||||
const [open, setOpen] = useState(null);
|
||||
const [openDelete, setOpenDelete] = useState(false);
|
||||
const [sortValve, setSort] = useState(item.sort);
|
||||
|
||||
const handleCloseMenu = () => {
|
||||
setOpen(null);
|
||||
};
|
||||
const handleOpenMenu = (event) => {
|
||||
setOpen(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleDeleteOpen = () => {
|
||||
handleCloseMenu();
|
||||
setOpenDelete(true);
|
||||
};
|
||||
|
||||
const handleDeleteClose = () => {
|
||||
setOpenDelete(false);
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
handleCloseMenu();
|
||||
await managePayment(item.id, 'delete', '');
|
||||
};
|
||||
|
||||
const handleSort = async (event) => {
|
||||
const currentValue = parseInt(event.target.value);
|
||||
if (isNaN(currentValue) || currentValue === sortValve) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentValue < 0) {
|
||||
showError('排序不能小于 0');
|
||||
return;
|
||||
}
|
||||
|
||||
await managePayment(item.id, 'sort', currentValue);
|
||||
setSort(currentValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableRow tabIndex={item.id}>
|
||||
<TableCell>{item.id}</TableCell>
|
||||
<TableCell>{item.uuid}</TableCell>
|
||||
<TableCell>{item.name}</TableCell>
|
||||
<TableCell>{PaymentType?.[item.type] || '未知'}</TableCell>
|
||||
<TableCell>
|
||||
<img src={item.icon} alt="icon" style={{ width: '24px', height: '24px' }} />
|
||||
</TableCell>
|
||||
<TableCell>{item.fixed_fee}</TableCell>
|
||||
<TableCell>{item.percent_fee}</TableCell>
|
||||
<TableCell>
|
||||
<TextField
|
||||
id={`sort-${item.id}`}
|
||||
onBlur={handleSort}
|
||||
type="number"
|
||||
label="排序"
|
||||
variant="standard"
|
||||
defaultValue={item.sort}
|
||||
inputProps={{ min: '0' }}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TableSwitch
|
||||
id={`switch-${item.id}`}
|
||||
checked={item.enable}
|
||||
onChange={() => {
|
||||
managePayment(item.id, 'status', !item.enable);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{timestamp2string(item.created_at)}</TableCell>
|
||||
<TableCell>
|
||||
<IconButton onClick={handleOpenMenu} sx={{ color: 'rgb(99, 115, 129)' }}>
|
||||
<IconDotsVertical />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
<Popover
|
||||
open={!!open}
|
||||
anchorEl={open}
|
||||
onClose={handleCloseMenu}
|
||||
anchorOrigin={{ vertical: 'top', horizontal: 'left' }}
|
||||
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||
PaperProps={{
|
||||
sx: { minWidth: 140 }
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
handleCloseMenu();
|
||||
handleOpenModal();
|
||||
setModalPaymentId(item.id);
|
||||
}}
|
||||
>
|
||||
<IconEdit style={{ marginRight: '16px' }} />
|
||||
编辑
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem onClick={handleDeleteOpen} sx={{ color: 'error.main' }}>
|
||||
<IconTrash style={{ marginRight: '16px' }} />
|
||||
删除
|
||||
</MenuItem>
|
||||
</Popover>
|
||||
|
||||
<Dialog open={openDelete} onClose={handleDeleteClose}>
|
||||
<DialogTitle>删除通道</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>是否删除通道 {item.name}?</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleDeleteClose}>关闭</Button>
|
||||
<Button onClick={handleDelete} sx={{ color: 'error.main' }} autoFocus>
|
||||
删除
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
PaymentTableRow.propTypes = {
|
||||
item: PropTypes.object,
|
||||
managePayment: PropTypes.func,
|
||||
handleOpenModal: PropTypes.func,
|
||||
setModalPaymentId: PropTypes.func
|
||||
};
|
||||
73
web/src/views/Payment/component/TableToolBar.js
Normal file
73
web/src/views/Payment/component/TableToolBar.js
Normal file
@@ -0,0 +1,73 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { OutlinedInput, Stack, FormControl, InputLabel, Select, MenuItem } from '@mui/material';
|
||||
import { PaymentType } from '../type/Config';
|
||||
require('dayjs/locale/zh-cn');
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export default function TableToolBar({ filterName, handleFilterName }) {
|
||||
return (
|
||||
<>
|
||||
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={{ xs: 3, sm: 2, md: 4 }} padding={'24px'} paddingBottom={'0px'}>
|
||||
<FormControl>
|
||||
<InputLabel htmlFor="channel-name-label">名称</InputLabel>
|
||||
<OutlinedInput
|
||||
id="name"
|
||||
name="name"
|
||||
sx={{
|
||||
minWidth: '100%'
|
||||
}}
|
||||
label="名称"
|
||||
value={filterName.name}
|
||||
onChange={handleFilterName}
|
||||
placeholder="名称"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<InputLabel htmlFor="channel-uuid-label">UUID</InputLabel>
|
||||
<OutlinedInput
|
||||
id="uuid"
|
||||
name="uuid"
|
||||
sx={{
|
||||
minWidth: '100%'
|
||||
}}
|
||||
label="模型名称"
|
||||
value={filterName.uuid}
|
||||
onChange={handleFilterName}
|
||||
placeholder="UUID"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl sx={{ minWidth: '22%' }}>
|
||||
<InputLabel htmlFor="channel-type-label">类型</InputLabel>
|
||||
<Select
|
||||
id="channel-type-label"
|
||||
label="类型"
|
||||
value={filterName.type}
|
||||
name="type"
|
||||
onChange={handleFilterName}
|
||||
sx={{
|
||||
minWidth: '100%'
|
||||
}}
|
||||
MenuProps={{
|
||||
PaperProps: {
|
||||
style: {
|
||||
maxHeight: 200
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{Object.entries(PaymentType).map(([value, text]) => (
|
||||
<MenuItem key={value} value={value}>
|
||||
{text}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
TableToolBar.propTypes = {
|
||||
filterName: PropTypes.object,
|
||||
handleFilterName: PropTypes.func
|
||||
};
|
||||
86
web/src/views/Payment/index.js
Normal file
86
web/src/views/Payment/index.js
Normal file
@@ -0,0 +1,86 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Tabs, Tab, Box, Card } from '@mui/material';
|
||||
import Gateway from './Gateway';
|
||||
import Order from './Order';
|
||||
import AdminContainer from 'ui-component/AdminContainer';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
function CustomTabPanel(props) {
|
||||
const { children, value, index, ...other } = props;
|
||||
|
||||
return (
|
||||
<div role="tabpanel" hidden={value !== index} id={`setting-tabpanel-${index}`} aria-labelledby={`setting-tab-${index}`} {...other}>
|
||||
{value === index && <Box sx={{ p: 3 }}>{children}</Box>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
CustomTabPanel.propTypes = {
|
||||
children: PropTypes.node,
|
||||
index: PropTypes.number.isRequired,
|
||||
value: PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
function a11yProps(index) {
|
||||
return {
|
||||
id: `setting-tab-${index}`,
|
||||
'aria-controls': `setting-tabpanel-${index}`
|
||||
};
|
||||
}
|
||||
|
||||
const Payment = () => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const hash = location.hash.replace('#', '');
|
||||
const tabMap = useMemo(
|
||||
() => ({
|
||||
order: 0,
|
||||
gateway: 1
|
||||
}),
|
||||
[]
|
||||
);
|
||||
const [value, setValue] = useState(tabMap[hash] || 0);
|
||||
|
||||
const handleChange = (event, newValue) => {
|
||||
setValue(newValue);
|
||||
const hashArray = Object.keys(tabMap);
|
||||
navigate(`#${hashArray[newValue]}`);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleHashChange = () => {
|
||||
const hash = location.hash.replace('#', '');
|
||||
setValue(tabMap[hash] || 0);
|
||||
};
|
||||
window.addEventListener('hashchange', handleHashChange);
|
||||
return () => {
|
||||
window.removeEventListener('hashchange', handleHashChange);
|
||||
};
|
||||
}, [location, tabMap]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card>
|
||||
<AdminContainer>
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
||||
<Tabs value={value} onChange={handleChange} variant="scrollable" scrollButtons="auto">
|
||||
<Tab label="订单列表" {...a11yProps(0)} />
|
||||
<Tab label="网关设置" {...a11yProps(1)} />
|
||||
</Tabs>
|
||||
</Box>
|
||||
<CustomTabPanel value={value} index={0}>
|
||||
<Order />
|
||||
</CustomTabPanel>
|
||||
<CustomTabPanel value={value} index={1}>
|
||||
<Gateway />
|
||||
</CustomTabPanel>
|
||||
</Box>
|
||||
</AdminContainer>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Payment;
|
||||
73
web/src/views/Payment/type/Config.js
Normal file
73
web/src/views/Payment/type/Config.js
Normal file
@@ -0,0 +1,73 @@
|
||||
const PaymentType = {
|
||||
epay: '易支付'
|
||||
};
|
||||
|
||||
const CurrencyType = {
|
||||
CNY: '人民币',
|
||||
USD: '美元'
|
||||
};
|
||||
|
||||
const PaymentConfig = {
|
||||
epay: {
|
||||
pay_domain: {
|
||||
name: '支付域名',
|
||||
description: '支付域名',
|
||||
type: 'text',
|
||||
value: ''
|
||||
},
|
||||
partner_id: {
|
||||
name: '商户号',
|
||||
description: '商户号',
|
||||
type: 'text',
|
||||
value: ''
|
||||
},
|
||||
key: {
|
||||
name: '密钥',
|
||||
description: '密钥',
|
||||
type: 'text',
|
||||
value: ''
|
||||
},
|
||||
pay_type: {
|
||||
name: '支付类型',
|
||||
description: '支付类型,如果需要跳转到易支付收银台,请选择收银台',
|
||||
type: 'select',
|
||||
value: '',
|
||||
options: [
|
||||
{
|
||||
name: '收银台',
|
||||
value: ''
|
||||
},
|
||||
{
|
||||
name: '支付宝',
|
||||
value: 'alipay'
|
||||
},
|
||||
{
|
||||
name: '微信',
|
||||
value: 'wxpay'
|
||||
},
|
||||
{
|
||||
name: 'QQ',
|
||||
value: 'qqpay'
|
||||
},
|
||||
{
|
||||
name: '京东',
|
||||
value: 'jdpay'
|
||||
},
|
||||
{
|
||||
name: '银联',
|
||||
value: 'bank'
|
||||
},
|
||||
{
|
||||
name: 'Paypal',
|
||||
value: 'paypal'
|
||||
},
|
||||
{
|
||||
name: 'USDT',
|
||||
value: 'usdt'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export { PaymentConfig, PaymentType, CurrencyType };
|
||||
@@ -36,7 +36,9 @@ const OperationSetting = () => {
|
||||
MjNotifyEnabled: '',
|
||||
ChatCacheEnabled: '',
|
||||
ChatCacheExpireMinute: 5,
|
||||
ChatImageRequestProxy: ''
|
||||
ChatImageRequestProxy: '',
|
||||
PaymentUSDRate: 0,
|
||||
PaymentMinAmount: 1
|
||||
});
|
||||
const [originInputs, setOriginInputs] = useState({});
|
||||
let [loading, setLoading] = useState(false);
|
||||
@@ -178,6 +180,14 @@ const OperationSetting = () => {
|
||||
await updateOption('ChatImageRequestProxy', inputs.ChatImageRequestProxy);
|
||||
}
|
||||
break;
|
||||
case 'payment':
|
||||
if (originInputs['PaymentUSDRate'] !== inputs.PaymentUSDRate) {
|
||||
await updateOption('PaymentUSDRate', inputs.PaymentUSDRate);
|
||||
}
|
||||
if (originInputs['PaymentMinAmount'] !== inputs.PaymentMinAmount) {
|
||||
await updateOption('PaymentMinAmount', inputs.PaymentMinAmount);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
showSuccess('保存成功!');
|
||||
@@ -532,6 +542,56 @@ const OperationSetting = () => {
|
||||
</Button>
|
||||
</Stack>
|
||||
</SubCard>
|
||||
<SubCard title="支付设置">
|
||||
<Stack justifyContent="flex-start" alignItems="flex-start" spacing={2}>
|
||||
<FormControl fullWidth>
|
||||
<Alert severity="info">
|
||||
支付设置: <br />
|
||||
1. 美元汇率:用于计算充值金额的美元金额 <br />
|
||||
2. 最低充值金额(美元):最低充值金额,单位为美元,填写整数 <br />
|
||||
3. 页面都以美元为单位计算,实际用户支付的货币,按照支付网关设置的货币进行转换 <br />
|
||||
例如: A 网关设置货币为 CNY,用户支付 100 美元,那么实际支付金额为 100 * 美元汇率 <br />B 网关设置货币为 USD,用户支付 100
|
||||
美元,那么实际支付金额为 100 美元
|
||||
</Alert>
|
||||
</FormControl>
|
||||
<Stack direction={{ sm: 'column', md: 'row' }} spacing={{ xs: 3, sm: 2, md: 4 }}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel htmlFor="PaymentUSDRate">美元汇率</InputLabel>
|
||||
<OutlinedInput
|
||||
id="PaymentUSDRate"
|
||||
name="PaymentUSDRate"
|
||||
type="number"
|
||||
value={inputs.PaymentUSDRate}
|
||||
onChange={handleInputChange}
|
||||
label="美元汇率"
|
||||
placeholder="例如:7.3"
|
||||
disabled={loading}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel htmlFor="PaymentMinAmount">最低充值金额(美元)</InputLabel>
|
||||
<OutlinedInput
|
||||
id="PaymentMinAmount"
|
||||
name="PaymentMinAmount"
|
||||
type="number"
|
||||
value={inputs.PaymentMinAmount}
|
||||
onChange={handleInputChange}
|
||||
label="最低充值金额(美元)"
|
||||
placeholder="例如:1,那么最低充值金额为1美元,请填写整数"
|
||||
disabled={loading}
|
||||
/>
|
||||
</FormControl>
|
||||
</Stack>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
submitConfig('payment').then();
|
||||
}}
|
||||
>
|
||||
保存支付设置
|
||||
</Button>
|
||||
</Stack>
|
||||
</SubCard>
|
||||
<SubCard title="倍率设置">
|
||||
<Stack justifyContent="flex-start" alignItems="flex-start" spacing={2}>
|
||||
<FormControl fullWidth>
|
||||
|
||||
129
web/src/views/Topup/component/PayDialog.js
Normal file
129
web/src/views/Topup/component/PayDialog.js
Normal file
@@ -0,0 +1,129 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Dialog, DialogContent, DialogTitle, IconButton, Stack, Typography } from '@mui/material';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { QRCode } from 'react-qrcode-logo';
|
||||
import successSvg from 'assets/images/success.svg';
|
||||
import { API } from 'utils/api';
|
||||
import { showError } from 'utils/common';
|
||||
|
||||
const PayDialog = ({ open, onClose, amount, uuid }) => {
|
||||
const theme = useTheme();
|
||||
const defaultLogo = theme.palette.mode === 'light' ? '/logo-loading.svg' : '/logo-loading-white.svg';
|
||||
const [message, setMessage] = useState('正在拉起支付中...');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [qrCodeUrl, setQrCodeUrl] = useState(null);
|
||||
const [success, setSuccess] = useState(false);
|
||||
const [intervalId, setIntervalId] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
return;
|
||||
}
|
||||
setMessage('正在拉起支付中...');
|
||||
setLoading(true);
|
||||
|
||||
API.post('/api/user/order', {
|
||||
uuid: uuid,
|
||||
amount: Number(amount)
|
||||
}).then((response) => {
|
||||
if (!response.data.success) {
|
||||
showError(response.data.message);
|
||||
setLoading(false);
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
|
||||
const { type, data } = response.data.data;
|
||||
if (type === 1) {
|
||||
setMessage('等待支付中...');
|
||||
const form = document.createElement('form');
|
||||
form.method = data.method;
|
||||
form.action = data.url;
|
||||
form.target = '_blank';
|
||||
for (const key in data.params) {
|
||||
const input = document.createElement('input');
|
||||
input.name = key;
|
||||
input.value = data.params[key];
|
||||
form.appendChild(input);
|
||||
}
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
document.body.removeChild(form);
|
||||
} else if (type === 2) {
|
||||
setQrCodeUrl(data.url);
|
||||
setLoading(false);
|
||||
setMessage('请扫码支付');
|
||||
}
|
||||
pollOrderStatus(response.data.data.trade_no);
|
||||
});
|
||||
}, [open, onClose, amount, uuid]);
|
||||
|
||||
const pollOrderStatus = (tradeNo) => {
|
||||
const id = setInterval(() => {
|
||||
API.get(`/api/user/order/status?trade_no=${tradeNo}`).then((response) => {
|
||||
if (response.data.success) {
|
||||
setMessage('支付成功');
|
||||
setLoading(false);
|
||||
setSuccess(true);
|
||||
clearInterval(id);
|
||||
setIntervalId(null);
|
||||
}
|
||||
});
|
||||
}, 3000);
|
||||
setIntervalId(id);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} fullWidth maxWidth={'sm'} disableEscapeKeyDown>
|
||||
<DialogTitle sx={{ margin: '0px', fontWeight: 700, lineHeight: '1.55556', padding: '24px', fontSize: '1.125rem' }}>支付</DialogTitle>
|
||||
<IconButton
|
||||
aria-label="close"
|
||||
onClick={() => {
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId);
|
||||
setIntervalId(null);
|
||||
}
|
||||
onClose();
|
||||
}}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
right: 8,
|
||||
top: 8,
|
||||
color: (theme) => theme.palette.grey[500]
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
<DialogContent>
|
||||
<DialogContent>
|
||||
<Stack direction="column" justifyContent="center" alignItems="center" spacing={2}>
|
||||
{loading && <img src={defaultLogo} alt="loading" height="100" />}
|
||||
{qrCodeUrl && (
|
||||
<QRCode
|
||||
value={qrCodeUrl}
|
||||
size={256}
|
||||
qrStyle="dots"
|
||||
eyeRadius={20}
|
||||
fgColor={theme.palette.primary.main}
|
||||
bgColor={theme.palette.background.paper}
|
||||
/>
|
||||
)}
|
||||
{success && <img src={successSvg} alt="success" height="100" />}
|
||||
<Typography variant="h3">{message}</Typography>
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default PayDialog;
|
||||
|
||||
PayDialog.propTypes = {
|
||||
open: PropTypes.bool,
|
||||
onClose: PropTypes.func,
|
||||
amount: PropTypes.number,
|
||||
uuid: PropTypes.string
|
||||
};
|
||||
@@ -1,8 +1,24 @@
|
||||
import { Typography, Stack, OutlinedInput, InputAdornment, Button, InputLabel, FormControl } from '@mui/material';
|
||||
import {
|
||||
Typography,
|
||||
Stack,
|
||||
OutlinedInput,
|
||||
InputAdornment,
|
||||
Button,
|
||||
InputLabel,
|
||||
FormControl,
|
||||
useMediaQuery,
|
||||
TextField,
|
||||
Box,
|
||||
Grid,
|
||||
Divider
|
||||
} from '@mui/material';
|
||||
import { IconBuildingBank } from '@tabler/icons-react';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import SubCard from 'ui-component/cards/SubCard';
|
||||
import UserCard from 'ui-component/cards/UserCard';
|
||||
import AnimateButton from 'ui-component/extended/AnimateButton';
|
||||
import { useSelector } from 'react-redux';
|
||||
import PayDialog from './PayDialog';
|
||||
|
||||
import { API } from 'utils/api';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
@@ -11,10 +27,17 @@ import { showError, showInfo, showSuccess, renderQuota, trims } from 'utils/comm
|
||||
const TopupCard = () => {
|
||||
const theme = useTheme();
|
||||
const [redemptionCode, setRedemptionCode] = useState('');
|
||||
const [topUpLink, setTopUpLink] = useState('');
|
||||
const [userQuota, setUserQuota] = useState(0);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const [payment, setPayment] = useState([]);
|
||||
const [selectedPayment, setSelectedPayment] = useState(null);
|
||||
const [amount, setAmount] = useState(0);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const matchDownSM = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const siteInfo = useSelector((state) => state.siteInfo);
|
||||
|
||||
const topUp = async () => {
|
||||
if (redemptionCode === '') {
|
||||
showInfo('请输入充值码!');
|
||||
@@ -42,12 +65,53 @@ const TopupCard = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handlePay = () => {
|
||||
if (!selectedPayment) {
|
||||
showError('请选择支付方式');
|
||||
return;
|
||||
}
|
||||
|
||||
if (amount <= 0 || amount < siteInfo.PaymentMinAmount) {
|
||||
showError('金额不能小于' + siteInfo.PaymentMinAmount);
|
||||
return;
|
||||
}
|
||||
|
||||
if (amount > 1000000) {
|
||||
showError('金额不能大于1000000');
|
||||
return;
|
||||
}
|
||||
|
||||
// 判读金额是否是正整数
|
||||
if (!/^[1-9]\d*$/.test(amount)) {
|
||||
showError('请输入正整数金额');
|
||||
return;
|
||||
}
|
||||
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const getPayment = async () => {
|
||||
try {
|
||||
let res = await API.get(`/api/user/payment`);
|
||||
const { success, data } = res.data;
|
||||
if (success) {
|
||||
if (data.length > 0) {
|
||||
data.sort((a, b) => b.sort - a.sort);
|
||||
setPayment(data);
|
||||
setSelectedPayment(data[0]);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const openTopUpLink = () => {
|
||||
if (!topUpLink) {
|
||||
if (!siteInfo.top_up_link) {
|
||||
showError('超级管理员未设置充值链接!');
|
||||
return;
|
||||
}
|
||||
window.open(topUpLink, '_blank');
|
||||
window.open(siteInfo.top_up_link, '_blank');
|
||||
};
|
||||
|
||||
const getUserQuota = async () => {
|
||||
@@ -64,14 +128,36 @@ const TopupCard = () => {
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let status = localStorage.getItem('siteInfo');
|
||||
if (status) {
|
||||
status = JSON.parse(status);
|
||||
if (status.top_up_link) {
|
||||
setTopUpLink(status.top_up_link);
|
||||
}
|
||||
const handlePaymentSelect = (payment) => {
|
||||
setSelectedPayment(payment);
|
||||
};
|
||||
|
||||
const handleAmountChange = (event) => {
|
||||
const value = event.target.value;
|
||||
setAmount(value);
|
||||
};
|
||||
const calculateFee = () => {
|
||||
if (!selectedPayment) return 0;
|
||||
|
||||
if (selectedPayment.fixed_fee > 0) {
|
||||
return Number(selectedPayment.fixed_fee);
|
||||
}
|
||||
|
||||
return parseFloat(selectedPayment.percent_fee * Number(amount)).toFixed(2);
|
||||
};
|
||||
|
||||
const calculateTotal = () => {
|
||||
if (amount === 0) return 0;
|
||||
|
||||
let total = Number(amount) + Number(calculateFee());
|
||||
if (selectedPayment && selectedPayment.currency === 'CNY') {
|
||||
total = parseFloat((total * 7.3).toFixed(2));
|
||||
}
|
||||
return total;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getPayment().then();
|
||||
getUserQuota().then();
|
||||
}, []);
|
||||
|
||||
@@ -82,10 +168,90 @@ const TopupCard = () => {
|
||||
<Typography variant="h4">当前额度:</Typography>
|
||||
<Typography variant="h4">{renderQuota(userQuota)}</Typography>
|
||||
</Stack>
|
||||
|
||||
{payment.length > 0 && (
|
||||
<SubCard
|
||||
sx={{
|
||||
marginTop: '40px'
|
||||
}}
|
||||
title="在线充值"
|
||||
>
|
||||
<Stack spacing={2}>
|
||||
{payment.map((item, index) => (
|
||||
<AnimateButton key={index}>
|
||||
<Button
|
||||
disableElevation
|
||||
fullWidth
|
||||
size="large"
|
||||
variant="outlined"
|
||||
onClick={() => handlePaymentSelect(item)}
|
||||
sx={{
|
||||
...theme.typography.LoginButton,
|
||||
border: selectedPayment === item ? `1px solid ${theme.palette.primary.main}` : '1px solid transparent'
|
||||
}}
|
||||
>
|
||||
<Box sx={{ mr: { xs: 1, sm: 2, width: 20 }, display: 'flex', alignItems: 'center' }}>
|
||||
<img src={item.icon} alt="github" width={25} height={25} style={{ marginRight: matchDownSM ? 8 : 16 }} />
|
||||
</Box>
|
||||
{item.name}
|
||||
</Button>
|
||||
</AnimateButton>
|
||||
))}
|
||||
<TextField label="金额" type="number" onChange={handleAmountChange} value={amount} />
|
||||
<Divider />
|
||||
<Grid container direction="row" justifyContent="flex-end" spacing={2}>
|
||||
<Grid item xs={9}>
|
||||
<Typography variant="h6" style={{ textAlign: 'right', fontSize: '0.875rem' }}>
|
||||
充值金额:{' '}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={3}>
|
||||
${Number(amount)}
|
||||
</Grid>
|
||||
{selectedPayment && (selectedPayment.percent_fee > 0 || selectedPayment.fixed_fee > 0) && (
|
||||
<>
|
||||
<Grid item xs={9}>
|
||||
<Typography variant="h6" style={{ textAlign: 'right', fontSize: '0.875rem' }}>
|
||||
手续费:
|
||||
{selectedPayment &&
|
||||
(selectedPayment.fixed_fee > 0
|
||||
? '(固定)'
|
||||
: selectedPayment.percent_fee > 0
|
||||
? `(${selectedPayment.percent_fee * 100}%)`
|
||||
: '')}{' '}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={3}>
|
||||
${calculateFee()}
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Grid item xs={9}>
|
||||
<Typography variant="h6" style={{ textAlign: 'right', fontSize: '0.875rem' }}>
|
||||
实际支付金额:{' '}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={3}>
|
||||
{calculateTotal()}{' '}
|
||||
{selectedPayment &&
|
||||
(selectedPayment.currency === 'CNY' ? `CNY (汇率:${siteInfo.PaymentUSDRate})` : selectedPayment.currency)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Divider />
|
||||
<Button variant="contained" onClick={handlePay}>
|
||||
充值
|
||||
</Button>
|
||||
</Stack>
|
||||
<PayDialog open={open} onClose={() => setOpen(false)} amount={amount} uuid={selectedPayment.uuid} />
|
||||
</SubCard>
|
||||
)}
|
||||
|
||||
<SubCard
|
||||
sx={{
|
||||
marginTop: '40px'
|
||||
}}
|
||||
title="兑换码充值"
|
||||
>
|
||||
<FormControl fullWidth variant="outlined">
|
||||
<InputLabel htmlFor="key">兑换码</InputLabel>
|
||||
|
||||
@@ -6709,6 +6709,11 @@ lodash.debounce@^4.0.8:
|
||||
resolved "https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
|
||||
integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==
|
||||
|
||||
lodash.isequal@^4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.npmmirror.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
|
||||
integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==
|
||||
|
||||
lodash.memoize@^4.1.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.npmmirror.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
|
||||
@@ -8077,6 +8082,11 @@ q@^1.1.2:
|
||||
resolved "https://registry.npmmirror.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
|
||||
integrity sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==
|
||||
|
||||
qrcode-generator@^1.4.4:
|
||||
version "1.4.4"
|
||||
resolved "https://registry.npmmirror.com/qrcode-generator/-/qrcode-generator-1.4.4.tgz#63f771224854759329a99048806a53ed278740e7"
|
||||
integrity sha512-HM7yY8O2ilqhmULxGMpcHSF1EhJJ9yBj8gvDEuZ6M+KGJ0YY2hKpnXvRD+hZPLrDVck3ExIGhmPtSdcjC+guuw==
|
||||
|
||||
qs@6.11.0:
|
||||
version "6.11.0"
|
||||
resolved "https://registry.npmmirror.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a"
|
||||
@@ -8220,6 +8230,14 @@ react-perfect-scrollbar@^1.5.8:
|
||||
perfect-scrollbar "^1.5.0"
|
||||
prop-types "^15.6.1"
|
||||
|
||||
react-qrcode-logo@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmmirror.com/react-qrcode-logo/-/react-qrcode-logo-3.0.0.tgz#71cb43ddef9b338cc151800968276ca210086d11"
|
||||
integrity sha512-2+vZ3GNBdUpYxIKyt6SFZsDGXa0xniyUQ0wPI4O0hJTzRjttPIx1pPnH9IWQmp/4nDMoN47IBhi3Breu1KudYw==
|
||||
dependencies:
|
||||
lodash.isequal "^4.5.0"
|
||||
qrcode-generator "^1.4.4"
|
||||
|
||||
react-redux@^9.1.0:
|
||||
version "9.1.0"
|
||||
resolved "https://registry.npmmirror.com/react-redux/-/react-redux-9.1.0.tgz#46a46d4cfed4e534ce5452bb39ba18e1d98a8197"
|
||||
@@ -9000,7 +9018,16 @@ string-natural-compare@^3.0.1:
|
||||
resolved "https://registry.npmmirror.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
|
||||
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0:
|
||||
"string-width-cjs@npm:string-width@^4.2.0":
|
||||
version "4.2.3"
|
||||
resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
string-width@^4.1.0, string-width@^4.2.0:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
@@ -9087,7 +9114,14 @@ stringify-object@^3.3.0:
|
||||
is-obj "^1.0.1"
|
||||
is-regexp "^1.0.0"
|
||||
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
@@ -10198,7 +10232,16 @@ workbox-window@6.6.1:
|
||||
"@types/trusted-types" "^2.0.2"
|
||||
workbox-core "6.6.1"
|
||||
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
||||
version "7.0.0"
|
||||
resolved "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
|
||||
Reference in New Issue
Block a user