diff --git a/controller/token.go b/controller/token.go
index fa42c00..0f37fd0 100644
--- a/controller/token.go
+++ b/controller/token.go
@@ -39,7 +39,8 @@ func GetAllTokens(c *gin.Context) {
func SearchTokens(c *gin.Context) {
userId := c.GetInt("id")
keyword := c.Query("keyword")
- tokens, err := model.SearchUserTokens(userId, keyword)
+ token := c.Query("token")
+ tokens, err := model.SearchUserTokens(userId, keyword, token)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
diff --git a/model/token.go b/model/token.go
index 0fa984d..9121ca4 100644
--- a/model/token.go
+++ b/model/token.go
@@ -5,6 +5,7 @@ import (
"fmt"
"gorm.io/gorm"
"one-api/common"
+ "strings"
)
type Token struct {
@@ -28,8 +29,11 @@ func GetAllUserTokens(userId int, startIdx int, num int) ([]*Token, error) {
return tokens, err
}
-func SearchUserTokens(userId int, keyword string) (tokens []*Token, err error) {
- err = DB.Where("user_id = ?", userId).Where("name LIKE ?", keyword+"%").Find(&tokens).Error
+func SearchUserTokens(userId int, keyword string, token string) (tokens []*Token, err error) {
+ if token != "" {
+ token = strings.Trim(token, "sk-")
+ }
+ err = DB.Where("user_id = ?", userId).Where("name LIKE ? or key LIKE ?", keyword+"%", token+"%").Find(&tokens).Error
return tokens, err
}
diff --git a/web/src/components/RedemptionsTable.js b/web/src/components/RedemptionsTable.js
index 7505bb7..c7205bf 100644
--- a/web/src/components/RedemptionsTable.js
+++ b/web/src/components/RedemptionsTable.js
@@ -1,326 +1,375 @@
-import React, { useEffect, useState } from 'react';
-import { Form, Label, Popup, Pagination } from 'semantic-ui-react';
-import { Link } from 'react-router-dom';
-import { API, copy, showError, showInfo, showSuccess, showWarning, timestamp2string } from '../helpers';
+import React, {useEffect, useState} from 'react';
+import {Link} from 'react-router-dom';
+import {API, copy, showError, showInfo, showSuccess, showWarning, timestamp2string} from '../helpers';
-import { ITEMS_PER_PAGE } from '../constants';
-import { renderQuota } from '../helpers/render';
-import {Button, Modal, Popconfirm, Popover, Table, Tag} from "@douyinfe/semi-ui";
+import {ITEMS_PER_PAGE} from '../constants';
+import {renderQuota} from '../helpers/render';
+import {Button, Modal, Popconfirm, Popover, Table, Tag, Form} from "@douyinfe/semi-ui";
+import EditRedemption from "../pages/Redemption/EditRedemption";
+import editRedemption from "../pages/Redemption/EditRedemption";
function renderTimestamp(timestamp) {
- return (
- <>
- {timestamp2string(timestamp)}
- >
- );
+ return (
+ <>
+ {timestamp2string(timestamp)}
+ >
+ );
}
function renderStatus(status) {
- switch (status) {
- case 1:
- return 未使用;
- case 2:
- return 已禁用 ;
- case 3:
- return 已使用 ;
- default:
- return 未知状态 ;
- }
+ switch (status) {
+ case 1:
+ return 未使用;
+ case 2:
+ return 已禁用 ;
+ case 3:
+ return 已使用 ;
+ default:
+ return 未知状态 ;
+ }
}
const RedemptionsTable = () => {
- const columns = [
- {
- title: 'ID',
- dataIndex: 'id',
- },
- {
- title: '名称',
- dataIndex: 'name',
- },
- {
- title: '状态',
- dataIndex: 'status',
- key: 'status',
- render: (text, record, index) => {
- return (
-
- {renderStatus(text)}
-
- );
- },
- },
- {
- title: '额度',
- dataIndex: 'quota',
- render: (text, record, index) => {
- return (
-
- {renderQuota(parseInt(text))}
-
- );
- },
- },
- {
- title: '创建时间',
- dataIndex: 'created_time',
- render: (text, record, index) => {
- return (
-
- {renderTimestamp(text)}
-
- );
- },
- },
- {
- title: '',
- dataIndex: 'operate',
- render: (text, record, index) => (
-
-
-
-
-
-
{
- manageRedemption(record.id, 'delete', record).then(
- () => {
- removeRecord(record.key);
- }
- )
- }}
- >
-
-
- {
- record.status === 1 ?
-
+ ),
+ },
+ ];
- const [redemptions, setRedemptions] = useState([]);
- const [loading, setLoading] = useState(true);
- const [activePage, setActivePage] = useState(1);
- const [searchKeyword, setSearchKeyword] = useState('');
- const [searching, setSearching] = useState(false);
- const [tokenCount, setTokenCount] = useState(ITEMS_PER_PAGE);
- const [selectedKeys, setSelectedKeys] = useState([]);
-
- const loadRedemptions = async (startIdx) => {
- const res = await API.get(`/api/redemption/?p=${startIdx}`);
- const { success, message, data } = res.data;
- if (success) {
- if (startIdx === 0) {
- setRedemptions(data);
- } else {
- let newRedemptions = redemptions;
- newRedemptions.push(...data);
- setRedemptions(newRedemptions);
- }
- } else {
- showError(message);
- }
- setLoading(false);
- };
-
- const removeRecord = key => {
- let newDataSource = [...redemptions];
- if (key != null) {
- let idx = newDataSource.findIndex(data => data.key === key);
-
- if (idx > -1) {
- newDataSource.splice(idx, 1);
- setRedemptions(newDataSource);
- }
- }
- };
-
- const copyText = async (text) => {
- if (await copy(text)) {
- showSuccess('已复制到剪贴板!');
- } else {
- // setSearchKeyword(text);
- Modal.error({ title: '无法复制到剪贴板,请手动复制', content: text });
- }
- }
-
- const onPaginationChange = (e, { activePage }) => {
- (async () => {
- if (activePage === Math.ceil(redemptions.length / ITEMS_PER_PAGE) + 1) {
- // In this case we have to load more data and then append them.
- await loadRedemptions(activePage - 1);
- }
- setActivePage(activePage);
- })();
- };
-
- useEffect(() => {
- loadRedemptions(0)
- .then()
- .catch((reason) => {
- showError(reason);
- });
- }, []);
-
- const manageRedemption = async (id, action, record) => {
- let data = { id };
- let res;
- switch (action) {
- case 'delete':
- res = await API.delete(`/api/redemption/${id}/`);
- break;
- case 'enable':
- data.status = 1;
- res = await API.put('/api/redemption/?status_only=true', data);
- break;
- case 'disable':
- data.status = 2;
- res = await API.put('/api/redemption/?status_only=true', data);
- break;
- }
- const { success, message } = res.data;
- if (success) {
- showSuccess('操作成功完成!');
- let redemption = res.data.data;
- let newRedemptions = [...redemptions];
- // let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx;
- if (action === 'delete') {
-
- } else {
- record.status = redemption.status;
- }
- setRedemptions(newRedemptions);
- } else {
- showError(message);
- }
- };
-
- const searchRedemptions = async () => {
- if (searchKeyword === '') {
- // if keyword is blank, load files instead.
- await loadRedemptions(0);
- setActivePage(1);
- return;
- }
- setSearching(true);
- const res = await API.get(`/api/redemption/search?keyword=${searchKeyword}`);
- const { success, message, data } = res.data;
- if (success) {
- setRedemptions(data);
- setActivePage(1);
- } else {
- showError(message);
- }
- setSearching(false);
- };
-
- const handleKeywordChange = async (e, { value }) => {
- setSearchKeyword(value.trim());
- };
-
- const sortRedemption = (key) => {
- if (redemptions.length === 0) return;
- setLoading(true);
- let sortedRedemptions = [...redemptions];
- sortedRedemptions.sort((a, b) => {
- return ('' + a[key]).localeCompare(b[key]);
+ const [redemptions, setRedemptions] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [activePage, setActivePage] = useState(1);
+ const [searchKeyword, setSearchKeyword] = useState('');
+ const [searching, setSearching] = useState(false);
+ const [tokenCount, setTokenCount] = useState(ITEMS_PER_PAGE);
+ const [selectedKeys, setSelectedKeys] = useState([]);
+ const [editingRedemption, setEditingRedemption] = useState({
+ id: undefined,
});
- if (sortedRedemptions[0].id === redemptions[0].id) {
- sortedRedemptions.reverse();
+ const [showEdit, setShowEdit] = useState(false);
+
+ const closeEdit = () => {
+ setShowEdit(false);
}
- setRedemptions(sortedRedemptions);
- setLoading(false);
- };
- const handlePageChange = page => {
- setActivePage(page);
- if (page === Math.ceil(redemptions.length / ITEMS_PER_PAGE) + 1) {
- // In this case we have to load more data and then append them.
- loadRedemptions(page - 1).then(r => {});
+ const loadRedemptions = async (startIdx) => {
+ const res = await API.get(`/api/redemption/?p=${startIdx}`);
+ const {success, message, data} = res.data;
+ if (success) {
+ if (startIdx === 0) {
+ setRedemptions(data);
+ } else {
+ let newRedemptions = redemptions;
+ newRedemptions.push(...data);
+ setRedemptions(newRedemptions);
+ }
+ } else {
+ showError(message);
+ }
+ setLoading(false);
+ };
+
+ const removeRecord = key => {
+ let newDataSource = [...redemptions];
+ if (key != null) {
+ let idx = newDataSource.findIndex(data => data.key === key);
+
+ if (idx > -1) {
+ newDataSource.splice(idx, 1);
+ setRedemptions(newDataSource);
+ }
+ }
+ };
+
+ const copyText = async (text) => {
+ if (await copy(text)) {
+ showSuccess('已复制到剪贴板!');
+ } else {
+ // setSearchKeyword(text);
+ Modal.error({title: '无法复制到剪贴板,请手动复制', content: text});
+ }
}
- };
- let pageData = redemptions.slice((activePage - 1) * ITEMS_PER_PAGE, activePage * ITEMS_PER_PAGE);
- const rowSelection = {
- onSelect: (record, selected) => {
- },
- onSelectAll: (selected, selectedRows) => {
- },
- onChange: (selectedRowKeys, selectedRows) => {
- setSelectedKeys(selectedRows);
- },
- };
+ const onPaginationChange = (e, {activePage}) => {
+ (async () => {
+ if (activePage === Math.ceil(redemptions.length / ITEMS_PER_PAGE) + 1) {
+ // In this case we have to load more data and then append them.
+ await loadRedemptions(activePage - 1);
+ }
+ setActivePage(activePage);
+ })();
+ };
- return (
- <>
-
-
+ useEffect(() => {
+ loadRedemptions(0)
+ .then()
+ .catch((reason) => {
+ showError(reason);
+ });
+ }, []);
- `第 ${page.currentStart} - ${page.currentEnd} 条,共 ${redemptions.length} 条`,
- // onPageSizeChange: (size) => {
- // setPageSize(size);
- // setActivePage(1);
- // },
- onPageChange: handlePageChange,
- }} loading={loading} rowSelection={rowSelection}>
-
- >
- );
+ const refresh = async () => {
+ await loadRedemptions(activePage - 1);
+ };
+
+ const manageRedemption = async (id, action, record) => {
+ let data = {id};
+ let res;
+ switch (action) {
+ case 'delete':
+ res = await API.delete(`/api/redemption/${id}/`);
+ break;
+ case 'enable':
+ data.status = 1;
+ res = await API.put('/api/redemption/?status_only=true', data);
+ break;
+ case 'disable':
+ data.status = 2;
+ res = await API.put('/api/redemption/?status_only=true', data);
+ break;
+ }
+ const {success, message} = res.data;
+ if (success) {
+ showSuccess('操作成功完成!');
+ let redemption = res.data.data;
+ let newRedemptions = [...redemptions];
+ // let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx;
+ if (action === 'delete') {
+
+ } else {
+ record.status = redemption.status;
+ }
+ setRedemptions(newRedemptions);
+ } else {
+ showError(message);
+ }
+ };
+
+ const searchRedemptions = async () => {
+ if (searchKeyword === '') {
+ // if keyword is blank, load files instead.
+ await loadRedemptions(0);
+ setActivePage(1);
+ return;
+ }
+ setSearching(true);
+ const res = await API.get(`/api/redemption/search?keyword=${searchKeyword}`);
+ const {success, message, data} = res.data;
+ if (success) {
+ setRedemptions(data);
+ setActivePage(1);
+ } else {
+ showError(message);
+ }
+ setSearching(false);
+ };
+
+ const handleKeywordChange = async (value) => {
+ setSearchKeyword(value.trim());
+ };
+
+ const sortRedemption = (key) => {
+ if (redemptions.length === 0) return;
+ setLoading(true);
+ let sortedRedemptions = [...redemptions];
+ sortedRedemptions.sort((a, b) => {
+ return ('' + a[key]).localeCompare(b[key]);
+ });
+ if (sortedRedemptions[0].id === redemptions[0].id) {
+ sortedRedemptions.reverse();
+ }
+ setRedemptions(sortedRedemptions);
+ setLoading(false);
+ };
+
+ const handlePageChange = page => {
+ setActivePage(page);
+ if (page === Math.ceil(redemptions.length / ITEMS_PER_PAGE) + 1) {
+ // In this case we have to load more data and then append them.
+ loadRedemptions(page - 1).then(r => {
+ });
+ }
+ };
+
+ let pageData = redemptions.slice((activePage - 1) * ITEMS_PER_PAGE, activePage * ITEMS_PER_PAGE);
+ const rowSelection = {
+ onSelect: (record, selected) => {
+ },
+ onSelectAll: (selected, selectedRows) => {
+ },
+ onChange: (selectedRowKeys, selectedRows) => {
+ setSelectedKeys(selectedRows);
+ },
+ };
+
+ return (
+ <>
+
+
+
+
+ `第 ${page.currentStart} - ${page.currentEnd} 条,共 ${redemptions.length} 条`,
+ // onPageSizeChange: (size) => {
+ // setPageSize(size);
+ // setActivePage(1);
+ // },
+ onPageChange: handlePageChange,
+ }} loading={loading} rowSelection={rowSelection}>
+
+ {
+ setEditingRedemption({
+ id: undefined,
+ });
+ setShowEdit(true);
+ }
+ }>添加兑换码
+ {
+ if (selectedKeys.length === 0) {
+ showError('请至少选择一个兑换码!');
+ return;
+ }
+ let keys = "";
+ for (let i = 0; i < selectedKeys.length; i++) {
+ keys += selectedKeys[i].name + " sk-" + selectedKeys[i].key + "\n";
+ }
+ await copyText(keys);
+ }
+ }>复制所选兑换码到剪贴板
+ >
+ );
};
export default RedemptionsTable;
diff --git a/web/src/components/TokensTable.js b/web/src/components/TokensTable.js
index d267660..b04fb84 100644
--- a/web/src/components/TokensTable.js
+++ b/web/src/components/TokensTable.js
@@ -179,6 +179,7 @@ const TokensTable = () => {
const [loading, setLoading] = useState(true);
const [activePage, setActivePage] = useState(1);
const [searchKeyword, setSearchKeyword] = useState('');
+ const [searchToken, setSearchToken] = useState('');
const [searching, setSearching] = useState(false);
const [showTopUpModal, setShowTopUpModal] = useState(false);
const [targetTokenIdx, setTargetTokenIdx] = useState(0);
@@ -188,9 +189,6 @@ const TokensTable = () => {
const closeEdit = () => {
setShowEdit(false);
- // setEditingToken({
- // id: undefined,
- // });
}
const setTokensFormat = (tokens) => {
@@ -401,6 +399,10 @@ const TokensTable = () => {
setSearchKeyword(value.trim());
};
+ const handleSearchTokenChange = async (value) => {
+ setSearchToken(value.trim());
+ };
+
const sortToken = (key) => {
if (tokens.length === 0) return;
setLoading(true);
@@ -447,6 +449,14 @@ const TokensTable = () => {
loading={searching}
onChange={handleKeywordChange}
/>
+
查询
diff --git a/web/src/pages/Redemption/EditRedemption.js b/web/src/pages/Redemption/EditRedemption.js
index 7a33f77..54aa47d 100644
--- a/web/src/pages/Redemption/EditRedemption.js
+++ b/web/src/pages/Redemption/EditRedemption.js
@@ -1,15 +1,17 @@
import React, { useEffect, useState } from 'react';
-import { Button, Form, Header, Segment } from 'semantic-ui-react';
import { useParams, useNavigate } from 'react-router-dom';
-import { API, downloadTextAsFile, showError, showSuccess } from '../../helpers';
+import {API, downloadTextAsFile, isMobile, showError, showSuccess} from '../../helpers';
import { renderQuota, renderQuotaWithPrompt } from '../../helpers/render';
+import {SideSheet, Space, Spin, Button, Input, Typography, AutoComplete, Modal} from "@douyinfe/semi-ui";
+import Title from "@douyinfe/semi-ui/lib/es/typography/title";
+import {Divider} from "semantic-ui-react";
-const EditRedemption = () => {
- const params = useParams();
- const navigate = useNavigate();
- const redemptionId = params.id;
- const isEdit = redemptionId !== undefined;
+const EditRedemption = (props) => {
+ const isEdit = props.editingRedemption.id !== undefined;
const [loading, setLoading] = useState(isEdit);
+
+ const params = useParams();
+ const navigate = useNavigate()
const originInputs = {
name: '',
quota: 100000,
@@ -19,15 +21,16 @@ const EditRedemption = () => {
const { name, quota, count } = inputs;
const handleCancel = () => {
- navigate('/redemption');
- };
+ props.handleClose();
+ }
- const handleInputChange = (e, { name, value }) => {
+ const handleInputChange = (name, value) => {
setInputs((inputs) => ({ ...inputs, [name]: value }));
};
const loadRedemption = async () => {
- let res = await API.get(`/api/redemption/${redemptionId}`);
+ setLoading(true);
+ let res = await API.get(`/api/redemption/${props.editingRedemption.id}`);
const { success, message, data } = res.data;
if (success) {
setInputs(data);
@@ -36,20 +39,28 @@ const EditRedemption = () => {
}
setLoading(false);
};
+
useEffect(() => {
if (isEdit) {
- loadRedemption().then();
+ loadRedemption().then(
+ () => {
+ // console.log(inputs);
+ }
+ );
+ } else {
+ setInputs(originInputs);
}
- }, []);
+ }, [props.editingRedemption.id]);
const submit = async () => {
if (!isEdit && inputs.name === '') return;
+ setLoading(true);
let localInputs = inputs;
localInputs.count = parseInt(localInputs.count);
localInputs.quota = parseInt(localInputs.quota);
let res;
if (isEdit) {
- res = await API.put(`/api/redemption/`, { ...localInputs, id: parseInt(redemptionId) });
+ res = await API.put(`/api/redemption/`, { ...localInputs, id: parseInt(props.editingRedemption.id) });
} else {
res = await API.post(`/api/redemption/`, {
...localInputs
@@ -59,9 +70,13 @@ const EditRedemption = () => {
if (success) {
if (isEdit) {
showSuccess('兑换码更新成功!');
+ props.refresh();
+ props.handleClose();
} else {
showSuccess('兑换码创建成功!');
setInputs(originInputs);
+ props.refresh();
+ props.handleClose();
}
} else {
showError(message);
@@ -71,56 +86,94 @@ const EditRedemption = () => {
for (let i = 0; i < data.length; i++) {
text += data[i] + "\n";
}
- downloadTextAsFile(text, `${inputs.name}.txt`);
+ // downloadTextAsFile(text, `${inputs.name}.txt`);
+ Modal.confirm({
+ title: '兑换码创建成功',
+ content: (
+
+
兑换码创建成功,是否下载兑换码?
+
兑换码将以文本文件的形式下载,文件名为兑换码的名称。
+
+ ),
+ onOk: () => {
+ downloadTextAsFile(text, `${inputs.name}.txt`);
+ }
+ });
}
+ setLoading(false);
};
return (
<>
-
- {isEdit ? '更新兑换码信息' : '创建新的兑换码'}
-
- {isEdit ? '更新兑换码信息' : '创建新的兑换码'}}
+ headerStyle={{borderBottom: '1px solid var(--semi-color-border)'}}
+ bodyStyle={{borderBottom: '1px solid var(--semi-color-border)'}}
+ visible={props.visiable}
+ footer={
+
+
+ 提交
+ 取消
+
+
+ }
+ closeIcon={null}
+ onCancel={() => handleCancel()}
+ width={isMobile() ? '100%' : 600}
+ >
+
+ handleInputChange('name', value)}
value={name}
autoComplete='new-password'
required={!isEdit}
- />
-
-
-
+
+
+ {`额度${renderQuotaWithPrompt(quota)}`}
+
+ handleInputChange('quota', value)}
value={quota}
autoComplete='new-password'
type='number'
- />
-
+ position={'bottom'}
+ data={[
+ {value: 500000, label: '1$'},
+ {value: 5000000, label: '10$'},
+ {value: 25000000, label: '50$'},
+ {value: 50000000, label: '100$'},
+ {value: 250000000, label: '500$'},
+ {value: 500000000, label: '1000$'},
+ ]}
+ />
{
- !isEdit && <>
-
-
+
+ 生成数量
+ handleInputChange('count', value)}
+ value={count}
+ autoComplete='new-password'
+ type='number'
/>
-
- >
+ >
}
- 提交
- 取消
-
-
+
+
>
);
};
diff --git a/web/src/pages/Redemption/index.js b/web/src/pages/Redemption/index.js
index c064941..0b3c963 100644
--- a/web/src/pages/Redemption/index.js
+++ b/web/src/pages/Redemption/index.js
@@ -1,13 +1,19 @@
import React from 'react';
import { Segment, Header } from 'semantic-ui-react';
import RedemptionsTable from '../../components/RedemptionsTable';
+import TokensTable from "../../components/TokensTable";
+import {Layout} from "@douyinfe/semi-ui";
const Redemption = () => (
<>
-
-
-
-
+
+
+ 管理兑换码
+
+
+
+
+
>
);
diff --git a/web/src/pages/Token/EditToken.js b/web/src/pages/Token/EditToken.js
index e21e9cb..9512a02 100644
--- a/web/src/pages/Token/EditToken.js
+++ b/web/src/pages/Token/EditToken.js
@@ -63,7 +63,7 @@ const EditToken = (props) => {
if (isEdit) {
loadToken().then(
() => {
- console.log(inputs);
+ // console.log(inputs);
}
);
} else {
@@ -72,8 +72,8 @@ const EditToken = (props) => {
}, [props.editingToken.id]);
const submit = async () => {
- setLoading(true);
if (!isEdit && inputs.name === '') return;
+ setLoading(true);
let localInputs = inputs;
localInputs.remain_quota = parseInt(localInputs.remain_quota);
if (localInputs.expired_time !== -1) {
diff --git a/web/src/pages/Token/index.js b/web/src/pages/Token/index.js
index 5583bfb..eaa1f4b 100644
--- a/web/src/pages/Token/index.js
+++ b/web/src/pages/Token/index.js
@@ -1,16 +1,15 @@
import React from 'react';
import TokensTable from '../../components/TokensTable';
import {Layout} from "@douyinfe/semi-ui";
-const {Content, Header} = Layout;
const Token = () => (
<>
-
-
+
+
-
+
>
);