feat: add product_type support to frontend
- Add product_type field to Product types and forms - Filter NewPlan to show only raw_fabric products - Filter NewWashingPlan to show only textile products - Show all products by default in product selector (no search required) - Add type badges (原坯布/纺织成品/水洗成品) to product list - Add type filter tabs to warehouse management page Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f99322df5f
commit
d085054437
@ -18,6 +18,7 @@ interface ProductFormData {
|
||||
production_price: string;
|
||||
yarn_ratios: { yarn_name: string; ratio: number; yarn_type: 'warp' | 'weft' | 'all' }[];
|
||||
image_url?: string;
|
||||
product_type?: string;
|
||||
}
|
||||
|
||||
interface ProductFormProps {
|
||||
@ -63,7 +64,8 @@ const defaultInitialForm: ProductFormData = {
|
||||
color_code: '01',
|
||||
production_price: '',
|
||||
yarn_ratios: [{ yarn_name: '', ratio: 100, yarn_type: 'all' }],
|
||||
image_url: undefined
|
||||
image_url: undefined,
|
||||
product_type: 'raw_fabric'
|
||||
};
|
||||
|
||||
export function ProductForm({
|
||||
@ -104,7 +106,8 @@ export function ProductForm({
|
||||
yarn_ratios: ratios.length > 0
|
||||
? ratios.map(r => ({ yarn_name: r.yarn_name, ratio: r.ratio, yarn_type: (r.yarn_type as 'warp' | 'weft' | 'all') || 'all' }))
|
||||
: [{ yarn_name: '', ratio: 100, yarn_type: 'all' }],
|
||||
image_url: editingProduct.image_url || undefined
|
||||
image_url: editingProduct.image_url || undefined,
|
||||
product_type: (editingProduct as any).product_type || 'raw_fabric'
|
||||
});
|
||||
setPreviewUrl(editingProduct.image_url || null);
|
||||
} else if (initialFormData) {
|
||||
@ -525,6 +528,23 @@ export function ProductForm({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 产品类型选择 */}
|
||||
<div>
|
||||
<label className="flex items-center gap-2 text-sm font-medium text-gray-700 mb-2">
|
||||
<Package className="w-4 h-4 text-gray-400" />
|
||||
产品类型
|
||||
</label>
|
||||
<select
|
||||
value={formData.product_type || 'raw_fabric'}
|
||||
onChange={e => setFormData(prev => ({ ...prev, product_type: e.target.value }))}
|
||||
className="w-full px-3 py-2.5 border border-gray-200 rounded-xl text-sm focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all bg-white"
|
||||
>
|
||||
<option value="raw_fabric">原坯布</option>
|
||||
<option value="textile">纺织成品</option>
|
||||
<option value="washed">水洗成品</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* 三级结构:成品名称-颜色 */}
|
||||
<div className={`grid ${isMobile ? 'grid-cols-1 gap-4' : 'grid-cols-2 gap-4'}`}>
|
||||
{/* 成品名称 - 带搜索建议 */}
|
||||
|
||||
@ -15,6 +15,12 @@ interface ProductListProps {
|
||||
onDelete: (id: string) => void;
|
||||
}
|
||||
|
||||
const productTypeLabels: Record<string, { label: string; color: string }> = {
|
||||
raw_fabric: { label: '原坯布', color: 'bg-gray-100 text-gray-600' },
|
||||
textile: { label: '纺织成品', color: 'bg-blue-100 text-blue-600' },
|
||||
washed: { label: '水洗成品', color: 'bg-green-100 text-green-600' },
|
||||
};
|
||||
|
||||
export function ProductList({
|
||||
products,
|
||||
productRatios,
|
||||
@ -402,7 +408,14 @@ function ProductRow({ product, ratios, onViewRecords, onViewPriceHistory, onView
|
||||
)}
|
||||
<div>
|
||||
<p className="font-medium text-gray-900 text-sm">{product.color}</p>
|
||||
<div className="flex items-center gap-1.5 mt-0.5">
|
||||
<p className="text-xs text-gray-500">色号: {product.color_code}</p>
|
||||
{(() => {
|
||||
const pt = (product as any).product_type || 'raw_fabric';
|
||||
const info = productTypeLabels[pt] || productTypeLabels.raw_fabric;
|
||||
return <span className={`px-1.5 py-0.5 rounded text-xs font-medium ${info.color}`}>{info.label}</span>;
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@ -469,9 +482,16 @@ function ProductCardMobile({ product, ratios, onViewRecords, onViewPriceHistory,
|
||||
)}
|
||||
<div>
|
||||
<p className="font-medium text-gray-900 text-sm">{product.color}</p>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="px-1.5 py-0.5 bg-gray-100 rounded text-xs font-mono text-gray-600">
|
||||
{product.fabric_code}
|
||||
</span>
|
||||
{(() => {
|
||||
const pt = (product as any).product_type || 'raw_fabric';
|
||||
const info = productTypeLabels[pt] || productTypeLabels.raw_fabric;
|
||||
return <span className={`px-1.5 py-0.5 rounded text-xs font-medium ${info.color}`}>{info.label}</span>;
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
|
||||
@ -28,6 +28,7 @@ interface Product {
|
||||
production_price: number;
|
||||
image_url: string | null;
|
||||
yarn_usage_per_meter: number;
|
||||
product_type?: string;
|
||||
}
|
||||
|
||||
// 纱线配比类型
|
||||
@ -125,16 +126,19 @@ export function NewPlan() {
|
||||
setFactories(textileFactories);
|
||||
};
|
||||
|
||||
// 根据搜索词筛选产品
|
||||
// 根据搜索词筛选产品(仅原坯布类型)
|
||||
const filteredProducts = useMemo(() => {
|
||||
if (!searchQuery.trim()) return [];
|
||||
let list = products.filter(p => !p.product_type || p.product_type === 'raw_fabric');
|
||||
if (searchQuery.trim()) {
|
||||
const query = searchQuery.toLowerCase();
|
||||
return products.filter(product =>
|
||||
list = list.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条
|
||||
);
|
||||
}
|
||||
return list.slice(0, 20);
|
||||
}, [searchQuery, products]);
|
||||
|
||||
// 选择产品后自动填充表单
|
||||
|
||||
@ -43,7 +43,9 @@ export function NewWashingPlan() {
|
||||
const res = await purchaserApi.listProducts();
|
||||
const productsData = res.data || [];
|
||||
|
||||
const productsWithInventory: ProductWithInventory[] = productsData.map(product => ({
|
||||
const productsWithInventory: ProductWithInventory[] = productsData
|
||||
.filter((p: any) => p.product_type === 'textile')
|
||||
.map(product => ({
|
||||
...product,
|
||||
raw_fabric_meters: product.raw_fabric_meters || 0,
|
||||
raw_fabric_rolls: product.raw_fabric_rolls || 0,
|
||||
|
||||
@ -38,6 +38,7 @@ interface ProductFormData {
|
||||
production_price: string;
|
||||
yarn_ratios: { yarn_name: string; ratio: number; yarn_type: 'warp' | 'weft' | 'all' }[];
|
||||
image_url?: string;
|
||||
product_type?: string;
|
||||
}
|
||||
|
||||
interface InboundFormData {
|
||||
@ -103,6 +104,7 @@ export function WarehouseManage() {
|
||||
|
||||
// 搜索
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [productTypeFilter, setProductTypeFilter] = useState<string>('all');
|
||||
|
||||
// 展开状态
|
||||
const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>({});
|
||||
@ -145,13 +147,19 @@ export function WarehouseManage() {
|
||||
|
||||
// 筛选产品
|
||||
const filteredProducts = useMemo(() => {
|
||||
if (!searchQuery) return products;
|
||||
return products.filter(p =>
|
||||
let filtered = products;
|
||||
if (productTypeFilter !== 'all') {
|
||||
filtered = filtered.filter(p => (p as any).product_type === productTypeFilter || (!((p as any).product_type) && productTypeFilter === 'raw_fabric'));
|
||||
}
|
||||
if (searchQuery) {
|
||||
filtered = filtered.filter(p =>
|
||||
p.product_name.includes(searchQuery) ||
|
||||
p.fabric_code.includes(searchQuery) ||
|
||||
p.color.includes(searchQuery)
|
||||
);
|
||||
}, [products, searchQuery]);
|
||||
}
|
||||
return filtered;
|
||||
}, [products, searchQuery, productTypeFilter]);
|
||||
|
||||
// 保存产品
|
||||
const handleSaveProduct = useCallback(async (formData: ProductFormData) => {
|
||||
@ -167,7 +175,8 @@ export function WarehouseManage() {
|
||||
fabric_code: formData.fabric_code.toUpperCase(),
|
||||
color_code: formData.color_code,
|
||||
production_price: parseFloat(formData.production_price) || 0,
|
||||
image_url: formData.image_url || null
|
||||
image_url: formData.image_url || null,
|
||||
product_type: formData.product_type || 'raw_fabric'
|
||||
};
|
||||
|
||||
const yarnRatios = formData.yarn_ratios.filter(r => r.yarn_name && r.ratio > 0);
|
||||
@ -393,6 +402,28 @@ export function WarehouseManage() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 产品类型筛选 */}
|
||||
<div className="flex gap-2 mb-3 overflow-x-auto">
|
||||
{[
|
||||
{ key: 'all', label: '全部' },
|
||||
{ key: 'raw_fabric', label: '原坯布' },
|
||||
{ key: 'textile', label: '纺织成品' },
|
||||
{ key: 'washed', label: '水洗成品' },
|
||||
].map(tab => (
|
||||
<button
|
||||
key={tab.key}
|
||||
onClick={() => setProductTypeFilter(tab.key)}
|
||||
className={`px-3 py-1.5 rounded-lg text-sm font-medium whitespace-nowrap transition-colors ${
|
||||
productTypeFilter === tab.key
|
||||
? 'bg-blue-500 text-white shadow-sm'
|
||||
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 产品列表 */}
|
||||
{filteredProducts.length === 0 ? (
|
||||
<div className="text-center py-10 sm:py-12 bg-white rounded-2xl border border-dashed border-gray-300">
|
||||
|
||||
@ -1221,6 +1221,7 @@ export type Database = {
|
||||
id: string
|
||||
image_url: string | null
|
||||
product_name: string
|
||||
product_type: string
|
||||
production_price: number | null
|
||||
raw_fabric_meters: number
|
||||
raw_fabric_rolls: number
|
||||
@ -1243,6 +1244,7 @@ export type Database = {
|
||||
id?: string
|
||||
image_url?: string | null
|
||||
product_name: string
|
||||
product_type?: string
|
||||
production_price?: number | null
|
||||
raw_fabric_meters?: number
|
||||
raw_fabric_rolls?: number
|
||||
@ -1265,6 +1267,7 @@ export type Database = {
|
||||
id?: string
|
||||
image_url?: string | null
|
||||
product_name?: string
|
||||
product_type?: string
|
||||
production_price?: number | null
|
||||
raw_fabric_meters?: number
|
||||
raw_fabric_rolls?: number
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user