2026-05-28 05:54:23 +00:00
|
|
|
|
import React, { useState, useCallback, useEffect, useRef } from 'react';
|
2026-05-26 05:15:10 +00:00
|
|
|
|
import { motion } from 'framer-motion';
|
2026-05-27 03:17:46 +00:00
|
|
|
|
import { Share2, Copy, Check, X, AlertTriangle, Building2 } from 'lucide-react';
|
2026-05-26 05:15:10 +00:00
|
|
|
|
import { useResponsive } from '../../hooks/useResponsive';
|
2026-05-28 05:54:23 +00:00
|
|
|
|
import { encryptShareParamsSync } from '../../utils/shareCrypto';
|
2026-05-27 03:17:46 +00:00
|
|
|
|
import { supabase } from '../../supabase/client';
|
2026-05-26 05:15:10 +00:00
|
|
|
|
|
|
|
|
|
|
interface ShareModalProps {
|
|
|
|
|
|
isOpen: boolean;
|
|
|
|
|
|
onClose: () => void;
|
|
|
|
|
|
planId: string;
|
|
|
|
|
|
factoryType: 'textile' | 'washing';
|
2026-05-27 03:17:46 +00:00
|
|
|
|
factoryName?: string;
|
2026-05-26 05:15:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-27 03:17:46 +00:00
|
|
|
|
export function ShareModal({ isOpen, onClose, planId, factoryType, factoryName }: ShareModalProps) {
|
2026-05-26 05:15:10 +00:00
|
|
|
|
const { isMobile } = useResponsive();
|
|
|
|
|
|
const [copied, setCopied] = useState(false);
|
2026-05-27 03:17:46 +00:00
|
|
|
|
const [showConfirm, setShowConfirm] = useState(false);
|
|
|
|
|
|
const [recipientCompany, setRecipientCompany] = useState<string>('');
|
|
|
|
|
|
const [confirmFactoryName, setConfirmFactoryName] = useState('');
|
|
|
|
|
|
const [confirmCompanyName, setConfirmCompanyName] = useState('');
|
|
|
|
|
|
const [error, setError] = useState('');
|
2026-05-28 05:54:23 +00:00
|
|
|
|
const [shortCode, setShortCode] = useState<string>('');
|
|
|
|
|
|
const shortLinkGenerated = useRef(false);
|
2026-05-27 03:17:46 +00:00
|
|
|
|
|
|
|
|
|
|
// 获取当前用户的公司名称
|
|
|
|
|
|
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();
|
2026-05-28 05:54:23 +00:00
|
|
|
|
// 生成短链
|
|
|
|
|
|
if (!shortLinkGenerated.current) {
|
|
|
|
|
|
shortLinkGenerated.current = true;
|
|
|
|
|
|
generateShortLink();
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 弹窗关闭时重置
|
|
|
|
|
|
shortLinkGenerated.current = false;
|
|
|
|
|
|
setShortCode('');
|
2026-05-27 03:17:46 +00:00
|
|
|
|
}
|
|
|
|
|
|
}, [isOpen]);
|
2026-05-26 05:15:10 +00:00
|
|
|
|
|
2026-05-28 05:54:23 +00:00
|
|
|
|
// 生成短链
|
|
|
|
|
|
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]);
|
|
|
|
|
|
|
|
|
|
|
|
// 生成分享链接
|
2026-05-26 05:15:10 +00:00
|
|
|
|
const generateShareLink = useCallback(() => {
|
|
|
|
|
|
const baseUrl = window.location.origin + window.location.pathname;
|
2026-05-28 05:54:23 +00:00
|
|
|
|
if (shortCode) {
|
|
|
|
|
|
return `${baseUrl}#/import?s=${shortCode}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 如果短链还未生成,使用长链接
|
|
|
|
|
|
const token = encryptShareParamsSync(planId, factoryType);
|
2026-05-26 05:15:10 +00:00
|
|
|
|
return `${baseUrl}#/import?token=${token}`;
|
2026-05-28 05:54:23 +00:00
|
|
|
|
}, [planId, factoryType, shortCode]);
|
2026-05-26 05:15:10 +00:00
|
|
|
|
|
|
|
|
|
|
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]);
|
|
|
|
|
|
|
2026-05-27 03:17:46 +00:00
|
|
|
|
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('');
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-26 05:15:10 +00:00
|
|
|
|
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"
|
|
|
|
|
|
>
|
2026-05-27 03:17:46 +00:00
|
|
|
|
{!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>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
2026-05-26 05:15:10 +00:00
|
|
|
|
</motion.div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|