1
This commit is contained in:
Vendored
+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-C5wDengw.js"></script>
|
<script type="module" crossorigin src="/assets/index-DFBUW3T1.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-D7ShJUt4.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-D7ShJUt4.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Button, Card, Form, Input, message, Space, Typography, Upload,
|
Button, Card, Form, Input, message, Space, Switch, Typography, Upload,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
SettingOutlined, SaveOutlined, UploadOutlined, FilePdfOutlined, EyeOutlined,
|
SettingOutlined, SaveOutlined, UploadOutlined, FilePdfOutlined, EyeOutlined,
|
||||||
@@ -69,6 +69,7 @@ const AdminSettings: React.FC = () => {
|
|||||||
'站点信息': configs.filter(c => c.key.startsWith('site_')),
|
'站点信息': configs.filter(c => c.key.startsWith('site_')),
|
||||||
'协议配置': configs.filter(c => c.key === 'user_agreement_url' || c.key === 'privacy_policy_url'),
|
'协议配置': configs.filter(c => c.key === 'user_agreement_url' || c.key === 'privacy_policy_url'),
|
||||||
'SEO 设置': configs.filter(c => c.key.startsWith('seo_')),
|
'SEO 设置': configs.filter(c => c.key.startsWith('seo_')),
|
||||||
|
'用户积分配置': configs.filter(c => c.key.startsWith('user_') && c.key.includes('credits')),
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFieldDescription = (config: SystemConfig): string => {
|
const getFieldDescription = (config: SystemConfig): string => {
|
||||||
@@ -80,6 +81,9 @@ const AdminSettings: React.FC = () => {
|
|||||||
seo_title: '搜索引擎结果中显示的标题',
|
seo_title: '搜索引擎结果中显示的标题',
|
||||||
seo_description: '搜索引擎结果中显示的描述文字,建议150字以内',
|
seo_description: '搜索引擎结果中显示的描述文字,建议150字以内',
|
||||||
seo_keywords: '用逗号分隔的关键词列表',
|
seo_keywords: '用逗号分隔的关键词列表',
|
||||||
|
user_register_credits: '用户注册时赠送的初始积分,默认100',
|
||||||
|
user_login_credits: '用户每日登录赠送的积分数量',
|
||||||
|
user_login_credits_enabled: '是否启用每日登录赠送积分功能',
|
||||||
};
|
};
|
||||||
return descMap[config.key] || config.description || '';
|
return descMap[config.key] || config.description || '';
|
||||||
};
|
};
|
||||||
@@ -91,6 +95,19 @@ const AdminSettings: React.FC = () => {
|
|||||||
if (config.key === 'seo_keywords') {
|
if (config.key === 'seo_keywords') {
|
||||||
return <Input placeholder="关键词1, 关键词2, 关键词3" size="large" />;
|
return <Input placeholder="关键词1, 关键词2, 关键词3" size="large" />;
|
||||||
}
|
}
|
||||||
|
if (config.key === 'user_login_credits_enabled') {
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||||
|
<Switch defaultChecked={config.value === 'true'} />
|
||||||
|
<Typography.Text type="secondary" style={{ fontSize: 13 }}>
|
||||||
|
{config.value === 'true' ? '已启用' : '已禁用'}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (config.key === 'user_register_credits' || config.key === 'user_login_credits') {
|
||||||
|
return <Input type="number" min={0} placeholder={config.description} size="large" />;
|
||||||
|
}
|
||||||
return <Input placeholder={config.description} size="large" />;
|
return <Input placeholder={config.description} size="large" />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import {
|
|||||||
Button, Card, Checkbox, Form, Input, InputNumber, message, Modal, Popconfirm, Select, Space, Switch, Table, Tabs, Tag, Typography,
|
Button, Card, Checkbox, Form, Input, InputNumber, message, Modal, Popconfirm, Select, Space, Switch, Table, Tabs, Tag, Typography,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
UserOutlined, WalletOutlined, SearchOutlined, StopOutlined, CheckCircleOutlined, PlusOutlined, MenuOutlined, LockOutlined,
|
UserOutlined, WalletOutlined, SearchOutlined, StopOutlined, CheckCircleOutlined, PlusOutlined, MenuOutlined, LockOutlined, SettingOutlined, SaveOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { getAdminUsers, adjustCredits, toggleUserStatus, createUser, updateUserMenus, getMenuConfigs, resetUserPassword } from '../api';
|
import { getAdminUsers, adjustCredits, toggleUserStatus, createUser, updateUserMenus, getMenuConfigs, resetUserPassword, getSystemConfigs, updateSystemConfig } from '../api';
|
||||||
import type { AdminUser } from '../types';
|
import type { AdminUser, SystemConfig } from '../types';
|
||||||
import { formatDate } from '../utils/formatDate';
|
import { formatDate } from '../utils/formatDate';
|
||||||
|
|
||||||
const AdminUsers: React.FC = () => {
|
const AdminUsers: React.FC = () => {
|
||||||
@@ -29,6 +29,10 @@ const AdminUsers: React.FC = () => {
|
|||||||
const [pageSize, setPageSize] = useState(20);
|
const [pageSize, setPageSize] = useState(20);
|
||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
|
|
||||||
|
const [creditConfigs, setCreditConfigs] = useState<SystemConfig[]>([]);
|
||||||
|
const [configForm] = Form.useForm();
|
||||||
|
const [configSaving, setConfigSaving] = useState(false);
|
||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
@@ -41,6 +45,40 @@ const AdminUsers: React.FC = () => {
|
|||||||
|
|
||||||
useEffect(() => { load(); }, [page, pageSize, search]);
|
useEffect(() => { load(); }, [page, pageSize, search]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadCreditConfigs = async () => {
|
||||||
|
try {
|
||||||
|
const configs = await getSystemConfigs();
|
||||||
|
const credit = configs.filter(c => c.key.startsWith('user_') && c.key.includes('credits'));
|
||||||
|
setCreditConfigs(credit);
|
||||||
|
const formValues: Record<string, string> = {};
|
||||||
|
credit.forEach(c => { formValues[c.key] = c.value; });
|
||||||
|
configForm.setFieldsValue(formValues);
|
||||||
|
} catch { /* auth error handled by client */ }
|
||||||
|
};
|
||||||
|
loadCreditConfigs();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSaveCreditConfigs = async () => {
|
||||||
|
try {
|
||||||
|
const values = await configForm.validateFields();
|
||||||
|
setConfigSaving(true);
|
||||||
|
for (const config of creditConfigs) {
|
||||||
|
const newVal = values[config.key];
|
||||||
|
if (newVal !== undefined && newVal !== config.value) {
|
||||||
|
await updateSystemConfig(config.id, newVal ?? '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
message.success('积分配置已保存');
|
||||||
|
const configs = await getSystemConfigs();
|
||||||
|
setCreditConfigs(configs.filter(c => c.key.startsWith('user_') && c.key.includes('credits')));
|
||||||
|
setConfigSaving(false);
|
||||||
|
} catch (e: any) {
|
||||||
|
setConfigSaving(false);
|
||||||
|
message.error(e?.message || '保存失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handlePageChange = (p: number, ps: number) => {
|
const handlePageChange = (p: number, ps: number) => {
|
||||||
setPage(p);
|
setPage(p);
|
||||||
setPageSize(ps);
|
setPageSize(ps);
|
||||||
@@ -224,6 +262,60 @@ const AdminUsers: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5', marginBottom: 16 }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 20 }}>
|
||||||
|
<div style={{
|
||||||
|
width: 44, height: 44, borderRadius: 10,
|
||||||
|
background: 'rgba(99,102,241,0.08)',
|
||||||
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
|
fontSize: 20, color: '#6366f1',
|
||||||
|
}}>
|
||||||
|
<SettingOutlined />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography.Title level={4} style={{ margin: 0 }}>积分配置</Typography.Title>
|
||||||
|
<Typography.Text type="secondary">设置用户注册和登录赠送的积分</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Form form={configForm} layout="vertical">
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 20 }}>
|
||||||
|
<Form.Item
|
||||||
|
name="user_register_credits"
|
||||||
|
label={<span style={{ fontWeight: 500 }}>注册赠送积分</span>}
|
||||||
|
extra="用户注册时赠送的初始积分"
|
||||||
|
>
|
||||||
|
<InputNumber min={0} style={{ width: '100%' }} size="large" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="user_login_credits"
|
||||||
|
label={<span style={{ fontWeight: 500 }}>每日登录赠送积分</span>}
|
||||||
|
extra="用户每日首次登录赠送的积分"
|
||||||
|
>
|
||||||
|
<InputNumber min={0} style={{ width: '100%' }} size="large" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="user_login_credits_enabled"
|
||||||
|
label={<span style={{ fontWeight: 500 }}>启用每日登录积分</span>}
|
||||||
|
extra="是否开启每日登录赠送积分功能"
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||||
|
<Switch defaultChecked={creditConfigs.find(c => c.key === 'user_login_credits_enabled')?.value === 'true'} />
|
||||||
|
<Typography.Text type="secondary" style={{ fontSize: 13 }}>
|
||||||
|
{creditConfigs.find(c => c.key === 'user_login_credits_enabled')?.value === 'true' ? '已启用' : '已禁用'}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 16 }}>
|
||||||
|
<Button type="primary" icon={<SaveOutlined />} onClick={handleSaveCreditConfigs} loading={configSaving} size="large" style={{ borderRadius: 8 }}>
|
||||||
|
保存配置
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
|
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
|
||||||
{/* Search bar */}
|
{/* Search bar */}
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
|
||||||
|
|||||||
@@ -78,6 +78,38 @@ async def _assign_default_frontend_menus(db: AsyncSession, user: User) -> None:
|
|||||||
user.allowed_menus = [m.path for m in default_menus if m.path]
|
user.allowed_menus = [m.path for m in default_menus if m.path]
|
||||||
|
|
||||||
|
|
||||||
|
async def _get_register_credits(db: AsyncSession) -> int:
|
||||||
|
result = await db.execute(
|
||||||
|
select(SystemConfig.value).where(SystemConfig.key == "user_register_credits").limit(1)
|
||||||
|
)
|
||||||
|
value = result.scalar_one_or_none()
|
||||||
|
return int(value) if value else 100
|
||||||
|
|
||||||
|
|
||||||
|
async def _handle_daily_login_credits(db: AsyncSession, user: User) -> None:
|
||||||
|
enabled_result = await db.execute(
|
||||||
|
select(SystemConfig.value).where(SystemConfig.key == "user_login_credits_enabled").limit(1)
|
||||||
|
)
|
||||||
|
enabled = enabled_result.scalar_one_or_none() == "true"
|
||||||
|
if not enabled:
|
||||||
|
return
|
||||||
|
|
||||||
|
credits_result = await db.execute(
|
||||||
|
select(SystemConfig.value).where(SystemConfig.key == "user_login_credits").limit(1)
|
||||||
|
)
|
||||||
|
credits = int(credits_result.scalar_one_or_none() or "0")
|
||||||
|
if credits <= 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
today = datetime.now().date()
|
||||||
|
if user.last_login_at:
|
||||||
|
last_login_date = user.last_login_at.date()
|
||||||
|
if last_login_date >= today:
|
||||||
|
return
|
||||||
|
|
||||||
|
user.credits += credits
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/login",
|
"/login",
|
||||||
summary="客户端密码登录",
|
summary="客户端密码登录",
|
||||||
@@ -100,6 +132,7 @@ async def login(req: LoginRequest, db: AsyncSession = Depends(get_db)):
|
|||||||
detail="该账号不允许在此登录",
|
detail="该账号不允许在此登录",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await _handle_daily_login_credits(db, user)
|
||||||
user.last_login_at = datetime.now()
|
user.last_login_at = datetime.now()
|
||||||
await db.flush()
|
await db.flush()
|
||||||
return _token_response(user, req.remember_me)
|
return _token_response(user, req.remember_me)
|
||||||
@@ -130,6 +163,7 @@ async def sms_login(req: SmsLoginRequest, db: AsyncSession = Depends(get_db)):
|
|||||||
detail="该账号不允许在此登录",
|
detail="该账号不允许在此登录",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await _handle_daily_login_credits(db, user)
|
||||||
user.last_login_at = datetime.now()
|
user.last_login_at = datetime.now()
|
||||||
await db.flush()
|
await db.flush()
|
||||||
return _token_response(user, req.remember_me)
|
return _token_response(user, req.remember_me)
|
||||||
@@ -155,13 +189,15 @@ async def register(req: RegisterRequest, db: AsyncSession = Depends(get_db)):
|
|||||||
detail="该手机号已注册",
|
detail="该手机号已注册",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
register_credits = await _get_register_credits(db)
|
||||||
|
|
||||||
user = User(
|
user = User(
|
||||||
id=generate_id(),
|
id=generate_id(),
|
||||||
username=req.phone,
|
username=req.phone,
|
||||||
phone=req.phone,
|
phone=req.phone,
|
||||||
hashed_password=hash_password(req.password),
|
hashed_password=hash_password(req.password),
|
||||||
password_set_at=datetime.now(),
|
password_set_at=datetime.now(),
|
||||||
credits=100,
|
credits=register_credits,
|
||||||
is_admin=False,
|
is_admin=False,
|
||||||
user_type="frontend",
|
user_type="frontend",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -151,6 +151,10 @@ async def _seed_data():
|
|||||||
("payment_alipay_private_key", "", "支付宝私钥"),
|
("payment_alipay_private_key", "", "支付宝私钥"),
|
||||||
# Text credit config
|
# Text credit config
|
||||||
("text_credits_per_1000_tokens", "1", "每1000 token消耗文本积分"),
|
("text_credits_per_1000_tokens", "1", "每1000 token消耗文本积分"),
|
||||||
|
# User credits config
|
||||||
|
("user_register_credits", "100", "用户注册赠送积分"),
|
||||||
|
("user_login_credits", "0", "用户每日登录赠送积分"),
|
||||||
|
("user_login_credits_enabled", "false", "启用每日登录赠送积分"),
|
||||||
]
|
]
|
||||||
for key, value, desc in configs:
|
for key, value, desc in configs:
|
||||||
existing = await db.execute(
|
existing = await db.execute(
|
||||||
|
|||||||
Reference in New Issue
Block a user