2026-05-26 05:15:10 +00:00
|
|
|
|
import React, { useState, useEffect, useMemo } from 'react';
|
|
|
|
|
|
import { useNavigate } from 'react-router-dom';
|
|
|
|
|
|
import { useAuth } from '../../contexts/AuthContext';
|
|
|
|
|
|
import { supabase } from '../../supabase/client';
|
|
|
|
|
|
import { ArrowLeft, Plus, Trash2, Search, X, ChevronDown } from 'lucide-react';
|
2026-05-27 03:17:46 +00:00
|
|
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
2026-06-04 15:56:19 +00:00
|
|
|
|
import { useErrorHandler } from '../../hooks/useErrorHandler';
|
2026-06-04 13:57:36 +00:00
|
|
|
|
import type { Company, FactoryType } from '../../types';
|
2026-06-04 15:56:19 +00:00
|
|
|
|
import { PLAN_STATUS, STEP_STATUS } from '../../utils/constants';
|
2026-05-26 05:15:10 +00:00
|
|
|
|
|
|
|
|
|
|
interface YarnInput {
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
name: string;
|
|
|
|
|
|
ratio: number;
|
2026-05-27 03:17:46 +00:00
|
|
|
|
yarn_type?: 'warp' | 'weft' | 'all';
|
2026-05-26 05:15:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-28 05:54:23 +00:00
|
|
|
|
// 产品类型定义(来自产品信息)
|
2026-05-26 05:15:10 +00:00
|
|
|
|
interface Product {
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
product_name: string;
|
|
|
|
|
|
weight: number;
|
2026-05-27 03:17:46 +00:00
|
|
|
|
warp_weight: number | null;
|
|
|
|
|
|
weft_weight: number | null;
|
|
|
|
|
|
total_weight: number | null;
|
2026-05-26 05:15:10 +00:00
|
|
|
|
color: string;
|
|
|
|
|
|
fabric_code: string;
|
|
|
|
|
|
color_code: string;
|
|
|
|
|
|
production_price: number;
|
2026-05-27 03:17:46 +00:00
|
|
|
|
image_url: string | null;
|
|
|
|
|
|
yarn_usage_per_meter: number;
|
2026-05-26 05:15:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 纱线配比类型
|
|
|
|
|
|
interface ProductYarnRatio {
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
product_id: string;
|
|
|
|
|
|
yarn_name: string;
|
|
|
|
|
|
ratio: number;
|
2026-05-27 03:17:46 +00:00
|
|
|
|
yarn_type?: 'warp' | 'weft' | 'all';
|
2026-05-26 05:15:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-27 03:17:46 +00:00
|
|
|
|
// 纱线类型显示映射
|
|
|
|
|
|
const yarnTypeLabels: Record<string, string> = {
|
|
|
|
|
|
warp: '经线',
|
|
|
|
|
|
weft: '纬线',
|
|
|
|
|
|
all: '全部'
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-26 05:15:10 +00:00
|
|
|
|
export function NewPlan() {
|
|
|
|
|
|
const navigate = useNavigate();
|
|
|
|
|
|
const { auth } = useAuth();
|
2026-06-04 15:56:19 +00:00
|
|
|
|
const handleError = useErrorHandler();
|
2026-05-26 05:15:10 +00:00
|
|
|
|
|
2026-05-28 05:54:23 +00:00
|
|
|
|
// 从产品信息选择的产品
|
2026-05-26 05:15:10 +00:00
|
|
|
|
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
|
|
|
|
|
|
const [products, setProducts] = useState<Product[]>([]);
|
|
|
|
|
|
const [productRatios, setProductRatios] = useState<Record<string, ProductYarnRatio[]>>({});
|
|
|
|
|
|
|
|
|
|
|
|
// 搜索和筛选状态
|
|
|
|
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
|
|
|
|
const [showProductDropdown, setShowProductDropdown] = useState(false);
|
2026-05-27 03:17:46 +00:00
|
|
|
|
const [showProductModal, setShowProductModal] = useState(false);
|
2026-05-26 05:15:10 +00:00
|
|
|
|
|
|
|
|
|
|
// 表单字段
|
|
|
|
|
|
const [productName, setProductName] = useState('');
|
|
|
|
|
|
const [color, setColor] = useState('');
|
|
|
|
|
|
const [fabricCode, setFabricCode] = useState('');
|
|
|
|
|
|
const [colorCode, setColorCode] = useState('01');
|
|
|
|
|
|
const [remark, setRemark] = useState('');
|
|
|
|
|
|
const [textileFactory, setTextileFactory] = useState('');
|
|
|
|
|
|
const [targetQuantity, setTargetQuantity] = useState('');
|
|
|
|
|
|
const [productionPrice, setProductionPrice] = useState('');
|
|
|
|
|
|
const [yarns, setYarns] = useState<YarnInput[]>([{ id: '1', name: '', ratio: 1 }]);
|
|
|
|
|
|
const [factories, setFactories] = useState<Company[]>([]);
|
|
|
|
|
|
const [submitting, setSubmitting] = useState(false);
|
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-06-04 13:57:36 +00:00
|
|
|
|
if (auth.company) {
|
|
|
|
|
|
fetchFactories();
|
|
|
|
|
|
fetchProducts();
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [auth.company]);
|
2026-05-26 05:15:10 +00:00
|
|
|
|
|
2026-05-28 05:54:23 +00:00
|
|
|
|
// 从产品信息获取产品数据
|
2026-05-26 05:15:10 +00:00
|
|
|
|
const fetchProducts = async () => {
|
|
|
|
|
|
if (!auth.company) return;
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
|
|
|
|
|
|
const { data: prodData } = await supabase
|
|
|
|
|
|
.from('products')
|
|
|
|
|
|
.select('*')
|
|
|
|
|
|
.eq('company_id', auth.company.id)
|
|
|
|
|
|
.order('created_at', { ascending: false });
|
|
|
|
|
|
|
|
|
|
|
|
if (prodData) {
|
|
|
|
|
|
setProducts(prodData as Product[]);
|
|
|
|
|
|
|
|
|
|
|
|
// 获取每个产品的纱线配比
|
|
|
|
|
|
const productIds = prodData.map(p => p.id);
|
|
|
|
|
|
if (productIds.length > 0) {
|
|
|
|
|
|
const { data: ratiosData } = await supabase
|
|
|
|
|
|
.from('product_yarn_ratios')
|
|
|
|
|
|
.select('*')
|
|
|
|
|
|
.in('product_id', productIds);
|
|
|
|
|
|
|
|
|
|
|
|
if (ratiosData) {
|
|
|
|
|
|
const ratiosMap: Record<string, ProductYarnRatio[]> = {};
|
|
|
|
|
|
ratiosData.forEach((r: any) => {
|
|
|
|
|
|
if (!ratiosMap[r.product_id]) ratiosMap[r.product_id] = [];
|
|
|
|
|
|
ratiosMap[r.product_id].push(r as ProductYarnRatio);
|
|
|
|
|
|
});
|
|
|
|
|
|
setProductRatios(ratiosMap);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
setLoading(false);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const fetchFactories = async () => {
|
2026-06-04 13:57:36 +00:00
|
|
|
|
if (!auth.company) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 查询工厂管理中登记的纺织厂(companies表中role=textile的记录)
|
2026-05-26 05:15:10 +00:00
|
|
|
|
const { data } = await supabase
|
|
|
|
|
|
.from('companies')
|
|
|
|
|
|
.select('*')
|
2026-06-04 13:57:36 +00:00
|
|
|
|
.eq('role', 'textile')
|
|
|
|
|
|
.order('created_at', { ascending: false });
|
|
|
|
|
|
|
2026-05-26 05:15:10 +00:00
|
|
|
|
setFactories(data || []);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 根据搜索词筛选产品
|
|
|
|
|
|
const filteredProducts = useMemo(() => {
|
|
|
|
|
|
if (!searchQuery.trim()) return [];
|
|
|
|
|
|
const query = searchQuery.toLowerCase();
|
|
|
|
|
|
return products.filter(product =>
|
|
|
|
|
|
product.product_name?.toLowerCase().includes(query) ||
|
|
|
|
|
|
product.fabric_code?.toLowerCase().includes(query) ||
|
|
|
|
|
|
product.color?.toLowerCase().includes(query) ||
|
|
|
|
|
|
product.color_code?.toLowerCase().includes(query)
|
|
|
|
|
|
).slice(0, 10); // 最多显示10条
|
|
|
|
|
|
}, [searchQuery, products]);
|
|
|
|
|
|
|
|
|
|
|
|
// 选择产品后自动填充表单
|
|
|
|
|
|
const handleSelectProduct = (product: Product) => {
|
|
|
|
|
|
setSelectedProduct(product);
|
|
|
|
|
|
setProductName(product.product_name);
|
|
|
|
|
|
setColor(product.color);
|
|
|
|
|
|
setFabricCode(product.fabric_code);
|
|
|
|
|
|
setColorCode(product.color_code);
|
|
|
|
|
|
setProductionPrice(String(product.production_price || ''));
|
|
|
|
|
|
setSearchQuery(`${product.product_name}-${product.weight}g-${product.color}`);
|
|
|
|
|
|
setShowProductDropdown(false);
|
2026-05-27 03:17:46 +00:00
|
|
|
|
setShowProductModal(false);
|
2026-05-26 05:15:10 +00:00
|
|
|
|
|
|
|
|
|
|
// 自动填充纱线配比
|
|
|
|
|
|
const ratios = productRatios[product.id];
|
|
|
|
|
|
if (ratios && ratios.length > 0) {
|
|
|
|
|
|
setYarns(ratios.map((r, i) => ({
|
|
|
|
|
|
id: i.toString(),
|
|
|
|
|
|
name: r.yarn_name,
|
2026-05-27 03:17:46 +00:00
|
|
|
|
ratio: r.ratio,
|
|
|
|
|
|
yarn_type: r.yarn_type || 'all'
|
2026-05-26 05:15:10 +00:00
|
|
|
|
})));
|
|
|
|
|
|
} else {
|
2026-05-27 03:17:46 +00:00
|
|
|
|
setYarns([{ id: '1', name: '', ratio: 1, yarn_type: 'all' }]);
|
2026-05-26 05:15:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 清除选择的产品
|
|
|
|
|
|
const handleClearProduct = () => {
|
|
|
|
|
|
setSelectedProduct(null);
|
|
|
|
|
|
setProductName('');
|
|
|
|
|
|
setColor('');
|
|
|
|
|
|
setFabricCode('');
|
|
|
|
|
|
setColorCode('01');
|
|
|
|
|
|
setProductionPrice('');
|
|
|
|
|
|
setSearchQuery('');
|
|
|
|
|
|
setYarns([{ id: '1', name: '', ratio: 1 }]);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const addYarn = () => {
|
|
|
|
|
|
setYarns([...yarns, { id: Date.now().toString(), name: '', ratio: 1 }]);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const removeYarn = (id: string) => {
|
|
|
|
|
|
if (yarns.length > 1) setYarns(yarns.filter(y => y.id !== id));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const updateYarn = (id: string, field: 'name' | 'ratio', value: string | number) => {
|
|
|
|
|
|
setYarns(yarns.map(y => y.id === id ? { ...y, [field]: value } : y));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
if (!auth.company) return;
|
|
|
|
|
|
setSubmitting(true);
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 生成计划编号
|
|
|
|
|
|
const timestamp = Date.now().toString().slice(-6);
|
|
|
|
|
|
const planCode = `PLAN-2026-${timestamp}`;
|
|
|
|
|
|
|
|
|
|
|
|
// 创建计划
|
|
|
|
|
|
const { data: plan, error: planError } = await supabase
|
|
|
|
|
|
.from('production_plans')
|
|
|
|
|
|
.insert({
|
|
|
|
|
|
plan_code: planCode,
|
|
|
|
|
|
product_name: productName,
|
|
|
|
|
|
color: color,
|
|
|
|
|
|
fabric_code: fabricCode.toUpperCase(),
|
|
|
|
|
|
color_code: colorCode,
|
|
|
|
|
|
remark: remark,
|
|
|
|
|
|
purchaser_id: auth.company.id,
|
|
|
|
|
|
target_quantity: parseInt(targetQuantity) || 0,
|
|
|
|
|
|
yarn_usage_per_meter: selectedProduct?.weight || 0, // 使用产品克重作为纱用量
|
|
|
|
|
|
production_price: parseFloat(productionPrice) || 0,
|
2026-06-04 15:56:19 +00:00
|
|
|
|
status: PLAN_STATUS.PENDING,
|
2026-05-26 05:15:10 +00:00
|
|
|
|
created_by: auth.user?.id
|
|
|
|
|
|
})
|
|
|
|
|
|
.select()
|
|
|
|
|
|
.maybeSingle();
|
|
|
|
|
|
|
|
|
|
|
|
if (planError) {
|
2026-06-04 15:56:19 +00:00
|
|
|
|
handleError(planError, { userMessage: '创建计划失败' });
|
2026-05-26 05:15:10 +00:00
|
|
|
|
setSubmitting(false);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-04 13:57:36 +00:00
|
|
|
|
if (plan) {
|
2026-05-26 05:15:10 +00:00
|
|
|
|
// 创建纱线配比
|
2026-06-04 13:57:36 +00:00
|
|
|
|
const yarnErrors: string[] = [];
|
2026-05-26 05:15:10 +00:00
|
|
|
|
for (const yarn of yarns) {
|
|
|
|
|
|
if (yarn.name) {
|
|
|
|
|
|
const amountPerMeter = selectedProduct?.weight || 0;
|
|
|
|
|
|
const totalAmount = amountPerMeter * yarn.ratio * (parseInt(targetQuantity) || 0);
|
2026-06-04 13:57:36 +00:00
|
|
|
|
const { error: yarnError } = await supabase.from('yarn_ratios').insert({
|
2026-05-26 05:15:10 +00:00
|
|
|
|
plan_id: plan.id,
|
|
|
|
|
|
yarn_name: yarn.name,
|
|
|
|
|
|
ratio: yarn.ratio,
|
|
|
|
|
|
amount_per_meter: amountPerMeter * yarn.ratio,
|
|
|
|
|
|
total_amount: totalAmount
|
|
|
|
|
|
});
|
2026-06-04 13:57:36 +00:00
|
|
|
|
if (yarnError) {
|
|
|
|
|
|
yarnErrors.push(yarnError.message);
|
|
|
|
|
|
console.error('纱线配比创建失败:', yarnError);
|
|
|
|
|
|
}
|
2026-05-26 05:15:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 创建流程节点
|
2026-06-04 13:57:36 +00:00
|
|
|
|
const stepErrors: string[] = [];
|
2026-05-26 05:15:10 +00:00
|
|
|
|
const steps: ('confirm' | 'yarn_purchase' | 'dyeing' | 'machine_start' | 'fabric_warehouse')[] = ['confirm', 'yarn_purchase', 'dyeing', 'machine_start', 'fabric_warehouse'];
|
|
|
|
|
|
for (const step of steps) {
|
2026-06-04 13:57:36 +00:00
|
|
|
|
const { error: stepError } = await supabase.from('plan_process_steps').insert({
|
2026-05-26 05:15:10 +00:00
|
|
|
|
plan_id: plan.id,
|
|
|
|
|
|
step_type: step,
|
2026-06-04 15:56:19 +00:00
|
|
|
|
status: STEP_STATUS.PENDING
|
2026-05-26 05:15:10 +00:00
|
|
|
|
});
|
2026-06-04 13:57:36 +00:00
|
|
|
|
if (stepError) {
|
|
|
|
|
|
stepErrors.push(stepError.message);
|
|
|
|
|
|
console.error('流程节点创建失败:', stepError);
|
|
|
|
|
|
}
|
2026-05-26 05:15:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 记录初始价格(旧价格为0,表示新建)
|
|
|
|
|
|
const finalPrice = parseFloat(productionPrice) || 0;
|
|
|
|
|
|
if (finalPrice > 0) {
|
2026-06-04 13:57:36 +00:00
|
|
|
|
const { error: priceError } = await supabase.from('production_price_history').insert({
|
2026-05-26 05:15:10 +00:00
|
|
|
|
plan_id: plan.id,
|
|
|
|
|
|
old_price: 0,
|
|
|
|
|
|
new_price: finalPrice,
|
|
|
|
|
|
change_amount: finalPrice,
|
|
|
|
|
|
operator_id: auth.user?.id,
|
|
|
|
|
|
operator_name: auth.user?.username || '',
|
|
|
|
|
|
change_reason: '创建计划时设置初始价格'
|
|
|
|
|
|
});
|
2026-06-04 13:57:36 +00:00
|
|
|
|
if (priceError) {
|
|
|
|
|
|
console.error('价格历史记录失败:', priceError);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 注意:工厂关联不再在创建计划时自动进行
|
|
|
|
|
|
// 而是通过分享链接或推送功能,由工厂确认后才建立关联
|
|
|
|
|
|
|
|
|
|
|
|
// 如果有子操作失败,提示用户但允许继续
|
|
|
|
|
|
if (yarnErrors.length > 0 || stepErrors.length > 0) {
|
|
|
|
|
|
console.warn('计划创建完成,但部分数据创建失败:', { yarnErrors, stepErrors });
|
2026-05-26 05:15:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
alert('计划创建成功!');
|
|
|
|
|
|
navigate('/purchaser/plans');
|
|
|
|
|
|
} catch (err) {
|
2026-06-04 15:56:19 +00:00
|
|
|
|
handleError(err, { userMessage: '创建失败,请重试' });
|
2026-05-26 05:15:10 +00:00
|
|
|
|
}
|
|
|
|
|
|
setSubmitting(false);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="min-h-screen bg-gray-50">
|
|
|
|
|
|
<div className="bg-white shadow-sm sticky top-0 z-10">
|
|
|
|
|
|
<div className="max-w-4xl mx-auto px-4 py-4 flex items-center gap-4">
|
|
|
|
|
|
<button onClick={() => navigate('/purchaser')} className="p-2 hover:bg-gray-100 rounded-full">
|
|
|
|
|
|
<ArrowLeft className="w-5 h-5" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<h1 className="text-lg font-semibold">新建纺织计划</h1>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="max-w-4xl mx-auto px-4 py-6">
|
|
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
2026-05-28 05:54:23 +00:00
|
|
|
|
{/* 产品信息选择 - 搜索+筛选形式 */}
|
2026-05-26 05:15:10 +00:00
|
|
|
|
<div className="bg-white rounded-lg shadow-sm p-6">
|
|
|
|
|
|
<h2 className="text-base font-semibold mb-4 flex items-center gap-2">
|
2026-05-28 05:54:23 +00:00
|
|
|
|
<span className="w-1 h-4 bg-blue-500 rounded"></span>选择产品
|
2026-05-26 05:15:10 +00:00
|
|
|
|
</h2>
|
|
|
|
|
|
{loading ? (
|
|
|
|
|
|
<div className="text-center py-8 text-gray-400">加载中...</div>
|
|
|
|
|
|
) : products.length === 0 ? (
|
|
|
|
|
|
<div className="text-center py-8 text-gray-500">
|
|
|
|
|
|
<p>暂无产品,请先前往生产仓库管理创建产品</p>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => navigate('/purchaser/warehouse')}
|
|
|
|
|
|
className="mt-3 px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700"
|
|
|
|
|
|
>
|
|
|
|
|
|
前往生产仓库管理
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="relative">
|
2026-05-27 03:17:46 +00:00
|
|
|
|
{/* 搜索输入框 */}
|
2026-05-26 05:15:10 +00:00
|
|
|
|
<div className="relative">
|
|
|
|
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
value={searchQuery}
|
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
setSearchQuery(e.target.value);
|
|
|
|
|
|
setShowProductDropdown(true);
|
|
|
|
|
|
if (!e.target.value) {
|
|
|
|
|
|
handleClearProduct();
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
onFocus={() => setShowProductDropdown(true)}
|
|
|
|
|
|
placeholder="搜索产品名称、坯布码、颜色、色号..."
|
|
|
|
|
|
className="w-full pl-10 pr-10 py-3 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
|
|
|
|
/>
|
|
|
|
|
|
{searchQuery && (
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={handleClearProduct}
|
|
|
|
|
|
className="absolute right-3 top-1/2 -translate-y-1/2 p-1 hover:bg-gray-100 rounded"
|
|
|
|
|
|
>
|
|
|
|
|
|
<X className="w-4 h-4 text-gray-400" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 搜索结果下拉框 */}
|
2026-05-27 03:17:46 +00:00
|
|
|
|
{showProductDropdown && (
|
2026-05-26 05:15:10 +00:00
|
|
|
|
<div className="absolute z-20 w-full mt-1 bg-white border rounded-lg shadow-lg max-h-60 overflow-y-auto">
|
2026-05-27 03:17:46 +00:00
|
|
|
|
{filteredProducts.length > 0 ? (
|
|
|
|
|
|
filteredProducts.map(product => (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={product.id}
|
|
|
|
|
|
onClick={() => handleSelectProduct(product)}
|
|
|
|
|
|
className="p-3 hover:bg-blue-50 cursor-pointer border-b last:border-b-0"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
|
{product.image_url ? (
|
|
|
|
|
|
<img
|
|
|
|
|
|
src={product.image_url}
|
|
|
|
|
|
alt={product.product_name}
|
|
|
|
|
|
className="w-10 h-10 rounded-lg object-cover border border-gray-200 flex-shrink-0"
|
|
|
|
|
|
/>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="w-10 h-10 rounded-lg bg-gray-100 flex items-center justify-center flex-shrink-0">
|
|
|
|
|
|
<span className="text-gray-400 text-xs">无图</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<p className="font-medium text-gray-900">
|
|
|
|
|
|
{product.product_name}-{product.weight}g-{product.color}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p className="text-xs text-gray-500 mt-1">
|
|
|
|
|
|
产品码: {product.fabric_code} | 色号: {product.color_code} | 价格: ¥{product.production_price || 0}/米
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p className="text-xs text-gray-400">
|
|
|
|
|
|
克重: {product.weight}g/m |
|
|
|
|
|
|
经线: {product.warp_weight || '-'}g/m |
|
|
|
|
|
|
纬线: {product.weft_weight || '-'}g/m
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<ChevronDown className="w-4 h-4 text-gray-400" />
|
2026-05-26 05:15:10 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-05-27 03:17:46 +00:00
|
|
|
|
))
|
|
|
|
|
|
) : searchQuery.trim() ? (
|
|
|
|
|
|
<div className="p-4 text-center text-gray-500">
|
|
|
|
|
|
未找到匹配的产品
|
2026-05-26 05:15:10 +00:00
|
|
|
|
</div>
|
2026-05-27 03:17:46 +00:00
|
|
|
|
) : (
|
|
|
|
|
|
<div className="p-4 text-center text-gray-400">
|
|
|
|
|
|
输入关键词搜索产品
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2026-05-26 05:15:10 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 已选择产品展示 */}
|
|
|
|
|
|
{selectedProduct && (
|
|
|
|
|
|
<div className="mt-4 p-4 bg-blue-50 rounded-lg border border-blue-200">
|
2026-05-27 03:17:46 +00:00
|
|
|
|
<div className="flex items-start justify-between">
|
|
|
|
|
|
<div className="flex items-start gap-3">
|
|
|
|
|
|
{selectedProduct.image_url ? (
|
|
|
|
|
|
<img
|
|
|
|
|
|
src={selectedProduct.image_url}
|
|
|
|
|
|
alt={selectedProduct.product_name}
|
|
|
|
|
|
className="w-16 h-16 rounded-lg object-cover border border-gray-200 flex-shrink-0"
|
|
|
|
|
|
/>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="w-16 h-16 rounded-lg bg-gray-200 flex items-center justify-center flex-shrink-0">
|
|
|
|
|
|
<span className="text-gray-400 text-xs">无图片</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<p className="text-sm text-gray-500">已选择产品</p>
|
|
|
|
|
|
<p className="font-medium text-gray-900">
|
|
|
|
|
|
{selectedProduct.product_name}-{selectedProduct.weight}g-{selectedProduct.color}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p className="text-xs text-gray-500 mt-1">
|
|
|
|
|
|
产品码: {selectedProduct.fabric_code} | 色号: {selectedProduct.color_code}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<p className="text-xs text-gray-500">
|
|
|
|
|
|
克重: {selectedProduct.weight}g/m |
|
|
|
|
|
|
经线: {selectedProduct.warp_weight || '-'}g/m |
|
|
|
|
|
|
纬线: {selectedProduct.weft_weight || '-'}g/m
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
2026-05-26 05:15:10 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={handleClearProduct}
|
|
|
|
|
|
className="p-2 text-red-500 hover:bg-red-50 rounded-lg"
|
|
|
|
|
|
>
|
|
|
|
|
|
<X className="w-4 h-4" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 产品信息 - 自动从原坯布产品获取 */}
|
|
|
|
|
|
<div className="bg-white rounded-lg shadow-sm p-6">
|
|
|
|
|
|
<h2 className="text-base font-semibold mb-4 flex items-center gap-2">
|
|
|
|
|
|
<span className="w-1 h-4 bg-blue-500 rounded"></span>产品信息
|
|
|
|
|
|
</h2>
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm text-gray-600 mb-1">成品名称</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
value={productName}
|
|
|
|
|
|
disabled
|
|
|
|
|
|
className="w-full px-3 py-2 border rounded-lg bg-gray-100 text-gray-700"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<p className="text-xs text-gray-400 mt-1">自动从原坯布产品获取</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm text-gray-600 mb-1">颜色</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
value={color}
|
|
|
|
|
|
disabled
|
|
|
|
|
|
className="w-full px-3 py-2 border rounded-lg bg-gray-100 text-gray-700"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<p className="text-xs text-gray-400 mt-1">自动从原坯布产品获取</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm text-gray-600 mb-1">坯布唯一识别码</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
value={fabricCode}
|
|
|
|
|
|
disabled
|
|
|
|
|
|
className="w-full px-3 py-2 border rounded-lg bg-gray-100 text-gray-700"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<p className="text-xs text-gray-400 mt-1">自动从原坯布产品获取</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm text-gray-600 mb-1">色号</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
value={colorCode}
|
|
|
|
|
|
disabled
|
|
|
|
|
|
className="w-full px-3 py-2 border rounded-lg bg-gray-100 text-gray-700"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<p className="text-xs text-gray-400 mt-1">自动从原坯布产品获取</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm text-gray-600 mb-1">克重(g/m)</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
value={selectedProduct?.weight ? `${selectedProduct.weight}g/m` : ''}
|
|
|
|
|
|
disabled
|
|
|
|
|
|
className="w-full px-3 py-2 border rounded-lg bg-gray-100 text-gray-700"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<p className="text-xs text-gray-400 mt-1">自动从原坯布产品获取</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
2026-05-27 03:17:46 +00:00
|
|
|
|
<label className="block text-sm text-gray-600 mb-1">经线克重(g/m)</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
value={selectedProduct?.warp_weight ? `${selectedProduct.warp_weight}g/m` : '-'}
|
|
|
|
|
|
disabled
|
|
|
|
|
|
className="w-full px-3 py-2 border rounded-lg bg-gray-100 text-gray-700"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<p className="text-xs text-gray-400 mt-1">自动从原坯布产品获取</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm text-gray-600 mb-1">纬线克重(g/m)</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
value={selectedProduct?.weft_weight ? `${selectedProduct.weft_weight}g/m` : '-'}
|
|
|
|
|
|
disabled
|
|
|
|
|
|
className="w-full px-3 py-2 border rounded-lg bg-gray-100 text-gray-700"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<p className="text-xs text-gray-400 mt-1">自动从原坯布产品获取</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm text-gray-600 mb-1">每米纱用量(g/m)</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
2026-05-28 05:54:23 +00:00
|
|
|
|
value={selectedProduct?.total_weight ? `${selectedProduct.total_weight}g/m` : (selectedProduct?.weight ? `${selectedProduct.weight}g/m` : '-')}
|
2026-05-27 03:17:46 +00:00
|
|
|
|
disabled
|
|
|
|
|
|
className="w-full px-3 py-2 border rounded-lg bg-gray-100 text-gray-700"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<p className="text-xs text-gray-400 mt-1">自动从原坯布产品获取</p>
|
|
|
|
|
|
</div>
|
2026-05-28 05:54:23 +00:00
|
|
|
|
<div className="col-span-2">
|
2026-05-27 03:17:46 +00:00
|
|
|
|
<label className="block text-sm text-gray-600 mb-1">纱线名称/占比</label>
|
2026-05-28 05:54:23 +00:00
|
|
|
|
<div className="w-full px-3 py-2 border rounded-lg bg-gray-100 text-gray-700 min-h-[42px] flex flex-wrap items-center gap-x-4">
|
2026-05-26 05:15:10 +00:00
|
|
|
|
{yarns.filter(y => y.name).map((yarn, idx) => (
|
2026-05-28 05:54:23 +00:00
|
|
|
|
<span key={yarn.id} className="inline-block whitespace-nowrap">
|
|
|
|
|
|
{yarnTypeLabels[yarn.yarn_type || 'all']}:{yarn.name} 占比:{Number(yarn.ratio).toFixed(0)}%
|
2026-05-26 05:15:10 +00:00
|
|
|
|
</span>
|
|
|
|
|
|
))}
|
|
|
|
|
|
{!yarns.some(y => y.name) && <span className="text-gray-400">-</span>}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p className="text-xs text-gray-400 mt-1">自动从原坯布产品获取</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="col-span-2">
|
|
|
|
|
|
<label className="block text-sm text-gray-600 mb-1">备注</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
value={remark}
|
|
|
|
|
|
onChange={(e) => setRemark(e.target.value)}
|
|
|
|
|
|
placeholder="如:60*40,60双股等"
|
|
|
|
|
|
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 生产信息 */}
|
|
|
|
|
|
<div className="bg-white rounded-lg shadow-sm p-6">
|
|
|
|
|
|
<h2 className="text-base font-semibold mb-4 flex items-center gap-2">
|
|
|
|
|
|
<span className="w-1 h-4 bg-green-500 rounded"></span>生产信息
|
|
|
|
|
|
</h2>
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm text-gray-600 mb-1">纺织厂</label>
|
|
|
|
|
|
<select
|
|
|
|
|
|
value={textileFactory}
|
|
|
|
|
|
onChange={(e) => setTextileFactory(e.target.value)}
|
|
|
|
|
|
required
|
|
|
|
|
|
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
|
|
|
|
>
|
|
|
|
|
|
<option value="">请选择纺织厂</option>
|
|
|
|
|
|
{factories.map(f => <option key={f.id} value={f.id}>{f.name}</option>)}
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm text-gray-600 mb-1">计划产量(米)</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
value={targetQuantity}
|
|
|
|
|
|
onChange={(e) => setTargetQuantity(e.target.value)}
|
|
|
|
|
|
placeholder="请输入计划产量"
|
|
|
|
|
|
required
|
|
|
|
|
|
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="col-span-2">
|
|
|
|
|
|
<label className="block text-sm text-gray-600 mb-1">生产采购价(元/米)</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
value={productionPrice}
|
|
|
|
|
|
onChange={(e) => setProductionPrice(e.target.value)}
|
|
|
|
|
|
placeholder={selectedProduct ? `原坯布价格: ${selectedProduct.production_price || 0} 元/米` : "请输入采购价"}
|
|
|
|
|
|
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
|
|
|
|
/>
|
|
|
|
|
|
{selectedProduct && (
|
|
|
|
|
|
<p className="text-xs text-gray-400 mt-1">已从原坯布产品自动填充,可修改</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="flex gap-4">
|
|
|
|
|
|
<button type="button" onClick={() => navigate('/purchaser')} className="flex-1 py-3 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50">取消</button>
|
|
|
|
|
|
<button type="submit" disabled={submitting || !selectedProduct} className="flex-1 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50">
|
|
|
|
|
|
{submitting ? '创建中...' : '创建计划'}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
</div>
|
2026-05-27 03:17:46 +00:00
|
|
|
|
|
|
|
|
|
|
{/* 产品选择弹窗 */}
|
|
|
|
|
|
<AnimatePresence>
|
|
|
|
|
|
{showProductModal && (
|
|
|
|
|
|
<motion.div
|
|
|
|
|
|
initial={{ opacity: 0 }}
|
|
|
|
|
|
animate={{ opacity: 1 }}
|
|
|
|
|
|
exit={{ opacity: 0 }}
|
|
|
|
|
|
className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4"
|
|
|
|
|
|
onClick={() => setShowProductModal(false)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<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-xl shadow-2xl w-full max-w-2xl max-h-[80vh] overflow-hidden"
|
|
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
|
>
|
|
|
|
|
|
{/* 弹窗头部 */}
|
|
|
|
|
|
<div className="p-4 border-b flex items-center justify-between bg-gray-50">
|
2026-05-28 05:54:23 +00:00
|
|
|
|
<h3 className="text-lg font-semibold text-gray-900">选择产品</h3>
|
2026-05-27 03:17:46 +00:00
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => setShowProductModal(false)}
|
|
|
|
|
|
className="p-2 hover:bg-gray-200 rounded-full transition-colors"
|
|
|
|
|
|
>
|
|
|
|
|
|
<X className="w-5 h-5 text-gray-500" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 搜索框 */}
|
|
|
|
|
|
<div className="p-4 border-b">
|
|
|
|
|
|
<div className="relative">
|
|
|
|
|
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
value={searchQuery}
|
|
|
|
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
|
|
|
|
placeholder="搜索产品名称、坯布码、颜色、色号..."
|
|
|
|
|
|
className="w-full pl-10 pr-4 py-3 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 产品列表 */}
|
|
|
|
|
|
<div className="overflow-y-auto max-h-[50vh]">
|
|
|
|
|
|
{filteredProducts.length > 0 ? (
|
|
|
|
|
|
<div className="divide-y">
|
|
|
|
|
|
{filteredProducts.map((product) => (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={product.id}
|
|
|
|
|
|
onClick={() => handleSelectProduct(product)}
|
|
|
|
|
|
className="p-4 hover:bg-blue-50 cursor-pointer transition-colors"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<div className="flex items-center gap-3 flex-1">
|
|
|
|
|
|
{product.image_url ? (
|
|
|
|
|
|
<img
|
|
|
|
|
|
src={product.image_url}
|
|
|
|
|
|
alt={product.product_name}
|
|
|
|
|
|
className="w-12 h-12 rounded-lg object-cover border border-gray-200 flex-shrink-0"
|
|
|
|
|
|
/>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="w-12 h-12 rounded-lg bg-gray-100 flex items-center justify-center flex-shrink-0">
|
|
|
|
|
|
<span className="text-gray-400 text-xs">无图</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<div className="flex-1">
|
|
|
|
|
|
<p className="font-medium text-gray-900">
|
|
|
|
|
|
{product.product_name}-{product.weight}g-{product.color}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<div className="flex items-center gap-4 mt-1 text-sm text-gray-500">
|
|
|
|
|
|
<span>产品码: {product.fabric_code}</span>
|
|
|
|
|
|
<span>色号: {product.color_code}</span>
|
|
|
|
|
|
<span>价格: ¥{product.production_price || 0}/米</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex items-center gap-4 mt-1 text-xs text-gray-400">
|
|
|
|
|
|
<span>克重: {product.weight}g/m</span>
|
|
|
|
|
|
<span>经线: {product.warp_weight || '-'}g/m</span>
|
|
|
|
|
|
<span>纬线: {product.weft_weight || '-'}g/m</span>
|
|
|
|
|
|
<span>总克重: {product.total_weight || product.weight}g/m</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center">
|
|
|
|
|
|
<ChevronDown className="w-4 h-4 text-blue-600 -rotate-90" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : searchQuery.trim() ? (
|
|
|
|
|
|
<div className="p-8 text-center text-gray-500">
|
|
|
|
|
|
<Search className="w-12 h-12 mx-auto mb-3 text-gray-300" />
|
|
|
|
|
|
<p>未找到匹配的产品</p>
|
|
|
|
|
|
<p className="text-sm text-gray-400 mt-1">请尝试其他关键词</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="p-8 text-center text-gray-500">
|
|
|
|
|
|
<p className="text-gray-400">输入关键词搜索产品</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 弹窗底部 */}
|
|
|
|
|
|
<div className="p-4 border-t bg-gray-50 flex justify-between items-center">
|
|
|
|
|
|
<span className="text-sm text-gray-500">
|
|
|
|
|
|
共 {products.length} 个产品
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => setShowProductModal(false)}
|
|
|
|
|
|
className="px-4 py-2 text-gray-600 hover:text-gray-800 transition-colors"
|
|
|
|
|
|
>
|
|
|
|
|
|
取消
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</motion.div>
|
|
|
|
|
|
</motion.div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</AnimatePresence>
|
2026-05-26 05:15:10 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|