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;
|
production_price: string;
|
||||||
yarn_ratios: { yarn_name: string; ratio: number; yarn_type: 'warp' | 'weft' | 'all' }[];
|
yarn_ratios: { yarn_name: string; ratio: number; yarn_type: 'warp' | 'weft' | 'all' }[];
|
||||||
image_url?: string;
|
image_url?: string;
|
||||||
|
product_type?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProductFormProps {
|
interface ProductFormProps {
|
||||||
@ -63,7 +64,8 @@ const defaultInitialForm: ProductFormData = {
|
|||||||
color_code: '01',
|
color_code: '01',
|
||||||
production_price: '',
|
production_price: '',
|
||||||
yarn_ratios: [{ yarn_name: '', ratio: 100, yarn_type: 'all' }],
|
yarn_ratios: [{ yarn_name: '', ratio: 100, yarn_type: 'all' }],
|
||||||
image_url: undefined
|
image_url: undefined,
|
||||||
|
product_type: 'raw_fabric'
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ProductForm({
|
export function ProductForm({
|
||||||
@ -104,7 +106,8 @@ export function ProductForm({
|
|||||||
yarn_ratios: ratios.length > 0
|
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' }))
|
? 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' }],
|
: [{ 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);
|
setPreviewUrl(editingProduct.image_url || null);
|
||||||
} else if (initialFormData) {
|
} else if (initialFormData) {
|
||||||
@ -525,6 +528,23 @@ export function ProductForm({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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'}`}>
|
<div className={`grid ${isMobile ? 'grid-cols-1 gap-4' : 'grid-cols-2 gap-4'}`}>
|
||||||
{/* 成品名称 - 带搜索建议 */}
|
{/* 成品名称 - 带搜索建议 */}
|
||||||
|
|||||||
@ -15,6 +15,12 @@ interface ProductListProps {
|
|||||||
onDelete: (id: string) => void;
|
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({
|
export function ProductList({
|
||||||
products,
|
products,
|
||||||
productRatios,
|
productRatios,
|
||||||
@ -402,7 +408,14 @@ function ProductRow({ product, ratios, onViewRecords, onViewPriceHistory, onView
|
|||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-gray-900 text-sm">{product.color}</p>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@ -469,9 +482,16 @@ function ProductCardMobile({ product, ratios, onViewRecords, onViewPriceHistory,
|
|||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-gray-900 text-sm">{product.color}</p>
|
<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">
|
<div className="flex items-center gap-1.5">
|
||||||
{product.fabric_code}
|
<span className="px-1.5 py-0.5 bg-gray-100 rounded text-xs font-mono text-gray-600">
|
||||||
</span>
|
{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>
|
</div>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
|
|||||||
@ -28,6 +28,7 @@ interface Product {
|
|||||||
production_price: number;
|
production_price: number;
|
||||||
image_url: string | null;
|
image_url: string | null;
|
||||||
yarn_usage_per_meter: number;
|
yarn_usage_per_meter: number;
|
||||||
|
product_type?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 纱线配比类型
|
// 纱线配比类型
|
||||||
@ -125,16 +126,19 @@ export function NewPlan() {
|
|||||||
setFactories(textileFactories);
|
setFactories(textileFactories);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 根据搜索词筛选产品
|
// 根据搜索词筛选产品(仅原坯布类型)
|
||||||
const filteredProducts = useMemo(() => {
|
const filteredProducts = useMemo(() => {
|
||||||
if (!searchQuery.trim()) return [];
|
let list = products.filter(p => !p.product_type || p.product_type === 'raw_fabric');
|
||||||
const query = searchQuery.toLowerCase();
|
if (searchQuery.trim()) {
|
||||||
return products.filter(product =>
|
const query = searchQuery.toLowerCase();
|
||||||
product.product_name?.toLowerCase().includes(query) ||
|
list = list.filter(product =>
|
||||||
product.fabric_code?.toLowerCase().includes(query) ||
|
product.product_name?.toLowerCase().includes(query) ||
|
||||||
product.color?.toLowerCase().includes(query) ||
|
product.fabric_code?.toLowerCase().includes(query) ||
|
||||||
product.color_code?.toLowerCase().includes(query)
|
product.color?.toLowerCase().includes(query) ||
|
||||||
).slice(0, 10); // 最多显示10条
|
product.color_code?.toLowerCase().includes(query)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return list.slice(0, 20);
|
||||||
}, [searchQuery, products]);
|
}, [searchQuery, products]);
|
||||||
|
|
||||||
// 选择产品后自动填充表单
|
// 选择产品后自动填充表单
|
||||||
|
|||||||
@ -43,16 +43,18 @@ export function NewWashingPlan() {
|
|||||||
const res = await purchaserApi.listProducts();
|
const res = await purchaserApi.listProducts();
|
||||||
const productsData = res.data || [];
|
const productsData = res.data || [];
|
||||||
|
|
||||||
const productsWithInventory: ProductWithInventory[] = productsData.map(product => ({
|
const productsWithInventory: ProductWithInventory[] = productsData
|
||||||
...product,
|
.filter((p: any) => p.product_type === 'textile')
|
||||||
raw_fabric_meters: product.raw_fabric_meters || 0,
|
.map(product => ({
|
||||||
raw_fabric_rolls: product.raw_fabric_rolls || 0,
|
...product,
|
||||||
total_stock: product.total_stock || 0,
|
raw_fabric_meters: product.raw_fabric_meters || 0,
|
||||||
total_in_meters: 0,
|
raw_fabric_rolls: product.raw_fabric_rolls || 0,
|
||||||
total_in_rolls: 0,
|
total_stock: product.total_stock || 0,
|
||||||
total_out_meters: 0,
|
total_in_meters: 0,
|
||||||
total_out_rolls: 0
|
total_in_rolls: 0,
|
||||||
}));
|
total_out_meters: 0,
|
||||||
|
total_out_rolls: 0
|
||||||
|
}));
|
||||||
|
|
||||||
setProducts(productsWithInventory);
|
setProducts(productsWithInventory);
|
||||||
}, [auth.company]);
|
}, [auth.company]);
|
||||||
|
|||||||
@ -38,6 +38,7 @@ interface ProductFormData {
|
|||||||
production_price: string;
|
production_price: string;
|
||||||
yarn_ratios: { yarn_name: string; ratio: number; yarn_type: 'warp' | 'weft' | 'all' }[];
|
yarn_ratios: { yarn_name: string; ratio: number; yarn_type: 'warp' | 'weft' | 'all' }[];
|
||||||
image_url?: string;
|
image_url?: string;
|
||||||
|
product_type?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InboundFormData {
|
interface InboundFormData {
|
||||||
@ -103,6 +104,7 @@ export function WarehouseManage() {
|
|||||||
|
|
||||||
// 搜索
|
// 搜索
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [productTypeFilter, setProductTypeFilter] = useState<string>('all');
|
||||||
|
|
||||||
// 展开状态
|
// 展开状态
|
||||||
const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>({});
|
const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>({});
|
||||||
@ -145,13 +147,19 @@ export function WarehouseManage() {
|
|||||||
|
|
||||||
// 筛选产品
|
// 筛选产品
|
||||||
const filteredProducts = useMemo(() => {
|
const filteredProducts = useMemo(() => {
|
||||||
if (!searchQuery) return products;
|
let filtered = products;
|
||||||
return products.filter(p =>
|
if (productTypeFilter !== 'all') {
|
||||||
p.product_name.includes(searchQuery) ||
|
filtered = filtered.filter(p => (p as any).product_type === productTypeFilter || (!((p as any).product_type) && productTypeFilter === 'raw_fabric'));
|
||||||
p.fabric_code.includes(searchQuery) ||
|
}
|
||||||
p.color.includes(searchQuery)
|
if (searchQuery) {
|
||||||
);
|
filtered = filtered.filter(p =>
|
||||||
}, [products, searchQuery]);
|
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) => {
|
const handleSaveProduct = useCallback(async (formData: ProductFormData) => {
|
||||||
@ -167,7 +175,8 @@ export function WarehouseManage() {
|
|||||||
fabric_code: formData.fabric_code.toUpperCase(),
|
fabric_code: formData.fabric_code.toUpperCase(),
|
||||||
color_code: formData.color_code,
|
color_code: formData.color_code,
|
||||||
production_price: parseFloat(formData.production_price) || 0,
|
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);
|
const yarnRatios = formData.yarn_ratios.filter(r => r.yarn_name && r.ratio > 0);
|
||||||
@ -393,6 +402,28 @@ export function WarehouseManage() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</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 ? (
|
{filteredProducts.length === 0 ? (
|
||||||
<div className="text-center py-10 sm:py-12 bg-white rounded-2xl border border-dashed border-gray-300">
|
<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
|
id: string
|
||||||
image_url: string | null
|
image_url: string | null
|
||||||
product_name: string
|
product_name: string
|
||||||
|
product_type: string
|
||||||
production_price: number | null
|
production_price: number | null
|
||||||
raw_fabric_meters: number
|
raw_fabric_meters: number
|
||||||
raw_fabric_rolls: number
|
raw_fabric_rolls: number
|
||||||
@ -1243,6 +1244,7 @@ export type Database = {
|
|||||||
id?: string
|
id?: string
|
||||||
image_url?: string | null
|
image_url?: string | null
|
||||||
product_name: string
|
product_name: string
|
||||||
|
product_type?: string
|
||||||
production_price?: number | null
|
production_price?: number | null
|
||||||
raw_fabric_meters?: number
|
raw_fabric_meters?: number
|
||||||
raw_fabric_rolls?: number
|
raw_fabric_rolls?: number
|
||||||
@ -1265,6 +1267,7 @@ export type Database = {
|
|||||||
id?: string
|
id?: string
|
||||||
image_url?: string | null
|
image_url?: string | null
|
||||||
product_name?: string
|
product_name?: string
|
||||||
|
product_type?: string
|
||||||
production_price?: number | null
|
production_price?: number | null
|
||||||
raw_fabric_meters?: number
|
raw_fabric_meters?: number
|
||||||
raw_fabric_rolls?: number
|
raw_fabric_rolls?: number
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user