WHY: migrating from Supabase BaaS to self-hosted iloom backend (Go microservices) deployed on K3s. Supabase client and realtime subscriptions no longer applicable. HOW: - add src/api/ layer (client.ts, auth.ts, purchaser.ts, textile.ts, washing.ts, websocket.ts) targeting iloom-gateway REST endpoints - rewrite all hooks (usePlanData, useInventoryData, useDashboardData, useRealtime, etc.) to use new API client instead of Supabase queries - rewrite all page/component data fetching and mutations accordingly - delete src/supabase/client.ts, remove @supabase/supabase-js dep - add Dockerfile (multi-stage node build + nginx) and nginx.conf with reverse proxy to iloom-gateway for /api/ and /ws/ routes - adjust webpack.config.js for API_URL environment variable injection Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
805 lines
32 KiB
TypeScript
805 lines
32 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
||
import { useNavigate } from 'react-router-dom';
|
||
import { useAuth } from '../contexts/AuthContext';
|
||
import { commonApi, http } from '../api';
|
||
import { motion, AnimatePresence } from 'framer-motion';
|
||
import { Users, ArrowLeft, Plus, X, User, Phone, Shield, Trash2, AlertCircle, Edit2, Building2, BookOpen, MessageSquare, Info } from 'lucide-react';
|
||
import { useResponsive } from '../hooks/useResponsive';
|
||
import { encryptPhone, maskPhone } from '../utils/crypto';
|
||
import { UserManual } from '../components/UserManual';
|
||
import { FeedbackModal } from '../components/FeedbackModal';
|
||
import { AboutModal } from '../components/VersionUpdate';
|
||
|
||
interface Member {
|
||
id: string;
|
||
username: string;
|
||
phone: string | null;
|
||
created_at: string;
|
||
}
|
||
|
||
export function MemberManage() {
|
||
const navigate = useNavigate();
|
||
const { auth } = useAuth();
|
||
const { isMobile } = useResponsive();
|
||
const [members, setMembers] = useState<Member[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [showModal, setShowModal] = useState(false);
|
||
const [showDeleteConfirm, setShowDeleteConfirm] = useState<string | null>(null);
|
||
|
||
// 表单状态
|
||
const [formData, setFormData] = useState({
|
||
username: '',
|
||
displayName: '',
|
||
phone: '',
|
||
password: ''
|
||
});
|
||
const [submitting, setSubmitting] = useState(false);
|
||
const [error, setError] = useState('');
|
||
|
||
// 主账号编辑状态
|
||
const [showEditMasterModal, setShowEditMasterModal] = useState(false);
|
||
const [masterFormData, setMasterFormData] = useState({
|
||
displayName: '',
|
||
phone: '',
|
||
password: ''
|
||
});
|
||
const [editingMaster, setEditingMaster] = useState(false);
|
||
|
||
// 公司信息
|
||
const [companyInfo, setCompanyInfo] = useState<{
|
||
name: string;
|
||
contact_phone: string | null;
|
||
role: string;
|
||
} | null>(null);
|
||
|
||
// 帮助系统弹窗状态
|
||
const [showUserManual, setShowUserManual] = useState(false);
|
||
const [showFeedback, setShowFeedback] = useState(false);
|
||
const [showAbout, setShowAbout] = useState(false);
|
||
|
||
useEffect(() => {
|
||
if (!auth.user) return;
|
||
// 只有主账号可以管理子账号
|
||
if (!auth.user.is_master) {
|
||
navigate('/');
|
||
return;
|
||
}
|
||
fetchMembers();
|
||
fetchCompanyInfo();
|
||
}, [auth.user]);
|
||
|
||
const fetchCompanyInfo = async () => {
|
||
if (!auth.user?.company_id) return;
|
||
try {
|
||
const data = await commonApi.getCompany(auth.user.company_id);
|
||
if (data) {
|
||
setCompanyInfo(data as any);
|
||
}
|
||
} catch {
|
||
// ignore
|
||
}
|
||
};
|
||
|
||
const fetchMembers = async () => {
|
||
setLoading(true);
|
||
try {
|
||
const data = await commonApi.listMembers();
|
||
const subMembers = (data || [])
|
||
.filter((m: any) => !m.profile?.is_master)
|
||
.map((m: any) => ({
|
||
id: m.profile?.id || m.id,
|
||
username: m.profile?.username || '',
|
||
phone: m.profile?.phone || null,
|
||
created_at: m.profile?.created_at || m.created_at
|
||
}));
|
||
setMembers(subMembers);
|
||
} catch {
|
||
setMembers([]);
|
||
}
|
||
setLoading(false);
|
||
};
|
||
|
||
const handleOpenModal = () => {
|
||
setFormData({ username: '', displayName: '', phone: '', password: '' });
|
||
setError('');
|
||
setShowModal(true);
|
||
};
|
||
|
||
const handleCloseModal = () => {
|
||
setShowModal(false);
|
||
setFormData({ username: '', displayName: '', phone: '', password: '' });
|
||
setError('');
|
||
};
|
||
|
||
const handleOpenEditMaster = () => {
|
||
setMasterFormData({
|
||
displayName: (auth.user as any)?.display_name || auth.user?.username || '',
|
||
phone: auth.user?.phone || '',
|
||
password: ''
|
||
});
|
||
setShowEditMasterModal(true);
|
||
};
|
||
|
||
const handleCloseEditMaster = () => {
|
||
setShowEditMasterModal(false);
|
||
setMasterFormData({ displayName: '', phone: '', password: '' });
|
||
};
|
||
|
||
const handleUpdateMaster = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
if (!masterFormData.phone.trim()) {
|
||
setError('请输入手机号');
|
||
return;
|
||
}
|
||
if (!/^1[3-9]\d{9}$/.test(masterFormData.phone)) {
|
||
setError('请输入正确的手机号');
|
||
return;
|
||
}
|
||
|
||
setEditingMaster(true);
|
||
try {
|
||
await http.put('/common/profile', {
|
||
display_name: masterFormData.displayName,
|
||
phone: encryptPhone(masterFormData.phone)
|
||
});
|
||
|
||
if (masterFormData.password && masterFormData.password.length >= 6) {
|
||
await http.put('/common/password', {
|
||
password: masterFormData.password
|
||
});
|
||
}
|
||
|
||
window.location.reload();
|
||
} catch (err: any) {
|
||
setError('更新失败:' + (err.message || ''));
|
||
}
|
||
setEditingMaster(false);
|
||
};
|
||
|
||
const validateForm = () => {
|
||
if (!formData.username.trim()) return '请输入用户名';
|
||
if (formData.username.length < 3) return '用户名至少3个字符';
|
||
if (!formData.displayName.trim()) return '请输入姓名';
|
||
if (!formData.password.trim()) return '请输入密码';
|
||
if (formData.password.length < 6) return '密码至少6个字符';
|
||
if (!formData.phone.trim()) return '请输入手机号';
|
||
if (!/^1[3-9]\d{9}$/.test(formData.phone)) {
|
||
return '请输入正确的手机号';
|
||
}
|
||
return '';
|
||
};
|
||
|
||
const handleSubmit = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
const validationError = validateForm();
|
||
if (validationError) {
|
||
setError(validationError);
|
||
return;
|
||
}
|
||
|
||
if (!auth.user?.company_id) {
|
||
setError('请先登录');
|
||
return;
|
||
}
|
||
|
||
setSubmitting(true);
|
||
setError('');
|
||
|
||
try {
|
||
await commonApi.createMember({
|
||
username: formData.username.trim(),
|
||
password: formData.password,
|
||
display_name: formData.displayName,
|
||
phone: formData.phone || undefined
|
||
});
|
||
|
||
await fetchMembers();
|
||
handleCloseModal();
|
||
} catch (err: any) {
|
||
setError('创建子账号失败:' + (err.message || ''));
|
||
}
|
||
|
||
setSubmitting(false);
|
||
};
|
||
|
||
const handleDelete = async (memberId: string) => {
|
||
try {
|
||
await commonApi.deleteMember(memberId);
|
||
setMembers(members.filter(m => m.id !== memberId));
|
||
setShowDeleteConfirm(null);
|
||
} catch {
|
||
// ignore
|
||
}
|
||
};
|
||
|
||
// 根据角色获取主题色
|
||
const getThemeColors = () => {
|
||
const role = companyInfo?.role || 'purchaser';
|
||
switch (role) {
|
||
case 'textile':
|
||
return {
|
||
primary: 'emerald',
|
||
bg: 'bg-emerald-50',
|
||
bgHover: 'hover:bg-emerald-100',
|
||
text: 'text-emerald-500',
|
||
border: 'border-emerald-200',
|
||
ring: 'focus:ring-emerald-400',
|
||
button: 'bg-emerald-400 hover:bg-emerald-500',
|
||
gradient: 'from-emerald-400 to-teal-400'
|
||
};
|
||
case 'washing':
|
||
return {
|
||
primary: 'violet',
|
||
bg: 'bg-violet-50',
|
||
bgHover: 'hover:bg-violet-100',
|
||
text: 'text-violet-500',
|
||
border: 'border-violet-200',
|
||
ring: 'focus:ring-violet-400',
|
||
button: 'bg-violet-400 hover:bg-violet-500',
|
||
gradient: 'from-violet-400 to-fuchsia-400'
|
||
};
|
||
default:
|
||
return {
|
||
primary: 'amber',
|
||
bg: 'bg-amber-50',
|
||
bgHover: 'hover:bg-amber-100',
|
||
text: 'text-amber-500',
|
||
border: 'border-amber-200',
|
||
ring: 'focus:ring-amber-400',
|
||
button: 'bg-amber-400 hover:bg-amber-500',
|
||
gradient: 'from-amber-400 to-orange-400'
|
||
};
|
||
}
|
||
};
|
||
|
||
const theme = getThemeColors();
|
||
|
||
// 如果不是主账号,显示无权限
|
||
if (!auth.user?.is_master) {
|
||
return (
|
||
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
|
||
<div className="bg-white rounded-lg shadow-sm p-8 text-center max-w-md">
|
||
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||
<Shield className="w-8 h-8 text-red-500" />
|
||
</div>
|
||
<h2 className="text-lg font-bold text-gray-800 mb-2">无权限访问</h2>
|
||
<p className="text-gray-500 mb-4">只有主账号可以管理账号</p>
|
||
<button
|
||
onClick={() => navigate(-1)}
|
||
className={`px-6 py-2 ${theme.button} text-white rounded-lg transition-colors`}
|
||
>
|
||
返回
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen bg-gray-50 p-4 md:p-6">
|
||
<div className="max-w-4xl mx-auto">
|
||
{/* 头部 */}
|
||
<div className="flex items-center justify-between mb-6">
|
||
<div className="flex items-center gap-3 md:gap-4">
|
||
<button
|
||
onClick={() => navigate(-1)}
|
||
className="p-2 hover:bg-gray-200 rounded-full transition-colors"
|
||
>
|
||
<ArrowLeft className="w-5 h-5 md:w-6 md:h-6 text-gray-600" />
|
||
</button>
|
||
<div>
|
||
<h1 className="text-xl md:text-2xl font-bold text-gray-800">账号管理</h1>
|
||
<p className="text-sm text-gray-500">管理公司成员账号</p>
|
||
</div>
|
||
</div>
|
||
<motion.button
|
||
whileHover={{ scale: isMobile ? 1 : 1.05 }}
|
||
whileTap={{ scale: 0.95 }}
|
||
onClick={handleOpenModal}
|
||
className={`flex items-center gap-2 px-4 py-2 ${theme.button} text-white rounded-lg transition-colors text-sm md:text-base`}
|
||
>
|
||
<Plus size={18} />
|
||
添加子账号
|
||
</motion.button>
|
||
</div>
|
||
|
||
{/* 主账号信息卡片 */}
|
||
<div className={`bg-gradient-to-r ${theme.gradient} rounded-xl p-4 md:p-6 mb-6 text-white`}>
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-12 h-12 bg-white/20 rounded-full flex items-center justify-center">
|
||
<Shield className="w-6 h-6" />
|
||
</div>
|
||
<div>
|
||
<p className="text-sm text-white/70">当前登录账号</p>
|
||
<p className="font-semibold text-lg">{auth.user?.username}</p>
|
||
<span className="inline-flex items-center gap-1 text-xs bg-white/20 px-2 py-0.5 rounded mt-1">
|
||
<Shield size={12} />
|
||
主账号
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<motion.button
|
||
whileTap={{ scale: 0.98 }}
|
||
onClick={handleOpenEditMaster}
|
||
className="flex items-center gap-1.5 px-3 py-1.5 text-sm text-white bg-white/20 hover:bg-white/30 rounded-lg transition-colors"
|
||
>
|
||
<Edit2 className="w-4 h-4" />
|
||
编辑
|
||
</motion.button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 数据完整性监控入口 */}
|
||
<div className="bg-white rounded-lg shadow-sm p-4 md:p-6 mb-6">
|
||
<div className="flex items-center gap-2 mb-4">
|
||
<Shield className={`w-5 h-5 ${theme.text}`} />
|
||
<h2 className="font-semibold text-gray-800">系统管理</h2>
|
||
</div>
|
||
<motion.button
|
||
whileHover={{ scale: 1.02 }}
|
||
whileTap={{ scale: 0.98 }}
|
||
onClick={() => navigate('/admin/data-integrity')}
|
||
className={`flex items-center gap-3 p-4 w-full ${theme.bg} rounded-xl ${theme.bgHover} transition-colors text-left`}
|
||
>
|
||
<div className={`w-10 h-10 ${theme.bg} rounded-lg flex items-center justify-center`}>
|
||
<Shield className={`w-5 h-5 ${theme.text}`} />
|
||
</div>
|
||
<div>
|
||
<p className="font-medium text-gray-800">数据完整性监控</p>
|
||
<p className="text-xs text-gray-500">查看系统数据一致性与错误率</p>
|
||
</div>
|
||
</motion.button>
|
||
</div>
|
||
|
||
{/* 帮助与支持卡片 */}
|
||
<div className="bg-white rounded-lg shadow-sm p-4 md:p-6 mb-6">
|
||
<div className="flex items-center gap-2 mb-4">
|
||
<BookOpen className={`w-5 h-5 ${theme.text}`} />
|
||
<h2 className="font-semibold text-gray-800">帮助与支持</h2>
|
||
</div>
|
||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||
<motion.button
|
||
whileHover={{ scale: 1.02 }}
|
||
whileTap={{ scale: 0.98 }}
|
||
onClick={() => setShowUserManual(true)}
|
||
className={`flex items-center gap-3 p-4 ${theme.bg} rounded-xl ${theme.bgHover} transition-colors text-left`}
|
||
>
|
||
<div className={`w-10 h-10 ${theme.bg} rounded-lg flex items-center justify-center`}>
|
||
<BookOpen className={`w-5 h-5 ${theme.text}`} />
|
||
</div>
|
||
<div>
|
||
<p className="font-medium text-gray-800">用户手册</p>
|
||
<p className="text-xs text-gray-500">详细使用指南</p>
|
||
</div>
|
||
</motion.button>
|
||
|
||
<motion.button
|
||
whileHover={{ scale: 1.02 }}
|
||
whileTap={{ scale: 0.98 }}
|
||
onClick={() => setShowFeedback(true)}
|
||
className={`flex items-center gap-3 p-4 ${theme.bg} rounded-xl ${theme.bgHover} transition-colors text-left`}
|
||
>
|
||
<div className={`w-10 h-10 ${theme.bg} rounded-lg flex items-center justify-center`}>
|
||
<MessageSquare className={`w-5 h-5 ${theme.text}`} />
|
||
</div>
|
||
<div>
|
||
<p className="font-medium text-gray-800">用户反馈</p>
|
||
<p className="text-xs text-gray-500">提交问题或建议</p>
|
||
</div>
|
||
</motion.button>
|
||
|
||
<motion.button
|
||
whileHover={{ scale: 1.02 }}
|
||
whileTap={{ scale: 0.98 }}
|
||
onClick={() => setShowAbout(true)}
|
||
className={`flex items-center gap-3 p-4 ${theme.bg} rounded-xl ${theme.bgHover} transition-colors text-left`}
|
||
>
|
||
<div className={`w-10 h-10 ${theme.bg} rounded-lg flex items-center justify-center`}>
|
||
<Info className={`w-5 h-5 ${theme.text}`} />
|
||
</div>
|
||
<div>
|
||
<p className="font-medium text-gray-800">关于</p>
|
||
<p className="text-xs text-gray-500">版本信息与版权</p>
|
||
</div>
|
||
</motion.button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 子账号列表 */}
|
||
{loading ? (
|
||
<div className="text-center py-12 text-gray-400">加载中...</div>
|
||
) : members.length === 0 ? (
|
||
<div className="bg-white rounded-lg shadow-sm p-8 text-center">
|
||
<div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||
<Users className="w-8 h-8 text-gray-400" />
|
||
</div>
|
||
<p className="text-gray-500 mb-2">暂无子账号</p>
|
||
<p className="text-gray-400 text-sm mb-4">点击右上角"添加子账号"按钮创建</p>
|
||
<motion.button
|
||
whileHover={{ scale: isMobile ? 1 : 1.05 }}
|
||
whileTap={{ scale: 0.95 }}
|
||
onClick={handleOpenModal}
|
||
className={`px-6 py-2 ${theme.button} text-white rounded-lg transition-colors`}
|
||
>
|
||
添加子账号
|
||
</motion.button>
|
||
</div>
|
||
) : (
|
||
<div className="bg-white rounded-lg shadow-sm overflow-hidden">
|
||
<div className="p-4 border-b border-gray-100">
|
||
<h2 className="font-semibold text-gray-800">
|
||
子账号列表 <span className="text-sm text-gray-500">({members.length})</span>
|
||
</h2>
|
||
</div>
|
||
<div className="divide-y divide-gray-100">
|
||
{members.map((member) => (
|
||
<div
|
||
key={member.id}
|
||
className="flex items-center justify-between p-4 hover:bg-gray-50 transition-colors"
|
||
>
|
||
<div className="flex items-center gap-3">
|
||
<div className="w-10 h-10 bg-gray-100 rounded-full flex items-center justify-center">
|
||
<User className="w-5 h-5 text-gray-500" />
|
||
</div>
|
||
<div>
|
||
<p className="font-medium text-gray-800">{member.username}</p>
|
||
<div className="flex items-center gap-2 text-sm text-gray-500">
|
||
{member.phone && (
|
||
<span className="flex items-center gap-1">
|
||
<Phone size={12} />
|
||
{maskPhone(member.phone)}
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center gap-3">
|
||
<span className="text-xs text-gray-400">
|
||
{new Date(member.created_at).toLocaleDateString('zh-CN')} 创建
|
||
</span>
|
||
<button
|
||
onClick={() => setShowDeleteConfirm(member.id)}
|
||
className="p-2 text-red-500 hover:bg-red-50 rounded-lg transition-colors"
|
||
>
|
||
<Trash2 size={18} />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 添加子账号弹窗 */}
|
||
<AnimatePresence>
|
||
{showModal && (
|
||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||
<motion.div
|
||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
||
transition={{ duration: 0.2 }}
|
||
className="bg-white rounded-2xl w-full max-w-md shadow-xl"
|
||
>
|
||
<div className="flex items-center justify-between p-4 md:p-6 border-b border-gray-100">
|
||
<h2 className="text-lg md:text-xl font-bold text-gray-800">添加子账号</h2>
|
||
<button
|
||
onClick={handleCloseModal}
|
||
className="p-2 hover:bg-gray-100 rounded-full transition-colors"
|
||
>
|
||
<X size={20} className="text-gray-500" />
|
||
</button>
|
||
</div>
|
||
|
||
<form onSubmit={handleSubmit} className="p-4 md:p-6 space-y-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1.5">
|
||
用户名 <span className="text-red-500">*</span>
|
||
</label>
|
||
<div className="relative">
|
||
<User className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
|
||
<input
|
||
type="text"
|
||
value={formData.username}
|
||
onChange={(e) => setFormData({ ...formData, username: e.target.value })}
|
||
className={`w-full pl-10 pr-4 py-2.5 border border-gray-300 rounded-lg ${theme.ring} focus:border-transparent transition-all`}
|
||
placeholder="请输入用户名(至少3个字符)"
|
||
required
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1.5">
|
||
姓名 <span className="text-red-500">*</span>
|
||
</label>
|
||
<div className="relative">
|
||
<User className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
|
||
<input
|
||
type="text"
|
||
value={formData.displayName}
|
||
onChange={(e) => setFormData({ ...formData, displayName: e.target.value })}
|
||
className={`w-full pl-10 pr-4 py-2.5 border border-gray-300 rounded-lg ${theme.ring} focus:border-transparent transition-all`}
|
||
placeholder="请输入姓名"
|
||
required
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1.5">
|
||
手机号 <span className="text-red-500">*</span>
|
||
</label>
|
||
<div className="relative">
|
||
<Phone className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
|
||
<input
|
||
type="tel"
|
||
value={formData.phone}
|
||
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
|
||
className={`w-full pl-10 pr-4 py-2.5 border border-gray-300 rounded-lg ${theme.ring} focus:border-transparent transition-all`}
|
||
placeholder="请输入手机号"
|
||
required
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1.5">
|
||
密码 <span className="text-red-500">*</span>
|
||
</label>
|
||
<input
|
||
type="password"
|
||
value={formData.password}
|
||
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
||
className={`w-full px-4 py-2.5 border border-gray-300 rounded-lg ${theme.ring} focus:border-transparent transition-all`}
|
||
placeholder="请输入密码(至少6个字符)"
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
{error && (
|
||
<motion.p
|
||
initial={{ opacity: 0, y: -10 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
className="text-red-500 text-sm text-center bg-red-50 py-2 rounded-lg flex items-center justify-center gap-2"
|
||
>
|
||
<AlertCircle size={16} />
|
||
{error}
|
||
</motion.p>
|
||
)}
|
||
|
||
<div className="flex gap-3 pt-2">
|
||
<button
|
||
type="button"
|
||
onClick={handleCloseModal}
|
||
className="flex-1 py-2.5 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors font-medium"
|
||
>
|
||
取消
|
||
</button>
|
||
<motion.button
|
||
whileHover={{ scale: isMobile ? 1 : 1.02 }}
|
||
whileTap={{ scale: 0.98 }}
|
||
type="submit"
|
||
disabled={submitting}
|
||
className={`flex-1 py-2.5 ${theme.button} text-white rounded-lg transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed`}
|
||
>
|
||
{submitting ? '创建中...' : '创建子账号'}
|
||
</motion.button>
|
||
</div>
|
||
</form>
|
||
</motion.div>
|
||
</div>
|
||
)}
|
||
</AnimatePresence>
|
||
|
||
{/* 编辑主账号弹窗 */}
|
||
<AnimatePresence>
|
||
{showEditMasterModal && (
|
||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||
<motion.div
|
||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
||
transition={{ duration: 0.2 }}
|
||
className="bg-white rounded-2xl w-full max-w-md shadow-xl"
|
||
>
|
||
<div className="flex items-center justify-between p-4 md:p-6 border-b border-gray-100">
|
||
<h2 className="text-lg md:text-xl font-bold text-gray-800">编辑主账号</h2>
|
||
<button
|
||
onClick={handleCloseEditMaster}
|
||
className="p-2 hover:bg-gray-100 rounded-full transition-colors"
|
||
>
|
||
<X size={20} className="text-gray-500" />
|
||
</button>
|
||
</div>
|
||
|
||
<form onSubmit={handleUpdateMaster} className="p-4 md:p-6 space-y-4">
|
||
{/* 公司信息(只读) */}
|
||
<div className="bg-gray-50 rounded-lg p-3 space-y-3">
|
||
<p className="text-xs font-medium text-gray-500 uppercase tracking-wider">公司信息</p>
|
||
|
||
{/* 公司名称 - 只读 */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-500 mb-1">
|
||
公司名称
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={companyInfo?.name || '-'}
|
||
disabled
|
||
className="w-full px-3 py-2 border border-gray-200 bg-gray-100 text-gray-500 rounded-lg cursor-not-allowed text-sm"
|
||
/>
|
||
</div>
|
||
|
||
{/* 角色类型 - 只读 */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-500 mb-1">
|
||
角色类型
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={companyInfo?.role === 'purchaser' ? '采购商' : companyInfo?.role === 'textile' ? '纺织厂' : companyInfo?.role === 'washing' ? '水洗厂' : '-'}
|
||
disabled
|
||
className="w-full px-3 py-2 border border-gray-200 bg-gray-100 text-gray-500 rounded-lg cursor-not-allowed text-sm"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 账号信息(只读) */}
|
||
<div className="bg-gray-50 rounded-lg p-3 space-y-3">
|
||
<p className="text-xs font-medium text-gray-500 uppercase tracking-wider">账号信息</p>
|
||
|
||
{/* 用户名 - 只读 */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-500 mb-1">
|
||
用户名
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={auth.user?.username || ''}
|
||
disabled
|
||
className="w-full px-3 py-2 border border-gray-200 bg-gray-100 text-gray-500 rounded-lg cursor-not-allowed text-sm"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 可编辑字段 */}
|
||
<div className="border-t border-gray-200 pt-4 space-y-4">
|
||
{/* 姓名 - 可编辑 */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1.5">
|
||
姓名 <span className="text-red-500">*</span>
|
||
</label>
|
||
<div className="relative">
|
||
<User className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
|
||
<input
|
||
type="text"
|
||
value={masterFormData.displayName}
|
||
onChange={(e) => setMasterFormData({ ...masterFormData, displayName: e.target.value })}
|
||
className={`w-full pl-10 pr-4 py-2.5 border border-gray-300 rounded-lg ${theme.ring} focus:border-transparent transition-all`}
|
||
placeholder="请输入姓名"
|
||
required
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 手机号 - 可编辑 */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1.5">
|
||
手机号 <span className="text-red-500">*</span>
|
||
</label>
|
||
<div className="relative">
|
||
<Phone className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
|
||
<input
|
||
type="tel"
|
||
value={masterFormData.phone}
|
||
onChange={(e) => setMasterFormData({ ...masterFormData, phone: e.target.value })}
|
||
className={`w-full pl-10 pr-4 py-2.5 border border-gray-300 rounded-lg ${theme.ring} focus:border-transparent transition-all`}
|
||
placeholder="请输入手机号"
|
||
required
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 登录密码 - 可编辑 */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-1.5">
|
||
登录密码
|
||
</label>
|
||
<input
|
||
type="password"
|
||
value={masterFormData.password || ''}
|
||
onChange={(e) => setMasterFormData({ ...masterFormData, password: e.target.value })}
|
||
className={`w-full px-4 py-2.5 border border-gray-300 rounded-lg ${theme.ring} focus:border-transparent transition-all`}
|
||
placeholder="请输入新密码(至少6个字符)"
|
||
/>
|
||
<p className="text-xs text-gray-400 mt-1">留空则保持原密码不变</p>
|
||
</div>
|
||
</div>
|
||
|
||
{error && (
|
||
<motion.p
|
||
initial={{ opacity: 0, y: -10 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
className="text-red-500 text-sm text-center bg-red-50 py-2 rounded-lg flex items-center justify-center gap-2"
|
||
>
|
||
<AlertCircle size={16} />
|
||
{error}
|
||
</motion.p>
|
||
)}
|
||
|
||
<div className="flex gap-3 pt-2">
|
||
<button
|
||
type="button"
|
||
onClick={handleCloseEditMaster}
|
||
className="flex-1 py-2.5 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors font-medium"
|
||
>
|
||
取消
|
||
</button>
|
||
<motion.button
|
||
whileHover={{ scale: isMobile ? 1 : 1.02 }}
|
||
whileTap={{ scale: 0.98 }}
|
||
type="submit"
|
||
disabled={editingMaster}
|
||
className={`flex-1 py-2.5 ${theme.button} text-white rounded-lg transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed`}
|
||
>
|
||
{editingMaster ? '保存中...' : '保存修改'}
|
||
</motion.button>
|
||
</div>
|
||
</form>
|
||
</motion.div>
|
||
</div>
|
||
)}
|
||
</AnimatePresence>
|
||
|
||
{/* 删除确认弹窗 */}
|
||
<AnimatePresence>
|
||
{showDeleteConfirm && (
|
||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||
<motion.div
|
||
initial={{ opacity: 0, scale: 0.95 }}
|
||
animate={{ opacity: 1, scale: 1 }}
|
||
exit={{ opacity: 0, scale: 0.95 }}
|
||
transition={{ duration: 0.2 }}
|
||
className="bg-white rounded-2xl w-full max-w-sm shadow-xl p-6"
|
||
>
|
||
<div className="text-center">
|
||
<div className="w-12 h-12 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||
<AlertCircle className="w-6 h-6 text-red-500" />
|
||
</div>
|
||
<h3 className="text-lg font-bold text-gray-800 mb-2">确认删除</h3>
|
||
<p className="text-gray-500 mb-6">删除后该子账号将无法登录,是否确认删除?</p>
|
||
<div className="flex gap-3">
|
||
<button
|
||
onClick={() => setShowDeleteConfirm(null)}
|
||
className="flex-1 py-2.5 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors font-medium"
|
||
>
|
||
取消
|
||
</button>
|
||
<button
|
||
onClick={() => handleDelete(showDeleteConfirm)}
|
||
className="flex-1 py-2.5 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors font-medium"
|
||
>
|
||
确认删除
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</motion.div>
|
||
</div>
|
||
)}
|
||
</AnimatePresence>
|
||
|
||
{/* 用户手册弹窗 */}
|
||
<UserManual isOpen={showUserManual} onClose={() => setShowUserManual(false)} />
|
||
|
||
{/* 用户反馈弹窗 */}
|
||
<FeedbackModal isOpen={showFeedback} onClose={() => setShowFeedback(false)} />
|
||
|
||
{/* 关于窗口 */}
|
||
<AboutModal isOpen={showAbout} onClose={() => setShowAbout(false)} />
|
||
</div>
|
||
);
|
||
}
|