iloom-flatten/src/pages/MemberManage.tsx
Chever John b1c2c33aa2
refactor: replace Supabase SDK with self-hosted REST API client layer
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>
2026-06-14 12:38:47 +08:00

805 lines
32 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}