272 lines
10 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, useCallback, useEffect, useRef } from 'react';
import { motion } from 'framer-motion';
import { Share2, Copy, Check, X, AlertTriangle, Building2 } from 'lucide-react';
import { useResponsive } from '../../hooks/useResponsive';
import { encryptShareParamsSync } from '../../utils/shareCrypto';
import { supabase } from '../../supabase/client';
interface ShareModalProps {
isOpen: boolean;
onClose: () => void;
planId: string;
factoryType: 'textile' | 'washing';
factoryName?: string;
}
export function ShareModal({ isOpen, onClose, planId, factoryType, factoryName }: ShareModalProps) {
const { isMobile } = useResponsive();
const [copied, setCopied] = useState(false);
const [showConfirm, setShowConfirm] = useState(false);
const [recipientCompany, setRecipientCompany] = useState<string>('');
const [confirmFactoryName, setConfirmFactoryName] = useState('');
const [confirmCompanyName, setConfirmCompanyName] = useState('');
const [error, setError] = useState('');
const [shortCode, setShortCode] = useState<string>('');
const shortLinkGenerated = useRef(false);
// 获取当前用户的公司名称
useEffect(() => {
const fetchCompanyName = async () => {
const { data: { user } } = await supabase.auth.getUser();
if (user) {
const { data: profile } = await supabase
.from('profiles')
.select('company_id')
.eq('id', user.id)
.single();
if (profile?.company_id) {
const { data: company } = await supabase
.from('companies')
.select('name')
.eq('id', profile.company_id)
.single();
if (company) {
setRecipientCompany(company.name);
}
}
}
};
if (isOpen) {
fetchCompanyName();
// 生成短链
if (!shortLinkGenerated.current) {
shortLinkGenerated.current = true;
generateShortLink();
}
} else {
// 弹窗关闭时重置
shortLinkGenerated.current = false;
setShortCode('');
}
}, [isOpen]);
// 生成短链
const generateShortLink = useCallback(async () => {
const baseUrl = window.location.origin + window.location.pathname;
// 生成短码6位随机字符
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let code = '';
for (let i = 0; i < 6; i++) {
code += chars.charAt(Math.floor(Math.random() * chars.length));
}
// 保存到数据库
const { data: { user } } = await supabase.auth.getUser();
if (user) {
const { error } = await supabase.from('share_links').insert({
short_code: code,
plan_id: planId,
factory_type: factoryType,
created_by: user.id,
expires_at: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString() // 7天过期
});
if (!error) {
setShortCode(code);
console.log('短链生成成功:', code);
} else {
console.error('短链保存失败:', error);
}
} else {
console.error('用户未登录,无法生成短链');
}
}, [planId, factoryType]);
// 生成分享链接
const generateShareLink = useCallback(() => {
const baseUrl = window.location.origin + window.location.pathname;
if (shortCode) {
return `${baseUrl}#/import?s=${shortCode}`;
}
// 如果短链还未生成,使用长链接
const token = encryptShareParamsSync(planId, factoryType);
return `${baseUrl}#/import?token=${token}`;
}, [planId, factoryType, shortCode]);
const handleCopy = useCallback(async () => {
const link = generateShareLink();
try {
await navigator.clipboard.writeText(link);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch {
const textarea = document.createElement('textarea');
textarea.value = link;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
}, [generateShareLink]);
const handleConfirmShare = () => {
setError('');
if (confirmFactoryName.trim() !== factoryName?.trim()) {
setError('工厂名称不匹配,请确认工厂名称');
return;
}
if (confirmCompanyName.trim() !== recipientCompany.trim()) {
setError('对方公司名称不匹配,请确认对方公司名称');
return;
}
// 验证通过,显示分享链接
setShowConfirm(false);
};
const resetConfirm = () => {
setShowConfirm(false);
setConfirmFactoryName('');
setConfirmCompanyName('');
setError('');
};
if (!isOpen) return null;
const link = generateShareLink();
const factoryLabel = factoryType === 'textile' ? '织布厂' : '水洗厂';
return (
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-3 sm:p-4">
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
className="bg-white rounded-2xl p-4 sm:p-6 w-full max-w-md shadow-2xl"
>
{!showConfirm ? (
<>
<div className="flex items-center gap-2 sm:gap-3 mb-3 sm:mb-4">
<div className="w-9 h-9 sm:w-10 sm:h-10 bg-blue-100 rounded-xl flex items-center justify-center flex-shrink-0">
<Share2 className="w-4 h-4 sm:w-5 sm:h-5 text-blue-600" />
</div>
<div className="min-w-0">
<h3 className="text-base sm:text-lg font-semibold text-gray-900"></h3>
<p className="text-xs sm:text-sm text-gray-500">{factoryLabel}</p>
</div>
</div>
<div className="bg-gray-50 rounded-xl p-2.5 sm:p-3 mb-3 sm:mb-4 border border-gray-200">
<p className="text-xs sm:text-sm text-gray-700 break-all font-mono">{link}</p>
</div>
<div className="flex gap-2 sm:gap-3">
<button
onClick={onClose}
className="flex-1 py-2 sm:py-2.5 border border-gray-300 rounded-xl text-gray-700 hover:bg-gray-50 text-xs sm:text-sm font-medium transition-colors"
>
</button>
<button
onClick={handleCopy}
className={`flex-1 py-2 sm:py-2.5 rounded-xl text-white flex items-center justify-center gap-1.5 sm:gap-2 transition-colors text-xs sm:text-sm font-medium ${
copied ? 'bg-emerald-500' : 'bg-blue-600 hover:bg-blue-700'
}`}
>
{copied ? <Check size={isMobile ? 14 : 16} /> : <Copy size={isMobile ? 14 : 16} />}
{copied ? '已复制' : '复制链接'}
</button>
</div>
</>
) : (
<>
<div className="flex items-center gap-2 sm:gap-3 mb-4">
<div className="w-9 h-9 sm:w-10 sm:h-10 bg-amber-100 rounded-xl flex items-center justify-center flex-shrink-0">
<AlertTriangle className="w-4 h-4 sm:w-5 sm:h-5 text-amber-600" />
</div>
<div className="min-w-0">
<h3 className="text-base sm:text-lg font-semibold text-gray-900"></h3>
<p className="text-xs sm:text-sm text-gray-500"></p>
</div>
</div>
<div className="space-y-3 mb-4">
<div className="bg-blue-50 rounded-xl p-3 border border-blue-100">
<div className="flex items-center gap-2 mb-1">
<Building2 className="w-4 h-4 text-blue-600" />
<span className="text-xs text-blue-600 font-medium"></span>
</div>
<p className="text-sm text-gray-900 font-medium">{factoryName || '未指定'}</p>
</div>
<div className="bg-green-50 rounded-xl p-3 border border-green-100">
<div className="flex items-center gap-2 mb-1">
<Building2 className="w-4 h-4 text-green-600" />
<span className="text-xs text-green-600 font-medium"></span>
</div>
<p className="text-sm text-gray-900 font-medium">{recipientCompany || '加载中...'}</p>
</div>
<div className="space-y-2">
<div>
<label className="text-xs text-gray-500 mb-1 block"></label>
<input
type="text"
value={confirmFactoryName}
onChange={(e) => setConfirmFactoryName(e.target.value)}
placeholder="输入工厂名称"
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:ring-2 focus:ring-amber-500/20 focus:border-amber-500"
/>
</div>
<div>
<label className="text-xs text-gray-500 mb-1 block"></label>
<input
type="text"
value={confirmCompanyName}
onChange={(e) => setConfirmCompanyName(e.target.value)}
placeholder="输入对方公司名称"
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:ring-2 focus:ring-amber-500/20 focus:border-amber-500"
/>
</div>
</div>
{error && (
<div className="bg-red-50 rounded-lg p-2 border border-red-100">
<p className="text-xs text-red-600">{error}</p>
</div>
)}
</div>
<div className="flex gap-2 sm:gap-3">
<button
onClick={resetConfirm}
className="flex-1 py-2 sm:py-2.5 border border-gray-300 rounded-xl text-gray-700 hover:bg-gray-50 text-xs sm:text-sm font-medium transition-colors"
>
</button>
<button
onClick={handleConfirmShare}
className="flex-1 py-2 sm:py-2.5 bg-amber-500 hover:bg-amber-600 rounded-xl text-white text-xs sm:text-sm font-medium transition-colors"
>
</button>
</div>
</>
)}
</motion.div>
</div>
);
}