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:
Chever John 2026-07-03 02:10:40 +08:00
parent f99322df5f
commit d085054437
No known key found for this signature in database
GPG Key ID: 2894D52ED1211DF0
6 changed files with 113 additions and 33 deletions

View File

@ -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'}`}>
{/* 成品名称 - 带搜索建议 */}

View File

@ -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>
<p className="text-xs text-gray-500">: {product.color_code}</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>
<span className="px-1.5 py-0.5 bg-gray-100 rounded text-xs font-mono text-gray-600">
{product.fabric_code}
</span>
<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">

View File

@ -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 [];
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条
let list = products.filter(p => !p.product_type || p.product_type === 'raw_fabric');
if (searchQuery.trim()) {
const query = searchQuery.toLowerCase();
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)
);
}
return list.slice(0, 20);
}, [searchQuery, products]);
// 选择产品后自动填充表单

View File

@ -43,16 +43,18 @@ export function NewWashingPlan() {
const res = await purchaserApi.listProducts();
const productsData = res.data || [];
const productsWithInventory: ProductWithInventory[] = productsData.map(product => ({
...product,
raw_fabric_meters: product.raw_fabric_meters || 0,
raw_fabric_rolls: product.raw_fabric_rolls || 0,
total_stock: product.total_stock || 0,
total_in_meters: 0,
total_in_rolls: 0,
total_out_meters: 0,
total_out_rolls: 0
}));
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,
total_stock: product.total_stock || 0,
total_in_meters: 0,
total_in_rolls: 0,
total_out_meters: 0,
total_out_rolls: 0
}));
setProducts(productsWithInventory);
}, [auth.company]);

View File

@ -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 =>
p.product_name.includes(searchQuery) ||
p.fabric_code.includes(searchQuery) ||
p.color.includes(searchQuery)
);
}, [products, searchQuery]);
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)
);
}
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">

View File

@ -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