初始提交: Qihu-AIFlow v2.0.1 完整项目
- 前端 React+TypeScript (源码+构建产物) - 后端 Python FastAPI - 数据库迁移 001-015 - Docker部署配置 - 微信ClawBot API频道绑定 - 项目文档 README.md
This commit is contained in:
commit
78ff261b5a
199 files changed
+69126
No files matched your search
@@ -0,0 +1,561 @@
|
||||
import React, { useState, useMemo, useEffect } from 'react';
|
||||
import { adminAPI } from '@/api';
|
||||
import { mockUsers, mockMembershipPlans } from '@/mock/data';
|
||||
import type { User, RegisterSettings } from '@/types';
|
||||
import './Users.css';
|
||||
|
||||
// ===== SVG Icons =====
|
||||
const IconSearch = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const IconEye = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const IconCheck = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="20 6 9 17 4 12"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const IconX = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const IconShield = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const IconPhone = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="5" y="2" width="14" height="20" rx="2" ry="2"/><line x1="12" y1="18" x2="12.01" y2="18"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const IconMail = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const IconServer = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="2" y="2" width="20" height="8" rx="2" ry="2"/><rect x="2" y="14" width="20" height="8" rx="2" ry="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const IconClose = () => (
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
// ===== Helpers =====
|
||||
const realNameStatusMap: Record<string, { label: string; color: string; bg: string }> = {
|
||||
verified: { label: '已实名', color: 'var(--color-success)', bg: 'var(--color-success-light)' },
|
||||
pending: { label: '审核中', color: 'var(--color-warning)', bg: 'var(--color-warning-light)' },
|
||||
unverified: { label: '未实名', color: 'var(--color-text-tertiary)', bg: 'var(--color-bg-tertiary)' },
|
||||
};
|
||||
|
||||
const statusMap: Record<string, { label: string; color: string; bg: string }> = {
|
||||
active: { label: '正常', color: 'var(--color-success)', bg: 'var(--color-success-light)' },
|
||||
banned: { label: '已封禁', color: 'var(--color-error)', bg: 'var(--color-error-light)' },
|
||||
};
|
||||
|
||||
// ===== User Detail Modal =====
|
||||
interface UserDetailModalProps {
|
||||
user: User;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const UserDetailModal: React.FC<UserDetailModalProps> = ({ user, onClose }) => {
|
||||
const plan = mockMembershipPlans.find(p => p.id === user.membership?.planId);
|
||||
const rn = realNameStatusMap[user.realNameStatus || 'unverified'];
|
||||
|
||||
return (
|
||||
<div className="admin-modal-overlay" onClick={onClose}>
|
||||
<div className="admin-modal users-detail-modal" onClick={e => e.stopPropagation()}>
|
||||
<div className="admin-modal-header">
|
||||
<h2>用户详情</h2>
|
||||
<button className="admin-modal-close" onClick={onClose}><IconClose /></button>
|
||||
</div>
|
||||
<div className="admin-modal-body">
|
||||
<div className="user-detail-top">
|
||||
<div className="user-detail-avatar">
|
||||
{user.username.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
<div className="user-detail-basic">
|
||||
<h3>{user.username}</h3>
|
||||
<div className="user-detail-tags">
|
||||
<span className="user-detail-tag" style={{ color: rn.color, background: rn.bg }}>
|
||||
<IconShield />
|
||||
{rn.label}
|
||||
</span>
|
||||
<span className="user-detail-tag" style={{ color: statusMap[user.status].color, background: statusMap[user.status].bg }}>
|
||||
{statusMap[user.status].label}
|
||||
</span>
|
||||
<span className="user-detail-tag role-tag">
|
||||
{user.role === 'admin' ? '管理员' : '普通用户'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="user-detail-grid">
|
||||
<div className="user-detail-item">
|
||||
<span className="detail-label">用户ID</span>
|
||||
<span className="detail-value">{user.id}</span>
|
||||
</div>
|
||||
<div className="user-detail-item">
|
||||
<span className="detail-label">邮箱</span>
|
||||
<span className="detail-value">{user.email}</span>
|
||||
</div>
|
||||
<div className="user-detail-item">
|
||||
<span className="detail-label">手机号</span>
|
||||
<span className="detail-value">{user.phone || '未绑定'}</span>
|
||||
</div>
|
||||
<div className="user-detail-item">
|
||||
<span className="detail-label">实名姓名</span>
|
||||
<span className="detail-value">{user.realName || '--'}</span>
|
||||
</div>
|
||||
<div className="user-detail-item">
|
||||
<span className="detail-label">实名状态</span>
|
||||
<span className="detail-value" style={{ color: rn.color, fontWeight: 500 }}>{rn.label}</span>
|
||||
</div>
|
||||
<div className="user-detail-item">
|
||||
<span className="detail-label">会员等级</span>
|
||||
<span className="detail-value">{user.membership?.planName || '免费版'}</span>
|
||||
</div>
|
||||
<div className="user-detail-item">
|
||||
<span className="detail-label">会员到期</span>
|
||||
<span className="detail-value">{user.membership?.expireAt ? new Date(user.membership.expireAt).toLocaleDateString('zh-CN') : '--'}</span>
|
||||
</div>
|
||||
<div className="user-detail-item">
|
||||
<span className="detail-label">注册时间</span>
|
||||
<span className="detail-value">{new Date(user.createdAt).toLocaleString('zh-CN')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ===== Main Component =====
|
||||
const UsersPage: React.FC = () => {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [statusFilter, setStatusFilter] = useState<string>('all');
|
||||
const [levelFilter, setLevelFilter] = useState<string>('all');
|
||||
const [realNameFilter, setRealNameFilter] = useState<string>('all');
|
||||
const [selectedUser, setSelectedUser] = useState<User | null>(null);
|
||||
const [localUsers, setLocalUsers] = useState<User[]>(mockUsers);
|
||||
|
||||
// Register settings state
|
||||
const [registerSettings, setRegisterSettings] = useState<RegisterSettings>({
|
||||
phoneRequired: true,
|
||||
phoneSmsRequired: true,
|
||||
emailRequired: true,
|
||||
emailVerifyRequired: true,
|
||||
realNameRequired: false,
|
||||
aliyunAccessKey: '',
|
||||
aliyunSecretKey: '',
|
||||
aliyunEndpoint: 'https://cloudauth.aliyuncs.com',
|
||||
});
|
||||
const [settingsSaving, setSettingsSaving] = useState(false);
|
||||
const [settingsMsg, setSettingsMsg] = useState<{ type: string; text: string } | null>(null);
|
||||
|
||||
// 加载注册设置
|
||||
useEffect(() => {
|
||||
loadRegisterSettings();
|
||||
}, []);
|
||||
|
||||
const loadRegisterSettings = async () => {
|
||||
try {
|
||||
const res = await adminAPI.getRegisterSettings();
|
||||
const data = res?.data || res;
|
||||
if (data && typeof data === 'object') {
|
||||
setRegisterSettings({
|
||||
phoneRequired: data.phone_required !== undefined ? data.phone_required : true,
|
||||
phoneSmsRequired: data.phone_sms_required !== undefined ? data.phone_sms_required : true,
|
||||
emailRequired: data.email_required !== undefined ? data.email_required : true,
|
||||
emailVerifyRequired: data.email_verify_required !== undefined ? data.email_verify_required : true,
|
||||
realNameRequired: data.real_name_required || false,
|
||||
aliyunAccessKey: data.aliyun_access_key || '',
|
||||
aliyunSecretKey: data.aliyun_secret_key || '',
|
||||
aliyunEndpoint: data.aliyun_endpoint || 'https://dysmsapi.aliyuncs.com',
|
||||
smsSignName: data.sms_sign_name || '',
|
||||
smsTemplateCode: data.sms_template_code || '',
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载注册设置失败', e);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredUsers = useMemo(() => {
|
||||
let result = localUsers.filter(u => u.role === 'user');
|
||||
if (searchQuery.trim()) {
|
||||
const q = searchQuery.toLowerCase();
|
||||
result = result.filter(u =>
|
||||
u.username.toLowerCase().includes(q) ||
|
||||
u.email.toLowerCase().includes(q) ||
|
||||
(u.phone && u.phone.includes(q)) ||
|
||||
(u.realName && u.realName.includes(q))
|
||||
);
|
||||
}
|
||||
if (statusFilter !== 'all') result = result.filter(u => u.status === statusFilter);
|
||||
if (levelFilter !== 'all') result = result.filter(u => (u.membership?.level ?? 0).toString() === levelFilter);
|
||||
if (realNameFilter !== 'all') result = result.filter(u => u.realNameStatus === realNameFilter);
|
||||
return result;
|
||||
}, [localUsers, searchQuery, statusFilter, levelFilter, realNameFilter]);
|
||||
|
||||
const handleToggleSetting = (key: keyof RegisterSettings) => {
|
||||
setRegisterSettings(prev => ({
|
||||
...prev,
|
||||
[key]: !prev[key as keyof RegisterSettings],
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSettingChange = (key: keyof RegisterSettings, value: string) => {
|
||||
setRegisterSettings(prev => ({ ...prev, [key]: value }));
|
||||
};
|
||||
|
||||
const handleSaveRegisterSettings = async () => {
|
||||
setSettingsSaving(true);
|
||||
setSettingsMsg(null);
|
||||
try {
|
||||
await adminAPI.updateRegisterSettings({
|
||||
phone_required: registerSettings.phoneRequired,
|
||||
phone_sms_required: registerSettings.phoneSmsRequired,
|
||||
email_required: registerSettings.emailRequired,
|
||||
email_verify_required: registerSettings.emailVerifyRequired,
|
||||
real_name_required: registerSettings.realNameRequired,
|
||||
aliyun_access_key: registerSettings.aliyunAccessKey,
|
||||
aliyun_secret_key: registerSettings.aliyunSecretKey,
|
||||
aliyun_endpoint: registerSettings.aliyunEndpoint,
|
||||
sms_sign_name: registerSettings.smsSignName,
|
||||
sms_template_code: registerSettings.smsTemplateCode,
|
||||
});
|
||||
setSettingsMsg({ type: 'success', text: '注册设置已保存' });
|
||||
setTimeout(() => setSettingsMsg(null), 3000);
|
||||
} catch (e: any) {
|
||||
setSettingsMsg({ type: 'error', text: e?.message || e?.detail || '保存失败' });
|
||||
} finally {
|
||||
setSettingsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleUserStatus = (userId: string) => {
|
||||
setLocalUsers(prev =>
|
||||
prev.map(u => u.id === userId ? {
|
||||
...u,
|
||||
status: u.status === 'active' ? 'banned' as const : 'active' as const,
|
||||
} : u)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="admin-users-page">
|
||||
<h1 className="admin-page-title">用户管理</h1>
|
||||
|
||||
{/* ===== 用户列表 ===== */}
|
||||
<div className="admin-card">
|
||||
<div className="admin-card-header">
|
||||
<h2 className="admin-card-title">用户列表</h2>
|
||||
<span className="admin-card-count">共 {filteredUsers.length} 个用户</span>
|
||||
</div>
|
||||
|
||||
{/* 搜索和筛选 */}
|
||||
<div className="users-toolbar">
|
||||
<div className="users-search">
|
||||
<IconSearch />
|
||||
<input
|
||||
type="text"
|
||||
className="users-search-input"
|
||||
placeholder="搜索用户名、邮箱、手机号..."
|
||||
value={searchQuery}
|
||||
onChange={e => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="users-filters">
|
||||
<select className="users-filter-select" value={statusFilter} onChange={e => setStatusFilter(e.target.value)}>
|
||||
<option value="all">全部状态</option>
|
||||
<option value="active">正常</option>
|
||||
<option value="banned">已封禁</option>
|
||||
</select>
|
||||
<select className="users-filter-select" value={levelFilter} onChange={e => setLevelFilter(e.target.value)}>
|
||||
<option value="all">全部等级</option>
|
||||
<option value="0">免费版</option>
|
||||
<option value="2">专业版</option>
|
||||
<option value="3">企业版</option>
|
||||
</select>
|
||||
<select className="users-filter-select" value={realNameFilter} onChange={e => setRealNameFilter(e.target.value)}>
|
||||
<option value="all">全部实名</option>
|
||||
<option value="verified">已实名</option>
|
||||
<option value="pending">审核中</option>
|
||||
<option value="unverified">未实名</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 用户表格 */}
|
||||
<div className="users-table-wrap">
|
||||
<table className="users-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>用户名</th>
|
||||
<th>邮箱</th>
|
||||
<th>手机号</th>
|
||||
<th>实名状态</th>
|
||||
<th>会员等级</th>
|
||||
<th>注册时间</th>
|
||||
<th>状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredUsers.map(u => {
|
||||
const rn = realNameStatusMap[u.realNameStatus || 'unverified'];
|
||||
const st = statusMap[u.status || 'active'];
|
||||
return (
|
||||
<tr key={u.id}>
|
||||
<td>
|
||||
<div className="users-cell-user">
|
||||
<div className="users-cell-avatar">{u.username.charAt(0).toUpperCase()}</div>
|
||||
<span>{u.username}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="users-cell-mono">{u.email}</td>
|
||||
<td>{u.phone || '--'}</td>
|
||||
<td>
|
||||
<span className="users-badge" style={{ color: rn.color, background: rn.bg }}>
|
||||
{rn.label}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span className="users-level-badge">
|
||||
{u.membership?.planName || '免费版'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="users-cell-date">{new Date(u.createdAt).toLocaleDateString('zh-CN')}</td>
|
||||
<td>
|
||||
<span className="users-badge" style={{ color: st.color, background: st.bg }}>
|
||||
{st.label}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div className="users-cell-actions">
|
||||
<button className="users-action-btn" title="查看详情" onClick={() => setSelectedUser(u)}>
|
||||
<IconEye />
|
||||
</button>
|
||||
<button
|
||||
className={`users-action-btn ${u.status === 'active' ? 'danger' : 'success'}`}
|
||||
title={u.status === 'active' ? '封禁' : '解封'}
|
||||
onClick={() => handleToggleUserStatus(u.id)}
|
||||
>
|
||||
{u.status === 'active' ? <IconX /> : <IconCheck />}
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
{filteredUsers.length === 0 && (
|
||||
<div className="users-empty">无匹配的用户记录</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ===== 注册用户设置 ===== */}
|
||||
<div className="admin-card" style={{ marginTop: 'var(--space-2xl)' }}>
|
||||
<div className="admin-card-header">
|
||||
<h2 className="admin-card-title">注册用户设置</h2>
|
||||
<span className="admin-card-subtitle">配置用户注册方式与认证策略</span>
|
||||
</div>
|
||||
|
||||
<div className="register-settings">
|
||||
{/* 手机号注册 */}
|
||||
<div className="register-settings-section">
|
||||
<h3 className="register-section-title">
|
||||
<IconPhone />
|
||||
手机号注册
|
||||
</h3>
|
||||
<div className="register-setting-row">
|
||||
<div className="register-setting-info">
|
||||
<strong>手机号必填</strong>
|
||||
<span>要求用户注册时填写手机号码</span>
|
||||
</div>
|
||||
<label className="toggle-switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={registerSettings.phoneRequired}
|
||||
onChange={() => handleToggleSetting('phoneRequired')}
|
||||
/>
|
||||
<span className="toggle-slider" />
|
||||
</label>
|
||||
</div>
|
||||
<div className="register-setting-row">
|
||||
<div className="register-setting-info">
|
||||
<strong>短信验证码激活</strong>
|
||||
<span>注册时发送短信验证码验证手机号</span>
|
||||
</div>
|
||||
<label className="toggle-switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={registerSettings.phoneSmsRequired}
|
||||
onChange={() => handleToggleSetting('phoneSmsRequired')}
|
||||
disabled={!registerSettings.phoneRequired}
|
||||
/>
|
||||
<span className="toggle-slider" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 邮箱注册 */}
|
||||
<div className="register-settings-section">
|
||||
<h3 className="register-section-title">
|
||||
<IconMail />
|
||||
邮箱注册
|
||||
</h3>
|
||||
<div className="register-setting-row">
|
||||
<div className="register-setting-info">
|
||||
<strong>邮箱必填</strong>
|
||||
<span>要求用户注册时填写邮箱地址</span>
|
||||
</div>
|
||||
<label className="toggle-switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={registerSettings.emailRequired}
|
||||
onChange={() => handleToggleSetting('emailRequired')}
|
||||
/>
|
||||
<span className="toggle-slider" />
|
||||
</label>
|
||||
</div>
|
||||
<div className="register-setting-row">
|
||||
<div className="register-setting-info">
|
||||
<strong>邮箱认证激活</strong>
|
||||
<span>发送验证邮件到用户邮箱完成激活</span>
|
||||
</div>
|
||||
<label className="toggle-switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={registerSettings.emailVerifyRequired}
|
||||
onChange={() => handleToggleSetting('emailVerifyRequired')}
|
||||
disabled={!registerSettings.emailRequired}
|
||||
/>
|
||||
<span className="toggle-slider" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 实名认证 */}
|
||||
<div className="register-settings-section">
|
||||
<h3 className="register-section-title">
|
||||
<IconShield />
|
||||
实名认证
|
||||
</h3>
|
||||
<div className="register-setting-row">
|
||||
<div className="register-setting-info">
|
||||
<strong>强制实名认证</strong>
|
||||
<span>要求用户完成实名认证后方可使用全部功能</span>
|
||||
</div>
|
||||
<label className="toggle-switch">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={registerSettings.realNameRequired}
|
||||
onChange={() => handleToggleSetting('realNameRequired')}
|
||||
/>
|
||||
<span className="toggle-slider" />
|
||||
</label>
|
||||
</div>
|
||||
<div className="aliyun-config">
|
||||
<div className="aliyun-config-header">
|
||||
<IconServer />
|
||||
<span>阿里云短信API配置(用于实名认证短信验证)</span>
|
||||
</div>
|
||||
<div className="aliyun-config-grid">
|
||||
<div className="form-group">
|
||||
<label>AccessKey ID</label>
|
||||
<input
|
||||
type="text"
|
||||
className="users-form-input"
|
||||
placeholder="请输入阿里云AccessKey ID"
|
||||
value={registerSettings.aliyunAccessKey || ''}
|
||||
onChange={e => handleSettingChange('aliyunAccessKey', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>AccessKey Secret</label>
|
||||
<input
|
||||
type="password"
|
||||
className="users-form-input"
|
||||
placeholder="请输入阿里云AccessKey Secret"
|
||||
value={registerSettings.aliyunSecretKey || ''}
|
||||
onChange={e => handleSettingChange('aliyunSecretKey', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>短信签名 (SignName)</label>
|
||||
<input
|
||||
type="text"
|
||||
className="users-form-input"
|
||||
placeholder="阿里云短信控制台中审核通过的签名"
|
||||
value={registerSettings.smsSignName || ''}
|
||||
onChange={e => handleSettingChange('smsSignName', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>短信模板 (TemplateCode)</label>
|
||||
<input
|
||||
type="text"
|
||||
className="users-form-input"
|
||||
placeholder="SMS_XXXXXXXXXX"
|
||||
value={registerSettings.smsTemplateCode || ''}
|
||||
onChange={e => handleSettingChange('smsTemplateCode', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{settingsMsg && (
|
||||
<div style={{
|
||||
padding: 'var(--space-md) var(--space-lg)',
|
||||
marginBottom: 'var(--space-lg)',
|
||||
borderRadius: 'var(--radius-md)',
|
||||
background: settingsMsg.type === 'success' ? 'var(--color-success-light)' : 'var(--color-error-light)',
|
||||
color: settingsMsg.type === 'success' ? 'var(--color-success)' : 'var(--color-error)',
|
||||
fontWeight: 500,
|
||||
}}>
|
||||
{settingsMsg.text}
|
||||
</div>
|
||||
)}
|
||||
<button className="users-save-btn" onClick={handleSaveRegisterSettings} disabled={settingsSaving}>
|
||||
{settingsSaving ? '保存中...' : '保存设置'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* User Detail Modal */}
|
||||
{selectedUser && (
|
||||
<UserDetailModal
|
||||
user={selectedUser}
|
||||
onClose={() => setSelectedUser(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UsersPage;
|
||||
Reference in new issue
Block a user