1
This commit is contained in:
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+36
-36
@@ -1,37 +1,37 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
|
||||||
<title>后台管理</title>
|
<title>后台管理</title>
|
||||||
<script>
|
<script>
|
||||||
(function() {
|
(function() {
|
||||||
var cached = localStorage.getItem('siteInfo');
|
var cached = localStorage.getItem('siteInfo');
|
||||||
if (cached) {
|
if (cached) {
|
||||||
try {
|
try {
|
||||||
var info = JSON.parse(cached);
|
var info = JSON.parse(cached);
|
||||||
if (info.siteName) {
|
if (info.siteName) {
|
||||||
document.title = info.siteName + ' - 管理后台';
|
document.title = info.siteName + ' - 管理后台';
|
||||||
}
|
}
|
||||||
if (info.siteLogo) {
|
if (info.siteLogo) {
|
||||||
var link = document.querySelector('link[rel="icon"]');
|
var link = document.querySelector('link[rel="icon"]');
|
||||||
if (link) {
|
if (link) {
|
||||||
link.href = info.siteLogo;
|
link.href = info.siteLogo;
|
||||||
link.type = 'image/png';
|
link.type = 'image/png';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<script type="module" crossorigin src="/assets/index-Dx-bvo36.js"></script>
|
<script type="module" crossorigin src="/assets/index-C4H6x_-U.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-D7ShJUt4.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-D7ShJUt4.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>␍
|
<div id="root"></div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ const RECORD_TYPE_MAP: Record<string, { text: string; color: string; icon: React
|
|||||||
recharge: { text: '充值', color: 'green', icon: <ArrowUpOutlined /> },
|
recharge: { text: '充值', color: 'green', icon: <ArrowUpOutlined /> },
|
||||||
consume: { text: '消费', color: 'red', icon: <ArrowDownOutlined /> },
|
consume: { text: '消费', color: 'red', icon: <ArrowDownOutlined /> },
|
||||||
refund: { text: '回退', color: 'blue', icon: <RollbackOutlined /> },
|
refund: { text: '回退', color: 'blue', icon: <RollbackOutlined /> },
|
||||||
|
team_internal: { text: '团队内部', color: 'cyan', icon: <WalletOutlined /> },
|
||||||
};
|
};
|
||||||
|
|
||||||
const userScopeOptions = [
|
const userScopeOptions = [
|
||||||
@@ -49,6 +50,7 @@ const recordTypeOptions = [
|
|||||||
{ value: 'recharge', label: '充值' },
|
{ value: 'recharge', label: '充值' },
|
||||||
{ value: 'consume', label: '消费' },
|
{ value: 'consume', label: '消费' },
|
||||||
{ value: 'refund', label: '回退' },
|
{ value: 'refund', label: '回退' },
|
||||||
|
{ value: 'team_internal', label: '团队内部' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const creditSubjectOptions = [
|
const creditSubjectOptions = [
|
||||||
|
|||||||
@@ -264,7 +264,7 @@ async def list_team_credit_records(
|
|||||||
page_size: int = Query(20, ge=1, le=100),
|
page_size: int = Query(20, ge=1, le=100),
|
||||||
user_id: str | None = Query(None),
|
user_id: str | None = Query(None),
|
||||||
phone: str | None = Query(None, description="按手机号搜索"),
|
phone: str | None = Query(None, description="按手机号搜索"),
|
||||||
record_type: str | None = Query(None, pattern="^(recharge|consume|refund|team_internal)$"),
|
record_type: str | None = Query(None, pattern="^(recharge|consume|refund|team_internal)$", description="流水类型"),
|
||||||
start_date: str | None = Query(None, description="起始日期 YYYY-MM-DD"),
|
start_date: str | None = Query(None, description="起始日期 YYYY-MM-DD"),
|
||||||
end_date: str | None = Query(None, description="截止日期 YYYY-MM-DD"),
|
end_date: str | None = Query(None, description="截止日期 YYYY-MM-DD"),
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
@@ -308,7 +308,7 @@ async def list_team_credit_records(
|
|||||||
async def export_team_credit_records(
|
async def export_team_credit_records(
|
||||||
user_id: str | None = Query(None),
|
user_id: str | None = Query(None),
|
||||||
phone: str | None = Query(None),
|
phone: str | None = Query(None),
|
||||||
record_type: str | None = Query(None, pattern="^(recharge|consume|refund|team_internal)$"),
|
record_type: str | None = Query(None, pattern="^(recharge|consume|refund|team_internal)$", description="流水类型"),
|
||||||
start_date: str | None = Query(None),
|
start_date: str | None = Query(None),
|
||||||
end_date: str | None = Query(None),
|
end_date: str | None = Query(None),
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -28,7 +28,7 @@
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<script type="module" crossorigin src="/assets/index-D7ovjI8j.js"></script>
|
<script type="module" crossorigin src="/assets/index-DlbOvnzQ.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-D9_3MPsN.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-D9_3MPsN.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -2,15 +2,20 @@ import React, { useEffect, useState, useCallback } from 'react';
|
|||||||
import {
|
import {
|
||||||
Button, Card, Empty, Form, Input, InputNumber, message, Modal, Radio, Select, Space, Table, Tabs, Tag, Tooltip, Typography,
|
Button, Card, Empty, Form, Input, InputNumber, message, Modal, Radio, Select, Space, Table, Tabs, Tag, Tooltip, Typography,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
|
import { DatePicker } from 'antd';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
import {
|
import {
|
||||||
CopyOutlined, DownloadOutlined, PlusOutlined, ReloadOutlined, UserOutlined, HistoryOutlined, WalletOutlined,
|
CopyOutlined, DownloadOutlined, PlusOutlined, ReloadOutlined, UserOutlined, HistoryOutlined, WalletOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
|
|
||||||
|
const { RangePicker } = DatePicker;
|
||||||
import {
|
import {
|
||||||
createTeamInvitation, getJoinTeamInfo, getManagedTeam, getPendingJoinRequests,
|
createTeamInvitation, getJoinTeamInfo, getManagedTeam, getPendingJoinRequests,
|
||||||
getTeamCreditExportUrl, getTeamCreditRecords, getTeamInvitations, getTeamMembers, handleJoinRequest, revokeInvitation, submitJoinRequest, transferCredits,
|
getTeamCreditExportUrl, getTeamCreditRecords, getTeamInvitations, getTeamMembers, handleJoinRequest, revokeInvitation, submitJoinRequest, transferCredits,
|
||||||
} from '../api';
|
} from '../api';
|
||||||
import type { ManagedTeam, TeamInvitation, TeamJoinRequest, TeamMember } from '../types';
|
import type { ManagedTeam, TeamInvitation, TeamJoinRequest, TeamMember } from '../types';
|
||||||
import { formatDate } from '../utils/formatDate';
|
import { formatDate } from '../utils/formatDate';
|
||||||
|
import { useAuthStore } from '../store/useAuthStore';
|
||||||
|
|
||||||
const TeamManagementPage: React.FC = () => {
|
const TeamManagementPage: React.FC = () => {
|
||||||
const [team, setTeam] = useState<ManagedTeam | null>(null);
|
const [team, setTeam] = useState<ManagedTeam | null>(null);
|
||||||
@@ -329,10 +334,10 @@ const TeamManagementPage: React.FC = () => {
|
|||||||
{/* 搜索栏 */}
|
{/* 搜索栏 */}
|
||||||
<div style={{ marginBottom: 12, display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' }}>
|
<div style={{ marginBottom: 12, display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||||
<Select
|
<Select
|
||||||
value={creditFilterType}
|
value={creditFilterType || undefined}
|
||||||
onChange={(v) => { setCreditFilterType(v || ''); setCreditPage(1); }}
|
onChange={(v) => { setCreditFilterType(v || ''); setCreditPage(1); }}
|
||||||
allowClear
|
allowClear
|
||||||
placeholder="全部类型"
|
placeholder="交易类型"
|
||||||
style={{ width: 130 }}
|
style={{ width: 130 }}
|
||||||
options={[
|
options={[
|
||||||
{ value: 'recharge', label: '充值' },
|
{ value: 'recharge', label: '充值' },
|
||||||
@@ -348,30 +353,28 @@ const TeamManagementPage: React.FC = () => {
|
|||||||
style={{ width: 160 }}
|
style={{ width: 160 }}
|
||||||
allowClear
|
allowClear
|
||||||
/>
|
/>
|
||||||
<Input
|
<RangePicker
|
||||||
placeholder="起始日期 YYYY-MM-DD"
|
value={creditFilterStart && creditFilterEnd ? [dayjs(creditFilterStart), dayjs(creditFilterEnd)] : undefined}
|
||||||
value={creditFilterStart}
|
onChange={(dates) => {
|
||||||
onChange={(e) => { setCreditFilterStart(e.target.value); setCreditPage(1); }}
|
if (dates && dates[0] && dates[1]) {
|
||||||
style={{ width: 160 }}
|
setCreditFilterStart(dates[0].format('YYYY-MM-DD'));
|
||||||
/>
|
setCreditFilterEnd(dates[1].format('YYYY-MM-DD'));
|
||||||
<Input
|
} else {
|
||||||
placeholder="截止日期 YYYY-MM-DD"
|
setCreditFilterStart('');
|
||||||
value={creditFilterEnd}
|
setCreditFilterEnd('');
|
||||||
onChange={(e) => { setCreditFilterEnd(e.target.value); setCreditPage(1); }}
|
}
|
||||||
style={{ width: 160 }}
|
setCreditPage(1);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Button onClick={resetCreditFilters}>重置</Button>
|
<Button onClick={resetCreditFilters}>重置</Button>
|
||||||
<Button type="primary" icon={<DownloadOutlined />} onClick={handleExportCredits}>导出 Excel</Button>
|
<Button type="primary" icon={<DownloadOutlined />} onClick={handleExportCredits}>导出 Excel</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 汇总统计 */}
|
{/* 汇总统计 */}
|
||||||
{creditSummary && (
|
<div style={{ marginBottom: 12, padding: '8px 12px', background: '#f8f9fc', borderRadius: 8, display: 'flex', gap: 20, flexWrap: 'wrap', fontSize: 13 }}>
|
||||||
<div style={{ marginBottom: 12, padding: '8px 12px', background: '#f8f9fc', borderRadius: 8, display: 'flex', gap: 20, flexWrap: 'wrap', fontSize: 13 }}>
|
<span>总充值:<strong style={{ color: '#16a34a' }}>+{creditSummary?.total_recharge ?? 0}</strong></span>
|
||||||
<span>总充值:<strong style={{ color: '#16a34a' }}>+{creditSummary.total_recharge ?? 0}</strong></span>
|
<span>总消费:<strong style={{ color: '#dc2626' }}>-{creditSummary?.total_consume ?? 0}</strong></span>
|
||||||
<span>总消费:<strong style={{ color: '#dc2626' }}>-{creditSummary.total_consume ?? 0}</strong></span>
|
</div>
|
||||||
<span>笔数:<strong>{creditSummary.transaction_count ?? 0}</strong></span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Table
|
<Table
|
||||||
size="small"
|
size="small"
|
||||||
@@ -481,14 +484,37 @@ const TeamManagementPage: React.FC = () => {
|
|||||||
width={480}
|
width={480}
|
||||||
>
|
>
|
||||||
<Form form={creditForm} layout="vertical" style={{ marginTop: 16 }} initialValues={{ direction: 'increase' }}>
|
<Form form={creditForm} layout="vertical" style={{ marginTop: 16 }} initialValues={{ direction: 'increase' }}>
|
||||||
<Form.Item name="direction" label="操作类型" rules={[{ required: true }]}>
|
{/* 显示管理人当前积分 */}
|
||||||
|
<div style={{ marginBottom: 16, padding: '8px 12px', background: '#f0f4ff', borderRadius: 8, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<Typography.Text type="secondary">我的当前积分</Typography.Text>
|
||||||
|
<Typography.Text strong style={{ fontSize: 18, color: '#6366f1' }}>
|
||||||
|
{useAuthStore.getState().user?.credits?.toFixed(2) ?? '0.00'}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
<Form.Item name="direction" label="操作类型" rules={[{ required: true, message: '请选择操作类型' }]}>
|
||||||
<Radio.Group buttonStyle="solid" size="large" style={{ width: '100%' }}>
|
<Radio.Group buttonStyle="solid" size="large" style={{ width: '100%' }}>
|
||||||
<Radio.Button value="increase" style={{ width: '50%', textAlign: 'center' }}>增加成员积分</Radio.Button>
|
<Radio.Button value="increase" style={{ width: '50%', textAlign: 'center' }}>增加成员积分</Radio.Button>
|
||||||
<Radio.Button value="decrease" style={{ width: '50%', textAlign: 'center' }}>扣减成员积分</Radio.Button>
|
<Radio.Button value="decrease" style={{ width: '50%', textAlign: 'center' }}>扣减成员积分</Radio.Button>
|
||||||
</Radio.Group>
|
</Radio.Group>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="amount" label="积分数量" rules={[{ required: true, message: '请输入数量' }, { type: 'number', min: 0.01, message: '必须大于 0' }]}>
|
<Form.Item
|
||||||
<InputNumber style={{ width: '100%' }} step={1} min={0.01} placeholder="请输入正数积分数量" size="large" />
|
name="amount"
|
||||||
|
label="积分数量"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请输入积分数量' },
|
||||||
|
{ type: 'number', min: 0.01, message: '必须大于 0' },
|
||||||
|
{ type: 'number', max: 9999999, message: '单次不能超过 9999999' },
|
||||||
|
{
|
||||||
|
validator: (_, value) => {
|
||||||
|
if (!value || value <= 0) return Promise.reject(new Error('请输入有效的正数'));
|
||||||
|
const decimals = String(value).split('.')[1];
|
||||||
|
if (decimals && decimals.length > 2) return Promise.reject(new Error('最多两位小数'));
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<InputNumber style={{ width: '100%' }} step={1} min={0.01} precision={2} placeholder="请输入正数积分数量" size="large" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="description" label="备注">
|
<Form.Item name="description" label="备注">
|
||||||
<Input.TextArea rows={2} maxLength={256} placeholder="选填,例如:活动奖励" />
|
<Input.TextArea rows={2} maxLength={256} placeholder="选填,例如:活动奖励" />
|
||||||
|
|||||||
Reference in New Issue
Block a user