sync: Add files via upload (A@87b0a27431d8596edf55a7e623f944ca2f39f234)
This commit is contained in:
parent
37ecf1a056
commit
3f7377b142
23
.microcompact/mpo0vye9/functions.Edit:1.txt
Normal file
23
.microcompact/mpo0vye9/functions.Edit:1.txt
Normal file
@ -0,0 +1,23 @@
|
||||
The file /home/project/src/pages/MemberManage.tsx has been updated. Made 1 replacement.
|
||||
|
||||
Here's the result of running `cat -n` on a snippet of the edited file:
|
||||
44→ password: ''
|
||||
45→ });
|
||||
46→ const [editingMaster, setEditingMaster] = useState(false);
|
||||
47→
|
||||
48→ // 公司信息
|
||||
49→ const [companyInfo, setCompanyInfo] = useState<{
|
||||
50→ name: string;
|
||||
51→ contact_phone: string | null;
|
||||
52→ role: string;
|
||||
53→ } | null>(null);
|
||||
54→
|
||||
55→ // 帮助系统弹窗状态
|
||||
56→ const [showUserManual, setShowUserManual] = useState(false);
|
||||
57→ const [showFeedback, setShowFeedback] = useState(false);
|
||||
58→ const [showAbout, setShowAbout] = useState(false);
|
||||
59→
|
||||
60→ useEffect(() => {
|
||||
61→ if (!auth.user) return;
|
||||
62→ // 只有主账号可以管理子账号
|
||||
63→ if (!auth.user.is_master) {
|
||||
68
.microcompact/mpo0vyj1/functions.Edit:2.txt
Normal file
68
.microcompact/mpo0vyj1/functions.Edit:2.txt
Normal file
@ -0,0 +1,68 @@
|
||||
The file /home/project/src/pages/MemberManage.tsx has been updated. Made 1 replacement.
|
||||
|
||||
Here's the result of running `cat -n` on a snippet of the edited file:
|
||||
382→ </motion.button>
|
||||
383→ </div>
|
||||
384→ </div>
|
||||
385→
|
||||
386→ {/* 帮助与支持卡片 */}
|
||||
387→ <div className="bg-white rounded-lg shadow-sm p-4 md:p-6 mb-6">
|
||||
388→ <div className="flex items-center gap-2 mb-4">
|
||||
389→ <BookOpen className="w-5 h-5 text-blue-600" />
|
||||
390→ <h2 className="font-semibold text-gray-800">帮助与支持</h2>
|
||||
391→ </div>
|
||||
392→ <div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
393→ <motion.button
|
||||
394→ whileHover={{ scale: 1.02 }}
|
||||
395→ whileTap={{ scale: 0.98 }}
|
||||
396→ onClick={() => setShowUserManual(true)}
|
||||
397→ className="flex items-center gap-3 p-4 bg-blue-50 rounded-xl hover:bg-blue-100 transition-colors text-left"
|
||||
398→ >
|
||||
399→ <div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
|
||||
400→ <BookOpen className="w-5 h-5 text-blue-600" />
|
||||
401→ </div>
|
||||
402→ <div>
|
||||
403→ <p className="font-medium text-gray-800">用户手册</p>
|
||||
404→ <p className="text-xs text-gray-500">详细使用指南</p>
|
||||
405→ </div>
|
||||
406→ </motion.button>
|
||||
407→
|
||||
408→ <motion.button
|
||||
409→ whileHover={{ scale: 1.02 }}
|
||||
410→ whileTap={{ scale: 0.98 }}
|
||||
411→ onClick={() => setShowFeedback(true)}
|
||||
412→ className="flex items-center gap-3 p-4 bg-emerald-50 rounded-xl hover:bg-emerald-100 transition-colors text-left"
|
||||
413→ >
|
||||
414→ <div className="w-10 h-10 bg-emerald-100 rounded-lg flex items-center justify-center">
|
||||
415→ <MessageSquare className="w-5 h-5 text-emerald-600" />
|
||||
416→ </div>
|
||||
417→ <div>
|
||||
418→ <p className="font-medium text-gray-800">用户反馈</p>
|
||||
419→ <p className="text-xs text-gray-500">提交问题或建议</p>
|
||||
420→ </div>
|
||||
421→ </motion.button>
|
||||
422→
|
||||
423→ <motion.button
|
||||
424→ whileHover={{ scale: 1.02 }}
|
||||
425→ whileTap={{ scale: 0.98 }}
|
||||
426→ onClick={() => setShowAbout(true)}
|
||||
427→ className="flex items-center gap-3 p-4 bg-purple-50 rounded-xl hover:bg-purple-100 transition-colors text-left"
|
||||
428→ >
|
||||
429→ <div className="w-10 h-10 bg-purple-100 rounded-lg flex items-center justify-center">
|
||||
430→ <Info className="w-5 h-5 text-purple-600" />
|
||||
431→ </div>
|
||||
432→ <div>
|
||||
433→ <p className="font-medium text-gray-800">关于</p>
|
||||
434→ <p className="text-xs text-gray-500">版本信息与版权</p>
|
||||
435→ </div>
|
||||
436→ </motion.button>
|
||||
437→ </div>
|
||||
438→ </div>
|
||||
439→
|
||||
440→ {/* 子账号列表 */}
|
||||
441→ {loading ? (
|
||||
442→ <div className="text-center py-12 text-gray-400">加载中...</div>
|
||||
443→ ) : members.length === 0 ? (
|
||||
444→ <div className="bg-white rounded-lg shadow-sm p-8 text-center">
|
||||
445→ <div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
446→ <Users className="w-8 h-8 text-gray-400" />
|
||||
56
.microcompact/mpo0vylh/functions.Edit:3.txt
Normal file
56
.microcompact/mpo0vylh/functions.Edit:3.txt
Normal file
@ -0,0 +1,56 @@
|
||||
The file /home/project/src/pages/MemberManage.tsx has been updated. Made 1 replacement.
|
||||
|
||||
Here's the result of running `cat -n` on a snippet of the edited file:
|
||||
784→ </div>
|
||||
785→ )}
|
||||
786→ </AnimatePresence>
|
||||
787→
|
||||
788→ {/* 删除确认弹窗 */}
|
||||
789→ <AnimatePresence>
|
||||
790→ {showDeleteConfirm && (
|
||||
791→ <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
792→ <motion.div
|
||||
793→ initial={{ opacity: 0, scale: 0.95 }}
|
||||
794→ animate={{ opacity: 1, scale: 1 }}
|
||||
795→ exit={{ opacity: 0, scale: 0.95 }}
|
||||
796→ transition={{ duration: 0.2 }}
|
||||
797→ className="bg-white rounded-2xl w-full max-w-sm shadow-xl p-6"
|
||||
798→ >
|
||||
799→ <div className="text-center">
|
||||
800→ <div className="w-12 h-12 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
801→ <AlertCircle className="w-6 h-6 text-red-500" />
|
||||
802→ </div>
|
||||
803→ <h3 className="text-lg font-bold text-gray-800 mb-2">确认删除</h3>
|
||||
804→ <p className="text-gray-500 mb-6">删除后该子账号将无法登录,是否确认删除?</p>
|
||||
805→ <div className="flex gap-3">
|
||||
806→ <button
|
||||
807→ onClick={() => setShowDeleteConfirm(null)}
|
||||
808→ className="flex-1 py-2.5 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors font-medium"
|
||||
809→ >
|
||||
810→ 取消
|
||||
811→ </button>
|
||||
812→ <button
|
||||
813→ onClick={() => handleDelete(showDeleteConfirm)}
|
||||
814→ className="flex-1 py-2.5 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors font-medium"
|
||||
815→ >
|
||||
816→ 确认删除
|
||||
817→ </button>
|
||||
818→ </div>
|
||||
819→ </div>
|
||||
820→ </motion.div>
|
||||
821→ </div>
|
||||
822→ )}
|
||||
823→ </AnimatePresence>
|
||||
824→
|
||||
825→ {/* 用户手册弹窗 */}
|
||||
826→ <UserManual isOpen={showUserManual} onClose={() => setShowUserManual(false)} />
|
||||
827→
|
||||
828→ {/* 用户反馈弹窗 */}
|
||||
829→ <FeedbackModal isOpen={showFeedback} onClose={() => setShowFeedback(false)} />
|
||||
830→
|
||||
831→ {/* 关于窗口 */}
|
||||
832→ <AboutModal isOpen={showAbout} onClose={() => setShowAbout(false)} />
|
||||
833→ </div>
|
||||
834→ );
|
||||
835→}
|
||||
836→
|
||||
1
.microcompact/mpo0vynn/functions.Bash:4.txt
Normal file
1
.microcompact/mpo0vynn/functions.Bash:4.txt
Normal file
@ -0,0 +1 @@
|
||||
{"exitCode":0,"output":"[OpenTelemetry] Trace 已启动,serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production\n\n> procurement-planning@1.0.0 typecheck /home/project\n> tsc --noEmit\n\n[OpenTelemetry] Trace 已启动,serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production"}
|
||||
1
.microcompact/mpo3ts3j/functions.Grep:1.txt
Normal file
1
.microcompact/mpo3ts3j/functions.Grep:1.txt
Normal file
@ -0,0 +1 @@
|
||||
{"files":["src/components/UserManual.tsx","src/types/index.ts","src/hooks/usePlanData.ts","src/styles/index.css","src/supabase/types.ts","src/utils/helpers.ts","src/components/plans/PlanGroup.tsx","src/components/warehouse/OutboundForm.tsx","src/components/warehouse/ProductList.tsx","src/components/warehouse/InboundRecords.tsx","src/components/warehouse/InboundForm.tsx","src/components/warehouse/ProductForm.tsx","src/pages/purchaser/WarehouseManage.tsx","src/pages/purchaser/NewPlan.tsx","src/pages/purchaser/NewWashingPlan.tsx","src/pages/textile/PlanOverview.tsx","src/pages/textile/FabricWarehouse.tsx","src/__tests__/utils/helpers.test.ts"],"pattern":"克重|weight","path":"src","totalFiles":18}
|
||||
29
.microcompact/mpo3ts88/functions.Edit:3.txt
Normal file
29
.microcompact/mpo3ts88/functions.Edit:3.txt
Normal file
@ -0,0 +1,29 @@
|
||||
The file /home/project/src/components/warehouse/ProductList.tsx has been updated. Made 1 replacement.
|
||||
|
||||
Here's the result of running `cat -n` on a snippet of the edited file:
|
||||
29→ onDelete
|
||||
30→}: ProductListProps) {
|
||||
31→ const { isMobile } = useResponsive();
|
||||
32→
|
||||
33→ // 按名称和克重分组(优先使用 total_weight),无克重的产品直接按名称分组
|
||||
34→ const groupProductsByNameAndWeight = () => {
|
||||
35→ const groups: Record<string, Record<string, ProductWithInventory[]>> = {};
|
||||
36→ products.forEach(product => {
|
||||
37→ if (!groups[product.product_name]) {
|
||||
38→ groups[product.product_name] = {};
|
||||
39→ }
|
||||
40→ const weight = product.total_weight || product.weight;
|
||||
41→ // 如果没有克重,使用特殊标记 '__no_weight__'
|
||||
42→ const weightKey = weight ? String(weight) : '__no_weight__';
|
||||
43→ if (!groups[product.product_name][weightKey]) {
|
||||
44→ groups[product.product_name][weightKey] = [];
|
||||
45→ }
|
||||
46→ groups[product.product_name][weightKey].push(product);
|
||||
47→ });
|
||||
48→ return groups;
|
||||
49→ };
|
||||
50→
|
||||
51→ // 格式化克重显示(含经线/纬线细分)
|
||||
52→ const formatWeightDisplay = (product: ProductWithInventory) => {
|
||||
53→ const total = product.total_weight || product.weight;
|
||||
54→ if (product.warp_weight && product.weft_weight) {
|
||||
108
.microcompact/mpo3tsaf/functions.Edit:4.txt
Normal file
108
.microcompact/mpo3tsaf/functions.Edit:4.txt
Normal file
@ -0,0 +1,108 @@
|
||||
The file /home/project/src/components/warehouse/ProductList.tsx has been updated. Made 1 replacement.
|
||||
|
||||
Here's the result of running `cat -n` on a snippet of the edited file:
|
||||
121→ exit={{ height: 0, opacity: 0 }}
|
||||
122→ transition={animationConfig.transition}
|
||||
123→ className="border-t border-gray-100"
|
||||
124→ >
|
||||
125→ <div className="p-4 space-y-3">
|
||||
126→ {Object.entries(weightGroups).map(([weight, products]) => {
|
||||
127→ const weightKey = `${productName}-${weight}`;
|
||||
128→ const isWeightExpanded = expandedGroups[weightKey];
|
||||
129→ const hasNoWeight = weight === '__no_weight__';
|
||||
130→
|
||||
131→ // 如果没有克重,直接显示产品列表,不显示克重折叠卡片
|
||||
132→ if (hasNoWeight) {
|
||||
133→ return (
|
||||
134→ <div key={weight} className="bg-gray-50 rounded-xl overflow-hidden">
|
||||
135→ <table className="w-full">
|
||||
136→ <thead className="bg-white/50 border-b border-gray-100">
|
||||
137→ <tr>
|
||||
138→ <th className="px-3 py-2 text-left text-xs font-medium text-gray-500">颜色</th>
|
||||
139→ <th className="px-3 py-2 text-left text-xs font-medium text-gray-500">产品码</th>
|
||||
140→ <th className="px-3 py-2 text-left text-xs font-medium text-gray-500">库存</th>
|
||||
141→ <th className="px-3 py-2 text-right text-xs font-medium text-gray-500">操作</th>
|
||||
142→ </tr>
|
||||
143→ </thead>
|
||||
144→ <tbody className="divide-y divide-gray-100">
|
||||
145→ {products.map(product => (
|
||||
146→ <ProductRow
|
||||
147→ key={product.id}
|
||||
148→ product={product}
|
||||
149→ ratios={productRatios[product.id]}
|
||||
150→ onViewRecords={() => onViewRecords(product)}
|
||||
151→ onViewPriceHistory={() => onViewPriceHistory(product)}
|
||||
152→ onViewStockHistory={() => onViewStockHistory(product)}
|
||||
153→ onEdit={() => onEdit(product)}
|
||||
154→ onDelete={() => onDelete(product.id)}
|
||||
155→ />
|
||||
156→ ))}
|
||||
157→ </tbody>
|
||||
158→ </table>
|
||||
159→ </div>
|
||||
160→ );
|
||||
161→ }
|
||||
162→
|
||||
163→ return (
|
||||
164→ <div key={weight} className="bg-gray-50 rounded-xl overflow-hidden">
|
||||
165→ <div
|
||||
166→ onClick={() => onToggleGroup(weightKey)}
|
||||
167→ className="flex items-center justify-between p-3 cursor-pointer hover:bg-gray-100 transition-colors"
|
||||
168→ >
|
||||
169→ <div className="flex items-center gap-2">
|
||||
170→ <div className={`w-6 h-6 rounded flex items-center justify-center transition-colors ${isWeightExpanded ? 'bg-blue-100' : 'bg-white'}`}>
|
||||
171→ {isWeightExpanded ? <ChevronUp size={14} className="text-blue-600" /> : <ChevronDown size={14} className="text-gray-500" />}
|
||||
172→ </div>
|
||||
173→ <span className="font-medium text-gray-800">{weight}g</span>
|
||||
174→ {products[0]?.warp_weight && products[0]?.weft_weight && (
|
||||
175→ <span className="text-xs text-gray-400 ml-1">经{products[0].warp_weight}+纬{products[0].weft_weight}</span>
|
||||
176→ )}
|
||||
177→ <span className="text-xs text-gray-500">({products.length}个颜色)</span>
|
||||
178→ </div>
|
||||
179→ </div>
|
||||
180→
|
||||
181→ <AnimatePresence>
|
||||
182→ {isWeightExpanded && (
|
||||
183→ <motion.div
|
||||
184→ initial={{ height: 0, opacity: 0 }}
|
||||
185→ animate={{ height: 'auto', opacity: 1 }}
|
||||
186→ exit={{ height: 0, opacity: 0 }}
|
||||
187→ transition={animationConfig.fastTransition}
|
||||
188→ className="border-t border-gray-200"
|
||||
189→ >
|
||||
190→ <table className="w-full">
|
||||
191→ <thead className="bg-white/50 border-b border-gray-100">
|
||||
192→ <tr>
|
||||
193→ <th className="px-3 py-2 text-left text-xs font-medium text-gray-500">颜色</th>
|
||||
194→ <th className="px-3 py-2 text-left text-xs font-medium text-gray-500">产品码</th>
|
||||
195→ <th className="px-3 py-2 text-left text-xs font-medium text-gray-500">库存</th>
|
||||
196→ <th className="px-3 py-2 text-right text-xs font-medium text-gray-500">操作</th>
|
||||
197→ </tr>
|
||||
198→ </thead>
|
||||
199→ <tbody className="divide-y divide-gray-100">
|
||||
200→ {products.map(product => (
|
||||
201→ <ProductRow
|
||||
202→ key={product.id}
|
||||
203→ product={product}
|
||||
204→ ratios={productRatios[product.id]}
|
||||
205→ onViewRecords={() => onViewRecords(product)}
|
||||
206→ onViewPriceHistory={() => onViewPriceHistory(product)}
|
||||
207→ onViewStockHistory={() => onViewStockHistory(product)}
|
||||
208→ onEdit={() => onEdit(product)}
|
||||
209→ onDelete={() => onDelete(product.id)}
|
||||
210→ />
|
||||
211→ ))}
|
||||
212→ </tbody>
|
||||
213→ </table>
|
||||
214→ </motion.div>
|
||||
215→ )}
|
||||
216→ </AnimatePresence>
|
||||
217→ </div>
|
||||
218→ );
|
||||
219→ })}
|
||||
220→ </div>
|
||||
221→ </motion.div>
|
||||
222→ )}
|
||||
223→ </AnimatePresence>
|
||||
224→ </motion.div>
|
||||
225→ );
|
||||
149
.microcompact/mpo3tscr/functions.Edit:8.txt
Normal file
149
.microcompact/mpo3tscr/functions.Edit:8.txt
Normal file
@ -0,0 +1,149 @@
|
||||
The file /home/project/src/components/warehouse/ProductList.tsx has been updated. Made 1 replacement.
|
||||
|
||||
Here's the result of running `cat -n` on a snippet of the edited file:
|
||||
121→ exit={{ height: 0, opacity: 0 }}
|
||||
122→ transition={animationConfig.transition}
|
||||
123→ className="border-t border-gray-100"
|
||||
124→ >
|
||||
125→ <div className="p-4 space-y-3">
|
||||
126→ {Object.entries(weightGroups).map(([weight, products]) => {
|
||||
127→ const weightKey = `${productName}-${weight}`;
|
||||
128→ const isWeightExpanded = expandedGroups[weightKey];
|
||||
129→ const hasNoWeight = weight === '__no_weight__';
|
||||
130→ const weightCount = Object.keys(weightGroups).length;
|
||||
131→ const isSingleWeight = weightCount === 1;
|
||||
132→
|
||||
133→ // 如果没有克重,直接显示产品列表,不显示克重折叠卡片
|
||||
134→ if (hasNoWeight) {
|
||||
135→ return (
|
||||
136→ <div key={weight} className="bg-gray-50 rounded-xl overflow-hidden">
|
||||
137→ <table className="w-full">
|
||||
138→ <thead className="bg-white/50 border-b border-gray-100">
|
||||
139→ <tr>
|
||||
140→ <th className="px-3 py-2 text-left text-xs font-medium text-gray-500">颜色</th>
|
||||
141→ <th className="px-3 py-2 text-left text-xs font-medium text-gray-500">产品码</th>
|
||||
142→ <th className="px-3 py-2 text-left text-xs font-medium text-gray-500">库存</th>
|
||||
143→ <th className="px-3 py-2 text-right text-xs font-medium text-gray-500">操作</th>
|
||||
144→ </tr>
|
||||
145→ </thead>
|
||||
146→ <tbody className="divide-y divide-gray-100">
|
||||
147→ {products.map(product => (
|
||||
148→ <ProductRow
|
||||
149→ key={product.id}
|
||||
150→ product={product}
|
||||
151→ ratios={productRatios[product.id]}
|
||||
152→ onViewRecords={() => onViewRecords(product)}
|
||||
153→ onViewPriceHistory={() => onViewPriceHistory(product)}
|
||||
154→ onViewStockHistory={() => onViewStockHistory(product)}
|
||||
155→ onEdit={() => onEdit(product)}
|
||||
156→ onDelete={() => onDelete(product.id)}
|
||||
157→ />
|
||||
158→ ))}
|
||||
159→ </tbody>
|
||||
160→ </table>
|
||||
161→ </div>
|
||||
162→ );
|
||||
163→ }
|
||||
164→
|
||||
165→ // 如果只有一种克重,直接显示克重信息和产品列表,不需要折叠
|
||||
166→ if (isSingleWeight) {
|
||||
167→ return (
|
||||
168→ <div key={weight} className="bg-gray-50 rounded-xl overflow-hidden">
|
||||
169→ <div className="flex items-center gap-2 p-3 bg-gray-100 border-b border-gray-200">
|
||||
170→ <span className="font-medium text-gray-800">{weight}g</span>
|
||||
171→ {products[0]?.warp_weight && products[0]?.weft_weight && (
|
||||
172→ <span className="text-xs text-gray-500 ml-1">经{products[0].warp_weight}+纬{products[0].weft_weight}</span>
|
||||
173→ )}
|
||||
174→ <span className="text-xs text-gray-500">({products.length}个颜色)</span>
|
||||
175→ </div>
|
||||
176→ <table className="w-full">
|
||||
177→ <thead className="bg-white/50 border-b border-gray-100">
|
||||
178→ <tr>
|
||||
179→ <th className="px-3 py-2 text-left text-xs font-medium text-gray-500">颜色</th>
|
||||
180→ <th className="px-3 py-2 text-left text-xs font-medium text-gray-500">产品码</th>
|
||||
181→ <th className="px-3 py-2 text-left text-xs font-medium text-gray-500">库存</th>
|
||||
182→ <th className="px-3 py-2 text-right text-xs font-medium text-gray-500">操作</th>
|
||||
183→ </tr>
|
||||
184→ </thead>
|
||||
185→ <tbody className="divide-y divide-gray-100">
|
||||
186→ {products.map(product => (
|
||||
187→ <ProductRow
|
||||
188→ key={product.id}
|
||||
189→ product={product}
|
||||
190→ ratios={productRatios[product.id]}
|
||||
191→ onViewRecords={() => onViewRecords(product)}
|
||||
192→ onViewPriceHistory={() => onViewPriceHistory(product)}
|
||||
193→ onViewStockHistory={() => onViewStockHistory(product)}
|
||||
194→ onEdit={() => onEdit(product)}
|
||||
195→ onDelete={() => onDelete(product.id)}
|
||||
196→ />
|
||||
197→ ))}
|
||||
198→ </tbody>
|
||||
199→ </table>
|
||||
200→ </div>
|
||||
201→ );
|
||||
202→ }
|
||||
203→
|
||||
204→ return (
|
||||
205→ <div key={weight} className="bg-gray-50 rounded-xl overflow-hidden">
|
||||
206→ <div
|
||||
207→ onClick={() => onToggleGroup(weightKey)}
|
||||
208→ className="flex items-center justify-between p-3 cursor-pointer hover:bg-gray-100 transition-colors"
|
||||
209→ >
|
||||
210→ <div className="flex items-center gap-2">
|
||||
211→ <div className={`w-6 h-6 rounded flex items-center justify-center transition-colors ${isWeightExpanded ? 'bg-blue-100' : 'bg-white'}`}>
|
||||
212→ {isWeightExpanded ? <ChevronUp size={14} className="text-blue-600" /> : <ChevronDown size={14} className="text-gray-500" />}
|
||||
213→ </div>
|
||||
214→ <span className="font-medium text-gray-800">{weight}g</span>
|
||||
215→ {products[0]?.warp_weight && products[0]?.weft_weight && (
|
||||
216→ <span className="text-xs text-gray-400 ml-1">经{products[0].warp_weight}+纬{products[0].weft_weight}</span>
|
||||
217→ )}
|
||||
218→ <span className="text-xs text-gray-500">({products.length}个颜色)</span>
|
||||
219→ </div>
|
||||
220→ </div>
|
||||
221→
|
||||
222→ <AnimatePresence>
|
||||
223→ {isWeightExpanded && (
|
||||
224→ <motion.div
|
||||
225→ initial={{ height: 0, opacity: 0 }}
|
||||
226→ animate={{ height: 'auto', opacity: 1 }}
|
||||
227→ exit={{ height: 0, opacity: 0 }}
|
||||
228→ transition={animationConfig.fastTransition}
|
||||
229→ className="border-t border-gray-200"
|
||||
230→ >
|
||||
231→ <table className="w-full">
|
||||
232→ <thead className="bg-white/50 border-b border-gray-100">
|
||||
233→ <tr>
|
||||
234→ <th className="px-3 py-2 text-left text-xs font-medium text-gray-500">颜色</th>
|
||||
235→ <th className="px-3 py-2 text-left text-xs font-medium text-gray-500">产品码</th>
|
||||
236→ <th className="px-3 py-2 text-left text-xs font-medium text-gray-500">库存</th>
|
||||
237→ <th className="px-3 py-2 text-right text-xs font-medium text-gray-500">操作</th>
|
||||
238→ </tr>
|
||||
239→ </thead>
|
||||
240→ <tbody className="divide-y divide-gray-100">
|
||||
241→ {products.map(product => (
|
||||
242→ <ProductRow
|
||||
243→ key={product.id}
|
||||
244→ product={product}
|
||||
245→ ratios={productRatios[product.id]}
|
||||
246→ onViewRecords={() => onViewRecords(product)}
|
||||
247→ onViewPriceHistory={() => onViewPriceHistory(product)}
|
||||
248→ onViewStockHistory={() => onViewStockHistory(product)}
|
||||
249→ onEdit={() => onEdit(product)}
|
||||
250→ onDelete={() => onDelete(product.id)}
|
||||
251→ />
|
||||
252→ ))}
|
||||
253→ </tbody>
|
||||
254→ </table>
|
||||
255→ </motion.div>
|
||||
256→ )}
|
||||
257→ </AnimatePresence>
|
||||
258→ </div>
|
||||
259→ );
|
||||
260→ })}
|
||||
261→ </div>
|
||||
262→ </motion.div>
|
||||
263→ )}
|
||||
264→ </AnimatePresence>
|
||||
265→ </motion.div>
|
||||
266→ );
|
||||
1
.microcompact/mpo3tsew/functions.Bash:10.txt
Normal file
1
.microcompact/mpo3tsew/functions.Bash:10.txt
Normal file
@ -0,0 +1 @@
|
||||
{"exitCode":0,"output":"[OpenTelemetry] Trace 已启动,serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production\n\n> procurement-planning@1.0.0 typecheck /home/project\n> tsc --noEmit\n\n[OpenTelemetry] Trace 已启动,serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production"}
|
||||
1
.microcompact/mpo3tshq/functions.Grep:11.txt
Normal file
1
.microcompact/mpo3tshq/functions.Grep:11.txt
Normal file
@ -0,0 +1 @@
|
||||
{"results":[{"file":"src/components/OnboardingGuide.tsx","matches":[{"line":42,"content":" description: '在\"原坯布仓库\"中查看和管理所有入库的坯布,支持出入库历史查询。',"}]},{"file":"src/components/UserManual.tsx","matches":[{"line":30,"content":" { title: '库存管理', content: '在\"原坯布仓库\"中管理所有入库的坯布,支持按产品名称、克重、颜色分组查看,可查询出入库历史。' },"}]},{"file":"src/pages/purchaser/WarehouseManage.tsx","matches":[{"line":418,"content":" <h1 className=\"text-base sm:text-lg font-bold text-gray-900\">原坯布仓库</h1>"}]},{"file":"src/pages/purchaser/NewWashingPlan.tsx","matches":[{"line":39,"content":" // 获取原坯布仓库产品(包含库存计算)"},{"line":298,"content":" 原坯布仓库中没有可用产品,请先创建产品并入库"}]},{"file":"src/pages/purchaser/NewPlan.tsx","matches":[{"line":16,"content":"// 产品类型定义(来自原坯布仓库)"},{"line":52,"content":" // 从原坯布仓库选择的产品"},{"line":81,"content":" // 从原坯布仓库获取产品数据"},{"line":291,"content":" {/* 原坯布仓库产品选择 - 搜索+筛选形式 */}"}]},{"file":"src/pages/purchaser/Dashboard.tsx","matches":[{"line":27,"content":" { id: 'warehouse', icon: Warehouse, label: '原坯布仓库', path: '/purchaser/warehouse', gradient: 'from-amber-500 to-orange-500', description: '管理原坯布库存' },"}]}],"pattern":"原坯布仓库","path":"src","totalMatches":10}
|
||||
1
.microcompact/mpo3tskd/functions.Bash:16.txt
Normal file
1
.microcompact/mpo3tskd/functions.Bash:16.txt
Normal file
@ -0,0 +1 @@
|
||||
{"exitCode":0,"output":"[OpenTelemetry] Trace 已启动,serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production\n\n> procurement-planning@1.0.0 typecheck /home/project\n> tsc --noEmit\n\n[OpenTelemetry] Trace 已启动,serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production"}
|
||||
1
.microcompact/mpo3tsmc/functions.Grep:17.txt
Normal file
1
.microcompact/mpo3tsmc/functions.Grep:17.txt
Normal file
@ -0,0 +1 @@
|
||||
{"results":[{"file":"src/pages/purchaser/NewPlan.tsx","matches":[{"line":294,"content":" <span className=\"w-1 h-4 bg-blue-500 rounded\"></span>选择原坯布产品"},{"line":632,"content":" <h3 className=\"text-lg font-semibold text-gray-900\">选择原坯布产品</h3>"}]}],"pattern":"选择原坯布产品","path":"src","totalMatches":2}
|
||||
1
.microcompact/mpo3tso9/functions.Bash:20.txt
Normal file
1
.microcompact/mpo3tso9/functions.Bash:20.txt
Normal file
@ -0,0 +1 @@
|
||||
{"exitCode":0,"output":"[OpenTelemetry] Trace 已启动,serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production\n\n> procurement-planning@1.0.0 typecheck /home/project\n> tsc --noEmit\n\n[OpenTelemetry] Trace 已启动,serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production"}
|
||||
1
.microcompact/mpo3tst3/functions.Bash:24.txt
Normal file
1
.microcompact/mpo3tst3/functions.Bash:24.txt
Normal file
@ -0,0 +1 @@
|
||||
{"exitCode":0,"output":"[OpenTelemetry] Trace 已启动,serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production\n\n> procurement-planning@1.0.0 typecheck /home/project\n> tsc --noEmit\n\n[OpenTelemetry] Trace 已启动,serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production\nsrc/pages/purchaser/WarehouseManage.tsx(192,27): error TS2345: Argument of type 'number | null' is not assignable to parameter of type 'number'.\n Type 'null' is not assignable to type 'number'.\n ELIFECYCLE Command failed with exit code 2."}
|
||||
1
.microcompact/mpo3tsva/functions.Bash:26.txt
Normal file
1
.microcompact/mpo3tsva/functions.Bash:26.txt
Normal file
@ -0,0 +1 @@
|
||||
{"exitCode":0,"output":"[OpenTelemetry] Trace 已启动,serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production\n\n> procurement-planning@1.0.0 typecheck /home/project\n> tsc --noEmit\n\n[OpenTelemetry] Trace 已启动,serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production"}
|
||||
28
.microcompact/mpo3tsxk/functions.Edit:27.txt
Normal file
28
.microcompact/mpo3tsxk/functions.Edit:27.txt
Normal file
@ -0,0 +1,28 @@
|
||||
The file /home/project/src/pages/purchaser/WarehouseManage.tsx has been updated. Made 1 replacement.
|
||||
|
||||
Here's the result of running `cat -n` on a snippet of the edited file:
|
||||
182→ alert(`产品码 "${productData.fabric_code}" 已存在,请使用其他产品码`);
|
||||
183→ return;
|
||||
184→ }
|
||||
185→
|
||||
186→ // 校验2:色号在"成品名称-克重-颜色"组合中唯一
|
||||
187→ // 即:同一个"成品名称-克重-颜色"下,色号不能重复
|
||||
188→ const { data: existingColorCode } = await supabase
|
||||
189→ .from('products')
|
||||
190→ .select('id, fabric_code, color_code')
|
||||
191→ .eq('company_id', auth.company.id)
|
||||
192→ .eq('product_name', productData.product_name)
|
||||
193→ .eq('total_weight', productData.total_weight || 0)
|
||||
194→ .eq('color', productData.color)
|
||||
195→ .neq('id', editingProduct?.id || '00000000-0000-0000-0000-000000000000')
|
||||
196→ .maybeSingle();
|
||||
197→
|
||||
198→ if (existingColorCode) {
|
||||
199→ alert(`"${productData.product_name}-${productData.total_weight}g-${productData.color}" 组合中已存在色号 "${existingColorCode.color_code}",请使用其他色号`);
|
||||
200→ return;
|
||||
201→ }
|
||||
202→
|
||||
203→ let productId: string;
|
||||
204→
|
||||
205→ if (editingProduct) {
|
||||
206→ // 更新产品
|
||||
29
.microcompact/mpo3tt1c/functions.Edit:28.txt
Normal file
29
.microcompact/mpo3tt1c/functions.Edit:28.txt
Normal file
@ -0,0 +1,29 @@
|
||||
The file /home/project/src/pages/purchaser/WarehouseManage.tsx has been updated. Made 1 replacement.
|
||||
|
||||
Here's the result of running `cat -n` on a snippet of the edited file:
|
||||
182→ alert(`产品码 "${productData.fabric_code}" 已存在,请使用其他产品码`);
|
||||
183→ return;
|
||||
184→ }
|
||||
185→
|
||||
186→ // 校验2:色号在"成品名称-克重-颜色"组合中唯一
|
||||
187→ // 即:同一个"成品名称-克重-颜色"下,色号不能重复
|
||||
188→ const { data: existingColorCode } = await supabase
|
||||
189→ .from('products')
|
||||
190→ .select('id, fabric_code')
|
||||
191→ .eq('company_id', auth.company.id)
|
||||
192→ .eq('product_name', productData.product_name)
|
||||
193→ .eq('total_weight', productData.total_weight || 0)
|
||||
194→ .eq('color', productData.color)
|
||||
195→ .eq('color_code', productData.color_code)
|
||||
196→ .neq('id', editingProduct?.id || '00000000-0000-0000-0000-000000000000')
|
||||
197→ .maybeSingle();
|
||||
198→
|
||||
199→ if (existingColorCode) {
|
||||
200→ alert(`"${productData.product_name}-${productData.total_weight}g-${productData.color}" 组合中已存在色号 "${productData.color_code}",请使用其他色号`);
|
||||
201→ return;
|
||||
202→ }
|
||||
203→
|
||||
204→ let productId: string;
|
||||
205→
|
||||
206→ if (editingProduct) {
|
||||
207→ // 更新产品
|
||||
1
.microcompact/mpo3tt39/functions.Bash:29.txt
Normal file
1
.microcompact/mpo3tt39/functions.Bash:29.txt
Normal file
@ -0,0 +1 @@
|
||||
{"exitCode":0,"output":"[OpenTelemetry] Trace 已启动,serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production\n\n> procurement-planning@1.0.0 typecheck /home/project\n> tsc --noEmit\n\n[OpenTelemetry] Trace 已启动,serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production"}
|
||||
49
.microcompact/mpo3tt5a/functions.Edit:30.txt
Normal file
49
.microcompact/mpo3tt5a/functions.Edit:30.txt
Normal file
@ -0,0 +1,49 @@
|
||||
The file /home/project/src/components/warehouse/ProductForm.tsx has been updated. Made 1 replacement.
|
||||
|
||||
Here's the result of running `cat -n` on a snippet of the edited file:
|
||||
208→ setProductSuggestions([]);
|
||||
209→ }
|
||||
210→ };
|
||||
211→
|
||||
212→ // 从数据库查询已存在的最大产品码数字,生成下一个
|
||||
213→ const generateNextFabricCode = async (companyId: string, prefix: string): Promise<string> => {
|
||||
214→ if (!prefix) return '';
|
||||
215→
|
||||
216→ try {
|
||||
217→ // 查询该公司所有以该前缀开头的产品码
|
||||
218→ const { data, error } = await supabase
|
||||
219→ .from('products')
|
||||
220→ .select('fabric_code')
|
||||
221→ .eq('company_id', companyId)
|
||||
222→ .ilike('fabric_code', `${prefix}%`);
|
||||
223→
|
||||
224→ if (error || !data || data.length === 0) {
|
||||
225→ // 如果没有匹配的产品,从1开始
|
||||
226→ return `${prefix}1`;
|
||||
227→ }
|
||||
228→
|
||||
229→ // 提取所有产品码中的数字,找出最大值
|
||||
230→ let maxNum = 0;
|
||||
231→ data.forEach(item => {
|
||||
232→ const code = item.fabric_code;
|
||||
233→ const match = code.match(/^(.*?)(\d+)$/);
|
||||
234→ if (match && match[1].toUpperCase() === prefix.toUpperCase()) {
|
||||
235→ const num = parseInt(match[2], 10);
|
||||
236→ if (num > maxNum) {
|
||||
237→ maxNum = num;
|
||||
238→ }
|
||||
239→ }
|
||||
240→ });
|
||||
241→
|
||||
242→ // 返回前缀 + (最大数字 + 1)
|
||||
243→ return `${prefix}${maxNum + 1}`;
|
||||
244→ } catch (err) {
|
||||
245→ console.error('生成产品码失败:', err);
|
||||
246→ return `${prefix}1`;
|
||||
247→ }
|
||||
248→ };
|
||||
249→
|
||||
250→ // 生成递增的色号(如 02 → 03)
|
||||
251→ const generateNextColorCode = (currentCode: string): string => {
|
||||
252→ if (!currentCode) return '01';
|
||||
253→ const num = parseInt(currentCode, 10);
|
||||
58
.microcompact/mpo3tt75/functions.Edit:31.txt
Normal file
58
.microcompact/mpo3tt75/functions.Edit:31.txt
Normal file
@ -0,0 +1,58 @@
|
||||
The file /home/project/src/components/warehouse/ProductForm.tsx has been updated. Made 1 replacement.
|
||||
|
||||
Here's the result of running `cat -n` on a snippet of the edited file:
|
||||
255→ // 保持两位数格式
|
||||
256→ return nextNum < 10 ? `0${nextNum}` : String(nextNum);
|
||||
257→ };
|
||||
258→
|
||||
259→ // 选择产品建议
|
||||
260→ const selectProductSuggestion = async (suggestion: ProductSuggestion) => {
|
||||
261→ // 获取该产品的纱线配比
|
||||
262→ const ratios = productRatios[suggestion.id] || [];
|
||||
263→
|
||||
264→ // 提取产品码前缀并生成新的产品码
|
||||
265→ let newFabricCode = '';
|
||||
266→ const match = suggestion.fabric_code.match(/^(.*?)(\d+)$/);
|
||||
267→ if (match && companyId) {
|
||||
268→ const prefix = match[1];
|
||||
269→ newFabricCode = await generateNextFabricCode(companyId, prefix);
|
||||
270→ } else {
|
||||
271→ newFabricCode = suggestion.fabric_code;
|
||||
272→ }
|
||||
273→
|
||||
274→ setFormData(prev => ({
|
||||
275→ ...prev,
|
||||
276→ product_name: suggestion.product_name,
|
||||
277→ // 自动填写克重
|
||||
278→ total_weight: suggestion.total_weight ? String(suggestion.total_weight) : '',
|
||||
279→ warp_weight: suggestion.warp_weight ? String(suggestion.warp_weight) : '',
|
||||
280→ weft_weight: suggestion.weft_weight ? String(suggestion.weft_weight) : '',
|
||||
281→ // 颜色需要用户自己输入,不自动填写
|
||||
282→ color: '',
|
||||
283→ // 产品码自动生成(基于数据库最大值+1)
|
||||
284→ fabric_code: newFabricCode,
|
||||
285→ // 色号自动生成(递增)
|
||||
286→ color_code: generateNextColorCode(suggestion.color_code),
|
||||
287→ // 自动填写生产采购价
|
||||
288→ production_price: suggestion.production_price ? String(suggestion.production_price) : '',
|
||||
289→ // 自动填写纱线配比
|
||||
290→ yarn_ratios: ratios.length > 0
|
||||
291→ ? ratios.map(r => ({ yarn_name: r.yarn_name, ratio: r.ratio, yarn_type: (r.yarn_type as 'warp' | 'weft' | 'all') || 'all' }))
|
||||
292→ : [{ yarn_name: '', ratio: 100, yarn_type: 'all' }],
|
||||
293→ image_url: suggestion.image_url || undefined
|
||||
294→ }));
|
||||
295→
|
||||
296→ if (suggestion.image_url) {
|
||||
297→ setPreviewUrl(suggestion.image_url);
|
||||
298→ }
|
||||
299→
|
||||
300→ setShowProductSuggestions(false);
|
||||
301→
|
||||
302→ // 加载该产品的克重建议
|
||||
303→ searchWeights(suggestion.product_name);
|
||||
304→ };
|
||||
305→
|
||||
306→ // 处理总克重输入
|
||||
307→ const handleTotalWeightChange = (value: string) => {
|
||||
308→ setFormData(prev => ({ ...prev, total_weight: value }));
|
||||
309→
|
||||
1
.microcompact/mpo3tt8z/functions.Bash:32.txt
Normal file
1
.microcompact/mpo3tt8z/functions.Bash:32.txt
Normal file
@ -0,0 +1 @@
|
||||
{"exitCode":0,"output":"[OpenTelemetry] Trace 已启动,serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production\n\n> procurement-planning@1.0.0 typecheck /home/project\n> tsc --noEmit\n\n[OpenTelemetry] Trace 已启动,serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production"}
|
||||
52
.microcompact/mpo3ttb1/functions.Edit:33.txt
Normal file
52
.microcompact/mpo3ttb1/functions.Edit:33.txt
Normal file
@ -0,0 +1,52 @@
|
||||
The file /home/project/src/components/warehouse/ProductForm.tsx has been updated. Made 1 replacement.
|
||||
|
||||
Here's the result of running `cat -n` on a snippet of the edited file:
|
||||
246→ return `${prefix}1`;
|
||||
247→ }
|
||||
248→ };
|
||||
249→
|
||||
250→ // 从数据库查询已存在的最大色号数字,生成下一个
|
||||
251→ const generateNextColorCode = async (
|
||||
252→ companyId: string,
|
||||
253→ productName: string,
|
||||
254→ totalWeight: number | null,
|
||||
255→ color: string
|
||||
256→ ): Promise<string> => {
|
||||
257→ try {
|
||||
258→ // 查询该产品名称-克重-颜色组合下的所有色号
|
||||
259→ const { data, error } = await supabase
|
||||
260→ .from('products')
|
||||
261→ .select('color_code')
|
||||
262→ .eq('company_id', companyId)
|
||||
263→ .eq('product_name', productName)
|
||||
264→ .eq('total_weight', totalWeight || 0)
|
||||
265→ .eq('color', color);
|
||||
266→
|
||||
267→ if (error || !data || data.length === 0) {
|
||||
268→ // 如果没有匹配的产品,从01开始
|
||||
269→ return '01';
|
||||
270→ }
|
||||
271→
|
||||
272→ // 提取所有色号中的数字,找出最大值
|
||||
273→ let maxNum = 0;
|
||||
274→ data.forEach(item => {
|
||||
275→ const code = item.color_code;
|
||||
276→ const num = parseInt(code, 10);
|
||||
277→ if (!isNaN(num) && num > maxNum) {
|
||||
278→ maxNum = num;
|
||||
279→ }
|
||||
280→ });
|
||||
281→
|
||||
282→ // 返回最大数字 + 1,保持两位数格式
|
||||
283→ const nextNum = maxNum + 1;
|
||||
284→ return nextNum < 10 ? `0${nextNum}` : String(nextNum);
|
||||
285→ } catch (err) {
|
||||
286→ console.error('生成色号失败:', err);
|
||||
287→ return '01';
|
||||
288→ }
|
||||
289→ };
|
||||
290→
|
||||
291→ // 选择产品建议
|
||||
292→ const selectProductSuggestion = async (suggestion: ProductSuggestion) => {
|
||||
293→ // 获取该产品的纱线配比
|
||||
294→ const ratios = productRatios[suggestion.id] || [];
|
||||
71
.microcompact/mpo3ttcy/functions.Edit:34.txt
Normal file
71
.microcompact/mpo3ttcy/functions.Edit:34.txt
Normal file
@ -0,0 +1,71 @@
|
||||
The file /home/project/src/components/warehouse/ProductForm.tsx has been updated. Made 1 replacement.
|
||||
|
||||
Here's the result of running `cat -n` on a snippet of the edited file:
|
||||
287→ return '01';
|
||||
288→ }
|
||||
289→ };
|
||||
290→
|
||||
291→ // 选择产品建议
|
||||
292→ const selectProductSuggestion = async (suggestion: ProductSuggestion) => {
|
||||
293→ // 获取该产品的纱线配比
|
||||
294→ const ratios = productRatios[suggestion.id] || [];
|
||||
295→
|
||||
296→ // 提取产品码前缀并生成新的产品码
|
||||
297→ let newFabricCode = '';
|
||||
298→ const fabricMatch = suggestion.fabric_code.match(/^(.*?)(\d+)$/);
|
||||
299→ if (fabricMatch && companyId) {
|
||||
300→ const prefix = fabricMatch[1];
|
||||
301→ newFabricCode = await generateNextFabricCode(companyId, prefix);
|
||||
302→ } else {
|
||||
303→ newFabricCode = suggestion.fabric_code;
|
||||
304→ }
|
||||
305→
|
||||
306→ // 生成色号(基于该产品名称-克重-颜色组合的最大值+1)
|
||||
307→ // 注意:颜色字段为空,因为用户需要手动输入新颜色
|
||||
308→ // 这里先使用建议产品的颜色来查询色号,实际保存时会用用户输入的颜色
|
||||
309→ let newColorCode = '01';
|
||||
310→ if (companyId) {
|
||||
311→ newColorCode = await generateNextColorCode(
|
||||
312→ companyId,
|
||||
313→ suggestion.product_name,
|
||||
314→ suggestion.total_weight,
|
||||
315→ suggestion.color
|
||||
316→ );
|
||||
317→ }
|
||||
318→
|
||||
319→ setFormData(prev => ({
|
||||
320→ ...prev,
|
||||
321→ product_name: suggestion.product_name,
|
||||
322→ // 自动填写克重
|
||||
323→ total_weight: suggestion.total_weight ? String(suggestion.total_weight) : '',
|
||||
324→ warp_weight: suggestion.warp_weight ? String(suggestion.warp_weight) : '',
|
||||
325→ weft_weight: suggestion.weft_weight ? String(suggestion.weft_weight) : '',
|
||||
326→ // 颜色需要用户自己输入,不自动填写
|
||||
327→ color: '',
|
||||
328→ // 产品码自动生成(基于数据库最大值+1)
|
||||
329→ fabric_code: newFabricCode,
|
||||
330→ // 色号自动生成(基于该产品名称-克重-颜色组合的最大值+1)
|
||||
331→ color_code: newColorCode,
|
||||
332→ // 自动填写生产采购价
|
||||
333→ production_price: suggestion.production_price ? String(suggestion.production_price) : '',
|
||||
334→ // 自动填写纱线配比
|
||||
335→ yarn_ratios: ratios.length > 0
|
||||
336→ ? ratios.map(r => ({ yarn_name: r.yarn_name, ratio: r.ratio, yarn_type: (r.yarn_type as 'warp' | 'weft' | 'all') || 'all' }))
|
||||
337→ : [{ yarn_name: '', ratio: 100, yarn_type: 'all' }],
|
||||
338→ image_url: suggestion.image_url || undefined
|
||||
339→ }));
|
||||
340→
|
||||
341→ if (suggestion.image_url) {
|
||||
342→ setPreviewUrl(suggestion.image_url);
|
||||
343→ }
|
||||
344→
|
||||
345→ setShowProductSuggestions(false);
|
||||
346→
|
||||
347→ // 加载该产品的克重建议
|
||||
348→ searchWeights(suggestion.product_name);
|
||||
349→ };
|
||||
350→
|
||||
351→ // 处理总克重输入
|
||||
352→ const handleTotalWeightChange = (value: string) => {
|
||||
353→ setFormData(prev => ({ ...prev, total_weight: value }));
|
||||
354→
|
||||
1
.microcompact/mpo3ttf0/functions.Bash:35.txt
Normal file
1
.microcompact/mpo3ttf0/functions.Bash:35.txt
Normal file
@ -0,0 +1 @@
|
||||
{"exitCode":0,"output":"[OpenTelemetry] Trace 已启动,serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production\n\n> procurement-planning@1.0.0 typecheck /home/project\n> tsc --noEmit\n\n[OpenTelemetry] Trace 已启动,serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production"}
|
||||
50
.microcompact/mpo3ttgq/functions.Edit:36.txt
Normal file
50
.microcompact/mpo3ttgq/functions.Edit:36.txt
Normal file
@ -0,0 +1,50 @@
|
||||
The file /home/project/src/components/warehouse/ProductForm.tsx has been updated. Made 1 replacement.
|
||||
|
||||
Here's the result of running `cat -n` on a snippet of the edited file:
|
||||
246→ return `${prefix}1`;
|
||||
247→ }
|
||||
248→ };
|
||||
249→
|
||||
250→ // 从数据库查询已存在的最大色号数字,生成下一个
|
||||
251→ const generateNextColorCode = async (
|
||||
252→ companyId: string,
|
||||
253→ productName: string,
|
||||
254→ totalWeight: number | null
|
||||
255→ ): Promise<string> => {
|
||||
256→ try {
|
||||
257→ // 查询该产品名称-克重组合下的所有色号(不区分颜色)
|
||||
258→ const { data, error } = await supabase
|
||||
259→ .from('products')
|
||||
260→ .select('color_code')
|
||||
261→ .eq('company_id', companyId)
|
||||
262→ .eq('product_name', productName)
|
||||
263→ .eq('total_weight', totalWeight || 0);
|
||||
264→
|
||||
265→ if (error || !data || data.length === 0) {
|
||||
266→ // 如果没有匹配的产品,从01开始
|
||||
267→ return '01';
|
||||
268→ }
|
||||
269→
|
||||
270→ // 提取所有色号中的数字,找出最大值
|
||||
271→ let maxNum = 0;
|
||||
272→ data.forEach(item => {
|
||||
273→ const code = item.color_code;
|
||||
274→ const num = parseInt(code, 10);
|
||||
275→ if (!isNaN(num) && num > maxNum) {
|
||||
276→ maxNum = num;
|
||||
277→ }
|
||||
278→ });
|
||||
279→
|
||||
280→ // 返回最大数字 + 1,保持两位数格式
|
||||
281→ const nextNum = maxNum + 1;
|
||||
282→ return nextNum < 10 ? `0${nextNum}` : String(nextNum);
|
||||
283→ } catch (err) {
|
||||
284→ console.error('生成色号失败:', err);
|
||||
285→ return '01';
|
||||
286→ }
|
||||
287→ };
|
||||
288→
|
||||
289→ // 选择产品建议
|
||||
290→ const selectProductSuggestion = async (suggestion: ProductSuggestion) => {
|
||||
291→ // 获取该产品的纱线配比
|
||||
292→ const ratios = productRatios[suggestion.id] || [];
|
||||
21
.microcompact/mpo3ttil/functions.Edit:37.txt
Normal file
21
.microcompact/mpo3ttil/functions.Edit:37.txt
Normal file
@ -0,0 +1,21 @@
|
||||
The file /home/project/src/components/warehouse/ProductForm.tsx has been updated. Made 1 replacement.
|
||||
|
||||
Here's the result of running `cat -n` on a snippet of the edited file:
|
||||
300→ } else {
|
||||
301→ newFabricCode = suggestion.fabric_code;
|
||||
302→ }
|
||||
303→
|
||||
304→ // 生成色号(基于该产品名称-克重组合的最大值+1)
|
||||
305→ let newColorCode = '01';
|
||||
306→ if (companyId) {
|
||||
307→ newColorCode = await generateNextColorCode(
|
||||
308→ companyId,
|
||||
309→ suggestion.product_name,
|
||||
310→ suggestion.total_weight
|
||||
311→ );
|
||||
312→ }
|
||||
313→
|
||||
314→ setFormData(prev => ({
|
||||
315→ ...prev,
|
||||
316→ product_name: suggestion.product_name,
|
||||
317→ // 自动填写克重
|
||||
1
.microcompact/mpo3ttkk/functions.Bash:38.txt
Normal file
1
.microcompact/mpo3ttkk/functions.Bash:38.txt
Normal file
@ -0,0 +1 @@
|
||||
{"exitCode":0,"output":"[OpenTelemetry] Trace 已启动,serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production\n\n> procurement-planning@1.0.0 typecheck /home/project\n> tsc --noEmit\n\n[OpenTelemetry] Trace 已启动,serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production"}
|
||||
52
.microcompact/mpo3ttmc/functions.Edit:39.txt
Normal file
52
.microcompact/mpo3ttmc/functions.Edit:39.txt
Normal file
@ -0,0 +1,52 @@
|
||||
The file /home/project/src/components/warehouse/ProductForm.tsx has been updated. Made 1 replacement.
|
||||
|
||||
Here's the result of running `cat -n` on a snippet of the edited file:
|
||||
246→ return `${prefix}1`;
|
||||
247→ }
|
||||
248→ };
|
||||
249→
|
||||
250→ // 从数据库查询已存在的最大色号数字,生成下一个
|
||||
251→ const generateNextColorCode = async (
|
||||
252→ companyId: string,
|
||||
253→ productName: string,
|
||||
254→ totalWeight: number | null,
|
||||
255→ color: string
|
||||
256→ ): Promise<string> => {
|
||||
257→ try {
|
||||
258→ // 查询该产品名称-克重-颜色组合下的所有色号
|
||||
259→ const { data, error } = await supabase
|
||||
260→ .from('products')
|
||||
261→ .select('color_code')
|
||||
262→ .eq('company_id', companyId)
|
||||
263→ .eq('product_name', productName)
|
||||
264→ .eq('total_weight', totalWeight || 0)
|
||||
265→ .eq('color', color);
|
||||
266→
|
||||
267→ if (error || !data || data.length === 0) {
|
||||
268→ // 如果没有匹配的产品,从01开始
|
||||
269→ return '01';
|
||||
270→ }
|
||||
271→
|
||||
272→ // 提取所有色号中的数字,找出最大值
|
||||
273→ let maxNum = 0;
|
||||
274→ data.forEach(item => {
|
||||
275→ const code = item.color_code;
|
||||
276→ const num = parseInt(code, 10);
|
||||
277→ if (!isNaN(num) && num > maxNum) {
|
||||
278→ maxNum = num;
|
||||
279→ }
|
||||
280→ });
|
||||
281→
|
||||
282→ // 返回最大数字 + 1,保持两位数格式
|
||||
283→ const nextNum = maxNum + 1;
|
||||
284→ return nextNum < 10 ? `0${nextNum}` : String(nextNum);
|
||||
285→ } catch (err) {
|
||||
286→ console.error('生成色号失败:', err);
|
||||
287→ return '01';
|
||||
288→ }
|
||||
289→ };
|
||||
290→
|
||||
291→ // 选择产品建议
|
||||
292→ const selectProductSuggestion = async (suggestion: ProductSuggestion) => {
|
||||
293→ // 获取该产品的纱线配比
|
||||
294→ const ratios = productRatios[suggestion.id] || [];
|
||||
50
.microcompact/mpo3ttpi/functions.Edit:40.txt
Normal file
50
.microcompact/mpo3ttpi/functions.Edit:40.txt
Normal file
@ -0,0 +1,50 @@
|
||||
The file /home/project/src/components/warehouse/ProductForm.tsx has been updated. Made 1 replacement.
|
||||
|
||||
Here's the result of running `cat -n` on a snippet of the edited file:
|
||||
246→ return `${prefix}1`;
|
||||
247→ }
|
||||
248→ };
|
||||
249→
|
||||
250→ // 从数据库查询已存在的最大色号数字,生成下一个
|
||||
251→ const generateNextColorCode = async (
|
||||
252→ companyId: string,
|
||||
253→ productName: string,
|
||||
254→ totalWeight: number | null
|
||||
255→ ): Promise<string> => {
|
||||
256→ try {
|
||||
257→ // 查询该产品名称-克重组合下的所有色号(色号在产品名称-克重下连续递增)
|
||||
258→ const { data, error } = await supabase
|
||||
259→ .from('products')
|
||||
260→ .select('color_code')
|
||||
261→ .eq('company_id', companyId)
|
||||
262→ .eq('product_name', productName)
|
||||
263→ .eq('total_weight', totalWeight || 0);
|
||||
264→
|
||||
265→ if (error || !data || data.length === 0) {
|
||||
266→ // 如果没有匹配的产品,从01开始
|
||||
267→ return '01';
|
||||
268→ }
|
||||
269→
|
||||
270→ // 提取所有色号中的数字,找出最大值
|
||||
271→ let maxNum = 0;
|
||||
272→ data.forEach(item => {
|
||||
273→ const code = item.color_code;
|
||||
274→ const num = parseInt(code, 10);
|
||||
275→ if (!isNaN(num) && num > maxNum) {
|
||||
276→ maxNum = num;
|
||||
277→ }
|
||||
278→ });
|
||||
279→
|
||||
280→ // 返回最大数字 + 1,保持两位数格式
|
||||
281→ const nextNum = maxNum + 1;
|
||||
282→ return nextNum < 10 ? `0${nextNum}` : String(nextNum);
|
||||
283→ } catch (err) {
|
||||
284→ console.error('生成色号失败:', err);
|
||||
285→ return '01';
|
||||
286→ }
|
||||
287→ };
|
||||
288→
|
||||
289→ // 选择产品建议
|
||||
290→ const selectProductSuggestion = async (suggestion: ProductSuggestion) => {
|
||||
291→ // 获取该产品的纱线配比
|
||||
292→ const ratios = productRatios[suggestion.id] || [];
|
||||
21
.microcompact/mpo3ttrd/functions.Edit:41.txt
Normal file
21
.microcompact/mpo3ttrd/functions.Edit:41.txt
Normal file
@ -0,0 +1,21 @@
|
||||
The file /home/project/src/components/warehouse/ProductForm.tsx has been updated. Made 1 replacement.
|
||||
|
||||
Here's the result of running `cat -n` on a snippet of the edited file:
|
||||
300→ } else {
|
||||
301→ newFabricCode = suggestion.fabric_code;
|
||||
302→ }
|
||||
303→
|
||||
304→ // 生成色号(基于该产品名称-克重组合的最大值+1,色号在产品名称-克重下连续递增)
|
||||
305→ let newColorCode = '01';
|
||||
306→ if (companyId) {
|
||||
307→ newColorCode = await generateNextColorCode(
|
||||
308→ companyId,
|
||||
309→ suggestion.product_name,
|
||||
310→ suggestion.total_weight
|
||||
311→ );
|
||||
312→ }
|
||||
313→
|
||||
314→ setFormData(prev => ({
|
||||
315→ ...prev,
|
||||
316→ product_name: suggestion.product_name,
|
||||
317→ // 自动填写克重
|
||||
1
.microcompact/mpo3ttta/functions.Bash:42.txt
Normal file
1
.microcompact/mpo3ttta/functions.Bash:42.txt
Normal file
@ -0,0 +1 @@
|
||||
{"exitCode":0,"output":"[OpenTelemetry] Trace 已启动,serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production\n\n> procurement-planning@1.0.0 typecheck /home/project\n> tsc --noEmit\n\n[OpenTelemetry] Trace 已启动,serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production"}
|
||||
25
.microcompact/mpoz0xpj/functions.Edit:2.txt
Normal file
25
.microcompact/mpoz0xpj/functions.Edit:2.txt
Normal file
@ -0,0 +1,25 @@
|
||||
The file /home/project/src/pages/washing/Dashboard.tsx has been updated. Made 1 replacement.
|
||||
|
||||
Here's the result of running `cat -n` on a snippet of the edited file:
|
||||
19→ processing: { label: '处理中', color: 'text-purple-600', bgColor: 'bg-purple-50', icon: TrendingUp },
|
||||
20→ completed: { label: '已完成', color: 'text-emerald-600', bgColor: 'bg-emerald-50', icon: Package }
|
||||
21→};
|
||||
22→
|
||||
23→const getQuickActions = (isMaster: boolean) => {
|
||||
24→ const actions = [
|
||||
25→ { id: 'plans', icon: FileText, label: '计划总览', path: '/washing/plans', gradient: 'from-violet-400 to-fuchsia-400', description: '查看所有计划' },
|
||||
26→ { id: 'pending', icon: Package, label: '待处理坯布', path: '/washing/pending', gradient: 'from-amber-500 to-orange-500', description: '处理待水洗坯布' },
|
||||
27→ { id: 'completed', icon: CheckCircle, label: '已完成坯布', path: '/washing/completed', gradient: 'from-emerald-500 to-green-500', description: '查看已完成水洗' },
|
||||
28→ { id: 'finished', icon: Warehouse, label: '成品仓库', path: '/washing/finished-warehouse', gradient: 'from-violet-400 to-fuchsia-400', description: '管理成品库存' },
|
||||
29→ { id: 'payment', icon: CreditCard, label: '待结款', path: '/washing/payments', gradient: 'from-rose-500 to-pink-500', description: '查看待结款项' }
|
||||
30→ ];
|
||||
31→ if (isMaster) {
|
||||
32→ actions.push({ id: 'members', icon: Users, label: '账号管理', path: '/members', gradient: 'from-fuchsia-400 to-purple-400', description: '管理团队账号' });
|
||||
33→ }
|
||||
34→ return actions;
|
||||
35→};
|
||||
36→
|
||||
37→const containerVariants = {
|
||||
38→ hidden: { opacity: 0 },
|
||||
39→ visible: { opacity: 1, transition: { staggerChildren: 0.1 } }
|
||||
40→};
|
||||
13
.microcompact/mpoz0xrm/functions.Edit:5.txt
Normal file
13
.microcompact/mpoz0xrm/functions.Edit:5.txt
Normal file
@ -0,0 +1,13 @@
|
||||
The file /home/project/src/pages/washing/Dashboard.tsx has been updated. Made 1 replacement.
|
||||
|
||||
Here's the result of running `cat -n` on a snippet of the edited file:
|
||||
108→ <motion.button
|
||||
109→ whileHover={{ scale: 1.05 }}
|
||||
110→ whileTap={{ scale: 0.95 }}
|
||||
111→ onClick={() => setShowAccountCard(true)}
|
||||
112→ className="w-8 h-8 sm:w-10 sm:h-10 bg-gradient-to-br from-violet-400 to-fuchsia-400 rounded-xl flex items-center justify-center shadow-lg shadow-violet-400/20 cursor-pointer overflow-hidden"
|
||||
113→ >
|
||||
114→ {(auth.user as any)?.image_url ? (
|
||||
115→ <img
|
||||
116→ src={(auth.user as any).image_url}
|
||||
117→ alt="头像"
|
||||
38
.todo/2e49aa1e-9fea-418a-b7f0-75b6b0205b1a/todos.json
Normal file
38
.todo/2e49aa1e-9fea-418a-b7f0-75b6b0205b1a/todos.json
Normal file
@ -0,0 +1,38 @@
|
||||
[
|
||||
{
|
||||
"id": "1",
|
||||
"status": "completed",
|
||||
"content": "分析纺织厂采纱环节和原料纱仓库的数据结构",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"status": "completed",
|
||||
"content": "设计纱线自动分配逻辑和库存检查机制",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"status": "completed",
|
||||
"content": "在确认环节添加纱线选择弹窗组件",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "4",
|
||||
"status": "completed",
|
||||
"content": "实现纱线库存联动和自动分配功能",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "5",
|
||||
"status": "completed",
|
||||
"content": "添加原料不足提示和采购提醒",
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "6",
|
||||
"status": "completed",
|
||||
"content": "验证功能完整性和测试",
|
||||
"priority": "medium"
|
||||
}
|
||||
]
|
||||
26
.todo/2fcceff3-9af6-4a6f-9bb7-18a3381bc821/todos.json
Normal file
26
.todo/2fcceff3-9af6-4a6f-9bb7-18a3381bc821/todos.json
Normal file
@ -0,0 +1,26 @@
|
||||
[
|
||||
{
|
||||
"id": "1",
|
||||
"content": "统一 PlanOverview.tsx 主题色为 emerald 绿色系",
|
||||
"status": "completed",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"content": "统一 FabricWarehouse.tsx 主题色为 emerald 绿色系",
|
||||
"status": "completed",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"content": "统一 YarnWarehouse.tsx 主题色为 emerald 绿色系",
|
||||
"status": "completed",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "4",
|
||||
"content": "统一 PaymentPending.tsx 主题色为 emerald 绿色系",
|
||||
"status": "completed",
|
||||
"priority": "high"
|
||||
}
|
||||
]
|
||||
26
.todo/59db6b8c-de1a-4d6a-9387-f3945f203393/todos.json
Normal file
26
.todo/59db6b8c-de1a-4d6a-9387-f3945f203393/todos.json
Normal file
@ -0,0 +1,26 @@
|
||||
[
|
||||
{
|
||||
"id": "1",
|
||||
"status": "completed",
|
||||
"content": "创建短链映射表 share_links",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"status": "completed",
|
||||
"content": "修改分享链接生成逻辑,使用短链",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"status": "completed",
|
||||
"content": "修改导入页面,支持短链解析",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "4",
|
||||
"status": "completed",
|
||||
"content": "运行类型检查",
|
||||
"priority": "medium"
|
||||
}
|
||||
]
|
||||
@ -1,26 +1,26 @@
|
||||
[
|
||||
{
|
||||
"id": "1",
|
||||
"content": "创建短链映射表 share_links",
|
||||
"status": "completed",
|
||||
"content": "纺织厂工作台集成帮助系统",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"content": "修改分享链接生成逻辑,使用短链",
|
||||
"status": "completed",
|
||||
"content": "水洗厂工作台集成帮助系统",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"content": "修改导入页面,支持短链解析",
|
||||
"status": "completed",
|
||||
"content": "通知中心按钮适配主题色",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "4",
|
||||
"status": "completed",
|
||||
"content": "运行类型检查",
|
||||
"status": "completed",
|
||||
"priority": "medium"
|
||||
}
|
||||
]
|
||||
20
.todo/a970d80e-23c3-4e7b-bb90-4371ffda53c7/todos.json
Normal file
20
.todo/a970d80e-23c3-4e7b-bb90-4371ffda53c7/todos.json
Normal file
@ -0,0 +1,20 @@
|
||||
[
|
||||
{
|
||||
"id": "1",
|
||||
"content": "更新 AGENTS.md 文档,记录产品信息相关修改",
|
||||
"status": "completed",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"content": "更新 README.md 文档,同步产品信息名称变更",
|
||||
"status": "completed",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"content": "记录已知 Bug 和修复方式到 AGENTS.md",
|
||||
"status": "completed",
|
||||
"priority": "high"
|
||||
}
|
||||
]
|
||||
38
.todo/aa72345a-56a6-4c2f-90a6-eb0c55bfeca7/todos.json
Normal file
38
.todo/aa72345a-56a6-4c2f-90a6-eb0c55bfeca7/todos.json
Normal file
@ -0,0 +1,38 @@
|
||||
[
|
||||
{
|
||||
"id": "1",
|
||||
"content": "分析纺织厂采纱环节和原料纱仓库的数据结构",
|
||||
"status": "completed",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"content": "设计纱线自动分配逻辑和库存检查机制",
|
||||
"status": "completed",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"content": "在确认环节添加纱线选择弹窗组件",
|
||||
"status": "completed",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "4",
|
||||
"content": "实现纱线库存联动和自动分配功能",
|
||||
"status": "completed",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "5",
|
||||
"content": "添加原料不足提示和采购提醒",
|
||||
"status": "completed",
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "6",
|
||||
"content": "验证功能完整性和测试",
|
||||
"status": "completed",
|
||||
"priority": "medium"
|
||||
}
|
||||
]
|
||||
20
.todo/ca576170-aef7-48ac-b4b1-ef19f11bccca/todos.json
Normal file
20
.todo/ca576170-aef7-48ac-b4b1-ef19f11bccca/todos.json
Normal file
@ -0,0 +1,20 @@
|
||||
[
|
||||
{
|
||||
"id": "1",
|
||||
"content": "优化水洗厂 Dashboard 布局,与采购商一致",
|
||||
"status": "completed",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"content": "统一水洗厂所有页面紫色主题色",
|
||||
"status": "completed",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"content": "检查并修复其他水洗厂页面主题色",
|
||||
"status": "completed",
|
||||
"priority": "medium"
|
||||
}
|
||||
]
|
||||
26
.todo/da383ad7-73d7-45a4-9b99-5956991b4fda/todos.json
Normal file
26
.todo/da383ad7-73d7-45a4-9b99-5956991b4fda/todos.json
Normal file
@ -0,0 +1,26 @@
|
||||
[
|
||||
{
|
||||
"id": "1",
|
||||
"content": "创建 yarn_stock_records 原料纱出入库记录表",
|
||||
"status": "completed",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"content": "添加 RLS 策略和索引",
|
||||
"status": "completed",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"content": "修改 YarnAllocationModal 记录出库到 yarn_stock_records",
|
||||
"status": "completed",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "4",
|
||||
"content": "在原料纱仓库页面显示出入库记录",
|
||||
"status": "completed",
|
||||
"priority": "medium"
|
||||
}
|
||||
]
|
||||
351
AGENTS.md
351
AGENTS.md
@ -468,6 +468,23 @@ CREATE TYPE payment_status AS ENUM ('pending', 'completed');
|
||||
|
||||
## 更新日志
|
||||
|
||||
### 2025-05-28 (v1.0.3)
|
||||
- **原料纱仓库出入库记录功能**
|
||||
- 创建 `yarn_stock_records` 表,记录原料纱出入库历史
|
||||
- 采纱环节自动记录出库,关联到对应生产计划
|
||||
- 原料纱入库时自动记录入库
|
||||
- 原料纱仓库页面新增"出入库记录"标签页
|
||||
- 显示出入库类型、数量、关联计划、时间等信息
|
||||
- **采纱环节优化**
|
||||
- 添加自动/手动分配模式切换
|
||||
- 自动匹配失败时提示切换到手动分配
|
||||
- 手动模式支持输入实际使用纱线名称和重量
|
||||
- 确认时显示实际消耗统计(各纱线用量+总计)
|
||||
- 库存不足时强制阻止确认,必须先补充库存
|
||||
- **Bug 修复**
|
||||
- 修复纱线占比显示为10000%的问题
|
||||
- 修复 YarnWarehouse.tsx TypeScript 类型错误
|
||||
|
||||
### 2025-05-27 (v1.0.2)
|
||||
- **账号管理页面帮助系统集成**
|
||||
- 新增"帮助与支持"卡片区域
|
||||
@ -2234,3 +2251,337 @@ class ErrorBoundary extends React.Component<
|
||||
- [ ] 移动端模拟器测试通过
|
||||
- [ ] 类型检查通过 `pnpm run typecheck`
|
||||
- [ ] 代码规范检查通过 `pnpm run lint`
|
||||
|
||||
---
|
||||
|
||||
## 2025-05-27 产品信息功能更新记录
|
||||
|
||||
### 功能变更
|
||||
|
||||
#### 1. 产品信息页面名称统一
|
||||
**变更内容**:将"原坯布仓库"统一更名为"产品信息"
|
||||
|
||||
**涉及文件**:
|
||||
- `src/pages/purchaser/Dashboard.tsx` - 快捷操作按钮标签
|
||||
- `src/pages/purchaser/WarehouseManage.tsx` - 页面标题
|
||||
- `src/components/OnboardingGuide.tsx` - 新手指引描述
|
||||
- `src/components/UserManual.tsx` - 用户手册内容
|
||||
- `src/pages/purchaser/NewPlan.tsx` - 新建计划页面产品选择标题
|
||||
|
||||
#### 2. 产品列表折叠逻辑优化
|
||||
**变更内容**:
|
||||
- 产品无克重时,直接显示产品列表,不显示克重折叠卡片
|
||||
- 产品仅有一种克重时,直接展开显示,不需要点击折叠
|
||||
|
||||
**涉及文件**:
|
||||
- `src/components/warehouse/ProductList.tsx`
|
||||
|
||||
#### 3. 产品唯一性校验
|
||||
**新增校验规则**:
|
||||
- **产品码 (fabric_code)**:全局唯一,保存时检查是否已存在
|
||||
- **色号 (color_code)**:在"成品名称-克重-颜色"组合中唯一
|
||||
|
||||
**涉及文件**:
|
||||
- `src/pages/purchaser/WarehouseManage.tsx`
|
||||
|
||||
#### 4. 产品码自动生成优化
|
||||
**变更内容**:
|
||||
- 选择已有产品后,自动生成产品码时查询数据库中该前缀的最大数字
|
||||
- 生成规则:`前缀 + (最大数字 + 1)`
|
||||
|
||||
**涉及文件**:
|
||||
- `src/components/warehouse/ProductForm.tsx`
|
||||
|
||||
#### 5. 色号自动生成优化
|
||||
**变更内容**:
|
||||
- 选择已有产品后,自动生成色号时查询"产品名称-克重"组合下的最大色号
|
||||
- 生成规则:`(最大色号数字 + 1)`,保持两位数格式
|
||||
|
||||
**涉及文件**:
|
||||
- `src/components/warehouse/ProductForm.tsx`
|
||||
|
||||
---
|
||||
|
||||
## Bug 修复记录(2025-05-27)
|
||||
|
||||
### 分类二十一:产品唯一性校验问题
|
||||
|
||||
#### Bug: 产品码和色号重复问题
|
||||
**现象**:
|
||||
- 可以创建相同产品码的产品
|
||||
- 同一"成品名称-克重-颜色"组合下可以有重复色号
|
||||
|
||||
**根本原因**:
|
||||
- 保存产品时未进行唯一性校验
|
||||
- 自动生成产品码/色号时未查询数据库最大值
|
||||
|
||||
**修复方案**:
|
||||
```typescript
|
||||
// 1. 产品码全局唯一校验
|
||||
const { data: existingFabricCode } = await supabase
|
||||
.from('products')
|
||||
.select('id')
|
||||
.eq('company_id', auth.company.id)
|
||||
.eq('fabric_code', productData.fabric_code)
|
||||
.neq('id', editingProduct?.id || '00000000-0000-0000-0000-000000000000')
|
||||
.maybeSingle();
|
||||
|
||||
if (existingFabricCode) {
|
||||
alert(`产品码 "${productData.fabric_code}" 已存在`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 色号在"成品名称-克重-颜色"组合中唯一校验
|
||||
const { data: existingColorCode } = await supabase
|
||||
.from('products')
|
||||
.select('id')
|
||||
.eq('company_id', auth.company.id)
|
||||
.eq('product_name', productData.product_name)
|
||||
.eq('total_weight', productData.total_weight || 0)
|
||||
.eq('color', productData.color)
|
||||
.eq('color_code', productData.color_code)
|
||||
.neq('id', editingProduct?.id || '00000000-0000-0000-0000-000000000000')
|
||||
.maybeSingle();
|
||||
|
||||
if (existingColorCode) {
|
||||
alert(`色号重复`);
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
**相关文件**:
|
||||
- `src/pages/purchaser/WarehouseManage.tsx`
|
||||
|
||||
---
|
||||
|
||||
### 分类二十二:产品码/色号自动生成问题
|
||||
|
||||
#### Bug: 自动生成的产品码/色号可能重复
|
||||
**现象**:
|
||||
- 选择产品后生成的产品码可能与已有产品码重复
|
||||
- 色号生成仅基于当前产品递增,未考虑数据库中最大值
|
||||
|
||||
**根本原因**:
|
||||
- 原逻辑仅对当前选中的产品码进行简单递增(如 M2 → M3)
|
||||
- 未查询数据库中该前缀的实际最大值
|
||||
|
||||
**修复方案**:
|
||||
```typescript
|
||||
// 产品码生成:查询数据库中该前缀的最大数字
|
||||
const generateNextFabricCode = async (companyId: string, prefix: string): Promise<string> => {
|
||||
const { data } = await supabase
|
||||
.from('products')
|
||||
.select('fabric_code')
|
||||
.eq('company_id', companyId)
|
||||
.ilike('fabric_code', `${prefix}%`);
|
||||
|
||||
let maxNum = 0;
|
||||
data?.forEach(item => {
|
||||
const match = item.fabric_code.match(/^(.*?)(\d+)$/);
|
||||
if (match && match[1].toUpperCase() === prefix.toUpperCase()) {
|
||||
const num = parseInt(match[2], 10);
|
||||
if (num > maxNum) maxNum = num;
|
||||
}
|
||||
});
|
||||
|
||||
return `${prefix}${maxNum + 1}`;
|
||||
};
|
||||
|
||||
// 色号生成:查询"产品名称-克重"组合下的最大色号
|
||||
const generateNextColorCode = async (companyId: string, productName: string, totalWeight: number | null): Promise<string> => {
|
||||
const { data } = await supabase
|
||||
.from('products')
|
||||
.select('color_code')
|
||||
.eq('company_id', companyId)
|
||||
.eq('product_name', productName)
|
||||
.eq('total_weight', totalWeight || 0);
|
||||
|
||||
let maxNum = 0;
|
||||
data?.forEach(item => {
|
||||
const num = parseInt(item.color_code, 10);
|
||||
if (!isNaN(num) && num > maxNum) maxNum = num;
|
||||
});
|
||||
|
||||
const nextNum = maxNum + 1;
|
||||
return nextNum < 10 ? `0${nextNum}` : String(nextNum);
|
||||
};
|
||||
```
|
||||
|
||||
**相关文件**:
|
||||
- `src/components/warehouse/ProductForm.tsx`
|
||||
|
||||
---
|
||||
|
||||
## Bug 修复记录(2025-05-28)
|
||||
|
||||
### 分类二十三:NotificationCenter主题色缺失问题
|
||||
|
||||
#### Bug: 水洗厂工作台崩溃 - Cannot read properties of undefined (reading 'bg')
|
||||
**现象**:
|
||||
- 用户点击"水洗厂"角色进入工作台时页面白屏
|
||||
- 控制台报错:`Cannot read properties of undefined (reading 'bg')`
|
||||
- 错误位置:`NotificationCenter.tsx:289:98`
|
||||
|
||||
**根本原因**:
|
||||
- NotificationCenter组件的`themeConfig`中未定义`violet`主题
|
||||
- 水洗厂Dashboard传入`theme="violet"`,导致`themeConfig[theme]`返回undefined
|
||||
- 访问`colors.bg`时抛出TypeError
|
||||
|
||||
**修复方案**:
|
||||
```typescript
|
||||
// 1. 扩展类型定义
|
||||
interface NotificationCenterProps {
|
||||
theme?: 'amber' | 'emerald' | 'purple' | 'blue' | 'violet'; // 添加violet
|
||||
}
|
||||
|
||||
// 2. 添加violet主题配置
|
||||
const themeConfig = {
|
||||
amber: { bg: 'bg-amber-100', hover: 'hover:bg-amber-200', icon: 'text-amber-700' },
|
||||
emerald: { bg: 'bg-emerald-100', hover: 'hover:bg-emerald-200', icon: 'text-emerald-700' },
|
||||
purple: { bg: 'bg-purple-100', hover: 'hover:bg-purple-200', icon: 'text-purple-700' },
|
||||
blue: { bg: 'bg-blue-100', hover: 'hover:bg-blue-200', icon: 'text-blue-700' },
|
||||
violet: { bg: 'bg-violet-100', hover: 'hover:bg-violet-200', icon: 'text-violet-500' }, // 新增
|
||||
};
|
||||
```
|
||||
|
||||
**相关文件**:
|
||||
- `src/components/NotificationCenter.tsx`
|
||||
- `src/pages/washing/Dashboard.tsx`
|
||||
|
||||
---
|
||||
|
||||
### 分类二十四:登录页角色颜色不一致问题
|
||||
|
||||
#### Bug: 登录页水洗厂颜色与Dashboard不一致
|
||||
**现象**:
|
||||
- 登录页水洗厂演示账号按钮使用`from-cyan-500 to-blue-600`(蓝青色)
|
||||
- 水洗厂Dashboard使用`from-violet-400 to-fuchsia-400`(紫罗兰色)
|
||||
- 同一角色在不同页面颜色不统一
|
||||
|
||||
**根本原因**:
|
||||
- 登录页`demoAccounts`配置未随Dashboard主题色更新而同步调整
|
||||
- 缺乏统一的主题色管理规范
|
||||
|
||||
**修复方案**:
|
||||
```typescript
|
||||
// LoginPage.tsx
|
||||
const demoAccounts = [
|
||||
{ username: 'purchaser', label: '采购商(布行)', color: 'from-blue-500 to-blue-600', icon: Building2, enabled: true },
|
||||
{ username: 'textile', label: '纺织厂', color: 'from-emerald-500 to-green-600', icon: Factory, enabled: true },
|
||||
{ username: 'washing', label: '水洗厂', color: 'from-violet-400 to-fuchsia-400', icon: Droplets, enabled: true } // 更新为紫罗兰色
|
||||
];
|
||||
```
|
||||
|
||||
**相关文件**:
|
||||
- `src/pages/LoginPage.tsx`
|
||||
|
||||
---
|
||||
|
||||
## 学习反思与最佳实践(2025-05-28)
|
||||
|
||||
### 主题色统一设计模式
|
||||
|
||||
**问题背景**:
|
||||
三个角色(采购商、纺织厂、水洗厂)原本使用不同深度的颜色(-500/-600),视觉上不够协调统一。
|
||||
|
||||
**解决方案**:
|
||||
统一使用`-400`色阶的柔和色调:
|
||||
|
||||
| 角色 | 原配色 | 新配色 | 特点 |
|
||||
|------|--------|--------|------|
|
||||
| 采购商 | `amber-500/600` | `amber-400/orange-400` | 温暖橙黄 |
|
||||
| 纺织厂 | `emerald-500/600` | `emerald-400/teal-400` | 清新青绿 |
|
||||
| 水洗厂 | `purple-500/600` | `violet-400/fuchsia-400` | 柔和紫粉 |
|
||||
|
||||
**设计原则**:
|
||||
1. **色阶统一**:所有主题色使用`-400`色阶,保持视觉一致性
|
||||
2. **渐变搭配**:使用相邻色系的柔和渐变(如violet→fuchsia)
|
||||
3. **全站同步**:登录页、Dashboard、MemberManage等页面统一更新
|
||||
4. **组件适配**:NotificationCenter等共享组件需支持所有主题色
|
||||
|
||||
**代码规范**:
|
||||
```typescript
|
||||
// 主题色配置模板
|
||||
const themeColors = {
|
||||
purchaser: {
|
||||
primary: 'amber-400',
|
||||
gradient: 'from-amber-400 to-orange-400',
|
||||
button: 'bg-amber-400 hover:bg-amber-500',
|
||||
text: 'text-amber-500',
|
||||
bg: 'bg-amber-50',
|
||||
border: 'border-amber-200',
|
||||
ring: 'focus:ring-amber-400'
|
||||
},
|
||||
textile: {
|
||||
primary: 'emerald-400',
|
||||
gradient: 'from-emerald-400 to-teal-400',
|
||||
button: 'bg-emerald-400 hover:bg-emerald-500',
|
||||
text: 'text-emerald-500',
|
||||
bg: 'bg-emerald-50',
|
||||
border: 'border-emerald-200',
|
||||
ring: 'focus:ring-emerald-400'
|
||||
},
|
||||
washing: {
|
||||
primary: 'violet-400',
|
||||
gradient: 'from-violet-400 to-fuchsia-400',
|
||||
button: 'bg-violet-400 hover:bg-violet-500',
|
||||
text: 'text-violet-500',
|
||||
bg: 'bg-violet-50',
|
||||
border: 'border-violet-200',
|
||||
ring: 'focus:ring-violet-400'
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**经验教训**:
|
||||
1. **组件设计时考虑扩展性**:NotificationCenter等共享组件应预留主题色扩展接口
|
||||
2. **变更同步机制**:主题色变更需同步更新所有相关页面(登录页、Dashboard、设置页等)
|
||||
3. **类型安全**:TypeScript类型定义需与实际配置保持一致,避免运行时错误
|
||||
4. **视觉回归测试**:主题色调整后需验证所有页面的视觉效果
|
||||
|
||||
---
|
||||
|
||||
## 项目统计(截至2025-05-28)
|
||||
|
||||
### Bug分类统计
|
||||
|
||||
| 分类 | 数量 | 占比 |
|
||||
|------|------|------|
|
||||
| JavaScript/TypeScript作用域问题 | 1 | 4% |
|
||||
| 数据一致性问题 | 2 | 8% |
|
||||
| 空值处理问题 | 1 | 4% |
|
||||
| UI/UX问题 | 2 | 8% |
|
||||
| 代码组织问题 | 1 | 4% |
|
||||
| React组件生命周期问题 | 1 | 4% |
|
||||
| Supabase RLS策略问题 | 1 | 4% |
|
||||
| Webpack构建问题 | 2 | 8% |
|
||||
| TypeScript类型问题 | 2 | 8% |
|
||||
| 状态管理问题 | 1 | 4% |
|
||||
| 表单处理问题 | 1 | 4% |
|
||||
| CSS/Tailwind问题 | 1 | 4% |
|
||||
| 性能优化问题 | 1 | 4% |
|
||||
| 沙箱环境兼容性问题 | 1 | 4% |
|
||||
| 帮助系统集成问题 | 1 | 4% |
|
||||
| 计划拒绝状态管理问题 | 1 | 4% |
|
||||
| 通知中心主题色适配问题 | 1 | 4% |
|
||||
| 新手指引弹窗位置问题 | 1 | 4% |
|
||||
| 浏览器兼容性问题 | 1 | 4% |
|
||||
| 测试相关问题 | 1 | 4% |
|
||||
| 产品唯一性校验问题 | 1 | 4% |
|
||||
| 产品码/色号自动生成问题 | 1 | 4% |
|
||||
| NotificationCenter主题色缺失问题 | 1 | 4% |
|
||||
| 登录页角色颜色不一致问题 | 1 | 4% |
|
||||
| **总计** | **24** | **100%** |
|
||||
|
||||
### 技术债务分析
|
||||
|
||||
**高频问题类别**:
|
||||
1. **UI/UX问题**(3个):主题色适配、弹窗位置、移动端适配
|
||||
2. **TypeScript类型问题**(2个):类型定义不匹配、泛型约束
|
||||
3. **Webpack构建问题**(2个):热更新失效、产物体积过大
|
||||
|
||||
**改进建议**:
|
||||
1. 建立主题色设计系统,统一管理颜色变量
|
||||
2. 完善TypeScript严格模式配置,提前发现类型问题
|
||||
3. 优化Webpack配置,建立构建性能监控
|
||||
4. 增加组件级单元测试,覆盖主题色渲染场景
|
||||
23
README.md
23
README.md
@ -16,10 +16,11 @@
|
||||
- 实时进度可视化,支持97%完成度自动提醒
|
||||
|
||||
### 库存管理
|
||||
- **原坯布仓库**:按坯布码分组管理,入库记录追溯
|
||||
- **产品信息**:按坯布码分组管理,入库记录追溯
|
||||
- 产品管理:支持按名称、克重、颜色多级分组展示
|
||||
- 出入库历史:查看每个产品的完整库存变动记录
|
||||
- 价格历史:追踪采购价格变动
|
||||
- 产品唯一性:产品码全局唯一,色号在"成品名称-克重-颜色"组合中唯一
|
||||
- **原料纱仓库**:库存预警、最低库存提醒
|
||||
- 实时库存数据同步,入库自动更新
|
||||
|
||||
@ -212,6 +213,22 @@ import { InventoryRecordsModal } from '../../components/InventoryRecordsModal';
|
||||
|
||||
## 更新日志
|
||||
|
||||
### 2025-05-28 (v1.0.3)
|
||||
- **原料纱仓库出入库记录功能**
|
||||
- 创建 `yarn_stock_records` 表,记录原料纱出入库历史
|
||||
- 采纱环节自动记录出库,关联到对应生产计划
|
||||
- 原料纱入库时自动记录入库
|
||||
- 原料纱仓库页面新增"出入库记录"标签页
|
||||
- 显示出入库类型、数量、关联计划、时间等信息
|
||||
- **采纱环节优化**
|
||||
- 添加自动/手动分配模式切换
|
||||
- 自动匹配失败时提示切换到手动分配
|
||||
- 手动模式支持输入实际使用纱线名称和重量
|
||||
- 确认时显示实际消耗统计(各纱线用量+总计)
|
||||
- 库存不足时强制阻止确认,必须先补充库存
|
||||
- **Bug 修复**
|
||||
- 修复纱线占比显示为10000%的问题
|
||||
|
||||
### 2025-05-27 (v1.0.2)
|
||||
- **账号管理页面帮助系统集成**
|
||||
- 新增"帮助与支持"卡片区域,集成用户手册、用户反馈、关于窗口
|
||||
@ -254,7 +271,7 @@ import { InventoryRecordsModal } from '../../components/InventoryRecordsModal';
|
||||
|
||||
### 2025-05-25
|
||||
- **产品图片上传功能**
|
||||
- 为采购商原坯布仓库产品管理添加图片上传功能
|
||||
- 为采购商产品信息管理添加图片上传功能
|
||||
- 支持在产品创建和编辑时上传产品图片(JPG、PNG格式,最大5MB)
|
||||
- 产品列表展示40x40px缩略图,无图片时显示默认占位图标
|
||||
- 产品表单支持图片预览、重新上传和删除功能
|
||||
@ -265,7 +282,7 @@ import { InventoryRecordsModal } from '../../components/InventoryRecordsModal';
|
||||
|
||||
### 2025-05-24
|
||||
- **新增出入库历史功能**
|
||||
- 采购商原坯布仓库产品列表新增"出入库历史"按钮
|
||||
- 采购商产品信息列表新增"出入库历史"按钮
|
||||
- 支持查看每个产品的完整库存变动记录(批号、匹数、米数、时间)
|
||||
- 桌面端和移动端均支持
|
||||
- **类型系统优化**
|
||||
|
||||
24
migrations/20260527_114150_fix_import_plan_select_rls.sql
Normal file
24
migrations/20260527_114150_fix_import_plan_select_rls.sql
Normal file
@ -0,0 +1,24 @@
|
||||
-- ============================================
|
||||
-- 此文件由 Meoo Cloud 自动生成,请勿修改
|
||||
-- This file is auto-generated by Meoo Cloud, DO NOT MODIFY
|
||||
-- ============================================
|
||||
|
||||
|
||||
-- 修改 production_plans 的 SELECT 策略,允许分享链接导入时查询
|
||||
DROP POLICY IF EXISTS users_select_plans ON production_plans;
|
||||
|
||||
CREATE POLICY users_select_plans ON production_plans
|
||||
FOR SELECT
|
||||
USING (
|
||||
purchaser_id = get_user_master_company_id()
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM plan_factories
|
||||
WHERE plan_factories.plan_id = production_plans.id
|
||||
AND plan_factories.factory_id = get_user_master_company_id()
|
||||
)
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM plan_factories pf
|
||||
WHERE pf.plan_id = production_plans.id
|
||||
)
|
||||
);
|
||||
|
||||
38
migrations/20260527_114253_create_share_links_table.sql
Normal file
38
migrations/20260527_114253_create_share_links_table.sql
Normal file
@ -0,0 +1,38 @@
|
||||
-- ============================================
|
||||
-- 此文件由 Meoo Cloud 自动生成,请勿修改
|
||||
-- This file is auto-generated by Meoo Cloud, DO NOT MODIFY
|
||||
-- ============================================
|
||||
|
||||
|
||||
-- 创建短链映射表
|
||||
CREATE TABLE IF NOT EXISTS share_links (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
short_code TEXT UNIQUE NOT NULL,
|
||||
plan_id UUID NOT NULL,
|
||||
factory_type TEXT NOT NULL,
|
||||
created_by UUID NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
expires_at TIMESTAMPTZ,
|
||||
click_count INTEGER DEFAULT 0
|
||||
);
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX IF NOT EXISTS idx_share_links_short_code ON share_links(short_code);
|
||||
CREATE INDEX IF NOT EXISTS idx_share_links_plan_id ON share_links(plan_id);
|
||||
|
||||
-- 启用 RLS
|
||||
ALTER TABLE share_links ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- 添加 RLS 策略
|
||||
CREATE POLICY users_select_share_links ON share_links
|
||||
FOR SELECT
|
||||
USING (true);
|
||||
|
||||
CREATE POLICY users_insert_share_links ON share_links
|
||||
FOR INSERT
|
||||
WITH CHECK (created_by = auth.uid());
|
||||
|
||||
CREATE POLICY users_delete_own_share_links ON share_links
|
||||
FOR DELETE
|
||||
USING (created_by = auth.uid());
|
||||
|
||||
@ -0,0 +1,16 @@
|
||||
-- ============================================
|
||||
-- 此文件由 Meoo Cloud 自动生成,请勿修改
|
||||
-- This file is auto-generated by Meoo Cloud, DO NOT MODIFY
|
||||
-- ============================================
|
||||
|
||||
|
||||
-- 创建短链点击计数函数
|
||||
CREATE OR REPLACE FUNCTION increment_share_link_clicks(code TEXT)
|
||||
RETURNS VOID AS $$
|
||||
BEGIN
|
||||
UPDATE share_links
|
||||
SET click_count = click_count + 1
|
||||
WHERE short_code = code;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
18
migrations/20260527_114946_fix_share_links_rls.sql
Normal file
18
migrations/20260527_114946_fix_share_links_rls.sql
Normal file
@ -0,0 +1,18 @@
|
||||
-- ============================================
|
||||
-- 此文件由 Meoo Cloud 自动生成,请勿修改
|
||||
-- This file is auto-generated by Meoo Cloud, DO NOT MODIFY
|
||||
-- ============================================
|
||||
|
||||
|
||||
-- 修复 share_links 表的 RLS 策略,使用 auth.uid()
|
||||
DROP POLICY IF EXISTS users_insert_share_links ON share_links;
|
||||
DROP POLICY IF EXISTS users_delete_own_share_links ON share_links;
|
||||
|
||||
CREATE POLICY users_insert_share_links ON share_links
|
||||
FOR INSERT
|
||||
WITH CHECK (created_by = auth.uid());
|
||||
|
||||
CREATE POLICY users_delete_own_share_links ON share_links
|
||||
FOR DELETE
|
||||
USING (created_by = auth.uid());
|
||||
|
||||
28
migrations/20260527_120913_create_avatars_bucket.sql
Normal file
28
migrations/20260527_120913_create_avatars_bucket.sql
Normal file
@ -0,0 +1,28 @@
|
||||
-- ============================================
|
||||
-- 此文件由 Meoo Cloud 自动生成,请勿修改
|
||||
-- This file is auto-generated by Meoo Cloud, DO NOT MODIFY
|
||||
-- ============================================
|
||||
|
||||
|
||||
-- 创建 avatars 存储桶(如果不存在)
|
||||
INSERT INTO storage.buckets (id, name, public)
|
||||
VALUES ('avatars', 'avatars', true)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 添加 RLS 策略
|
||||
CREATE POLICY avatars_select ON storage.objects
|
||||
FOR SELECT
|
||||
USING (bucket_id = 'avatars');
|
||||
|
||||
CREATE POLICY avatars_insert ON storage.objects
|
||||
FOR INSERT
|
||||
WITH CHECK (bucket_id = 'avatars' AND auth.uid() = owner);
|
||||
|
||||
CREATE POLICY avatars_update ON storage.objects
|
||||
FOR UPDATE
|
||||
USING (bucket_id = 'avatars' AND auth.uid() = owner);
|
||||
|
||||
CREATE POLICY avatars_delete ON storage.objects
|
||||
FOR DELETE
|
||||
USING (bucket_id = 'avatars' AND auth.uid() = owner);
|
||||
|
||||
6
migrations/20260527_121728_add_image_url_to_profiles.sql
Normal file
6
migrations/20260527_121728_add_image_url_to_profiles.sql
Normal file
@ -0,0 +1,6 @@
|
||||
-- ============================================
|
||||
-- 此文件由 Meoo Cloud 自动生成,请勿修改
|
||||
-- This file is auto-generated by Meoo Cloud, DO NOT MODIFY
|
||||
-- ============================================
|
||||
|
||||
ALTER TABLE profiles ADD COLUMN image_url TEXT
|
||||
@ -0,0 +1,26 @@
|
||||
-- ============================================
|
||||
-- 此文件由 Meoo Cloud 自动生成,请勿修改
|
||||
-- This file is auto-generated by Meoo Cloud, DO NOT MODIFY
|
||||
-- ============================================
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS public.yarn_stock_records (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
company_id UUID NOT NULL REFERENCES public.companies(id) ON DELETE CASCADE,
|
||||
yarn_stock_id UUID REFERENCES public.yarn_stock(id) ON DELETE SET NULL,
|
||||
plan_id UUID REFERENCES public.production_plans(id) ON DELETE SET NULL,
|
||||
record_type TEXT NOT NULL CHECK (record_type IN ('inbound', 'outbound')),
|
||||
quantity DECIMAL NOT NULL DEFAULT 0,
|
||||
unit TEXT NOT NULL DEFAULT 'kg',
|
||||
batch_no TEXT,
|
||||
notes TEXT,
|
||||
operator_id UUID REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX IF NOT EXISTS idx_yarn_stock_records_company_id ON public.yarn_stock_records(company_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_yarn_stock_records_yarn_stock_id ON public.yarn_stock_records(yarn_stock_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_yarn_stock_records_plan_id ON public.yarn_stock_records(plan_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_yarn_stock_records_created_at ON public.yarn_stock_records(created_at DESC);
|
||||
|
||||
25
migrations/20260528_053444_add_yarn_stock_records_rls.sql
Normal file
25
migrations/20260528_053444_add_yarn_stock_records_rls.sql
Normal file
@ -0,0 +1,25 @@
|
||||
-- ============================================
|
||||
-- 此文件由 Meoo Cloud 自动生成,请勿修改
|
||||
-- This file is auto-generated by Meoo Cloud, DO NOT MODIFY
|
||||
-- ============================================
|
||||
|
||||
|
||||
-- 启用 RLS
|
||||
ALTER TABLE public.yarn_stock_records ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- SELECT 策略:本公司成员可查看
|
||||
CREATE POLICY users_select_yarn_stock_records ON public.yarn_stock_records
|
||||
FOR SELECT USING (company_id = public.get_user_master_company_id());
|
||||
|
||||
-- INSERT 策略:本公司成员可插入
|
||||
CREATE POLICY users_insert_yarn_stock_records ON public.yarn_stock_records
|
||||
FOR INSERT WITH CHECK (company_id = public.get_user_master_company_id());
|
||||
|
||||
-- UPDATE 策略:本公司成员可更新自己的记录
|
||||
CREATE POLICY users_update_yarn_stock_records ON public.yarn_stock_records
|
||||
FOR UPDATE USING (company_id = public.get_user_master_company_id());
|
||||
|
||||
-- DELETE 策略:本公司成员可删除自己的记录
|
||||
CREATE POLICY users_delete_yarn_stock_records ON public.yarn_stock_records
|
||||
FOR DELETE USING (company_id = public.get_user_master_company_id());
|
||||
|
||||
12578
package-lock.json
generated
Normal file
12578
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -30,7 +30,7 @@ const typeConfig: Record<string, { icon: any; color: string; bgColor: string }>
|
||||
};
|
||||
|
||||
interface NotificationCenterProps {
|
||||
theme?: 'amber' | 'emerald' | 'purple' | 'blue';
|
||||
theme?: 'amber' | 'emerald' | 'purple' | 'blue' | 'violet';
|
||||
}
|
||||
|
||||
const themeConfig = {
|
||||
@ -38,6 +38,7 @@ const themeConfig = {
|
||||
emerald: { bg: 'bg-emerald-100', hover: 'hover:bg-emerald-200', icon: 'text-emerald-700' },
|
||||
purple: { bg: 'bg-purple-100', hover: 'hover:bg-purple-200', icon: 'text-purple-700' },
|
||||
blue: { bg: 'bg-blue-100', hover: 'hover:bg-blue-200', icon: 'text-blue-700' },
|
||||
violet: { bg: 'bg-violet-100', hover: 'hover:bg-violet-200', icon: 'text-violet-500' },
|
||||
};
|
||||
|
||||
export function NotificationCenter({ theme = 'amber' }: NotificationCenterProps = {}) {
|
||||
@ -51,7 +52,11 @@ export function NotificationCenter({ theme = 'amber' }: NotificationCenterProps
|
||||
useEffect(() => {
|
||||
if (!auth.user) return;
|
||||
fetchNotifications();
|
||||
subscribeToNotifications();
|
||||
const channel = subscribeToNotifications();
|
||||
|
||||
return () => {
|
||||
channel.unsubscribe();
|
||||
};
|
||||
}, [auth.user]);
|
||||
|
||||
const fetchNotifications = async () => {
|
||||
@ -79,8 +84,9 @@ export function NotificationCenter({ theme = 'amber' }: NotificationCenterProps
|
||||
};
|
||||
|
||||
const subscribeToNotifications = () => {
|
||||
supabase
|
||||
.channel('notifications')
|
||||
const channel = supabase.channel('notifications');
|
||||
|
||||
channel
|
||||
.on('postgres_changes', {
|
||||
event: 'INSERT',
|
||||
schema: 'public',
|
||||
@ -89,7 +95,13 @@ export function NotificationCenter({ theme = 'amber' }: NotificationCenterProps
|
||||
}, () => {
|
||||
fetchNotifications();
|
||||
})
|
||||
.subscribe();
|
||||
.subscribe((status) => {
|
||||
if (status === 'SUBSCRIBED') {
|
||||
console.log('Successfully subscribed to notifications');
|
||||
}
|
||||
});
|
||||
|
||||
return channel;
|
||||
};
|
||||
|
||||
const markAsRead = async (id: string) => {
|
||||
|
||||
@ -39,7 +39,7 @@ const purchaserSteps: OnboardingStep[] = [
|
||||
{
|
||||
id: 'manage-inventory',
|
||||
title: '管理库存',
|
||||
description: '在"原坯布仓库"中查看和管理所有入库的坯布,支持出入库历史查询。',
|
||||
description: '在"产品信息"中查看和管理所有入库的坯布,支持出入库历史查询。',
|
||||
icon: Package,
|
||||
action: '去仓库管理',
|
||||
actionPath: '/purchaser/warehouse',
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import React, { useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Check, Clock, Loader2, Package, AlertCircle, List } from 'lucide-react';
|
||||
import type { StepType, StepStatus } from '../types';
|
||||
import { YarnAllocationModal } from './YarnAllocationModal';
|
||||
import type { StepType, StepStatus, YarnRatio } from '../types';
|
||||
|
||||
interface ProcessNode {
|
||||
id: string;
|
||||
@ -23,6 +24,9 @@ interface ProcessFlowProps {
|
||||
isPlanLocked?: boolean;
|
||||
lastInventoryTime?: string | null;
|
||||
inventoryRecords?: { time: string; quantity: number; rolls: number | null }[];
|
||||
yarnRatios?: YarnRatio[];
|
||||
companyId?: string;
|
||||
planId?: string;
|
||||
}
|
||||
|
||||
const stepTypeConfig: Record<StepType, { name: string; icon: React.ReactNode }> = {
|
||||
@ -43,7 +47,10 @@ export function ProcessFlow({
|
||||
planCompletedQuantity = 0,
|
||||
isPlanLocked = false,
|
||||
lastInventoryTime,
|
||||
inventoryRecords = []
|
||||
inventoryRecords = [],
|
||||
yarnRatios = [],
|
||||
companyId,
|
||||
planId
|
||||
}: ProcessFlowProps) {
|
||||
const [inputQuantity, setInputQuantity] = useState<string>('');
|
||||
const [inputRolls, setInputRolls] = useState<string>('');
|
||||
@ -52,6 +59,8 @@ export function ProcessFlow({
|
||||
const [pendingQuantity, setPendingQuantity] = useState<number>(0);
|
||||
const [pendingRolls, setPendingRolls] = useState<number>(0);
|
||||
const [pendingNodeId, setPendingNodeId] = useState<string>('');
|
||||
const [showYarnModal, setShowYarnModal] = useState(false);
|
||||
const [pendingYarnStepId, setPendingYarnStepId] = useState<string>('');
|
||||
|
||||
// 按顺序排序节点
|
||||
const sortedNodes = React.useMemo(() => {
|
||||
@ -76,12 +85,27 @@ export function ProcessFlow({
|
||||
// 坯布入库需要输入数量
|
||||
setShowInput(nodeId);
|
||||
setInputQuantity('');
|
||||
} else if (stepType === 'yarn_purchase') {
|
||||
// 采纱环节需要检查纱线库存
|
||||
if (yarnRatios.length > 0 && companyId) {
|
||||
setPendingYarnStepId(nodeId);
|
||||
setShowYarnModal(true);
|
||||
} else {
|
||||
// 没有纱线配比数据,直接确认
|
||||
onConfirm?.(nodeId);
|
||||
}
|
||||
} else {
|
||||
// 其他节点直接确认
|
||||
onConfirm?.(nodeId);
|
||||
}
|
||||
};
|
||||
|
||||
const handleYarnAllocationConfirm = () => {
|
||||
// 纱线分配确认后,继续确认采纱步骤
|
||||
onConfirm?.(pendingYarnStepId);
|
||||
setPendingYarnStepId('');
|
||||
};
|
||||
|
||||
const handleWarehouseConfirm = (nodeId: string) => {
|
||||
const quantity = parseFloat(inputQuantity);
|
||||
const rolls = parseInt(inputRolls) || 0;
|
||||
@ -377,6 +401,17 @@ export function ProcessFlow({
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* 纱线分配弹窗 */}
|
||||
<YarnAllocationModal
|
||||
isOpen={showYarnModal}
|
||||
onClose={() => setShowYarnModal(false)}
|
||||
onConfirm={handleYarnAllocationConfirm}
|
||||
yarnRatios={yarnRatios}
|
||||
companyId={companyId || ''}
|
||||
planTargetQuantity={planTargetQuantity}
|
||||
planId={planId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ export const ProgressBar: React.FC<ProgressBarProps> = ({ progress, status }) =>
|
||||
case 'completed':
|
||||
return 'bg-green-500';
|
||||
case 'producing':
|
||||
return 'bg-blue-500';
|
||||
return 'bg-amber-500';
|
||||
default:
|
||||
return 'bg-gray-400';
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ interface StatusBadgeProps {
|
||||
|
||||
const statusConfig: Record<string, { label: string; gradient: string }> = {
|
||||
pending: { label: '待确定', gradient: 'from-amber-400 to-orange-400' },
|
||||
producing: { label: '生产中', gradient: 'from-blue-400 to-indigo-400' },
|
||||
producing: { label: '生产中', gradient: 'from-amber-400 to-orange-400' },
|
||||
completed: { label: '已完成', gradient: 'from-green-400 to-emerald-400' }
|
||||
};
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { X, BookOpen, FileText, Download, ExternalLink, ChevronRight, Search } from 'lucide-react';
|
||||
import { X, BookOpen, FileText, Download, ExternalLink, ChevronRight, Search, ChevronLeft } from 'lucide-react';
|
||||
|
||||
interface UserManualProps {
|
||||
isOpen: boolean;
|
||||
@ -27,7 +27,7 @@ const manualChapters = [
|
||||
{ title: '创建生产计划', content: '点击"新建计划"按钮,填写产品名称、颜色、坯布码、色号、计划产量等信息,配置纱线配比后提交。' },
|
||||
{ title: '分享计划', content: '在计划总览页面,点击"分享计划"按钮,通过二次确认后将链接发送给纺织厂。纺织厂确认后即建立关联。' },
|
||||
{ title: '查看进度', content: '在计划总览页面实时查看各工厂的生产进度,包括当前环节、完成米数、入库记录等。' },
|
||||
{ title: '库存管理', content: '在"原坯布仓库"中管理所有入库的坯布,支持按产品名称、克重、颜色分组查看,可查询出入库历史。' },
|
||||
{ title: '库存管理', content: '在"产品信息"中管理所有入库的坯布,支持按产品名称、克重、颜色分组查看,可查询出入库历史。' },
|
||||
{ title: '结款管理', content: '生产完成后,在"待结款"页面查看应付款项,确认后完成结款流程。' },
|
||||
]
|
||||
},
|
||||
@ -70,6 +70,7 @@ export function UserManual({ isOpen, onClose }: UserManualProps) {
|
||||
const [activeChapter, setActiveChapter] = useState(manualChapters[0]);
|
||||
const [expandedSection, setExpandedSection] = useState<number | null>(0);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [showChapterList, setShowChapterList] = useState(true);
|
||||
|
||||
// 搜索过滤
|
||||
const filteredChapters = searchQuery
|
||||
@ -82,46 +83,57 @@ export function UserManual({ isOpen, onClose }: UserManualProps) {
|
||||
})).filter(ch => ch.sections.length > 0)
|
||||
: manualChapters;
|
||||
|
||||
const handleChapterSelect = (chapter: typeof manualChapters[0]) => {
|
||||
setActiveChapter(chapter);
|
||||
setExpandedSection(0);
|
||||
setShowChapterList(false);
|
||||
};
|
||||
|
||||
const handleBackToChapters = () => {
|
||||
setShowChapterList(true);
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-3 sm:p-4">
|
||||
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-0 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 w-full max-w-4xl max-h-[85vh] overflow-hidden shadow-2xl"
|
||||
className="bg-white w-full h-full sm:h-auto sm:max-h-[90vh] sm:rounded-2xl sm:max-w-4xl overflow-hidden shadow-2xl"
|
||||
>
|
||||
{/* 头部 */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-100">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-blue-100 rounded-xl flex items-center justify-center">
|
||||
<BookOpen className="w-5 h-5 text-blue-600" />
|
||||
<div className="flex items-center justify-between p-3 sm:p-4 border-b border-gray-100">
|
||||
<div className="flex items-center gap-2 sm:gap-3">
|
||||
<div className="w-8 h-8 sm:w-10 sm:h-10 bg-blue-100 rounded-xl flex items-center justify-center">
|
||||
<BookOpen className="w-4 h-4 sm:w-5 sm:h-5 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-900">用户手册</h2>
|
||||
<p className="text-xs text-gray-500">详细的使用指南和操作说明</p>
|
||||
<h2 className="text-base sm:text-lg font-semibold text-gray-900">用户手册</h2>
|
||||
<p className="text-xs text-gray-500 hidden sm:block">详细的使用指南和操作说明</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1 sm:gap-2">
|
||||
<button
|
||||
onClick={() => window.open('/manual.pdf', '_blank')}
|
||||
className="flex items-center gap-2 px-3 py-2 text-sm text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
|
||||
className="flex items-center gap-1 sm:gap-2 px-2 sm:px-3 py-1.5 sm:py-2 text-xs sm:text-sm text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
下载PDF
|
||||
<Download className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
|
||||
<span className="hidden sm:inline">下载PDF</span>
|
||||
<span className="sm:hidden">下载</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
||||
className="p-1.5 sm:p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
||||
>
|
||||
<X className="w-5 h-5 text-gray-500" />
|
||||
<X className="w-4 h-4 sm:w-5 sm:h-5 text-gray-500" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 搜索栏 */}
|
||||
<div className="px-4 py-3 border-b border-gray-100 bg-gray-50">
|
||||
<div className="px-3 sm:px-4 py-2 sm:py-3 border-b border-gray-100 bg-gray-50">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||
<input
|
||||
@ -134,45 +146,89 @@ export function UserManual({ isOpen, onClose }: UserManualProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 移动端章节导航栏 */}
|
||||
{!showChapterList && (
|
||||
<div className="sm:hidden flex items-center gap-2 px-3 py-2 border-b border-gray-100 bg-gray-50">
|
||||
<button
|
||||
onClick={handleBackToChapters}
|
||||
className="flex items-center gap-1 text-sm text-blue-600 hover:text-blue-700"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
返回目录
|
||||
</button>
|
||||
<span className="text-gray-400">|</span>
|
||||
<span className="text-sm font-medium text-gray-700 truncate">{activeChapter.title}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 内容区域 */}
|
||||
<div className="flex h-[60vh]">
|
||||
{/* 左侧导航 */}
|
||||
<div className="w-64 border-r border-gray-100 overflow-y-auto bg-gray-50/50">
|
||||
<div className="flex h-[calc(100vh-140px)] sm:h-[60vh]">
|
||||
{/* 左侧章节列表 - 移动端全屏显示,桌面端固定宽度 */}
|
||||
<AnimatePresence mode="wait">
|
||||
{(showChapterList || window.innerWidth >= 640) && (
|
||||
<motion.div
|
||||
initial={{ x: -100, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{ x: -100, opacity: 0 }}
|
||||
className={`${showChapterList ? 'flex' : 'hidden sm:flex'} w-full sm:w-64 border-r border-gray-100 overflow-y-auto bg-gray-50/50 flex-col`}
|
||||
>
|
||||
<div className="p-2 space-y-1">
|
||||
{filteredChapters.map((chapter) => (
|
||||
<button
|
||||
key={chapter.id}
|
||||
onClick={() => {
|
||||
setActiveChapter(chapter);
|
||||
setExpandedSection(0);
|
||||
}}
|
||||
className={`w-full flex items-center gap-3 px-4 py-3 text-left transition-colors ${
|
||||
onClick={() => handleChapterSelect(chapter)}
|
||||
className={`w-full flex items-center gap-3 px-3 py-3 text-left transition-colors rounded-xl ${
|
||||
activeChapter.id === chapter.id
|
||||
? 'bg-blue-50 text-blue-700 border-r-2 border-blue-500'
|
||||
? 'bg-blue-50 text-blue-700 border-l-4 border-blue-500'
|
||||
: 'text-gray-700 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
<chapter.icon className={`w-4 h-4 ${activeChapter.id === chapter.id ? 'text-blue-600' : 'text-gray-400'}`} />
|
||||
<span className="text-sm font-medium">{chapter.title}</span>
|
||||
<chapter.icon className={`w-5 h-5 shrink-0 ${activeChapter.id === chapter.id ? 'text-blue-600' : 'text-gray-400'}`} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<span className="text-sm font-medium block">{chapter.title}</span>
|
||||
<span className="text-xs text-gray-400 block mt-0.5">{chapter.sections.length} 个章节</span>
|
||||
</div>
|
||||
<ChevronRight className={`w-4 h-4 shrink-0 ${activeChapter.id === chapter.id ? 'text-blue-500' : 'text-gray-300'}`} />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* 右侧内容 */}
|
||||
<div className="flex-1 overflow-y-auto p-6">
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-4">{activeChapter.title}</h3>
|
||||
<div className="space-y-3">
|
||||
{/* 右侧内容详情 */}
|
||||
<div className={`${!showChapterList ? 'flex' : 'hidden sm:flex'} flex-1 overflow-y-auto bg-white flex-col w-full`}>
|
||||
<div className="p-4 sm:p-6">
|
||||
{/* 章节标题 */}
|
||||
<div className="flex items-center gap-3 mb-4 sm:mb-6">
|
||||
<div className="w-10 h-10 sm:w-12 sm:h-12 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-xl flex items-center justify-center">
|
||||
<activeChapter.icon className="w-5 h-5 sm:w-6 sm:h-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg sm:text-xl font-bold text-gray-900">{activeChapter.title}</h3>
|
||||
<p className="text-xs sm:text-sm text-gray-500">{activeChapter.sections.length} 个操作指南</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 章节内容列表 */}
|
||||
<div className="space-y-2 sm:space-y-3">
|
||||
{activeChapter.sections.map((section, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="border border-gray-200 rounded-xl overflow-hidden"
|
||||
className="border border-gray-200 rounded-xl overflow-hidden bg-white shadow-sm"
|
||||
>
|
||||
<button
|
||||
onClick={() => setExpandedSection(expandedSection === index ? null : index)}
|
||||
className="w-full flex items-center justify-between p-4 text-left hover:bg-gray-50 transition-colors"
|
||||
className="w-full flex items-center justify-between p-3 sm:p-4 text-left hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<span className="font-medium text-gray-900">{section.title}</span>
|
||||
<div className="flex items-center gap-2 sm:gap-3">
|
||||
<span className="w-5 h-5 sm:w-6 sm:h-6 bg-gray-100 text-gray-600 rounded-full flex items-center justify-center text-xs sm:text-sm font-medium shrink-0">
|
||||
{index + 1}
|
||||
</span>
|
||||
<span className="font-medium text-gray-900 text-sm sm:text-base">{section.title}</span>
|
||||
</div>
|
||||
<ChevronRight
|
||||
className={`w-4 h-4 text-gray-400 transition-transform ${
|
||||
className={`w-4 h-4 sm:w-5 sm:h-5 text-gray-400 transition-transform shrink-0 ${
|
||||
expandedSection === index ? 'rotate-90' : ''
|
||||
}`}
|
||||
/>
|
||||
@ -180,12 +236,13 @@ export function UserManual({ isOpen, onClose }: UserManualProps) {
|
||||
<AnimatePresence>
|
||||
{expandedSection === index && (
|
||||
<motion.div
|
||||
initial={{ height: 0 }}
|
||||
animate={{ height: 'auto' }}
|
||||
exit={{ height: 0 }}
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<div className="p-4 pt-0 text-sm text-gray-600 leading-relaxed border-t border-gray-100">
|
||||
<div className="p-3 sm:p-4 pt-0 text-sm text-gray-600 leading-relaxed border-t border-gray-100 bg-gray-50/30">
|
||||
{section.content}
|
||||
</div>
|
||||
</motion.div>
|
||||
@ -196,6 +253,7 @@ export function UserManual({ isOpen, onClose }: UserManualProps) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -164,26 +164,9 @@ export function VersionUpdate({ isOpen, onClose, onUpdate }: VersionUpdateProps)
|
||||
<div className="flex gap-3 pt-2">
|
||||
<button
|
||||
onClick={handleSkip}
|
||||
className="flex-1 py-2.5 border border-gray-300 rounded-xl text-gray-700 hover:bg-gray-50 transition-colors text-sm font-medium"
|
||||
className="flex-1 py-2.5 bg-blue-600 hover:bg-blue-700 text-white rounded-xl transition-colors text-sm font-medium"
|
||||
>
|
||||
稍后更新
|
||||
</button>
|
||||
<button
|
||||
onClick={handleUpdate}
|
||||
disabled={isUpdating}
|
||||
className="flex-1 py-2.5 bg-blue-600 hover:bg-blue-700 text-white rounded-xl transition-colors text-sm font-medium flex items-center justify-center gap-2 disabled:opacity-50"
|
||||
>
|
||||
{isUpdating ? (
|
||||
<>
|
||||
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||
更新中...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="w-4 h-4" />
|
||||
立即更新
|
||||
</>
|
||||
)}
|
||||
知道了
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
|
||||
564
src/components/YarnAllocationModal.tsx
Normal file
564
src/components/YarnAllocationModal.tsx
Normal file
@ -0,0 +1,564 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { X, AlertTriangle, Package, Check, Loader2, Edit3, Search } from 'lucide-react';
|
||||
import { supabase } from '../supabase/client';
|
||||
import type { YarnRatio, YarnStock } from '../types';
|
||||
|
||||
interface YarnAllocation {
|
||||
yarnRatio: YarnRatio;
|
||||
allocatedYarn: YarnStock | null;
|
||||
requiredQuantity: number;
|
||||
availableQuantity: number;
|
||||
isSufficient: boolean;
|
||||
manualName?: string;
|
||||
manualQuantity?: number;
|
||||
}
|
||||
|
||||
interface YarnAllocationModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: (allocations: YarnAllocation[]) => void;
|
||||
yarnRatios: YarnRatio[];
|
||||
companyId: string;
|
||||
planTargetQuantity: number;
|
||||
planId?: string;
|
||||
}
|
||||
|
||||
export function YarnAllocationModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
onConfirm,
|
||||
yarnRatios,
|
||||
companyId,
|
||||
planTargetQuantity,
|
||||
planId
|
||||
}: YarnAllocationModalProps) {
|
||||
const [yarnStock, setYarnStock] = useState<YarnStock[]>([]);
|
||||
const [allocations, setAllocations] = useState<YarnAllocation[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [confirming, setConfirming] = useState(false);
|
||||
const [isManualMode, setIsManualMode] = useState(false);
|
||||
const [autoMatchFailed, setAutoMatchFailed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && companyId) {
|
||||
fetchYarnStock();
|
||||
}
|
||||
}, [isOpen, companyId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (yarnStock.length > 0 && yarnRatios.length > 0) {
|
||||
calculateAllocations();
|
||||
}
|
||||
}, [yarnStock, yarnRatios]);
|
||||
|
||||
const fetchYarnStock = async () => {
|
||||
setLoading(true);
|
||||
const { data } = await supabase
|
||||
.from('yarn_stock')
|
||||
.select('*')
|
||||
.eq('company_id', companyId)
|
||||
.order('name');
|
||||
|
||||
setYarnStock(data || []);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const calculateAllocations = () => {
|
||||
const newAllocations: YarnAllocation[] = yarnRatios.map(ratio => {
|
||||
const requiredQuantity = ratio.total_amount || 0;
|
||||
|
||||
// 尝试匹配相同名称的纱线
|
||||
const matchingYarn = yarnStock.find(yarn =>
|
||||
yarn.name.toLowerCase().includes(ratio.yarn_name.toLowerCase()) ||
|
||||
ratio.yarn_name.toLowerCase().includes(yarn.name.toLowerCase())
|
||||
);
|
||||
|
||||
const availableQuantity = matchingYarn?.quantity || 0;
|
||||
|
||||
return {
|
||||
yarnRatio: ratio,
|
||||
allocatedYarn: matchingYarn || null,
|
||||
requiredQuantity,
|
||||
availableQuantity,
|
||||
isSufficient: availableQuantity >= requiredQuantity,
|
||||
manualName: ratio.yarn_name,
|
||||
manualQuantity: requiredQuantity
|
||||
};
|
||||
});
|
||||
|
||||
setAllocations(newAllocations);
|
||||
|
||||
// 检查是否有自动匹配失败的情况
|
||||
const hasUnmatched = newAllocations.some(a => !a.allocatedYarn);
|
||||
const hasInsufficient = newAllocations.some(a => a.allocatedYarn && !a.isSufficient);
|
||||
setAutoMatchFailed(hasUnmatched || hasInsufficient);
|
||||
};
|
||||
|
||||
const handleYarnSelect = (ratioId: string, yarnId: string) => {
|
||||
const selectedYarn = yarnStock.find(y => y.id === yarnId);
|
||||
if (!selectedYarn) return;
|
||||
|
||||
setAllocations(prev => prev.map(alloc => {
|
||||
if (alloc.yarnRatio.id === ratioId) {
|
||||
return {
|
||||
...alloc,
|
||||
allocatedYarn: selectedYarn,
|
||||
availableQuantity: selectedYarn.quantity,
|
||||
isSufficient: selectedYarn.quantity >= alloc.requiredQuantity
|
||||
};
|
||||
}
|
||||
return alloc;
|
||||
}));
|
||||
};
|
||||
|
||||
const handleManualNameChange = (ratioId: string, name: string) => {
|
||||
setAllocations(prev => prev.map(alloc => {
|
||||
if (alloc.yarnRatio.id === ratioId) {
|
||||
return { ...alloc, manualName: name };
|
||||
}
|
||||
return alloc;
|
||||
}));
|
||||
};
|
||||
|
||||
const handleManualQuantityChange = (ratioId: string, quantity: string) => {
|
||||
const numQuantity = parseFloat(quantity) || 0;
|
||||
setAllocations(prev => prev.map(alloc => {
|
||||
if (alloc.yarnRatio.id === ratioId) {
|
||||
return { ...alloc, manualQuantity: numQuantity };
|
||||
}
|
||||
return alloc;
|
||||
}));
|
||||
};
|
||||
|
||||
const handleConfirm = async () => {
|
||||
if (isManualMode) {
|
||||
// 手动模式:验证输入
|
||||
const emptyName = allocations.filter(a => !a.manualName?.trim());
|
||||
if (emptyName.length > 0) {
|
||||
alert('请为所有纱线输入名称');
|
||||
return;
|
||||
}
|
||||
|
||||
const emptyQuantity = allocations.filter(a => !a.manualQuantity || a.manualQuantity <= 0);
|
||||
if (emptyQuantity.length > 0) {
|
||||
alert('请为所有纱线输入有效的重量');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// 自动模式:检查是否所有纱线都有分配
|
||||
const unallocated = allocations.filter(a => !a.allocatedYarn);
|
||||
if (unallocated.length > 0) {
|
||||
alert(`请为以下纱线选择库存:${unallocated.map(a => a.yarnRatio.yarn_name).join(', ')}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查库存是否足够
|
||||
const insufficient = allocations.filter(a => !a.isSufficient);
|
||||
if (insufficient.length > 0) {
|
||||
alert(
|
||||
`以下纱线库存不足,无法确认采纱:\n${insufficient.map(a =>
|
||||
`${a.yarnRatio.yarn_name}: 需要 ${a.requiredQuantity}kg,库存 ${a.availableQuantity}kg`
|
||||
).join('\n')}\n\n请切换到手动分配模式或补充库存后再确认。`
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 计算实际消耗总量
|
||||
const totalConsumption = isManualMode
|
||||
? allocations.reduce((sum, a) => sum + (a.manualQuantity || 0), 0)
|
||||
: allocations.reduce((sum, a) => sum + a.requiredQuantity, 0);
|
||||
|
||||
// 显示确认对话框,包含实际消耗统计
|
||||
const confirmMessage = isManualMode
|
||||
? `确认采纱完成?\n\n实际消耗统计:\n${allocations.map(a =>
|
||||
`${a.manualName}: ${a.manualQuantity}kg`
|
||||
).join('\n')}\n\n总计消耗: ${totalConsumption}kg`
|
||||
: `确认采纱完成?\n\n实际消耗统计:\n${allocations.map(a =>
|
||||
`${a.yarnRatio.yarn_name}: ${a.requiredQuantity}kg`
|
||||
).join('\n')}\n\n总计消耗: ${totalConsumption}kg`;
|
||||
|
||||
if (!window.confirm(confirmMessage)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setConfirming(true);
|
||||
|
||||
try {
|
||||
// 获取当前用户ID
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
const operatorId = user?.id;
|
||||
|
||||
if (isManualMode) {
|
||||
// 手动模式:在仓库中查找或创建纱线并扣除库存
|
||||
for (const alloc of allocations) {
|
||||
const manualName = alloc.manualName!;
|
||||
const manualQty = alloc.manualQuantity!;
|
||||
|
||||
// 查找是否已有同名纱线
|
||||
const existingYarn = yarnStock.find(y =>
|
||||
y.name.toLowerCase() === manualName.toLowerCase()
|
||||
);
|
||||
|
||||
if (existingYarn) {
|
||||
// 扣除现有纱线库存
|
||||
if (existingYarn.quantity >= manualQty) {
|
||||
const newQuantity = existingYarn.quantity - manualQty;
|
||||
await supabase
|
||||
.from('yarn_stock')
|
||||
.update({ quantity: newQuantity })
|
||||
.eq('id', existingYarn.id);
|
||||
|
||||
// 记录出库
|
||||
await supabase.from('yarn_stock_records').insert({
|
||||
company_id: companyId,
|
||||
yarn_stock_id: existingYarn.id,
|
||||
plan_id: planId || null,
|
||||
record_type: 'outbound',
|
||||
quantity: manualQty,
|
||||
unit: 'kg',
|
||||
notes: `采纱出库 - 计划: ${planId || '未知'}`,
|
||||
operator_id: operatorId
|
||||
});
|
||||
} else {
|
||||
alert(`纱线 "${manualName}" 库存不足,需要 ${manualQty}kg,库存 ${existingYarn.quantity}kg`);
|
||||
setConfirming(false);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// 创建新纱线记录并扣除(负库存表示已使用但未入库)
|
||||
const { data: newYarn } = await supabase
|
||||
.from('yarn_stock')
|
||||
.insert({
|
||||
company_id: companyId,
|
||||
name: manualName,
|
||||
quantity: -manualQty,
|
||||
unit: 'kg',
|
||||
min_stock: 0
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
// 记录出库
|
||||
if (newYarn) {
|
||||
await supabase.from('yarn_stock_records').insert({
|
||||
company_id: companyId,
|
||||
yarn_stock_id: newYarn.id,
|
||||
plan_id: planId || null,
|
||||
record_type: 'outbound',
|
||||
quantity: manualQty,
|
||||
unit: 'kg',
|
||||
notes: `采纱出库(新纱线) - 计划: ${planId || '未知'}`,
|
||||
operator_id: operatorId
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 自动模式:扣除库存
|
||||
for (const alloc of allocations) {
|
||||
if (alloc.allocatedYarn && alloc.isSufficient) {
|
||||
const newQuantity = alloc.allocatedYarn.quantity - alloc.requiredQuantity;
|
||||
await supabase
|
||||
.from('yarn_stock')
|
||||
.update({ quantity: newQuantity })
|
||||
.eq('id', alloc.allocatedYarn.id);
|
||||
|
||||
// 记录出库
|
||||
await supabase.from('yarn_stock_records').insert({
|
||||
company_id: companyId,
|
||||
yarn_stock_id: alloc.allocatedYarn.id,
|
||||
plan_id: planId || null,
|
||||
record_type: 'outbound',
|
||||
quantity: alloc.requiredQuantity,
|
||||
unit: 'kg',
|
||||
notes: `采纱出库 - 计划: ${planId || '未知'}`,
|
||||
operator_id: operatorId
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onConfirm(allocations);
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error('库存扣减失败:', error);
|
||||
alert('库存扣减失败,请重试');
|
||||
} finally {
|
||||
setConfirming(false);
|
||||
}
|
||||
};
|
||||
|
||||
const hasInsufficientYarn = allocations.some(a => !a.isSufficient && !isManualMode);
|
||||
const hasUnallocatedYarn = allocations.some(a => !a.allocatedYarn && !isManualMode);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4"
|
||||
onClick={onClose}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ scale: 0.95, opacity: 0, y: 20 }}
|
||||
animate={{ scale: 1, opacity: 1, y: 0 }}
|
||||
exit={{ scale: 0.95, opacity: 0, y: 20 }}
|
||||
className="bg-white rounded-2xl w-full max-w-lg shadow-xl overflow-hidden"
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
{/* 头部 */}
|
||||
<div className="bg-gradient-to-r from-emerald-500 to-teal-500 p-4 text-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Package className="w-5 h-5" />
|
||||
<h2 className="text-lg font-semibold">
|
||||
{isManualMode ? '手动纱线分配' : '纱线分配确认'}
|
||||
</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 hover:bg-white/20 rounded-lg transition-colors"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-sm text-white/80 mt-1">
|
||||
计划产量: {planTargetQuantity}米 | {isManualMode ? '请手动输入纱线信息' : '请确认纱线库存分配'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 模式切换按钮 */}
|
||||
{!loading && allocations.length > 0 && (
|
||||
<div className="px-4 pt-4">
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setIsManualMode(false)}
|
||||
className={`flex-1 py-2 px-3 rounded-lg text-sm font-medium transition-colors flex items-center justify-center gap-2 ${
|
||||
!isManualMode
|
||||
? 'bg-emerald-100 text-emerald-700 border border-emerald-300'
|
||||
: 'bg-gray-100 text-gray-600 border border-gray-200'
|
||||
}`}
|
||||
>
|
||||
<Search className="w-4 h-4" />
|
||||
自动匹配
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setIsManualMode(true)}
|
||||
className={`flex-1 py-2 px-3 rounded-lg text-sm font-medium transition-colors flex items-center justify-center gap-2 ${
|
||||
isManualMode
|
||||
? 'bg-blue-100 text-blue-700 border border-blue-300'
|
||||
: 'bg-gray-100 text-gray-600 border border-gray-200'
|
||||
}`}
|
||||
>
|
||||
<Edit3 className="w-4 h-4" />
|
||||
手动分配
|
||||
</button>
|
||||
</div>
|
||||
{autoMatchFailed && !isManualMode && (
|
||||
<div className="mt-2 p-2 bg-amber-50 border border-amber-200 rounded-lg">
|
||||
<p className="text-xs text-amber-700 flex items-center gap-1">
|
||||
<AlertTriangle className="w-3 h-3" />
|
||||
自动匹配失败,建议切换到手动分配模式
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 内容 */}
|
||||
<div className="p-4 max-h-[60vh] overflow-y-auto">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="w-6 h-6 animate-spin text-emerald-500" />
|
||||
<span className="ml-2 text-gray-500">加载库存数据...</span>
|
||||
</div>
|
||||
) : allocations.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">
|
||||
<AlertTriangle className="w-12 h-12 mx-auto mb-3 text-amber-400" />
|
||||
<p>该计划没有纱线配比数据</p>
|
||||
</div>
|
||||
) : isManualMode ? (
|
||||
/* 手动分配模式 */
|
||||
<div className="space-y-3">
|
||||
{allocations.map((alloc, index) => (
|
||||
<div
|
||||
key={alloc.yarnRatio.id}
|
||||
className="border border-blue-200 bg-blue-50/30 rounded-xl p-3"
|
||||
>
|
||||
<div className="mb-2">
|
||||
<h3 className="font-medium text-gray-800">
|
||||
{index + 1}. 所需纱线
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500">
|
||||
计划配比: {alloc.yarnRatio.yarn_name} |
|
||||
计划用量: {alloc.requiredQuantity}kg
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 手动输入纱线名称 */}
|
||||
<div className="mt-2">
|
||||
<label className="text-xs text-gray-600 mb-1 block font-medium">
|
||||
实际使用纱线名称
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={alloc.manualName || ''}
|
||||
onChange={(e) => handleManualNameChange(alloc.yarnRatio.id, e.target.value)}
|
||||
placeholder="输入纱线名称"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 手动输入重量 */}
|
||||
<div className="mt-2">
|
||||
<label className="text-xs text-gray-600 mb-1 block font-medium">
|
||||
实际使用重量 (kg)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={alloc.manualQuantity || ''}
|
||||
onChange={(e) => handleManualQuantityChange(alloc.yarnRatio.id, e.target.value)}
|
||||
placeholder="输入重量"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
/* 自动分配模式 */
|
||||
<div className="space-y-3">
|
||||
{allocations.map((alloc, index) => (
|
||||
<div
|
||||
key={alloc.yarnRatio.id}
|
||||
className={`border rounded-xl p-3 ${
|
||||
alloc.isSufficient
|
||||
? 'border-emerald-200 bg-emerald-50/30'
|
||||
: alloc.allocatedYarn
|
||||
? 'border-red-200 bg-red-50/30'
|
||||
: 'border-amber-200 bg-amber-50/30'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div>
|
||||
<h3 className="font-medium text-gray-800">
|
||||
{index + 1}. {alloc.yarnRatio.yarn_name}
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500">
|
||||
配比: {Number(alloc.yarnRatio.ratio).toFixed(0)}% |
|
||||
每米用量: {alloc.yarnRatio.amount_per_meter}g/m |
|
||||
总需: {alloc.requiredQuantity}kg
|
||||
</p>
|
||||
</div>
|
||||
{!alloc.allocatedYarn ? (
|
||||
<span className="text-xs bg-amber-100 text-amber-600 px-2 py-0.5 rounded-full">
|
||||
未匹配
|
||||
</span>
|
||||
) : !alloc.isSufficient ? (
|
||||
<span className="text-xs bg-red-100 text-red-600 px-2 py-0.5 rounded-full">
|
||||
库存不足
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-xs bg-emerald-100 text-emerald-600 px-2 py-0.5 rounded-full">
|
||||
匹配成功
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 纱线选择 */}
|
||||
<div className="mt-2">
|
||||
<label className="text-xs text-gray-500 mb-1 block">
|
||||
选择库存纱线
|
||||
</label>
|
||||
<select
|
||||
value={alloc.allocatedYarn?.id || ''}
|
||||
onChange={(e) => handleYarnSelect(alloc.yarnRatio.id, e.target.value)}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500"
|
||||
>
|
||||
<option value="">请选择纱线...</option>
|
||||
{yarnStock.map(yarn => (
|
||||
<option key={yarn.id} value={yarn.id}>
|
||||
{yarn.name} {yarn.spec ? `(${yarn.spec})` : ''} -
|
||||
库存: {yarn.quantity}kg
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* 库存状态 */}
|
||||
{alloc.allocatedYarn && (
|
||||
<div className="mt-2 flex items-center gap-2 text-xs">
|
||||
<span className={alloc.isSufficient ? 'text-emerald-600' : 'text-red-600'}>
|
||||
{alloc.isSufficient ? (
|
||||
<span className="flex items-center gap-1">
|
||||
<Check className="w-3 h-3" />
|
||||
库存充足 ({alloc.availableQuantity}kg)
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center gap-1">
|
||||
<AlertTriangle className="w-3 h-3" />
|
||||
库存不足 (缺 {alloc.requiredQuantity - alloc.availableQuantity}kg)
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 提示信息 */}
|
||||
{hasInsufficientYarn && !isManualMode && (
|
||||
<div className="mt-4 p-3 bg-amber-50 border border-amber-200 rounded-lg">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertTriangle className="w-4 h-4 text-amber-600 flex-shrink-0 mt-0.5" />
|
||||
<p className="text-xs text-amber-700">
|
||||
部分纱线库存不足或匹配失败,建议切换到手动分配模式。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 底部按钮 */}
|
||||
<div className="p-4 border-t border-gray-100 flex gap-3">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="flex-1 py-2.5 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors text-sm font-medium"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<motion.button
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onClick={handleConfirm}
|
||||
disabled={confirming || allocations.length === 0}
|
||||
className={`flex-1 py-2.5 text-white rounded-lg disabled:opacity-50 disabled:cursor-not-allowed transition-colors text-sm font-medium flex items-center justify-center gap-2 ${
|
||||
isManualMode
|
||||
? 'bg-blue-500 hover:bg-blue-600'
|
||||
: 'bg-emerald-500 hover:bg-emerald-600'
|
||||
}`}
|
||||
>
|
||||
{confirming ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
确认中...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Check className="w-4 h-4" />
|
||||
{isManualMode ? '确认手动分配' : '确认采纱'}
|
||||
</>
|
||||
)}
|
||||
</motion.button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
@ -352,7 +352,7 @@ function PlanCard({ plan, isExpanded, onToggle, onShare, onPlanUpdated, operator
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{plan.yarn_ratios.map((yarn, index) => (
|
||||
<span key={index} className="inline-flex items-center px-2 py-1 bg-blue-50 text-blue-700 text-xs rounded-lg">
|
||||
{yarnTypeLabels[yarn.yarn_type || 'all']}/{yarn.yarn_name}:{yarn.ratio}%
|
||||
{yarnTypeLabels[yarn.yarn_type || 'all']}/{yarn.yarn_name}:{Number(yarn.ratio).toFixed(0)}%
|
||||
{yarn.amount_per_meter && `(${yarn.amount_per_meter}g/m)`}
|
||||
</span>
|
||||
))}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import React, { useState, useCallback, useEffect, useRef } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Share2, Copy, Check, X, AlertTriangle, Building2 } from 'lucide-react';
|
||||
import { useResponsive } from '../../hooks/useResponsive';
|
||||
import { encryptShareParams } from '../../utils/shareCrypto';
|
||||
import { encryptShareParamsSync } from '../../utils/shareCrypto';
|
||||
import { supabase } from '../../supabase/client';
|
||||
|
||||
interface ShareModalProps {
|
||||
@ -21,6 +21,8 @@ export function ShareModal({ isOpen, onClose, planId, factoryType, factoryName }
|
||||
const [confirmFactoryName, setConfirmFactoryName] = useState('');
|
||||
const [confirmCompanyName, setConfirmCompanyName] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [shortCode, setShortCode] = useState<string>('');
|
||||
const shortLinkGenerated = useRef(false);
|
||||
|
||||
// 获取当前用户的公司名称
|
||||
useEffect(() => {
|
||||
@ -46,14 +48,61 @@ export function ShareModal({ isOpen, onClose, planId, factoryType, factoryName }
|
||||
};
|
||||
if (isOpen) {
|
||||
fetchCompanyName();
|
||||
// 生成短链
|
||||
if (!shortLinkGenerated.current) {
|
||||
shortLinkGenerated.current = true;
|
||||
generateShortLink();
|
||||
}
|
||||
} else {
|
||||
// 弹窗关闭时重置
|
||||
shortLinkGenerated.current = false;
|
||||
setShortCode('');
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
// 生成短链
|
||||
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]);
|
||||
|
||||
// 生成分享链接
|
||||
const generateShareLink = useCallback(() => {
|
||||
const baseUrl = window.location.origin + window.location.pathname;
|
||||
const token = encryptShareParams(planId, factoryType);
|
||||
if (shortCode) {
|
||||
return `${baseUrl}#/import?s=${shortCode}`;
|
||||
}
|
||||
// 如果短链还未生成,使用长链接
|
||||
const token = encryptShareParamsSync(planId, factoryType);
|
||||
return `${baseUrl}#/import?token=${token}`;
|
||||
}, [planId, factoryType]);
|
||||
}, [planId, factoryType, shortCode]);
|
||||
|
||||
const handleCopy = useCallback(async () => {
|
||||
const link = generateShareLink();
|
||||
@ -130,12 +179,6 @@ export function ShareModal({ isOpen, onClose, planId, factoryType, factoryName }
|
||||
>
|
||||
关闭
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowConfirm(true)}
|
||||
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>
|
||||
<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 ${
|
||||
|
||||
@ -209,34 +209,108 @@ export function ProductForm({
|
||||
}
|
||||
};
|
||||
|
||||
// 生成递增的产品码(如 M2 → M3)
|
||||
const generateNextFabricCode = (currentCode: string): string => {
|
||||
if (!currentCode) return '';
|
||||
// 匹配末尾的数字
|
||||
const match = currentCode.match(/^(.*?)(\d+)$/);
|
||||
if (match) {
|
||||
const prefix = match[1];
|
||||
const num = parseInt(match[2], 10);
|
||||
return `${prefix}${num + 1}`;
|
||||
// 从数据库查询已存在的最大产品码数字,生成下一个
|
||||
const generateNextFabricCode = async (companyId: string, prefix: string): Promise<string> => {
|
||||
if (!prefix) return '';
|
||||
|
||||
try {
|
||||
// 查询该公司所有以该前缀开头的产品码
|
||||
const { data, error } = await supabase
|
||||
.from('products')
|
||||
.select('fabric_code')
|
||||
.eq('company_id', companyId)
|
||||
.ilike('fabric_code', `${prefix}%`);
|
||||
|
||||
if (error || !data || data.length === 0) {
|
||||
// 如果没有匹配的产品,从1开始
|
||||
return `${prefix}1`;
|
||||
}
|
||||
|
||||
// 提取所有产品码中的数字,找出最大值
|
||||
let maxNum = 0;
|
||||
data.forEach(item => {
|
||||
const code = item.fabric_code;
|
||||
const match = code.match(/^(.*?)(\d+)$/);
|
||||
if (match && match[1].toUpperCase() === prefix.toUpperCase()) {
|
||||
const num = parseInt(match[2], 10);
|
||||
if (num > maxNum) {
|
||||
maxNum = num;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 返回前缀 + (最大数字 + 1)
|
||||
return `${prefix}${maxNum + 1}`;
|
||||
} catch (err) {
|
||||
console.error('生成产品码失败:', err);
|
||||
return `${prefix}1`;
|
||||
}
|
||||
// 如果没有数字,直接加 1
|
||||
return `${currentCode}1`;
|
||||
};
|
||||
|
||||
// 生成递增的色号(如 02 → 03)
|
||||
const generateNextColorCode = (currentCode: string): string => {
|
||||
if (!currentCode) return '01';
|
||||
const num = parseInt(currentCode, 10);
|
||||
const nextNum = num + 1;
|
||||
// 保持两位数格式
|
||||
// 从数据库查询已存在的最大色号数字,生成下一个
|
||||
const generateNextColorCode = async (
|
||||
companyId: string,
|
||||
productName: string,
|
||||
totalWeight: number | null
|
||||
): Promise<string> => {
|
||||
try {
|
||||
// 查询该产品名称-克重组合下的所有色号(色号在产品名称-克重下连续递增)
|
||||
const { data, error } = await supabase
|
||||
.from('products')
|
||||
.select('color_code')
|
||||
.eq('company_id', companyId)
|
||||
.eq('product_name', productName)
|
||||
.eq('total_weight', totalWeight || 0);
|
||||
|
||||
if (error || !data || data.length === 0) {
|
||||
// 如果没有匹配的产品,从01开始
|
||||
return '01';
|
||||
}
|
||||
|
||||
// 提取所有色号中的数字,找出最大值
|
||||
let maxNum = 0;
|
||||
data.forEach(item => {
|
||||
const code = item.color_code;
|
||||
const num = parseInt(code, 10);
|
||||
if (!isNaN(num) && num > maxNum) {
|
||||
maxNum = num;
|
||||
}
|
||||
});
|
||||
|
||||
// 返回最大数字 + 1,保持两位数格式
|
||||
const nextNum = maxNum + 1;
|
||||
return nextNum < 10 ? `0${nextNum}` : String(nextNum);
|
||||
} catch (err) {
|
||||
console.error('生成色号失败:', err);
|
||||
return '01';
|
||||
}
|
||||
};
|
||||
|
||||
// 选择产品建议
|
||||
const selectProductSuggestion = (suggestion: ProductSuggestion) => {
|
||||
const selectProductSuggestion = async (suggestion: ProductSuggestion) => {
|
||||
// 获取该产品的纱线配比
|
||||
const ratios = productRatios[suggestion.id] || [];
|
||||
|
||||
// 提取产品码前缀并生成新的产品码
|
||||
let newFabricCode = '';
|
||||
const fabricMatch = suggestion.fabric_code.match(/^(.*?)(\d+)$/);
|
||||
if (fabricMatch && companyId) {
|
||||
const prefix = fabricMatch[1];
|
||||
newFabricCode = await generateNextFabricCode(companyId, prefix);
|
||||
} else {
|
||||
newFabricCode = suggestion.fabric_code;
|
||||
}
|
||||
|
||||
// 生成色号(基于该产品名称-克重组合的最大值+1,色号在产品名称-克重下连续递增)
|
||||
let newColorCode = '01';
|
||||
if (companyId) {
|
||||
newColorCode = await generateNextColorCode(
|
||||
companyId,
|
||||
suggestion.product_name,
|
||||
suggestion.total_weight
|
||||
);
|
||||
}
|
||||
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
product_name: suggestion.product_name,
|
||||
@ -246,10 +320,10 @@ export function ProductForm({
|
||||
weft_weight: suggestion.weft_weight ? String(suggestion.weft_weight) : '',
|
||||
// 颜色需要用户自己输入,不自动填写
|
||||
color: '',
|
||||
// 产品码自动生成(递增)
|
||||
fabric_code: generateNextFabricCode(suggestion.fabric_code),
|
||||
// 色号自动生成(递增)
|
||||
color_code: generateNextColorCode(suggestion.color_code),
|
||||
// 产品码自动生成(基于数据库最大值+1)
|
||||
fabric_code: newFabricCode,
|
||||
// 色号自动生成(基于该产品名称-克重-颜色组合的最大值+1)
|
||||
color_code: newColorCode,
|
||||
// 自动填写生产采购价
|
||||
production_price: suggestion.production_price ? String(suggestion.production_price) : '',
|
||||
// 自动填写纱线配比
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
import React from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import React, { memo } from 'react';
|
||||
import { ChevronDown, ChevronUp, List, Image as ImageIcon } from 'lucide-react';
|
||||
import { useResponsive } from '../../hooks/useResponsive';
|
||||
import { animationConfig } from '../../utils/constants';
|
||||
import type { ProductWithInventory, ProductYarnRatio, ProductInRecord } from '../../types';
|
||||
|
||||
interface ProductListProps {
|
||||
@ -30,14 +28,16 @@ export function ProductList({
|
||||
}: ProductListProps) {
|
||||
const { isMobile } = useResponsive();
|
||||
|
||||
// 按名称和克重分组(优先使用 total_weight)
|
||||
// 按名称和克重分组(优先使用 total_weight),无克重的产品直接按名称分组
|
||||
const groupProductsByNameAndWeight = () => {
|
||||
const groups: Record<string, Record<string, ProductWithInventory[]>> = {};
|
||||
products.forEach(product => {
|
||||
if (!groups[product.product_name]) {
|
||||
groups[product.product_name] = {};
|
||||
}
|
||||
const weightKey = String(product.total_weight || product.weight);
|
||||
const weight = product.total_weight || product.weight;
|
||||
// 如果没有克重,使用特殊标记 '__no_weight__'
|
||||
const weightKey = weight ? String(weight) : '__no_weight__';
|
||||
if (!groups[product.product_name][weightKey]) {
|
||||
groups[product.product_name][weightKey] = [];
|
||||
}
|
||||
@ -46,15 +46,6 @@ export function ProductList({
|
||||
return groups;
|
||||
};
|
||||
|
||||
// 格式化克重显示(含经线/纬线细分)
|
||||
const formatWeightDisplay = (product: ProductWithInventory) => {
|
||||
const total = product.total_weight || product.weight;
|
||||
if (product.warp_weight && product.weft_weight) {
|
||||
return `${total}g (经${product.warp_weight}+纬${product.weft_weight})`;
|
||||
}
|
||||
return `${total}g`;
|
||||
};
|
||||
|
||||
const productGroups = groupProductsByNameAndWeight();
|
||||
|
||||
if (isMobile) {
|
||||
@ -69,32 +60,27 @@ export function ProductList({
|
||||
{Object.entries(productGroups).map(([productName, weightGroups]) => {
|
||||
const isNameExpanded = expandedGroups[productName];
|
||||
const allProducts = Object.values(weightGroups).flat();
|
||||
// 计算总入库量(从入库记录)
|
||||
const totalInRolls = allProducts.reduce((sum, p) => sum + (p.total_in_rolls || 0), 0);
|
||||
const totalInMeters = allProducts.reduce((sum, p) => sum + (p.total_in_meters || 0), 0);
|
||||
// 计算出库量
|
||||
const totalOutRolls = allProducts.reduce((sum, p) => sum + (p.total_out_rolls || 0), 0);
|
||||
const totalOutMeters = allProducts.reduce((sum, p) => sum + (p.total_out_meters || 0), 0);
|
||||
// 可用库存
|
||||
const availableRolls = allProducts.reduce((sum, p) => sum + (p.raw_fabric_rolls || 0), 0);
|
||||
const availableMeters = allProducts.reduce((sum, p) => sum + (p.raw_fabric_meters || 0), 0);
|
||||
const weightCount = Object.keys(weightGroups).length;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
<div
|
||||
key={productName}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden"
|
||||
>
|
||||
{/* 一级分组头部 */}
|
||||
<div
|
||||
onClick={() => onToggleGroup(productName)}
|
||||
className="flex items-center justify-between p-4 cursor-pointer hover:bg-gray-50 transition-colors"
|
||||
className="flex items-center justify-between p-4 cursor-pointer hover:bg-gray-50"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-8 h-8 rounded-lg flex items-center justify-center transition-colors ${isNameExpanded ? 'bg-blue-100' : 'bg-gray-100'}`}>
|
||||
{isNameExpanded ? <ChevronUp size={18} className="text-blue-600" /> : <ChevronDown size={18} className="text-gray-500" />}
|
||||
<div className={`w-8 h-8 rounded-lg flex items-center justify-center ${isNameExpanded ? 'bg-amber-100' : 'bg-gray-100'}`}>
|
||||
{isNameExpanded ? <ChevronUp size={18} className="text-amber-600" /> : <ChevronDown size={18} className="text-gray-500" />}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-900">{productName}</h3>
|
||||
@ -110,48 +96,26 @@ export function ProductList({
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 二级分组 */}
|
||||
<AnimatePresence>
|
||||
{isNameExpanded && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={animationConfig.transition}
|
||||
className="border-t border-gray-100"
|
||||
{/* 二级分组 - 使用CSS Grid实现平滑高度动画 */}
|
||||
<div
|
||||
className="border-t border-gray-100 grid transition-all duration-300 ease-out"
|
||||
style={{
|
||||
gridTemplateRows: isNameExpanded ? '1fr' : '0fr',
|
||||
opacity: isNameExpanded ? 1 : 0
|
||||
}}
|
||||
>
|
||||
<div className="overflow-hidden">
|
||||
<div className="p-4 space-y-3">
|
||||
{Object.entries(weightGroups).map(([weight, products]) => {
|
||||
const weightKey = `${productName}-${weight}`;
|
||||
const isWeightExpanded = expandedGroups[weightKey];
|
||||
const hasNoWeight = weight === '__no_weight__';
|
||||
const isSingleWeight = Object.keys(weightGroups).length === 1;
|
||||
|
||||
// 如果没有克重,直接显示产品列表
|
||||
if (hasNoWeight) {
|
||||
return (
|
||||
<div key={weight} className="bg-gray-50 rounded-xl overflow-hidden">
|
||||
<div
|
||||
onClick={() => onToggleGroup(weightKey)}
|
||||
className="flex items-center justify-between p-3 cursor-pointer hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-6 h-6 rounded flex items-center justify-center transition-colors ${isWeightExpanded ? 'bg-blue-100' : 'bg-white'}`}>
|
||||
{isWeightExpanded ? <ChevronUp size={14} className="text-blue-600" /> : <ChevronDown size={14} className="text-gray-500" />}
|
||||
</div>
|
||||
<span className="font-medium text-gray-800">{weight}g</span>
|
||||
{products[0]?.warp_weight && products[0]?.weft_weight && (
|
||||
<span className="text-xs text-gray-400 ml-1">经{products[0].warp_weight}+纬{products[0].weft_weight}</span>
|
||||
)}
|
||||
<span className="text-xs text-gray-500">({products.length}个颜色)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AnimatePresence>
|
||||
{isWeightExpanded && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={animationConfig.fastTransition}
|
||||
className="border-t border-gray-200"
|
||||
>
|
||||
<table className="w-full">
|
||||
<thead className="bg-white/50 border-b border-gray-100">
|
||||
<tr>
|
||||
@ -176,17 +140,110 @@ export function ProductList({
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 如果只有一种克重,直接显示
|
||||
if (isSingleWeight) {
|
||||
return (
|
||||
<div key={weight} className="bg-gray-50 rounded-xl overflow-hidden">
|
||||
<div className="flex items-center gap-2 p-3 bg-gray-100 border-b border-gray-200">
|
||||
<span className="font-medium text-gray-800">{weight}g</span>
|
||||
{products[0]?.warp_weight && products[0]?.weft_weight && (
|
||||
<span className="text-xs text-gray-500 ml-1">经{products[0].warp_weight}+纬{products[0].weft_weight}</span>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<span className="text-xs text-gray-500">({products.length}个颜色)</span>
|
||||
</div>
|
||||
<table className="w-full">
|
||||
<thead className="bg-white/50 border-b border-gray-100">
|
||||
<tr>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500">颜色</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500">产品码</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500">库存</th>
|
||||
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100">
|
||||
{products.map(product => (
|
||||
<ProductRow
|
||||
key={product.id}
|
||||
product={product}
|
||||
ratios={productRatios[product.id]}
|
||||
onViewRecords={() => onViewRecords(product)}
|
||||
onViewPriceHistory={() => onViewPriceHistory(product)}
|
||||
onViewStockHistory={() => onViewStockHistory(product)}
|
||||
onEdit={() => onEdit(product)}
|
||||
onDelete={() => onDelete(product.id)}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 多种克重,显示可折叠的克重卡片
|
||||
return (
|
||||
<div key={weight} className="bg-gray-50 rounded-xl overflow-hidden">
|
||||
<div
|
||||
onClick={() => onToggleGroup(weightKey)}
|
||||
className="flex items-center justify-between p-3 cursor-pointer hover:bg-gray-100"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-6 h-6 rounded flex items-center justify-center ${isWeightExpanded ? 'bg-amber-100' : 'bg-white'}`}>
|
||||
{isWeightExpanded ? <ChevronUp size={14} className="text-amber-600" /> : <ChevronDown size={14} className="text-gray-500" />}
|
||||
</div>
|
||||
<span className="font-medium text-gray-800">{weight}g</span>
|
||||
{products[0]?.warp_weight && products[0]?.weft_weight && (
|
||||
<span className="text-xs text-gray-400 ml-1">经{products[0].warp_weight}+纬{products[0].weft_weight}</span>
|
||||
)}
|
||||
<span className="text-xs text-gray-500">({products.length}个颜色)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 克重展开内容 - 使用CSS Grid实现平滑高度动画 */}
|
||||
<div
|
||||
className="border-t border-gray-200 grid transition-all duration-300 ease-out"
|
||||
style={{
|
||||
gridTemplateRows: isWeightExpanded ? '1fr' : '0fr',
|
||||
opacity: isWeightExpanded ? 1 : 0
|
||||
}}
|
||||
>
|
||||
<div className="overflow-hidden">
|
||||
<table className="w-full">
|
||||
<thead className="bg-white/50 border-b border-gray-100">
|
||||
<tr>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500">颜色</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500">产品码</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500">库存</th>
|
||||
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100">
|
||||
{products.map(product => (
|
||||
<ProductRow
|
||||
key={product.id}
|
||||
product={product}
|
||||
ratios={productRatios[product.id]}
|
||||
onViewRecords={() => onViewRecords(product)}
|
||||
onViewPriceHistory={() => onViewPriceHistory(product)}
|
||||
onViewStockHistory={() => onViewStockHistory(product)}
|
||||
onEdit={() => onEdit(product)}
|
||||
onDelete={() => onDelete(product.id)}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
@ -198,23 +255,20 @@ export function ProductList({
|
||||
<div className="space-y-3">
|
||||
{Object.entries(productGroups).map(([productName, weightGroups]) => {
|
||||
const isNameExpanded = expandedGroups[productName];
|
||||
const allProducts = Object.values(weightGroups).flat();
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
<div
|
||||
key={productName}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden"
|
||||
>
|
||||
<div
|
||||
onClick={() => onToggleGroup(productName)}
|
||||
className="p-4 cursor-pointer hover:bg-gray-50 transition-colors"
|
||||
className="p-4 cursor-pointer hover:bg-gray-50"
|
||||
>
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<div className={`w-7 h-7 rounded-lg flex items-center justify-center transition-colors flex-shrink-0 ${isNameExpanded ? 'bg-blue-100' : 'bg-gray-100'}`}>
|
||||
{isNameExpanded ? <ChevronUp size={16} className="text-blue-600" /> : <ChevronDown size={16} className="text-gray-500" />}
|
||||
<div className={`w-7 h-7 rounded-lg flex items-center justify-center flex-shrink-0 ${isNameExpanded ? 'bg-amber-100' : 'bg-gray-100'}`}>
|
||||
{isNameExpanded ? <ChevronUp size={16} className="text-amber-600" /> : <ChevronDown size={16} className="text-gray-500" />}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="font-medium text-gray-900 text-sm truncate">{productName}</p>
|
||||
@ -223,16 +277,67 @@ export function ProductList({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AnimatePresence>
|
||||
{isNameExpanded && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
className="border-t border-gray-100 bg-gray-50"
|
||||
{/* 移动端展开内容 - 使用CSS Grid实现平滑高度动画 */}
|
||||
<div
|
||||
className="border-t border-gray-100 grid transition-all duration-300 ease-out"
|
||||
style={{
|
||||
gridTemplateRows: isNameExpanded ? '1fr' : '0fr',
|
||||
opacity: isNameExpanded ? 1 : 0
|
||||
}}
|
||||
>
|
||||
<div className="overflow-hidden bg-gray-50">
|
||||
<div className="p-3 space-y-2">
|
||||
{Object.entries(weightGroups).map(([weight, products]) => (
|
||||
{Object.entries(weightGroups).map(([weight, products]) => {
|
||||
const hasNoWeight = weight === '__no_weight__';
|
||||
const isSingleWeight = Object.keys(weightGroups).length === 1;
|
||||
|
||||
if (hasNoWeight) {
|
||||
return (
|
||||
<div key={weight} className="space-y-2">
|
||||
{products.map(product => (
|
||||
<ProductCardMobile
|
||||
key={product.id}
|
||||
product={product}
|
||||
ratios={productRatios[product.id]}
|
||||
onViewRecords={() => onViewRecords(product)}
|
||||
onViewPriceHistory={() => onViewPriceHistory(product)}
|
||||
onViewStockHistory={() => onViewStockHistory(product)}
|
||||
onEdit={() => onEdit(product)}
|
||||
onDelete={() => onDelete(product.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isSingleWeight) {
|
||||
return (
|
||||
<div key={weight} className="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<div className="p-3 bg-gray-50 border-b border-gray-100">
|
||||
<span className="font-medium text-gray-800 text-sm">{weight}g</span>
|
||||
{products[0]?.warp_weight && products[0]?.weft_weight && (
|
||||
<span className="text-xs text-gray-500 ml-1">经{products[0].warp_weight}+纬{products[0].weft_weight}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="p-2 space-y-2">
|
||||
{products.map(product => (
|
||||
<ProductCardMobile
|
||||
key={product.id}
|
||||
product={product}
|
||||
ratios={productRatios[product.id]}
|
||||
onViewRecords={() => onViewRecords(product)}
|
||||
onViewPriceHistory={() => onViewPriceHistory(product)}
|
||||
onViewStockHistory={() => onViewStockHistory(product)}
|
||||
onEdit={() => onEdit(product)}
|
||||
onDelete={() => onDelete(product.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={weight} className="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<div className="p-3">
|
||||
<span className="font-medium text-gray-800 text-sm">{weight}g</span>
|
||||
@ -255,12 +360,12 @@ export function ProductList({
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
@ -281,7 +386,7 @@ interface ProductRowProps {
|
||||
|
||||
function ProductRow({ product, ratios, onViewRecords, onViewPriceHistory, onViewStockHistory, onEdit, onDelete }: ProductRowProps) {
|
||||
return (
|
||||
<tr className="bg-white hover:bg-gray-50/50 transition-colors">
|
||||
<tr className="bg-white hover:bg-gray-50/50">
|
||||
<td className="px-3 py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
{product.image_url ? (
|
||||
@ -316,7 +421,7 @@ function ProductRow({ product, ratios, onViewRecords, onViewPriceHistory, onView
|
||||
<div className="text-xs text-gray-400 mt-1">
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onViewRecords(); }}
|
||||
className="inline-flex items-center gap-1 px-2 py-0.5 bg-blue-50 text-blue-600 rounded text-xs font-medium hover:bg-blue-100 transition-colors"
|
||||
className="inline-flex items-center gap-1 px-2 py-0.5 bg-amber-50 text-amber-600 rounded text-xs font-medium hover:bg-amber-100"
|
||||
>
|
||||
<List className="w-3 h-3" />
|
||||
入库{product.in_records?.length || 0}条
|
||||
@ -328,7 +433,7 @@ function ProductRow({ product, ratios, onViewRecords, onViewPriceHistory, onView
|
||||
<td className="px-3 py-2 text-right">
|
||||
<button onClick={onViewStockHistory} className="text-purple-600 hover:text-purple-800 text-sm font-medium mr-2">出入库历史</button>
|
||||
<button onClick={onViewPriceHistory} className="text-emerald-600 hover:text-emerald-800 text-sm font-medium mr-2">价格历史</button>
|
||||
<button onClick={onEdit} className="text-blue-600 hover:text-blue-800 text-sm font-medium mr-2">编辑</button>
|
||||
<button onClick={onEdit} className="text-amber-600 hover:text-amber-800 text-sm font-medium mr-2">编辑</button>
|
||||
<button onClick={onDelete} className="text-red-600 hover:text-red-800 text-sm font-medium">删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
@ -378,7 +483,7 @@ function ProductCardMobile({ product, ratios, onViewRecords, onViewPriceHistory,
|
||||
<div className="text-xs text-gray-400 mb-1">
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onViewRecords(); }}
|
||||
className="inline-flex items-center gap-1 px-2 py-0.5 bg-blue-50 text-blue-600 rounded text-xs font-medium hover:bg-blue-100 transition-colors"
|
||||
className="inline-flex items-center gap-1 px-2 py-0.5 bg-blue-50 text-blue-600 rounded text-xs font-medium hover:bg-blue-100"
|
||||
>
|
||||
<List className="w-3 h-3" />
|
||||
入库{product.in_records?.length || 0}条
|
||||
|
||||
@ -4,7 +4,7 @@ import { useAuth } from '../contexts/AuthContext';
|
||||
import { supabase } from '../supabase/client';
|
||||
import { motion } from 'framer-motion';
|
||||
import { ArrowLeft, CheckCircle, AlertTriangle, Package, Droplet, Link2 } from 'lucide-react';
|
||||
import { decryptShareParams } from '../utils/shareCrypto';
|
||||
import { decryptShareParamsSync } from '../utils/shareCrypto';
|
||||
import { useResponsive } from '../hooks/useResponsive';
|
||||
import type { ProductionPlan, WashingPlan, YarnRatio, FactoryType } from '../types';
|
||||
|
||||
@ -25,22 +25,47 @@ export function ImportPlanPage() {
|
||||
const { isMobile } = useResponsive();
|
||||
|
||||
const token = searchParams.get('token') || '';
|
||||
const shortCode = searchParams.get('s') || '';
|
||||
const [planId, setPlanId] = useState('');
|
||||
const [factoryType, setFactoryType] = useState<FactoryType | null>(null);
|
||||
const [decrypting, setDecrypting] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
decryptShareParams(token).then(decrypted => {
|
||||
const resolveLink = async () => {
|
||||
if (shortCode) {
|
||||
// 短链解析
|
||||
const { data, error } = await supabase
|
||||
.from('share_links')
|
||||
.select('plan_id, factory_type')
|
||||
.eq('short_code', shortCode)
|
||||
.maybeSingle();
|
||||
|
||||
if (data) {
|
||||
setPlanId(data.plan_id);
|
||||
setFactoryType(data.factory_type as FactoryType);
|
||||
// 增加点击计数
|
||||
await supabase.rpc('increment_share_link_clicks', { code: shortCode });
|
||||
} else {
|
||||
console.error('短链查询失败:', error);
|
||||
}
|
||||
setDecrypting(false);
|
||||
} else if (token) {
|
||||
// 长链接解析
|
||||
const decrypted = decryptShareParamsSync(token);
|
||||
if (decrypted) {
|
||||
setPlanId(decrypted.planId);
|
||||
setFactoryType(decrypted.factoryType as FactoryType);
|
||||
}
|
||||
});
|
||||
setDecrypting(false);
|
||||
} else {
|
||||
setPlanId(searchParams.get('plan') || '');
|
||||
setFactoryType(searchParams.get('type') as FactoryType | null);
|
||||
setDecrypting(false);
|
||||
}
|
||||
}, [token]);
|
||||
};
|
||||
|
||||
resolveLink();
|
||||
}, [token, shortCode]);
|
||||
|
||||
const [plan, setPlan] = useState<ProductionPlan | WashingPlan | null>(null);
|
||||
const [yarns, setYarns] = useState<YarnRatio[]>([]);
|
||||
@ -93,6 +118,11 @@ export function ImportPlanPage() {
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
// 等待解密完成且 factoryType 有值
|
||||
if (decrypting || !factoryType) {
|
||||
return <div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-50 flex items-center justify-center"><p className="text-gray-500">加载中...</p></div>;
|
||||
}
|
||||
|
||||
// 角色匹配:当前角色必须与链接中的factoryType一致
|
||||
const roleMatches = auth.company && auth.currentRole === factoryType;
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ import { ComingSoonModal } from '../components/ComingSoonModal';
|
||||
const demoAccounts = [
|
||||
{ username: 'purchaser', label: '采购商(布行)', color: 'from-blue-500 to-blue-600', icon: Building2, enabled: true },
|
||||
{ username: 'textile', label: '纺织厂', color: 'from-emerald-500 to-green-600', icon: Factory, enabled: true },
|
||||
{ username: 'washing', label: '水洗厂', color: 'from-cyan-500 to-blue-600', icon: Droplets, enabled: true }
|
||||
{ username: 'washing', label: '水洗厂', color: 'from-violet-400 to-fuchsia-400', icon: Droplets, enabled: true }
|
||||
];
|
||||
|
||||
// 纺织主题背景动画组件
|
||||
|
||||
@ -307,6 +307,48 @@ export function MemberManage() {
|
||||
}
|
||||
};
|
||||
|
||||
// 根据角色获取主题色
|
||||
const getThemeColors = () => {
|
||||
const role = companyInfo?.role || 'purchaser';
|
||||
switch (role) {
|
||||
case 'textile':
|
||||
return {
|
||||
primary: 'emerald',
|
||||
bg: 'bg-emerald-50',
|
||||
bgHover: 'hover:bg-emerald-100',
|
||||
text: 'text-emerald-500',
|
||||
border: 'border-emerald-200',
|
||||
ring: 'focus:ring-emerald-400',
|
||||
button: 'bg-emerald-400 hover:bg-emerald-500',
|
||||
gradient: 'from-emerald-400 to-teal-400'
|
||||
};
|
||||
case 'washing':
|
||||
return {
|
||||
primary: 'violet',
|
||||
bg: 'bg-violet-50',
|
||||
bgHover: 'hover:bg-violet-100',
|
||||
text: 'text-violet-500',
|
||||
border: 'border-violet-200',
|
||||
ring: 'focus:ring-violet-400',
|
||||
button: 'bg-violet-400 hover:bg-violet-500',
|
||||
gradient: 'from-violet-400 to-fuchsia-400'
|
||||
};
|
||||
default:
|
||||
return {
|
||||
primary: 'amber',
|
||||
bg: 'bg-amber-50',
|
||||
bgHover: 'hover:bg-amber-100',
|
||||
text: 'text-amber-500',
|
||||
border: 'border-amber-200',
|
||||
ring: 'focus:ring-amber-400',
|
||||
button: 'bg-amber-400 hover:bg-amber-500',
|
||||
gradient: 'from-amber-400 to-orange-400'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const theme = getThemeColors();
|
||||
|
||||
// 如果不是主账号,显示无权限
|
||||
if (!auth.user?.is_master) {
|
||||
return (
|
||||
@ -319,7 +361,7 @@ export function MemberManage() {
|
||||
<p className="text-gray-500 mb-4">只有主账号可以管理账号</p>
|
||||
<button
|
||||
onClick={() => navigate(-1)}
|
||||
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||
className={`px-6 py-2 ${theme.button} text-white rounded-lg transition-colors`}
|
||||
>
|
||||
返回
|
||||
</button>
|
||||
@ -349,7 +391,7 @@ export function MemberManage() {
|
||||
whileHover={{ scale: isMobile ? 1 : 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={handleOpenModal}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors text-sm md:text-base"
|
||||
className={`flex items-center gap-2 px-4 py-2 ${theme.button} text-white rounded-lg transition-colors text-sm md:text-base`}
|
||||
>
|
||||
<Plus size={18} />
|
||||
添加子账号
|
||||
@ -357,14 +399,14 @@ export function MemberManage() {
|
||||
</div>
|
||||
|
||||
{/* 主账号信息卡片 */}
|
||||
<div className="bg-gradient-to-r from-blue-500 to-blue-600 rounded-xl p-4 md:p-6 mb-6 text-white">
|
||||
<div className={`bg-gradient-to-r ${theme.gradient} rounded-xl p-4 md:p-6 mb-6 text-white`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 bg-white/20 rounded-full flex items-center justify-center">
|
||||
<Shield className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-blue-100">当前登录账号</p>
|
||||
<p className="text-sm text-white/70">当前登录账号</p>
|
||||
<p className="font-semibold text-lg">{auth.user?.username}</p>
|
||||
<span className="inline-flex items-center gap-1 text-xs bg-white/20 px-2 py-0.5 rounded mt-1">
|
||||
<Shield size={12} />
|
||||
@ -386,7 +428,7 @@ export function MemberManage() {
|
||||
{/* 帮助与支持卡片 */}
|
||||
<div className="bg-white rounded-lg shadow-sm p-4 md:p-6 mb-6">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<BookOpen className="w-5 h-5 text-blue-600" />
|
||||
<BookOpen className={`w-5 h-5 ${theme.text}`} />
|
||||
<h2 className="font-semibold text-gray-800">帮助与支持</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
@ -394,10 +436,10 @@ export function MemberManage() {
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onClick={() => setShowUserManual(true)}
|
||||
className="flex items-center gap-3 p-4 bg-blue-50 rounded-xl hover:bg-blue-100 transition-colors text-left"
|
||||
className={`flex items-center gap-3 p-4 ${theme.bg} rounded-xl ${theme.bgHover} transition-colors text-left`}
|
||||
>
|
||||
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
|
||||
<BookOpen className="w-5 h-5 text-blue-600" />
|
||||
<div className={`w-10 h-10 ${theme.bg} rounded-lg flex items-center justify-center`}>
|
||||
<BookOpen className={`w-5 h-5 ${theme.text}`} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-800">用户手册</p>
|
||||
@ -409,10 +451,10 @@ export function MemberManage() {
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onClick={() => setShowFeedback(true)}
|
||||
className="flex items-center gap-3 p-4 bg-emerald-50 rounded-xl hover:bg-emerald-100 transition-colors text-left"
|
||||
className={`flex items-center gap-3 p-4 ${theme.bg} rounded-xl ${theme.bgHover} transition-colors text-left`}
|
||||
>
|
||||
<div className="w-10 h-10 bg-emerald-100 rounded-lg flex items-center justify-center">
|
||||
<MessageSquare className="w-5 h-5 text-emerald-600" />
|
||||
<div className={`w-10 h-10 ${theme.bg} rounded-lg flex items-center justify-center`}>
|
||||
<MessageSquare className={`w-5 h-5 ${theme.text}`} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-800">用户反馈</p>
|
||||
@ -424,10 +466,10 @@ export function MemberManage() {
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onClick={() => setShowAbout(true)}
|
||||
className="flex items-center gap-3 p-4 bg-purple-50 rounded-xl hover:bg-purple-100 transition-colors text-left"
|
||||
className={`flex items-center gap-3 p-4 ${theme.bg} rounded-xl ${theme.bgHover} transition-colors text-left`}
|
||||
>
|
||||
<div className="w-10 h-10 bg-purple-100 rounded-lg flex items-center justify-center">
|
||||
<Info className="w-5 h-5 text-purple-600" />
|
||||
<div className={`w-10 h-10 ${theme.bg} rounded-lg flex items-center justify-center`}>
|
||||
<Info className={`w-5 h-5 ${theme.text}`} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-800">关于</p>
|
||||
@ -451,7 +493,7 @@ export function MemberManage() {
|
||||
whileHover={{ scale: isMobile ? 1 : 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={handleOpenModal}
|
||||
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||
className={`px-6 py-2 ${theme.button} text-white rounded-lg transition-colors`}
|
||||
>
|
||||
添加子账号
|
||||
</motion.button>
|
||||
@ -535,7 +577,7 @@ export function MemberManage() {
|
||||
type="text"
|
||||
value={formData.username}
|
||||
onChange={(e) => setFormData({ ...formData, username: e.target.value })}
|
||||
className="w-full pl-10 pr-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all"
|
||||
className={`w-full pl-10 pr-4 py-2.5 border border-gray-300 rounded-lg ${theme.ring} focus:border-transparent transition-all`}
|
||||
placeholder="请输入用户名(至少3个字符)"
|
||||
required
|
||||
/>
|
||||
@ -552,7 +594,7 @@ export function MemberManage() {
|
||||
type="text"
|
||||
value={formData.displayName}
|
||||
onChange={(e) => setFormData({ ...formData, displayName: e.target.value })}
|
||||
className="w-full pl-10 pr-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all"
|
||||
className={`w-full pl-10 pr-4 py-2.5 border border-gray-300 rounded-lg ${theme.ring} focus:border-transparent transition-all`}
|
||||
placeholder="请输入姓名"
|
||||
required
|
||||
/>
|
||||
@ -569,7 +611,7 @@ export function MemberManage() {
|
||||
type="tel"
|
||||
value={formData.phone}
|
||||
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
|
||||
className="w-full pl-10 pr-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all"
|
||||
className={`w-full pl-10 pr-4 py-2.5 border border-gray-300 rounded-lg ${theme.ring} focus:border-transparent transition-all`}
|
||||
placeholder="请输入手机号"
|
||||
required
|
||||
/>
|
||||
@ -584,7 +626,7 @@ export function MemberManage() {
|
||||
type="password"
|
||||
value={formData.password}
|
||||
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
||||
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all"
|
||||
className={`w-full px-4 py-2.5 border border-gray-300 rounded-lg ${theme.ring} focus:border-transparent transition-all`}
|
||||
placeholder="请输入密码(至少6个字符)"
|
||||
required
|
||||
/>
|
||||
@ -614,7 +656,7 @@ export function MemberManage() {
|
||||
whileTap={{ scale: 0.98 }}
|
||||
type="submit"
|
||||
disabled={submitting}
|
||||
className="flex-1 py-2.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
className={`flex-1 py-2.5 ${theme.button} text-white rounded-lg transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed`}
|
||||
>
|
||||
{submitting ? '创建中...' : '创建子账号'}
|
||||
</motion.button>
|
||||
@ -709,7 +751,7 @@ export function MemberManage() {
|
||||
type="text"
|
||||
value={masterFormData.displayName}
|
||||
onChange={(e) => setMasterFormData({ ...masterFormData, displayName: e.target.value })}
|
||||
className="w-full pl-10 pr-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all"
|
||||
className={`w-full pl-10 pr-4 py-2.5 border border-gray-300 rounded-lg ${theme.ring} focus:border-transparent transition-all`}
|
||||
placeholder="请输入姓名"
|
||||
required
|
||||
/>
|
||||
@ -727,7 +769,7 @@ export function MemberManage() {
|
||||
type="tel"
|
||||
value={masterFormData.phone}
|
||||
onChange={(e) => setMasterFormData({ ...masterFormData, phone: e.target.value })}
|
||||
className="w-full pl-10 pr-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all"
|
||||
className={`w-full pl-10 pr-4 py-2.5 border border-gray-300 rounded-lg ${theme.ring} focus:border-transparent transition-all`}
|
||||
placeholder="请输入手机号"
|
||||
required
|
||||
/>
|
||||
@ -743,7 +785,7 @@ export function MemberManage() {
|
||||
type="password"
|
||||
value={masterFormData.password || ''}
|
||||
onChange={(e) => setMasterFormData({ ...masterFormData, password: e.target.value })}
|
||||
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all"
|
||||
className={`w-full px-4 py-2.5 border border-gray-300 rounded-lg ${theme.ring} focus:border-transparent transition-all`}
|
||||
placeholder="请输入新密码(至少6个字符)"
|
||||
/>
|
||||
<p className="text-xs text-gray-400 mt-1">留空则保持原密码不变</p>
|
||||
@ -774,7 +816,7 @@ export function MemberManage() {
|
||||
whileTap={{ scale: 0.98 }}
|
||||
type="submit"
|
||||
disabled={editingMaster}
|
||||
className="flex-1 py-2.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
className={`flex-1 py-2.5 ${theme.button} text-white rounded-lg transition-colors font-medium disabled:opacity-50 disabled:cursor-not-allowed`}
|
||||
>
|
||||
{editingMaster ? '保存中...' : '保存修改'}
|
||||
</motion.button>
|
||||
|
||||
@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { supabase } from '../../supabase/client';
|
||||
import { motion } from 'framer-motion';
|
||||
import { FileText, Plus, Warehouse, Factory, ArrowLeft, Users, TrendingUp, Package, Clock, Building2, CheckCircle2, DollarSign, Droplets, LogOut, HelpCircle } from 'lucide-react';
|
||||
import { FileText, Plus, Warehouse, Factory, ArrowLeft, Users, TrendingUp, Package, Clock, Building2, CheckCircle2, DollarSign, Droplets, LogOut, HelpCircle, X, BookOpen, MessageSquare, Info, Camera } from 'lucide-react';
|
||||
import { useResponsive } from '../../hooks/useResponsive';
|
||||
import { OnboardingGuide, shouldShowOnboarding } from '../../components/OnboardingGuide';
|
||||
import { NotificationCenter } from '../../components/NotificationCenter';
|
||||
@ -14,25 +14,21 @@ import { UserManual } from '../../components/UserManual';
|
||||
import type { ProductionPlan } from '../../types';
|
||||
|
||||
const statusMap: Record<string, { label: string; color: string; bgColor: string; icon: any }> = {
|
||||
pending: { label: '待确定', color: 'text-amber-600', bgColor: 'bg-amber-50', icon: Clock },
|
||||
producing: { label: '生产中', color: 'text-blue-600', bgColor: 'bg-blue-50', icon: TrendingUp },
|
||||
completed: { label: '已完成', color: 'text-emerald-600', bgColor: 'bg-emerald-50', icon: Package }
|
||||
pending: { label: '待确定', color: 'text-amber-500', bgColor: 'bg-amber-50', icon: Clock },
|
||||
producing: { label: '生产中', color: 'text-amber-500', bgColor: 'bg-amber-50', icon: TrendingUp },
|
||||
completed: { label: '已完成', color: 'text-emerald-500', bgColor: 'bg-emerald-50', icon: Package }
|
||||
};
|
||||
|
||||
const getQuickActions = (isMaster: boolean) => {
|
||||
const actions = [
|
||||
{ id: 'plans', icon: FileText, label: '计划总览', path: '/purchaser/plans', gradient: 'from-blue-500 to-cyan-500', description: '查看所有计划' },
|
||||
{ id: 'new', icon: Plus, label: '新建纺织计划', path: '/purchaser/plans/new', gradient: 'from-emerald-500 to-green-500', description: '创建新纺织计划' },
|
||||
{ id: 'washing', icon: Droplets, label: '新建水洗', path: '/purchaser/washing-plans/new', gradient: 'from-cyan-500 to-blue-500', description: '创建水洗计划' },
|
||||
{ id: 'warehouse', icon: Warehouse, label: '原坯布仓库', path: '/purchaser/warehouse', gradient: 'from-amber-500 to-orange-500', description: '管理原坯布库存' },
|
||||
{ id: 'finished', icon: Package, label: '水洗仓库', path: '/purchaser/finished-warehouse', gradient: 'from-emerald-500 to-teal-500', description: '管理水洗厂内坯布库存' },
|
||||
{ id: 'factories', icon: Factory, label: '工厂管理', path: '/purchaser/factories', gradient: 'from-purple-500 to-violet-500', description: '管理合作工厂' },
|
||||
{ id: 'accounts-payable', icon: DollarSign, label: '应付账款', path: '/purchaser/accounts-payable', gradient: 'from-rose-500 to-pink-500', description: '查看应付账款' }
|
||||
const getQuickActions = () => {
|
||||
return [
|
||||
{ id: 'plans', icon: FileText, label: '计划总览', path: '/purchaser/plans', gradient: 'from-amber-400 to-orange-400', description: '查看所有计划' },
|
||||
{ id: 'new', icon: Plus, label: '新建纺织计划', path: '/purchaser/plans/new', gradient: 'from-orange-400 to-amber-400', description: '创建新纺织计划' },
|
||||
{ id: 'washing', icon: Droplets, label: '新建水洗', path: '/purchaser/washing-plans/new', gradient: 'from-cyan-400 to-blue-400', description: '创建水洗计划' },
|
||||
{ id: 'warehouse', icon: Warehouse, label: '产品信息', path: '/purchaser/warehouse', gradient: 'from-amber-400 to-yellow-400', description: '管理产品库存' },
|
||||
{ id: 'finished', icon: Package, label: '水洗仓库', path: '/purchaser/finished-warehouse', gradient: 'from-yellow-400 to-amber-400', description: '管理水洗厂内坯布库存' },
|
||||
{ id: 'factories', icon: Factory, label: '工厂管理', path: '/purchaser/factories', gradient: 'from-orange-400 to-rose-400', description: '管理合作工厂' },
|
||||
{ id: 'accounts-payable', icon: DollarSign, label: '应付账款', path: '/purchaser/accounts-payable', gradient: 'from-rose-400 to-pink-400', description: '查看应付账款' }
|
||||
];
|
||||
if (isMaster) {
|
||||
actions.push({ id: 'members', icon: Users, label: '账号管理', path: '/members', gradient: 'from-gray-500 to-slate-500', description: '管理团队成员' });
|
||||
}
|
||||
return actions;
|
||||
};
|
||||
|
||||
const containerVariants = {
|
||||
@ -59,6 +55,9 @@ export function PurchaserDashboard() {
|
||||
const [showFeedback, setShowFeedback] = useState(false);
|
||||
const [showAbout, setShowAbout] = useState(false);
|
||||
|
||||
// 账户卡片弹窗状态
|
||||
const [showAccountCard, setShowAccountCard] = useState(false);
|
||||
|
||||
// 版本更新检查
|
||||
const { showUpdate, setShowUpdate } = useVersionCheck();
|
||||
|
||||
@ -107,7 +106,9 @@ export function PurchaserDashboard() {
|
||||
completed: plans.filter(p => p.status === 'completed').length
|
||||
};
|
||||
|
||||
const recentPlans = plans.slice(0, 5);
|
||||
// 获取5条进行中的计划,用于滑动展示
|
||||
const producingPlans = plans.filter(p => p.status === 'producing').slice(0, 5);
|
||||
const [currentPlanIndex, setCurrentPlanIndex] = useState(0);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-indigo-50/30">
|
||||
@ -116,15 +117,32 @@ export function PurchaserDashboard() {
|
||||
<header className="bg-white/80 backdrop-blur-xl border-b border-gray-200/50 sticky top-0 z-10">
|
||||
<div className="max-w-7xl mx-auto px-3 sm:px-4 md:px-8 py-3 sm:py-4 flex justify-between items-center">
|
||||
<div className="flex items-center gap-2 sm:gap-3">
|
||||
<div className="w-8 h-8 sm:w-10 sm:h-10 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-xl flex items-center justify-center shadow-lg shadow-blue-500/20">
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={() => setShowAccountCard(true)}
|
||||
className="w-8 h-8 sm:w-10 sm:h-10 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-xl flex items-center justify-center shadow-lg shadow-blue-500/20 cursor-pointer overflow-hidden"
|
||||
>
|
||||
{(auth.user as any)?.image_url ? (
|
||||
<img
|
||||
src={(auth.user as any).image_url}
|
||||
alt="头像"
|
||||
className="w-full h-full object-cover"
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Building2 className="w-4 h-4 sm:w-5 sm:h-5 text-white" />
|
||||
</div>
|
||||
)}
|
||||
</motion.button>
|
||||
<div>
|
||||
<h1 className="text-sm sm:text-lg font-bold text-gray-900 flex items-center gap-2 whitespace-nowrap">
|
||||
{auth.company?.name || '采购商'}工作台
|
||||
<span className="sm:hidden truncate max-w-[80px]">{(auth.user as any)?.display_name || auth.user?.username || '用户'}</span>
|
||||
<span className="hidden sm:inline">{auth.company?.name || '采购商'}工作台</span>
|
||||
<span className="text-[10px] sm:text-xs bg-gradient-to-r from-amber-500 to-orange-500 text-white px-1.5 sm:px-2 py-0.5 rounded-full font-medium whitespace-nowrap">限时免费</span>
|
||||
</h1>
|
||||
<p className="text-xs text-gray-500 hidden sm:block">{(auth.user as any)?.display_name || auth.user?.username || '用户'},欢迎回来</p>
|
||||
<p className="text-xs text-gray-500 hidden sm:block truncate max-w-[150px]">{(auth.user as any)?.display_name || auth.user?.username || '用户'},欢迎回来</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
@ -200,11 +218,118 @@ export function PurchaserDashboard() {
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* 最近计划 - 左右滑动展示 */}
|
||||
<motion.div variants={itemVariants} className="bg-white rounded-xl sm:rounded-2xl shadow-sm border border-gray-100 p-3 sm:p-4 md:p-6">
|
||||
<div className="flex justify-between items-center mb-3 sm:mb-4">
|
||||
<h2 className="text-sm sm:text-base font-semibold text-gray-900 whitespace-nowrap">最近计划</h2>
|
||||
<button
|
||||
onClick={() => navigate('/purchaser/plans')}
|
||||
className="text-xs sm:text-sm text-blue-600 hover:text-blue-700 font-medium flex items-center gap-1 whitespace-nowrap"
|
||||
>
|
||||
查看全部
|
||||
<ArrowLeft className="w-3 h-3 sm:w-4 sm:h-4 rotate-180" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{producingPlans.length === 0 ? (
|
||||
<div className="text-center py-8 sm:py-12 text-gray-400">
|
||||
<Package className="w-10 h-10 sm:w-12 sm:h-12 mx-auto mb-3 text-gray-300" />
|
||||
<p className="text-sm">暂无进行中的计划</p>
|
||||
<button
|
||||
onClick={() => navigate('/purchaser/plans/new')}
|
||||
className="mt-3 px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
创建计划
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative">
|
||||
{/* 滑动容器 */}
|
||||
<div className="overflow-hidden">
|
||||
<motion.div
|
||||
className="flex"
|
||||
animate={{ x: -currentPlanIndex * 100 + '%' }}
|
||||
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
|
||||
>
|
||||
{producingPlans.map((plan, i) => {
|
||||
const progress = plan.target_quantity > 0
|
||||
? Math.round((plan.completed_quantity / plan.target_quantity) * 100)
|
||||
: 0;
|
||||
const status = statusMap[plan.status] || statusMap.pending;
|
||||
const StatusIcon = status.icon;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={plan.id}
|
||||
className="w-full flex-shrink-0 px-1"
|
||||
onClick={() => navigate(`/purchaser/plans?id=${plan.id}`)}
|
||||
>
|
||||
<div className="group p-3 sm:p-4 rounded-xl border border-gray-100 hover:border-blue-200 hover:bg-blue-50/30 cursor-pointer transition-all">
|
||||
{/* 计划标题行 */}
|
||||
<div className="flex items-center gap-2 sm:gap-3">
|
||||
<div className={`w-8 h-8 sm:w-10 sm:h-10 rounded-lg flex items-center justify-center ${status.bgColor}`}>
|
||||
<StatusIcon className={`w-4 h-4 sm:w-5 sm:h-5 ${status.color}`} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-1.5 sm:gap-2">
|
||||
<h3 className="font-medium text-gray-900 text-sm sm:text-base truncate">{plan.product_name}-{plan.color}</h3>
|
||||
<span className={`px-1.5 py-0.5 text-xs rounded-full whitespace-nowrap ${status.bgColor} ${status.color}`}>
|
||||
{status.label}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">{plan.fabric_code}-{plan.color_code}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* 左右滑动按钮 */}
|
||||
{producingPlans.length > 1 && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setCurrentPlanIndex(prev => Math.max(0, prev - 1))}
|
||||
disabled={currentPlanIndex === 0}
|
||||
className="absolute left-0 top-1/2 -translate-y-1/2 -translate-x-2 w-8 h-8 bg-white shadow-lg rounded-full flex items-center justify-center disabled:opacity-30 disabled:cursor-not-allowed hover:bg-gray-50 transition-colors z-10"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 text-gray-600" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentPlanIndex(prev => Math.min(producingPlans.length - 1, prev + 1))}
|
||||
disabled={currentPlanIndex === producingPlans.length - 1}
|
||||
className="absolute right-0 top-1/2 -translate-y-1/2 translate-x-2 w-8 h-8 bg-white shadow-lg rounded-full flex items-center justify-center disabled:opacity-30 disabled:cursor-not-allowed hover:bg-gray-50 transition-colors z-10"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 text-gray-600 rotate-180" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 指示器 */}
|
||||
{producingPlans.length > 1 && (
|
||||
<div className="flex items-center justify-center gap-1.5 mt-4">
|
||||
{producingPlans.map((_, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => setCurrentPlanIndex(i)}
|
||||
className={`w-2 h-2 rounded-full transition-all ${
|
||||
i === currentPlanIndex ? 'bg-blue-500 w-4' : 'bg-gray-300 hover:bg-gray-400'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
{/* 快捷操作 - 移动端优化 */}
|
||||
<motion.div variants={itemVariants} className="bg-white rounded-xl sm:rounded-2xl shadow-sm border border-gray-100 p-3 sm:p-4 md:p-6">
|
||||
<h2 className="text-sm sm:text-base font-semibold text-gray-900 mb-3 sm:mb-4 whitespace-nowrap">快捷操作</h2>
|
||||
<div className="grid grid-cols-3 sm:grid-cols-4 gap-2 sm:gap-3">
|
||||
{getQuickActions(auth.user?.is_master || false).map((action, index) => (
|
||||
{getQuickActions().map((action, index) => (
|
||||
<motion.button
|
||||
key={action.id}
|
||||
whileHover={isMobile ? {} : { scale: 1.03, y: -2 }}
|
||||
@ -220,88 +345,6 @@ export function PurchaserDashboard() {
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* 最近计划 - 移动端优化 */}
|
||||
<motion.div variants={itemVariants} className="bg-white rounded-xl sm:rounded-2xl shadow-sm border border-gray-100 p-3 sm:p-4 md:p-6">
|
||||
<div className="flex justify-between items-center mb-3 sm:mb-4">
|
||||
<h2 className="text-sm sm:text-base font-semibold text-gray-900 whitespace-nowrap">最近计划</h2>
|
||||
<button
|
||||
onClick={() => navigate('/purchaser/plans')}
|
||||
className="text-xs sm:text-sm text-blue-600 hover:text-blue-700 font-medium flex items-center gap-1 whitespace-nowrap"
|
||||
>
|
||||
查看全部
|
||||
<ArrowLeft className="w-3 h-3 sm:w-4 sm:h-4 rotate-180" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{recentPlans.length === 0 ? (
|
||||
<div className="text-center py-8 sm:py-12 text-gray-400">
|
||||
<Package className="w-10 h-10 sm:w-12 sm:h-12 mx-auto mb-3 text-gray-300" />
|
||||
<p className="text-sm">暂无计划</p>
|
||||
<button
|
||||
onClick={() => navigate('/purchaser/plans/new')}
|
||||
className="mt-3 px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
创建计划
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2 sm:space-y-3">
|
||||
{recentPlans.map((plan, i) => {
|
||||
const progress = plan.target_quantity > 0
|
||||
? Math.round((plan.completed_quantity / plan.target_quantity) * 100)
|
||||
: 0;
|
||||
const status = statusMap[plan.status] || statusMap.pending;
|
||||
const StatusIcon = status.icon;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={plan.id}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: i * 0.1 }}
|
||||
onClick={() => navigate(`/purchaser/plans?id=${plan.id}`)}
|
||||
className="group flex items-center gap-2 sm:gap-4 p-3 sm:p-4 rounded-xl border border-gray-100 hover:border-blue-200 hover:bg-blue-50/30 cursor-pointer transition-all"
|
||||
>
|
||||
<div className={`w-8 h-8 sm:w-10 sm:h-10 rounded-lg flex items-center justify-center ${status.bgColor}`}>
|
||||
<StatusIcon className={`w-4 h-4 sm:w-5 sm:h-5 ${status.color}`} />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-1.5 sm:gap-2 mb-0.5 sm:mb-1">
|
||||
<h3 className="font-medium text-gray-900 text-xs sm:text-sm truncate">{plan.product_name}-{plan.color}</h3>
|
||||
<span className={`px-1.5 py-0.5 text-xs rounded-full whitespace-nowrap ${status.bgColor} ${status.color}`}>
|
||||
{status.label}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">{plan.fabric_code}-{plan.color_code}</p>
|
||||
</div>
|
||||
|
||||
<div className="text-right hidden sm:block">
|
||||
<p className="text-sm font-medium text-gray-900">{plan.target_quantity}米</p>
|
||||
<p className="text-xs text-gray-400">计划产量</p>
|
||||
</div>
|
||||
|
||||
<div className="w-16 sm:w-24">
|
||||
<div className="flex items-center justify-between text-xs text-gray-500 mb-1">
|
||||
<span className="hidden sm:inline">进度</span>
|
||||
<span>{progress}%</span>
|
||||
</div>
|
||||
<div className="h-1 sm:h-1.5 bg-gray-100 rounded-full overflow-hidden">
|
||||
<motion.div
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: `${progress}%` }}
|
||||
transition={{ duration: 0.8, delay: i * 0.1 }}
|
||||
className="h-full bg-gradient-to-r from-blue-500 to-indigo-500 rounded-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
@ -330,6 +373,173 @@ export function PurchaserDashboard() {
|
||||
isOpen={showUpdate}
|
||||
onClose={() => setShowUpdate(false)}
|
||||
/>
|
||||
|
||||
{/* 账户卡片弹窗 */}
|
||||
{showAccountCard && (
|
||||
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4" onClick={() => setShowAccountCard(false)}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
className="bg-white rounded-2xl w-full max-w-sm shadow-2xl overflow-hidden"
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
{/* 头部 */}
|
||||
<div className="bg-gradient-to-r from-blue-500 to-indigo-600 p-6 text-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xl font-bold">账户信息</h2>
|
||||
<button onClick={() => setShowAccountCard(false)} className="p-2 hover:bg-white/20 rounded-lg transition-colors">
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 账户详情 */}
|
||||
<div className="p-6 space-y-4">
|
||||
{/* 用户信息 */}
|
||||
<div className="flex items-center gap-4">
|
||||
<label className="w-16 h-16 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-xl flex items-center justify-center shadow-lg cursor-pointer hover:opacity-90 transition-opacity overflow-hidden relative group">
|
||||
{(auth.user as any)?.image_url ? (
|
||||
<img src={(auth.user as any).image_url} alt="头像" className="w-full h-full object-cover relative z-10" />
|
||||
) : (
|
||||
<Building2 className="w-8 h-8 text-white relative z-10" />
|
||||
)}
|
||||
<div className="absolute inset-0 bg-black/30 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity z-20">
|
||||
<Camera className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={async (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
// 上传头像到 Supabase Storage
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) return;
|
||||
|
||||
try {
|
||||
const fileExt = file.name.split('.').pop();
|
||||
const fileName = `${user.id}-${Date.now()}.${fileExt}`;
|
||||
|
||||
// 读取文件为 ArrayBuffer
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
|
||||
const { error: uploadError } = await supabase.storage
|
||||
.from('avatars')
|
||||
.upload(fileName, arrayBuffer, {
|
||||
contentType: file.type,
|
||||
upsert: true
|
||||
});
|
||||
|
||||
if (uploadError) {
|
||||
console.error('头像上传失败:', uploadError);
|
||||
alert('头像上传失败: ' + uploadError.message);
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取头像 URL
|
||||
const { data: { publicUrl } } = supabase.storage
|
||||
.from('avatars')
|
||||
.getPublicUrl(fileName);
|
||||
|
||||
// 更新用户头像
|
||||
const { error: updateError } = await supabase
|
||||
.from('profiles')
|
||||
.update({ image_url: publicUrl } as any)
|
||||
.eq('id', user.id);
|
||||
|
||||
if (updateError) {
|
||||
console.error('头像更新失败:', updateError);
|
||||
alert('头像更新失败: ' + updateError.message);
|
||||
} else {
|
||||
// 刷新页面以显示新头像
|
||||
window.location.reload();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('头像上传出错:', err);
|
||||
alert('头像上传出错,请重试');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900 text-lg truncate max-w-[200px]">{(auth.user as any)?.display_name || auth.user?.username || '用户'}</p>
|
||||
<p className="text-sm text-gray-500">{auth.company?.name || '采购商'}</p>
|
||||
<span className="inline-flex items-center gap-1 text-xs bg-amber-100 text-amber-700 px-2 py-0.5 rounded-full mt-1">
|
||||
{auth.user?.is_master ? '主账号' : '子账号'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 功能菜单 */}
|
||||
<div className="space-y-2 pt-4 border-t border-gray-100">
|
||||
<button
|
||||
onClick={() => { setShowAccountCard(false); navigate('/members'); }}
|
||||
className="w-full flex items-center gap-3 p-3 rounded-xl hover:bg-gray-50 transition-colors text-left"
|
||||
>
|
||||
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
|
||||
<Users className="w-5 h-5 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">账号管理</p>
|
||||
<p className="text-xs text-gray-500">管理团队成员和账户设置</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => { setShowAccountCard(false); setShowUserManual(true); }}
|
||||
className="w-full flex items-center gap-3 p-3 rounded-xl hover:bg-gray-50 transition-colors text-left"
|
||||
>
|
||||
<div className="w-10 h-10 bg-emerald-100 rounded-lg flex items-center justify-center">
|
||||
<BookOpen className="w-5 h-5 text-emerald-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">用户手册</p>
|
||||
<p className="text-xs text-gray-500">查看详细使用指南</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => { setShowAccountCard(false); setShowFeedback(true); }}
|
||||
className="w-full flex items-center gap-3 p-3 rounded-xl hover:bg-gray-50 transition-colors text-left"
|
||||
>
|
||||
<div className="w-10 h-10 bg-purple-100 rounded-lg flex items-center justify-center">
|
||||
<MessageSquare className="w-5 h-5 text-purple-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">用户反馈</p>
|
||||
<p className="text-xs text-gray-500">提交问题或建议</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => { setShowAccountCard(false); setShowAbout(true); }}
|
||||
className="w-full flex items-center gap-3 p-3 rounded-xl hover:bg-gray-50 transition-colors text-left"
|
||||
>
|
||||
<div className="w-10 h-10 bg-gray-100 rounded-lg flex items-center justify-center">
|
||||
<Info className="w-5 h-5 text-gray-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">关于</p>
|
||||
<p className="text-xs text-gray-500">版本信息与版权</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 退出登录 */}
|
||||
<button
|
||||
onClick={async () => { await logout(); navigate('/login'); }}
|
||||
className="w-full py-3 bg-red-50 hover:bg-red-100 text-red-600 rounded-xl transition-colors flex items-center justify-center gap-2 font-medium"
|
||||
>
|
||||
<LogOut className="w-5 h-5" />
|
||||
退出登录
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ interface YarnInput {
|
||||
yarn_type?: 'warp' | 'weft' | 'all';
|
||||
}
|
||||
|
||||
// 产品类型定义(来自原坯布仓库)
|
||||
// 产品类型定义(来自产品信息)
|
||||
interface Product {
|
||||
id: string;
|
||||
product_name: string;
|
||||
@ -49,7 +49,7 @@ export function NewPlan() {
|
||||
const navigate = useNavigate();
|
||||
const { auth } = useAuth();
|
||||
|
||||
// 从原坯布仓库选择的产品
|
||||
// 从产品信息选择的产品
|
||||
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
|
||||
const [products, setProducts] = useState<Product[]>([]);
|
||||
const [productRatios, setProductRatios] = useState<Record<string, ProductYarnRatio[]>>({});
|
||||
@ -78,7 +78,7 @@ export function NewPlan() {
|
||||
fetchProducts();
|
||||
}, []);
|
||||
|
||||
// 从原坯布仓库获取产品数据
|
||||
// 从产品信息获取产品数据
|
||||
const fetchProducts = async () => {
|
||||
if (!auth.company) return;
|
||||
setLoading(true);
|
||||
@ -288,10 +288,10 @@ export function NewPlan() {
|
||||
|
||||
<div className="max-w-4xl mx-auto px-4 py-6">
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* 原坯布仓库产品选择 - 搜索+筛选形式 */}
|
||||
{/* 产品信息选择 - 搜索+筛选形式 */}
|
||||
<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>选择原坯布产品
|
||||
<span className="w-1 h-4 bg-blue-500 rounded"></span>选择产品
|
||||
</h2>
|
||||
{loading ? (
|
||||
<div className="text-center py-8 text-gray-400">加载中...</div>
|
||||
@ -511,7 +511,7 @@ export function NewPlan() {
|
||||
<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>
|
||||
<label className="block text-sm text-gray-600 mb-1">每米纱用量(g/m)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={selectedProduct?.total_weight ? `${selectedProduct.total_weight}g/m` : (selectedProduct?.weight ? `${selectedProduct.weight}g/m` : '-')}
|
||||
@ -520,22 +520,12 @@ export function NewPlan() {
|
||||
/>
|
||||
<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?.yarn_usage_per_meter ? `${selectedProduct.yarn_usage_per_meter}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>
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm text-gray-600 mb-1">纱线名称/占比</label>
|
||||
<div className="w-full px-3 py-2 border rounded-lg bg-gray-100 text-gray-700 min-h-[42px]">
|
||||
<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">
|
||||
{yarns.filter(y => y.name).map((yarn, idx) => (
|
||||
<span key={yarn.id} className="inline-block mr-2">
|
||||
{yarnTypeLabels[yarn.yarn_type || 'all']}/{yarn.name}:{yarn.ratio}{idx < yarns.filter(y => y.name).length - 1 ? '、' : ''}
|
||||
<span key={yarn.id} className="inline-block whitespace-nowrap">
|
||||
{yarnTypeLabels[yarn.yarn_type || 'all']}:{yarn.name} 占比:{Number(yarn.ratio).toFixed(0)}%
|
||||
</span>
|
||||
))}
|
||||
{!yarns.some(y => y.name) && <span className="text-gray-400">-</span>}
|
||||
@ -629,7 +619,7 @@ export function NewPlan() {
|
||||
>
|
||||
{/* 弹窗头部 */}
|
||||
<div className="p-4 border-b flex items-center justify-between bg-gray-50">
|
||||
<h3 className="text-lg font-semibold text-gray-900">选择原坯布产品</h3>
|
||||
<h3 className="text-lg font-semibold text-gray-900">选择产品</h3>
|
||||
<button
|
||||
onClick={() => setShowProductModal(false)}
|
||||
className="p-2 hover:bg-gray-200 rounded-full transition-colors"
|
||||
|
||||
@ -36,7 +36,7 @@ export function NewWashingPlan() {
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
// 获取原坯布仓库产品(包含库存计算)
|
||||
// 获取产品信息(包含库存计算)
|
||||
const fetchProducts = useCallback(async () => {
|
||||
if (!auth.company) return;
|
||||
|
||||
@ -295,7 +295,7 @@ export function NewWashingPlan() {
|
||||
{products.length === 0 && (
|
||||
<p className="text-xs text-amber-600 mt-2 flex items-center gap-1">
|
||||
<AlertCircle className="w-3 h-3" />
|
||||
原坯布仓库中没有可用产品,请先创建产品并入库
|
||||
产品信息中没有可用产品,请先创建产品并入库
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -169,6 +169,38 @@ export function WarehouseManage() {
|
||||
image_url: formData.image_url || null
|
||||
};
|
||||
|
||||
// 校验1:产品码全局唯一
|
||||
const { data: existingFabricCode } = await supabase
|
||||
.from('products')
|
||||
.select('id, product_name, color')
|
||||
.eq('company_id', auth.company.id)
|
||||
.eq('fabric_code', productData.fabric_code)
|
||||
.neq('id', editingProduct?.id || '00000000-0000-0000-0000-000000000000')
|
||||
.maybeSingle();
|
||||
|
||||
if (existingFabricCode) {
|
||||
alert(`产品码 "${productData.fabric_code}" 已存在,请使用其他产品码`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 校验2:色号在"成品名称-克重-颜色"组合中唯一
|
||||
// 即:同一个"成品名称-克重-颜色"下,色号不能重复
|
||||
const { data: existingColorCode } = await supabase
|
||||
.from('products')
|
||||
.select('id, fabric_code')
|
||||
.eq('company_id', auth.company.id)
|
||||
.eq('product_name', productData.product_name)
|
||||
.eq('total_weight', productData.total_weight || 0)
|
||||
.eq('color', productData.color)
|
||||
.eq('color_code', productData.color_code)
|
||||
.neq('id', editingProduct?.id || '00000000-0000-0000-0000-000000000000')
|
||||
.maybeSingle();
|
||||
|
||||
if (existingColorCode) {
|
||||
alert(`"${productData.product_name}-${productData.total_weight}g-${productData.color}" 组合中已存在色号 "${productData.color_code}",请使用其他色号`);
|
||||
return;
|
||||
}
|
||||
|
||||
let productId: string;
|
||||
|
||||
if (editingProduct) {
|
||||
@ -415,7 +447,7 @@ export function WarehouseManage() {
|
||||
>
|
||||
<ArrowLeft className="w-5 h-5 text-gray-600" />
|
||||
</button>
|
||||
<h1 className="text-base sm:text-lg font-bold text-gray-900">原坯布仓库</h1>
|
||||
<h1 className="text-base sm:text-lg font-bold text-gray-900">产品信息</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { supabase } from '../../supabase/client';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Link2, Warehouse, CreditCard, ArrowLeft, Users, Factory, LogOut, HelpCircle } from 'lucide-react';
|
||||
import { Link2, Warehouse, CreditCard, ArrowLeft, Users, Factory, LogOut, HelpCircle, X, BookOpen, MessageSquare, Info, Camera, FileText, TrendingUp, Package } from 'lucide-react';
|
||||
import { useResponsive } from '../../hooks/useResponsive';
|
||||
import { OnboardingGuide, shouldShowOnboarding } from '../../components/OnboardingGuide';
|
||||
import { NotificationCenter } from '../../components/NotificationCenter';
|
||||
@ -14,60 +14,29 @@ import { UserManual } from '../../components/UserManual';
|
||||
import { CrashReporter } from '../../components/CrashReporter';
|
||||
import type { ProductionPlan } from '../../types';
|
||||
|
||||
const getQuickActions = (isMaster: boolean) => {
|
||||
const actions = [
|
||||
{ id: 'plans', icon: Link2, label: '计划总览', path: '/textile/plans', gradient: 'from-emerald-500 to-green-500' },
|
||||
{ id: 'fabric', icon: Warehouse, label: '已生产坯布', path: '/textile/fabric-warehouse', gradient: 'from-indigo-500 to-purple-500' },
|
||||
{ id: 'yarn', icon: Warehouse, label: '原料纱仓库', path: '/textile/yarn-warehouse', gradient: 'from-blue-500 to-cyan-500' },
|
||||
{ id: 'payment', icon: CreditCard, label: '待结款', path: '/textile/payments', gradient: 'from-amber-500 to-orange-500' }
|
||||
];
|
||||
if (isMaster) {
|
||||
actions.push({ id: 'members', icon: Users, label: '账号管理', path: '/members', gradient: 'from-rose-500 to-pink-500' });
|
||||
}
|
||||
return actions;
|
||||
const statusMap: Record<string, { label: string; color: string; bgColor: string; icon: any }> = {
|
||||
pending: { label: '待确定', color: 'text-emerald-500', bgColor: 'bg-emerald-50', icon: HelpCircle },
|
||||
producing: { label: '生产中', color: 'text-emerald-500', bgColor: 'bg-emerald-50', icon: TrendingUp },
|
||||
completed: { label: '已完成', color: 'text-teal-500', bgColor: 'bg-teal-50', icon: Package }
|
||||
};
|
||||
|
||||
const statusMap: Record<string, { label: string; color: string }> = {
|
||||
pending: { label: '待确定', color: 'bg-yellow-100 text-yellow-700' },
|
||||
producing: { label: '生产中', color: 'bg-blue-100 text-blue-700' },
|
||||
completed: { label: '已完成', color: 'bg-green-100 text-green-700' }
|
||||
const getQuickActions = () => {
|
||||
return [
|
||||
{ id: 'plans', icon: FileText, label: '计划总览', path: '/textile/plans', gradient: 'from-emerald-400 to-teal-400', description: '查看所有计划' },
|
||||
{ id: 'fabric', icon: Warehouse, label: '已生产坯布', path: '/textile/fabric-warehouse', gradient: 'from-teal-400 to-cyan-400', description: '管理坯布库存' },
|
||||
{ id: 'yarn', icon: Warehouse, label: '原料纱仓库', path: '/textile/yarn-warehouse', gradient: 'from-cyan-400 to-emerald-400', description: '管理纱线库存' },
|
||||
{ id: 'payment', icon: CreditCard, label: '待结款', path: '/textile/payments', gradient: 'from-emerald-400 to-green-400', description: '查看待结款项' }
|
||||
];
|
||||
};
|
||||
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.08,
|
||||
delayChildren: 0.05
|
||||
}
|
||||
}
|
||||
visible: { opacity: 1, transition: { staggerChildren: 0.1 } }
|
||||
};
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { opacity: 0, y: 15, scale: 0.98 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
scale: 1,
|
||||
transition: {
|
||||
duration: 0.35,
|
||||
ease: [0.25, 0.46, 0.45, 0.94]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const cardHoverVariants = {
|
||||
rest: { scale: 1, y: 0 },
|
||||
hover: {
|
||||
scale: 1.02,
|
||||
y: -4,
|
||||
transition: {
|
||||
duration: 0.25,
|
||||
ease: [0.25, 0.46, 0.45, 0.94]
|
||||
}
|
||||
},
|
||||
tap: { scale: 0.98, y: 0 }
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
visible: { opacity: 1, y: 0, transition: { duration: 0.4 } }
|
||||
};
|
||||
|
||||
export function TextileDashboard() {
|
||||
@ -85,6 +54,9 @@ export function TextileDashboard() {
|
||||
const [showFeedback, setShowFeedback] = useState(false);
|
||||
const [showAbout, setShowAbout] = useState(false);
|
||||
|
||||
// 账户卡片弹窗状态
|
||||
const [showAccountCard, setShowAccountCard] = useState(false);
|
||||
|
||||
// 版本更新检查
|
||||
const { showUpdate, setShowUpdate } = useVersionCheck();
|
||||
|
||||
@ -152,23 +124,41 @@ export function TextileDashboard() {
|
||||
|
||||
// 过滤掉被拒绝的计划
|
||||
const filteredPlans = plans.filter(p => !rejectedPlanIds.has(p.id));
|
||||
const recentPlans = filteredPlans.slice(0, 5);
|
||||
const producingPlans = filteredPlans.filter(p => p.status === 'producing').slice(0, 5);
|
||||
const [currentPlanIndex, setCurrentPlanIndex] = useState(0);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-green-50 to-emerald-50">
|
||||
{/* 新用户引导动画 - 已更新为弹窗模式 */}
|
||||
<header className="bg-gradient-to-r from-emerald-600 to-green-600 shadow-lg">
|
||||
<div className="max-w-7xl mx-auto px-4 md:px-8 py-4 md:py-5 flex justify-between items-center">
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-emerald-50/30 to-green-50/30">
|
||||
{/* Header - 与采购商保持一致 */}
|
||||
<header className="bg-white/80 backdrop-blur-xl border-b border-gray-200/50 sticky top-0 z-10">
|
||||
<div className="max-w-7xl mx-auto px-3 sm:px-4 md:px-8 py-3 sm:py-4 flex justify-between items-center">
|
||||
<div className="flex items-center gap-2 sm:gap-3">
|
||||
<div className="w-8 h-8 sm:w-10 sm:h-10 bg-white/20 rounded-xl flex items-center justify-center">
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={() => setShowAccountCard(true)}
|
||||
className="w-8 h-8 sm:w-10 sm:h-10 bg-gradient-to-br from-emerald-500 to-green-600 rounded-xl flex items-center justify-center shadow-lg shadow-emerald-500/20 cursor-pointer overflow-hidden"
|
||||
>
|
||||
{(auth.user as any)?.image_url ? (
|
||||
<img
|
||||
src={(auth.user as any).image_url}
|
||||
alt="头像"
|
||||
className="w-full h-full object-cover"
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Factory className="w-4 h-4 sm:w-5 sm:h-5 text-white" />
|
||||
</div>
|
||||
)}
|
||||
</motion.button>
|
||||
<div>
|
||||
<h1 className="text-sm sm:text-lg font-bold text-white flex items-center gap-2 whitespace-nowrap">
|
||||
{auth.company?.name || '纺织厂'}工作台
|
||||
<span className="text-[10px] sm:text-xs bg-white/20 text-white px-1.5 sm:px-2 py-0.5 rounded-full font-medium whitespace-nowrap">限时免费</span>
|
||||
<h1 className="text-sm sm:text-lg font-bold text-gray-900 flex items-center gap-2 whitespace-nowrap">
|
||||
<span className="sm:hidden truncate max-w-[80px]">{(auth.user as any)?.display_name || auth.user?.username || '用户'}</span>
|
||||
<span className="hidden sm:inline">{auth.company?.name || '纺织厂'}工作台</span>
|
||||
<span className="text-[10px] sm:text-xs bg-gradient-to-r from-emerald-500 to-green-500 text-white px-1.5 sm:px-2 py-0.5 rounded-full font-medium whitespace-nowrap">限时免费</span>
|
||||
</h1>
|
||||
<p className="text-xs text-white/70 hidden sm:block">{(auth.user as any)?.display_name || auth.user?.username || '用户'},欢迎回来</p>
|
||||
<p className="text-xs text-gray-500 hidden sm:block truncate max-w-[150px]">{(auth.user as any)?.display_name || auth.user?.username || '用户'},欢迎回来</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
@ -176,129 +166,208 @@ export function TextileDashboard() {
|
||||
<NotificationCenter theme="emerald" />
|
||||
<button
|
||||
onClick={async () => { await logout(); navigate('/login'); }}
|
||||
className="p-2 sm:px-3 sm:py-1 text-white/70 hover:text-white hover:bg-white/10 rounded-lg transition-colors"
|
||||
className="p-2 sm:px-3 sm:py-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors"
|
||||
title="退出登录"
|
||||
>
|
||||
<LogOut className="w-5 h-5 sm:hidden" />
|
||||
<span className="hidden sm:inline text-xs md:text-sm">退出登录</span>
|
||||
<span className="hidden sm:inline text-xs sm:text-sm">退出</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="p-4 md:p-8 max-w-7xl mx-auto">
|
||||
<div className="p-3 sm:p-4 md:p-8 max-w-7xl mx-auto">
|
||||
{loading ? (
|
||||
<div className="text-center py-12 text-gray-400">加载中...</div>
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<div className="w-8 h-8 border-2 border-emerald-500 border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
<motion.div variants={containerVariants} initial="hidden" animate="visible">
|
||||
<div className="grid grid-cols-3 gap-2 sm:gap-3 md:gap-6 mb-4 sm:mb-6 md:mb-8">
|
||||
<motion.div variants={containerVariants} initial="hidden" animate="visible" className="space-y-4 sm:space-y-6">
|
||||
{/* 统计卡片 - 与采购商保持一致 */}
|
||||
<div className="grid grid-cols-3 gap-2 sm:gap-4">
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
whileHover={isMobile ? {} : { scale: 1.02, y: -2 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
className="bg-gradient-to-br from-emerald-500 to-green-500 rounded-xl md:rounded-2xl shadow-lg shadow-emerald-500/20 p-3 sm:p-4 md:p-6 text-white will-change-transform"
|
||||
onClick={() => navigate('/textile/plans')}
|
||||
className="bg-white rounded-xl sm:rounded-2xl p-3 sm:p-5 shadow-sm border border-gray-100 cursor-pointer hover:border-emerald-300 hover:shadow-md transition-all"
|
||||
>
|
||||
<p className="text-[10px] sm:text-xs md:text-sm text-emerald-100 mb-0.5 sm:mb-1 md:mb-2">所有计划</p>
|
||||
<p className="text-xl sm:text-2xl md:text-4xl font-bold">{stats.all}</p>
|
||||
<div className="flex items-center justify-between mb-2 sm:mb-3">
|
||||
<p className="text-xs sm:text-sm text-gray-500">所有计划</p>
|
||||
<div className="w-6 h-6 sm:w-8 sm:h-8 bg-emerald-50 rounded-lg flex items-center justify-center">
|
||||
<FileText className="w-3 h-3 sm:w-4 sm:h-4 text-emerald-600" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xl sm:text-3xl font-bold text-gray-900">{stats.all}</p>
|
||||
<p className="text-xs text-gray-400 mt-0.5 sm:mt-1">累计关联</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
whileHover={isMobile ? {} : { scale: 1.02, y: -2 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
className="bg-gradient-to-br from-blue-500 to-cyan-500 rounded-xl md:rounded-2xl shadow-lg shadow-blue-500/20 p-3 sm:p-4 md:p-6 text-white will-change-transform"
|
||||
onClick={() => navigate('/textile/plans?filter=producing')}
|
||||
className="bg-white rounded-xl sm:rounded-2xl p-3 sm:p-5 shadow-sm border border-gray-100 cursor-pointer hover:border-emerald-300 hover:shadow-md transition-all"
|
||||
>
|
||||
<p className="text-[10px] sm:text-xs md:text-sm text-blue-100 mb-0.5 sm:mb-1 md:mb-2">进行中</p>
|
||||
<p className="text-xl sm:text-2xl md:text-4xl font-bold">{stats.producing}</p>
|
||||
<div className="flex items-center justify-between mb-2 sm:mb-3">
|
||||
<p className="text-xs sm:text-sm text-gray-500">进行中</p>
|
||||
<div className="w-6 h-6 sm:w-8 sm:h-8 bg-emerald-50 rounded-lg flex items-center justify-center">
|
||||
<TrendingUp className="w-3 h-3 sm:w-4 sm:h-4 text-emerald-600" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xl sm:text-3xl font-bold text-gray-900">{stats.producing}</p>
|
||||
<p className="text-xs text-gray-400 mt-0.5 sm:mt-1">正在生产</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
whileHover={isMobile ? {} : { scale: 1.02, y: -2 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
className="bg-gradient-to-br from-amber-500 to-orange-500 rounded-xl md:rounded-2xl shadow-lg shadow-amber-500/20 p-3 sm:p-4 md:p-6 text-white will-change-transform"
|
||||
onClick={() => navigate('/textile/plans?filter=completed')}
|
||||
className="bg-white rounded-xl sm:rounded-2xl p-3 sm:p-5 shadow-sm border border-gray-100 cursor-pointer hover:border-amber-300 hover:shadow-md transition-all"
|
||||
>
|
||||
<p className="text-[10px] sm:text-xs md:text-sm text-amber-100 mb-0.5 sm:mb-1 md:mb-2">已完成</p>
|
||||
<p className="text-xl sm:text-2xl md:text-4xl font-bold">{stats.completed}</p>
|
||||
<div className="flex items-center justify-between mb-2 sm:mb-3">
|
||||
<p className="text-xs sm:text-sm text-gray-500">已完成</p>
|
||||
<div className="w-6 h-6 sm:w-8 sm:h-8 bg-amber-50 rounded-lg flex items-center justify-center">
|
||||
<Package className="w-3 h-3 sm:w-4 sm:h-4 text-amber-600" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xl sm:text-3xl font-bold text-gray-900">{stats.completed}</p>
|
||||
<p className="text-xs text-gray-400 mt-0.5 sm:mt-1">已入库</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<motion.div variants={itemVariants} className="bg-white/80 backdrop-blur rounded-2xl shadow-sm p-3 sm:p-4 md:p-6 mb-4 sm:mb-6 md:mb-8 border border-white/50">
|
||||
<h2 className="text-sm sm:text-base md:text-lg font-semibold text-gray-800 mb-3 sm:mb-4 md:mb-6">快捷操作</h2>
|
||||
<div className="grid grid-cols-3 gap-2 sm:gap-3 md:gap-6">
|
||||
{getQuickActions(auth.user?.is_master || false).map((action, index) => (
|
||||
<motion.button
|
||||
key={action.id}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.1 + index * 0.05, duration: 0.3 }}
|
||||
whileHover={isMobile ? {} : { scale: 1.03, y: -3 }}
|
||||
whileTap={{ scale: 0.97 }}
|
||||
onClick={() => navigate(action.path)}
|
||||
className="flex flex-col items-center justify-center p-3 sm:p-4 md:p-6 rounded-xl border border-gray-200 hover:border-transparent hover:shadow-md transition-all duration-200 group will-change-transform"
|
||||
{/* 最近计划 - 左右滑动展示 - 与采购商保持一致 */}
|
||||
<motion.div variants={itemVariants} className="bg-white rounded-xl sm:rounded-2xl shadow-sm border border-gray-100 p-3 sm:p-4 md:p-6">
|
||||
<div className="flex justify-between items-center mb-3 sm:mb-4">
|
||||
<h2 className="text-sm sm:text-base font-semibold text-gray-900 whitespace-nowrap">最近计划</h2>
|
||||
<button
|
||||
onClick={() => navigate('/textile/plans')}
|
||||
className="text-xs sm:text-sm text-emerald-600 hover:text-emerald-700 font-medium flex items-center gap-1 whitespace-nowrap"
|
||||
>
|
||||
<div className={`w-10 h-10 sm:w-12 sm:h-12 md:w-14 md:h-14 rounded-xl flex items-center justify-center mb-2 sm:mb-3 md:mb-4 bg-gradient-to-br ${action.gradient} shadow-lg transition-shadow duration-200 group-hover:shadow-xl`}>
|
||||
<action.icon className="w-5 h-5 sm:w-6 sm:h-6 md:w-7 md:h-7 text-white" />
|
||||
查看全部
|
||||
<ArrowLeft className="w-3 h-3 sm:w-4 sm:h-4 rotate-180" />
|
||||
</button>
|
||||
</div>
|
||||
<span className="text-xs sm:text-sm md:text-base text-gray-700 group-hover:text-gray-900 transition-colors">{action.label}</span>
|
||||
</motion.button>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div variants={itemVariants} className="bg-white/80 backdrop-blur rounded-2xl shadow-sm p-3 sm:p-4 md:p-6 border border-white/50">
|
||||
<div className="flex justify-between items-center mb-3 sm:mb-4 md:mb-6">
|
||||
<h2 className="text-sm sm:text-base md:text-lg font-semibold text-gray-800">最近计划</h2>
|
||||
<button onClick={() => navigate('/textile/plans')} className="text-xs sm:text-sm text-emerald-600 hover:text-emerald-700 font-medium">查看全部</button>
|
||||
{producingPlans.length === 0 ? (
|
||||
<div className="text-center py-8 sm:py-12 text-gray-400">
|
||||
<Package className="w-10 h-10 sm:w-12 sm:h-12 mx-auto mb-3 text-gray-300" />
|
||||
<p className="text-sm">暂无进行中的计划</p>
|
||||
<p className="text-xs text-gray-400 mt-1">等待采购商分配计划</p>
|
||||
</div>
|
||||
{recentPlans.length === 0 ? (
|
||||
<p className="text-center text-gray-400 py-4 sm:py-6 md:py-8">暂无关联计划</p>
|
||||
) : (
|
||||
<div className="space-y-2 sm:space-y-3 md:space-y-4">
|
||||
{recentPlans.map((plan, i) => {
|
||||
<div className="relative">
|
||||
{/* 滑动容器 */}
|
||||
<div className="overflow-hidden">
|
||||
<motion.div
|
||||
className="flex"
|
||||
animate={{ x: -currentPlanIndex * 100 + '%' }}
|
||||
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
|
||||
>
|
||||
{producingPlans.map((plan, i) => {
|
||||
const progress = plan.target_quantity > 0
|
||||
? Math.round((plan.completed_quantity / plan.target_quantity) * 100)
|
||||
: 0;
|
||||
const status = statusMap[plan.status] || statusMap.pending;
|
||||
const StatusIcon = status.icon;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
<div
|
||||
key={plan.id}
|
||||
initial={{ opacity: 0, x: -15 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: i * 0.08, duration: 0.35, ease: [0.25, 0.46, 0.45, 0.94] }}
|
||||
whileHover={isMobile ? {} : { x: 4, backgroundColor: 'rgba(236, 253, 245, 0.8)' }}
|
||||
whileTap={{ scale: 0.99 }}
|
||||
className="w-full flex-shrink-0 px-1"
|
||||
onClick={() => navigate(`/textile/plans?id=${plan.id}`)}
|
||||
className="border border-gray-200 rounded-xl p-3 sm:p-4 md:p-5 hover:border-emerald-300 cursor-pointer transition-all duration-200 will-change-transform"
|
||||
>
|
||||
<div className="flex justify-between items-start mb-1.5 sm:mb-2 md:mb-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-xs sm:text-sm text-gray-600 truncate">{plan.fabric_code}</p>
|
||||
<p className="text-[10px] sm:text-xs text-gray-400 mt-0.5 sm:mt-1">{plan.plan_code}</p>
|
||||
<div className="group p-3 sm:p-4 rounded-xl border border-gray-100 hover:border-emerald-200 hover:bg-emerald-50/30 cursor-pointer transition-all">
|
||||
{/* 计划标题行 */}
|
||||
<div className="flex items-center gap-2 sm:gap-3">
|
||||
<div className={`w-8 h-8 sm:w-10 sm:h-10 rounded-lg flex items-center justify-center ${status.bgColor}`}>
|
||||
<StatusIcon className={`w-4 h-4 sm:w-5 sm:h-5 ${status.color}`} />
|
||||
</div>
|
||||
<span className={`px-2 py-0.5 sm:py-1 md:px-3 md:py-1.5 rounded-lg text-[10px] sm:text-xs md:text-sm font-medium flex-shrink-0 ml-2 ${statusMap[plan.status]?.color || 'bg-gray-100'}`}>
|
||||
{statusMap[plan.status]?.label || plan.status}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-1.5 sm:gap-2">
|
||||
<h3 className="font-medium text-gray-900 text-sm sm:text-base truncate">{plan.fabric_code}</h3>
|
||||
<span className={`px-1.5 py-0.5 text-xs rounded-full whitespace-nowrap ${status.bgColor} ${status.color}`}>
|
||||
{status.label}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 sm:gap-3 md:gap-4 text-[10px] sm:text-xs md:text-sm text-gray-600">
|
||||
<span>计划: {plan.target_quantity}米</span>
|
||||
<span>已入库: {plan.completed_quantity}米</span>
|
||||
<p className="text-xs text-gray-500">{plan.plan_code}</p>
|
||||
</div>
|
||||
{plan.status === 'producing' && (
|
||||
<div className="mt-2 sm:mt-3 md:mt-4">
|
||||
<div className="w-full bg-gray-200 rounded-full h-1.5 sm:h-2 md:h-3">
|
||||
<motion.div
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: `${progress}%` }}
|
||||
transition={{ duration: 0.8, delay: i * 0.08, ease: [0.25, 0.46, 0.45, 0.94] }}
|
||||
className="bg-gradient-to-r from-emerald-500 to-green-500 h-1.5 sm:h-2 md:h-3 rounded-full will-change-transform"
|
||||
</div>
|
||||
{/* 进度信息 */}
|
||||
<div className="mt-3 sm:mt-4">
|
||||
<div className="flex justify-between text-xs text-gray-500 mb-1.5">
|
||||
<span>进度 {progress}%</span>
|
||||
<span>{plan.completed_quantity}/{plan.target_quantity}米</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-200 rounded-full h-1.5 sm:h-2 overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-emerald-500 to-green-500 rounded-full transition-all duration-500"
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-[10px] sm:text-xs md:text-sm text-gray-500 mt-1 sm:mt-2">进度 {progress}%</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* 左右滑动按钮 */}
|
||||
{producingPlans.length > 1 && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setCurrentPlanIndex(prev => Math.max(0, prev - 1))}
|
||||
disabled={currentPlanIndex === 0}
|
||||
className="absolute left-0 top-1/2 -translate-y-1/2 -translate-x-2 w-8 h-8 bg-white shadow-lg rounded-full flex items-center justify-center disabled:opacity-30 disabled:cursor-not-allowed hover:bg-gray-50 transition-colors z-10"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 text-gray-600" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentPlanIndex(prev => Math.min(producingPlans.length - 1, prev + 1))}
|
||||
disabled={currentPlanIndex === producingPlans.length - 1}
|
||||
className="absolute right-0 top-1/2 -translate-y-1/2 translate-x-2 w-8 h-8 bg-white shadow-lg rounded-full flex items-center justify-center disabled:opacity-30 disabled:cursor-not-allowed hover:bg-gray-50 transition-colors z-10"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 text-gray-600 rotate-180" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 指示器 */}
|
||||
{producingPlans.length > 1 && (
|
||||
<div className="flex items-center justify-center gap-1.5 mt-4">
|
||||
{producingPlans.map((_, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => setCurrentPlanIndex(i)}
|
||||
className={`w-2 h-2 rounded-full transition-all ${
|
||||
i === currentPlanIndex ? 'bg-emerald-500 w-4' : 'bg-gray-300 hover:bg-gray-400'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* 快捷操作 - 与采购商保持一致 */}
|
||||
<motion.div variants={itemVariants} className="bg-white rounded-xl sm:rounded-2xl shadow-sm border border-gray-100 p-3 sm:p-4 md:p-6">
|
||||
<h2 className="text-sm sm:text-base font-semibold text-gray-900 mb-3 sm:mb-4 whitespace-nowrap">快捷操作</h2>
|
||||
<div className="grid grid-cols-3 sm:grid-cols-4 gap-2 sm:gap-3">
|
||||
{getQuickActions().map((action, index) => (
|
||||
<motion.button
|
||||
key={action.id}
|
||||
whileHover={isMobile ? {} : { scale: 1.03, y: -2 }}
|
||||
whileTap={{ scale: 0.97 }}
|
||||
onClick={() => navigate(action.path)}
|
||||
className="group flex flex-col items-center p-2 sm:p-3 rounded-lg sm:rounded-xl border border-gray-100 hover:border-transparent hover:shadow-md transition-all bg-gray-50/50 hover:bg-white"
|
||||
>
|
||||
<div className={`w-8 h-8 sm:w-10 sm:h-10 rounded-lg sm:rounded-xl flex items-center justify-center mb-1 sm:mb-2 bg-gradient-to-br ${action.gradient} shadow-sm group-hover:shadow-md transition-shadow`}>
|
||||
<action.icon className="w-4 h-4 sm:w-5 sm:h-5 text-white" />
|
||||
</div>
|
||||
<span className="text-[10px] sm:text-xs font-medium text-gray-700 group-hover:text-gray-900 text-center whitespace-nowrap">{action.label}</span>
|
||||
</motion.button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
@ -331,6 +400,160 @@ export function TextileDashboard() {
|
||||
|
||||
{/* 崩溃报告 */}
|
||||
<CrashReporter />
|
||||
|
||||
{/* 账户卡片弹窗 */}
|
||||
{showAccountCard && (
|
||||
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4" onClick={() => setShowAccountCard(false)}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
className="bg-white rounded-2xl w-full max-w-sm shadow-2xl overflow-hidden"
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
{/* 头部 */}
|
||||
<div className="bg-gradient-to-r from-emerald-500 to-green-600 p-6 text-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xl font-bold">账户信息</h2>
|
||||
<button onClick={() => setShowAccountCard(false)} className="p-2 hover:bg-white/20 rounded-lg transition-colors">
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 账户详情 */}
|
||||
<div className="p-6 space-y-4">
|
||||
{/* 用户信息 */}
|
||||
<div className="flex items-center gap-4">
|
||||
<label className="w-16 h-16 bg-gradient-to-br from-emerald-500 to-green-600 rounded-xl flex items-center justify-center shadow-lg cursor-pointer hover:opacity-90 transition-opacity overflow-hidden relative group">
|
||||
{(auth.user as any)?.image_url ? (
|
||||
<img src={(auth.user as any).image_url} alt="头像" className="w-full h-full object-cover relative z-10" />
|
||||
) : (
|
||||
<Factory className="w-8 h-8 text-white relative z-10" />
|
||||
)}
|
||||
<div className="absolute inset-0 bg-black/30 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity z-20">
|
||||
<Camera className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={async (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) return;
|
||||
try {
|
||||
const fileExt = file.name.split('.').pop();
|
||||
const fileName = `${user.id}-${Date.now()}.${fileExt}`;
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const { error: uploadError } = await supabase.storage
|
||||
.from('avatars')
|
||||
.upload(fileName, arrayBuffer, {
|
||||
contentType: file.type,
|
||||
upsert: true
|
||||
});
|
||||
if (uploadError) {
|
||||
console.error('头像上传失败:', uploadError);
|
||||
alert('头像上传失败: ' + uploadError.message);
|
||||
return;
|
||||
}
|
||||
const { data: { publicUrl } } = supabase.storage
|
||||
.from('avatars')
|
||||
.getPublicUrl(fileName);
|
||||
const { error: updateError } = await supabase
|
||||
.from('profiles')
|
||||
.update({ image_url: publicUrl } as any)
|
||||
.eq('id', user.id);
|
||||
if (updateError) {
|
||||
console.error('头像更新失败:', updateError);
|
||||
alert('头像更新失败: ' + updateError.message);
|
||||
} else {
|
||||
window.location.reload();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('头像上传出错:', err);
|
||||
alert('头像上传出错,请重试');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900 text-lg truncate max-w-[200px]">{(auth.user as any)?.display_name || auth.user?.username || '用户'}</p>
|
||||
<p className="text-sm text-gray-500">{auth.company?.name || '纺织厂'}</p>
|
||||
<span className="inline-flex items-center gap-1 text-xs bg-emerald-100 text-emerald-700 px-2 py-0.5 rounded-full mt-1">
|
||||
{auth.user?.is_master ? '主账号' : '子账号'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 功能菜单 */}
|
||||
<div className="space-y-2 pt-4 border-t border-gray-100">
|
||||
<button
|
||||
onClick={() => { setShowAccountCard(false); navigate('/members'); }}
|
||||
className="w-full flex items-center gap-3 p-3 rounded-xl hover:bg-gray-50 transition-colors text-left"
|
||||
>
|
||||
<div className="w-10 h-10 bg-emerald-100 rounded-lg flex items-center justify-center">
|
||||
<Users className="w-5 h-5 text-emerald-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">账号管理</p>
|
||||
<p className="text-xs text-gray-500">管理团队成员和账户设置</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => { setShowAccountCard(false); setShowUserManual(true); }}
|
||||
className="w-full flex items-center gap-3 p-3 rounded-xl hover:bg-gray-50 transition-colors text-left"
|
||||
>
|
||||
<div className="w-10 h-10 bg-emerald-100 rounded-lg flex items-center justify-center">
|
||||
<BookOpen className="w-5 h-5 text-emerald-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">用户手册</p>
|
||||
<p className="text-xs text-gray-500">查看详细使用指南</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => { setShowAccountCard(false); setShowFeedback(true); }}
|
||||
className="w-full flex items-center gap-3 p-3 rounded-xl hover:bg-gray-50 transition-colors text-left"
|
||||
>
|
||||
<div className="w-10 h-10 bg-purple-100 rounded-lg flex items-center justify-center">
|
||||
<MessageSquare className="w-5 h-5 text-purple-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">用户反馈</p>
|
||||
<p className="text-xs text-gray-500">提交问题或建议</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => { setShowAccountCard(false); setShowAbout(true); }}
|
||||
className="w-full flex items-center gap-3 p-3 rounded-xl hover:bg-gray-50 transition-colors text-left"
|
||||
>
|
||||
<div className="w-10 h-10 bg-gray-100 rounded-lg flex items-center justify-center">
|
||||
<Info className="w-5 h-5 text-gray-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">关于</p>
|
||||
<p className="text-xs text-gray-500">版本信息与版权</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 退出登录 */}
|
||||
<button
|
||||
onClick={async () => { await logout(); navigate('/login'); }}
|
||||
className="w-full flex items-center justify-center gap-2 p-3 rounded-xl bg-red-50 text-red-600 hover:bg-red-100 transition-colors"
|
||||
>
|
||||
<LogOut className="w-5 h-5" />
|
||||
<span className="font-medium">退出登录</span>
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -258,7 +258,7 @@ export function FabricWarehouse() {
|
||||
<motion.div
|
||||
animate={{ rotate: 360 }}
|
||||
transition={{ duration: 1, repeat: Infinity, ease: 'linear' }}
|
||||
className="w-6 h-6 sm:w-8 sm:h-8 border-2 border-blue-500 border-t-transparent rounded-full mb-3"
|
||||
className="w-6 h-6 sm:w-8 sm:h-8 border-2 border-emerald-500 border-t-transparent rounded-full mb-3"
|
||||
/>
|
||||
<p className="text-gray-500 text-xs sm:text-sm">加载中...</p>
|
||||
</div>
|
||||
@ -294,23 +294,26 @@ export function FabricWarehouse() {
|
||||
<motion.div
|
||||
onClick={() => toggleExpand(fabricCode)}
|
||||
whileTap={{ scale: 0.995 }}
|
||||
className="flex items-center justify-between p-3 sm:p-4 cursor-pointer hover:bg-gray-50 transition-colors will-change-transform"
|
||||
className="flex items-center justify-between p-4 sm:p-5 cursor-pointer hover:bg-gray-50/50 transition-colors will-change-transform"
|
||||
>
|
||||
<div className="flex items-center gap-2 sm:gap-3 min-w-0">
|
||||
<div className="flex items-center gap-3 sm:gap-4 min-w-0">
|
||||
<motion.div
|
||||
animate={{ rotate: isExpanded ? 180 : 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className={`w-7 h-7 sm:w-8 sm:h-8 rounded-lg flex items-center justify-center transition-colors flex-shrink-0 ${isExpanded ? 'bg-blue-100' : 'bg-gray-100'}`}
|
||||
className={`w-9 h-9 sm:w-10 sm:h-10 rounded-xl flex items-center justify-center transition-colors flex-shrink-0 ${isExpanded ? 'bg-emerald-100' : 'bg-gray-100'}`}
|
||||
>
|
||||
<ChevronDown size={16} className="sm:w-[18px] sm:h-[18px] text-gray-500" style={{ color: isExpanded ? '#2563eb' : undefined }} />
|
||||
<ChevronDown size={18} className="sm:w-5 sm:h-5 text-gray-500" style={{ color: isExpanded ? '#059669' : undefined }} />
|
||||
</motion.div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs sm:text-sm text-gray-500 truncate">
|
||||
坯布码: {fabricCode} · 已入库{totalInMeters}米 · 计划{totalTargetMeters}米
|
||||
<p className="text-sm font-medium text-gray-900 truncate">
|
||||
{fabricCode}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-0.5">
|
||||
已入库 <span className="text-emerald-600 font-medium">{totalInMeters.toLocaleString()}</span> 米 · 计划 {totalTargetMeters.toLocaleString()} 米
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className="px-2 py-0.5 sm:px-2.5 sm:py-1 bg-gray-100 text-gray-700 text-xs font-medium rounded-full flex-shrink-0 ml-2">
|
||||
<span className="px-3 py-1 bg-emerald-50 text-emerald-700 text-xs font-medium rounded-full flex-shrink-0 ml-2">
|
||||
{items.length} 个计划
|
||||
</span>
|
||||
</motion.div>
|
||||
@ -328,43 +331,49 @@ export function FabricWarehouse() {
|
||||
{/* 桌面端表格 */}
|
||||
<div className="hidden sm:block overflow-x-auto">
|
||||
<table className="w-full min-w-[500px]">
|
||||
<thead className="bg-gray-50/50 border-b border-gray-100">
|
||||
<thead className="bg-gray-50 border-b border-gray-100">
|
||||
<tr>
|
||||
<th className="px-3 sm:px-4 py-2 sm:py-3 text-left text-xs font-medium text-gray-500">计划编号</th>
|
||||
<th className="px-3 sm:px-4 py-2 sm:py-3 text-left text-xs font-medium text-gray-500">规格</th>
|
||||
<th className="px-3 sm:px-4 py-2 sm:py-3 text-left text-xs font-medium text-gray-500">计划/入库</th>
|
||||
<th className="px-3 sm:px-4 py-2 sm:py-3 text-left text-xs font-medium text-gray-500">入库记录</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">计划编号</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">规格</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">计划/入库</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">入库记录</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-50">
|
||||
<tbody className="divide-y divide-gray-100">
|
||||
{items.map((item, itemIndex) => (
|
||||
<motion.tr
|
||||
key={item.plan_id}
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: itemIndex * 0.05, duration: 0.25 }}
|
||||
className="hover:bg-gray-50/50 transition-colors"
|
||||
className="hover:bg-emerald-50/30 transition-colors"
|
||||
>
|
||||
<td className="px-3 sm:px-4 py-2 sm:py-3">
|
||||
<span className="px-2 py-1 bg-gray-100 rounded-lg text-xs sm:text-sm font-mono text-gray-700">
|
||||
<td className="px-4 py-3">
|
||||
<span className="px-2.5 py-1 bg-emerald-50 text-emerald-700 rounded-lg text-xs font-mono font-medium">
|
||||
{item.plan_code}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-3 sm:px-4 py-2 sm:py-3">
|
||||
<p className="text-xs sm:text-sm text-gray-900">{item.weight}g</p>
|
||||
<td className="px-4 py-3">
|
||||
<p className="text-sm text-gray-900 font-medium">{item.weight}g</p>
|
||||
</td>
|
||||
<td className="px-3 sm:px-4 py-2 sm:py-3">
|
||||
<div className="text-xs sm:text-sm">
|
||||
<span className="text-gray-600">{item.target_quantity}米</span>
|
||||
<span className="text-gray-400 mx-1">/</span>
|
||||
<span className="text-emerald-600 font-medium">{item.completed_quantity}米</span>
|
||||
<td className="px-4 py-3">
|
||||
<div className="text-sm">
|
||||
<span className="text-gray-500">{item.target_quantity.toLocaleString()}米</span>
|
||||
<span className="text-gray-300 mx-2">/</span>
|
||||
<span className="text-emerald-600 font-semibold">{item.completed_quantity.toLocaleString()}米</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-3 sm:px-4 py-2 sm:py-3">
|
||||
<td className="px-4 py-3">
|
||||
{item.in_records.length > 0 ? (
|
||||
<div className="text-xs text-gray-500">
|
||||
<span className="inline-block">
|
||||
{new Date(item.in_records[0].time).toLocaleDateString('zh-CN')}({item.in_records[0].quantity}米/{item.in_records[0].rolls}匹/件)
|
||||
<div className="text-xs text-gray-600">
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<svg className="w-3.5 h-3.5 text-emerald-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
{new Date(item.in_records[0].time).toLocaleDateString('zh-CN')}
|
||||
<span className="text-emerald-600 font-medium">{item.in_records[0].quantity}米</span>
|
||||
<span className="text-gray-400">/</span>
|
||||
<span>{item.in_records[0].rolls}匹/件</span>
|
||||
</span>
|
||||
{item.in_records.length > 1 && (
|
||||
<button
|
||||
@ -374,15 +383,20 @@ export function FabricWarehouse() {
|
||||
setSelectedPlanCode(item.plan_code);
|
||||
setRecordsModalOpen(true);
|
||||
}}
|
||||
className="inline-flex items-center gap-1 px-2 py-0.5 bg-blue-50 text-blue-600 rounded text-xs font-medium hover:bg-blue-100 transition-colors ml-2"
|
||||
className="inline-flex items-center gap-1 px-2.5 py-1 bg-emerald-50 text-emerald-600 rounded-lg text-xs font-medium hover:bg-emerald-100 transition-colors ml-2"
|
||||
>
|
||||
<List className="w-3 h-3" />
|
||||
全部记录 ({item.in_records.length}条)
|
||||
<List className="w-3.5 h-3.5" />
|
||||
全部 ({item.in_records.length}条)
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-xs text-gray-400">暂无入库</span>
|
||||
<span className="text-xs text-gray-400 flex items-center gap-1">
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 12H4" />
|
||||
</svg>
|
||||
暂无入库
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
</motion.tr>
|
||||
@ -392,36 +406,41 @@ export function FabricWarehouse() {
|
||||
</div>
|
||||
|
||||
{/* 移动端卡片 */}
|
||||
<div className="sm:hidden p-3 space-y-2">
|
||||
<div className="sm:hidden p-4 space-y-3">
|
||||
{items.map((item, itemIndex) => (
|
||||
<motion.div
|
||||
key={item.plan_id}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: itemIndex * 0.05, duration: 0.25 }}
|
||||
className="bg-white rounded-xl border border-gray-100 p-3"
|
||||
className="bg-white rounded-2xl border border-gray-100 p-4 shadow-sm"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="px-2 py-1 bg-gray-100 rounded-lg text-xs font-mono text-gray-700">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="px-2.5 py-1 bg-emerald-50 text-emerald-700 rounded-lg text-xs font-mono font-medium">
|
||||
{item.plan_code}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">{item.weight}g</span>
|
||||
<span className="text-xs text-gray-600 font-medium">{item.weight}g</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="text-xs">
|
||||
<span className="text-gray-500">计划:</span>
|
||||
<span className="text-gray-700 ml-1">{item.target_quantity}米</span>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="text-sm">
|
||||
<span className="text-gray-400">计划</span>
|
||||
<span className="text-gray-700 font-medium ml-1.5">{item.target_quantity.toLocaleString()}米</span>
|
||||
</div>
|
||||
<div className="text-xs">
|
||||
<span className="text-gray-500">已入库:</span>
|
||||
<span className="text-emerald-600 font-medium ml-1">{item.completed_quantity}米</span>
|
||||
<div className="text-sm">
|
||||
<span className="text-gray-400">已入</span>
|
||||
<span className="text-emerald-600 font-semibold ml-1.5">{item.completed_quantity.toLocaleString()}米</span>
|
||||
</div>
|
||||
</div>
|
||||
{item.in_records.length > 0 ? (
|
||||
<div className="flex items-center justify-between pt-2 border-t border-gray-50">
|
||||
<div className="text-xs text-gray-500">
|
||||
<div className="flex items-center justify-between pt-3 border-t border-gray-100">
|
||||
<div className="text-xs text-gray-600 flex items-center gap-1.5">
|
||||
<svg className="w-3.5 h-3.5 text-emerald-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<span>{new Date(item.in_records[0].time).toLocaleDateString('zh-CN')}</span>
|
||||
<span className="text-gray-700 ml-1">({item.in_records[0].quantity}米/{item.in_records[0].rolls}匹/件)</span>
|
||||
<span className="text-emerald-600 font-medium">{item.in_records[0].quantity}米</span>
|
||||
<span className="text-gray-400">/</span>
|
||||
<span>{item.in_records[0].rolls}匹/件</span>
|
||||
</div>
|
||||
{item.in_records.length > 1 && (
|
||||
<button
|
||||
@ -431,15 +450,20 @@ export function FabricWarehouse() {
|
||||
setSelectedPlanCode(item.plan_code);
|
||||
setRecordsModalOpen(true);
|
||||
}}
|
||||
className="inline-flex items-center gap-1 px-2 py-0.5 bg-blue-50 text-blue-600 rounded text-xs font-medium hover:bg-blue-100 transition-colors"
|
||||
className="inline-flex items-center gap-1 px-2.5 py-1 bg-emerald-50 text-emerald-600 rounded-lg text-xs font-medium hover:bg-emerald-100 transition-colors"
|
||||
>
|
||||
<List className="w-3 h-3" />
|
||||
<List className="w-3.5 h-3.5" />
|
||||
全部 ({item.in_records.length}条)
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-xs text-gray-400 pt-2 border-t border-gray-50">暂无入库</div>
|
||||
<div className="text-xs text-gray-400 pt-3 border-t border-gray-100 flex items-center gap-1">
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 12H4" />
|
||||
</svg>
|
||||
暂无入库
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
))}
|
||||
|
||||
@ -89,7 +89,7 @@ export function PaymentPending() {
|
||||
<motion.div
|
||||
animate={{ rotate: 360 }}
|
||||
transition={{ duration: 1, repeat: Infinity, ease: 'linear' }}
|
||||
className="w-6 h-6 sm:w-8 sm:h-8 border-2 border-blue-500 border-t-transparent rounded-full mb-3"
|
||||
className="w-6 h-6 sm:w-8 sm:h-8 border-2 border-emerald-500 border-t-transparent rounded-full mb-3"
|
||||
/>
|
||||
<p className="text-gray-400 text-sm">加载中...</p>
|
||||
</div>
|
||||
@ -99,12 +99,12 @@ export function PaymentPending() {
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="bg-gradient-to-r from-blue-500 to-blue-600 rounded-xl p-4 sm:p-5 md:p-6 text-white mb-4 sm:mb-5 md:mb-6"
|
||||
className="bg-gradient-to-r from-emerald-500 to-green-600 rounded-xl p-4 sm:p-5 md:p-6 text-white mb-4 sm:mb-5 md:mb-6"
|
||||
>
|
||||
<div className="flex items-center gap-2 sm:gap-3">
|
||||
<DollarSign className="w-6 h-6 sm:w-7 sm:h-7 md:w-8 md:h-8" />
|
||||
<div>
|
||||
<p className="text-blue-100 text-xs sm:text-sm">待结款总额</p>
|
||||
<p className="text-emerald-100 text-xs sm:text-sm">待结款总额</p>
|
||||
<p className="text-2xl sm:text-3xl md:text-4xl font-bold">¥{totalAmount.toLocaleString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -139,7 +139,7 @@ export function PaymentPending() {
|
||||
<p className="text-xs sm:text-sm text-gray-500">已完成: {item.quantity}米 × ¥{item.price_per_meter}/米</p>
|
||||
</div>
|
||||
<div className="text-right flex-shrink-0">
|
||||
<p className="text-lg sm:text-xl md:text-2xl font-bold text-blue-600">¥{item.amount.toLocaleString()}</p>
|
||||
<p className="text-lg sm:text-xl md:text-2xl font-bold text-emerald-600">¥{item.amount.toLocaleString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 sm:mt-4 pt-3 sm:pt-4 border-t border-gray-100 flex justify-end">
|
||||
|
||||
@ -10,8 +10,8 @@ import { getSupabaseUrl } from '../../supabase/client';
|
||||
import type { ProductionPlan, PlanProcessStep, Company, Profile, YarnRatio } from '../../types';
|
||||
|
||||
const statusMap: Record<string, { label: string; color: string }> = {
|
||||
pending: { label: '待确定', color: 'bg-yellow-100 text-yellow-700' },
|
||||
producing: { label: '生产中', color: 'bg-blue-100 text-blue-700' },
|
||||
pending: { label: '待确定', color: 'bg-amber-100 text-amber-700' },
|
||||
producing: { label: '生产中', color: 'bg-emerald-100 text-emerald-700' },
|
||||
completed: { label: '已完成', color: 'bg-green-100 text-green-700' },
|
||||
rejected: { label: '已拒绝', color: 'bg-gray-100 text-gray-600' }
|
||||
};
|
||||
@ -236,7 +236,8 @@ export function TextilePlanOverview() {
|
||||
return {
|
||||
token: params.get('token'),
|
||||
plan: params.get('plan'),
|
||||
type: params.get('type')
|
||||
type: params.get('type'),
|
||||
shortCode: params.get('s')
|
||||
};
|
||||
}
|
||||
try {
|
||||
@ -244,24 +245,27 @@ export function TextilePlanOverview() {
|
||||
return {
|
||||
token: url.searchParams.get('token'),
|
||||
plan: url.searchParams.get('plan'),
|
||||
type: url.searchParams.get('type')
|
||||
type: url.searchParams.get('type'),
|
||||
shortCode: url.searchParams.get('s')
|
||||
};
|
||||
} catch {
|
||||
return { token: null, plan: null, type: null };
|
||||
return { token: null, plan: null, type: null, shortCode: null };
|
||||
}
|
||||
};
|
||||
|
||||
const handleImportFromLink = () => {
|
||||
if (!importLink.trim()) return;
|
||||
|
||||
const { token, plan, type } = extractParamsFromLink(importLink);
|
||||
const { token, plan, type, shortCode } = extractParamsFromLink(importLink);
|
||||
|
||||
if (token) {
|
||||
if (shortCode) {
|
||||
navigate(`/import?s=${shortCode}`);
|
||||
} else if (token) {
|
||||
navigate(`/import?token=${token}`);
|
||||
} else if (plan && type) {
|
||||
navigate(`/import?plan=${plan}&type=${type}`);
|
||||
} else {
|
||||
alert('无效的链接格式,请确保链接包含 token 或 plan 参数');
|
||||
alert('无效的链接格式,请确保链接包含 token、s 或 plan 参数');
|
||||
}
|
||||
};
|
||||
|
||||
@ -270,9 +274,11 @@ export function TextilePlanOverview() {
|
||||
const text = await navigator.clipboard.readText();
|
||||
if (text.includes('/import?') || text.includes('#/import?')) {
|
||||
setImportLink(text);
|
||||
const { token, plan, type } = extractParamsFromLink(text);
|
||||
const { token, plan, type, shortCode } = extractParamsFromLink(text);
|
||||
|
||||
if (token) {
|
||||
if (shortCode) {
|
||||
navigate(`/import?s=${shortCode}`);
|
||||
} else if (token) {
|
||||
navigate(`/import?token=${token}`);
|
||||
} else if (plan && type) {
|
||||
navigate(`/import?plan=${plan}&type=${type}`);
|
||||
@ -647,13 +653,13 @@ export function TextilePlanOverview() {
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2 sm:mb-3">
|
||||
<h2 className="font-semibold text-gray-800 flex items-center gap-2 text-sm sm:text-base">
|
||||
<Link2 className="w-4 h-4 sm:w-5 sm:h-5 text-blue-600" />
|
||||
<Link2 className="w-4 h-4 sm:w-5 sm:h-5 text-emerald-600" />
|
||||
导入关联计划
|
||||
</h2>
|
||||
<motion.button
|
||||
whileTap={{ scale: 0.97 }}
|
||||
onClick={handlePasteFromClipboard}
|
||||
className="flex items-center gap-1 px-2 sm:px-3 py-1 sm:py-1.5 text-xs sm:text-sm text-blue-600 hover:bg-blue-50 rounded-lg transition-colors will-change-transform"
|
||||
className="flex items-center gap-1 px-2 sm:px-3 py-1 sm:py-1.5 text-xs sm:text-sm text-emerald-600 hover:bg-emerald-50 rounded-lg transition-colors will-change-transform"
|
||||
>
|
||||
<Clipboard className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
|
||||
识别剪贴板
|
||||
@ -665,13 +671,13 @@ export function TextilePlanOverview() {
|
||||
value={importLink}
|
||||
onChange={(e) => setImportLink(e.target.value)}
|
||||
placeholder="粘贴采购商分享的链接..."
|
||||
className="flex-1 px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-xs sm:text-sm"
|
||||
className="flex-1 px-3 sm:px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 text-xs sm:text-sm"
|
||||
/>
|
||||
<motion.button
|
||||
whileTap={{ scale: 0.97 }}
|
||||
onClick={handleImportFromLink}
|
||||
disabled={!importLink.trim()}
|
||||
className="px-3 sm:px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed text-xs sm:text-sm font-medium will-change-transform"
|
||||
className="px-3 sm:px-4 py-2 bg-emerald-600 text-white rounded-lg hover:bg-emerald-700 disabled:opacity-50 disabled:cursor-not-allowed text-xs sm:text-sm font-medium will-change-transform"
|
||||
>
|
||||
导入
|
||||
</motion.button>
|
||||
@ -683,7 +689,7 @@ export function TextilePlanOverview() {
|
||||
<motion.div
|
||||
animate={{ rotate: 360 }}
|
||||
transition={{ duration: 1, repeat: Infinity, ease: 'linear' }}
|
||||
className="w-6 h-6 sm:w-8 sm:h-8 border-2 border-blue-500 border-t-transparent rounded-full mb-3"
|
||||
className="w-6 h-6 sm:w-8 sm:h-8 border-2 border-emerald-500 border-t-transparent rounded-full mb-3"
|
||||
/>
|
||||
<p className="text-gray-400 text-sm">加载中...</p>
|
||||
</div>
|
||||
@ -747,45 +753,50 @@ export function TextilePlanOverview() {
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: planIndex * 0.05, duration: 0.3 }}
|
||||
className={`border rounded-lg overflow-hidden ${isRejected ? 'border-gray-200 bg-gray-100' : 'border-gray-200'}`}
|
||||
className={`border rounded-2xl overflow-hidden transition-all duration-300 ${isRejected ? 'border-gray-200 bg-gray-50' : 'border-gray-200 bg-white hover:border-emerald-200 hover:shadow-lg hover:shadow-gray-100/50'}`}
|
||||
>
|
||||
<motion.div
|
||||
onClick={() => !isRejected && setExpandedPlan(expandedPlan === plan.id ? null : plan.id)}
|
||||
whileTap={isRejected ? {} : { scale: 0.995 }}
|
||||
className={`p-3 sm:p-4 transition-colors will-change-transform ${isRejected ? 'cursor-default' : 'cursor-pointer hover:bg-gray-50'}`}
|
||||
className={`p-4 sm:p-5 transition-colors will-change-transform ${isRejected ? 'cursor-default' : 'cursor-pointer hover:bg-gray-50/50'}`}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2 mb-1.5 sm:mb-2">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
{isRejected ? (
|
||||
<span className="px-2 py-0.5 sm:py-1 text-[10px] sm:text-xs rounded-full bg-gray-400 text-white">
|
||||
<span className="px-2.5 py-1 text-xs rounded-full bg-gray-400 text-white font-medium">
|
||||
已拒绝
|
||||
</span>
|
||||
) : (
|
||||
<span className={`px-2 py-0.5 sm:py-1 text-[10px] sm:text-xs rounded-full ${statusMap[plan.status]?.color || 'bg-gray-100'}`}>
|
||||
<span className={`px-2.5 py-1 text-xs rounded-full font-medium ${statusMap[plan.status]?.color || 'bg-gray-100'}`}>
|
||||
{statusMap[plan.status]?.label || plan.status}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-xs text-gray-400 font-mono">{plan.plan_code}</span>
|
||||
</div>
|
||||
<p className={`text-xs sm:text-sm truncate ${isRejected ? 'text-gray-500' : 'text-gray-600'}`}>{plan.fabric_code}</p>
|
||||
<p className="text-[10px] sm:text-xs text-gray-400">{plan.plan_code}</p>
|
||||
<p className={`text-sm font-medium truncate ${isRejected ? 'text-gray-500' : 'text-gray-800'}`}>{plan.fabric_code}</p>
|
||||
</div>
|
||||
<div className={`text-right text-xs sm:text-sm flex-shrink-0 ${isRejected ? 'text-gray-500' : 'text-gray-600'}`}>
|
||||
<p>计划: {plan.target_quantity}米</p>
|
||||
<p>已入库: {plan.completed_quantity}米</p>
|
||||
<div className={`text-right text-sm flex-shrink-0 ${isRejected ? 'text-gray-500' : 'text-gray-700'}`}>
|
||||
<p className="font-medium">{plan.target_quantity.toLocaleString()}<span className="text-xs text-gray-500 ml-1">米</span></p>
|
||||
<p className="text-xs text-emerald-600">已入 {plan.completed_quantity.toLocaleString()}米</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 sm:mt-3">
|
||||
<div className="flex justify-between text-[10px] sm:text-xs text-gray-500 mb-1">
|
||||
<span>进度</span>
|
||||
<span>{progress}%</span>
|
||||
<div className="mt-4">
|
||||
<div className="flex justify-between text-xs text-gray-500 mb-1.5">
|
||||
<span className="flex items-center gap-1">
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
生产进度
|
||||
</span>
|
||||
<span className="font-medium text-emerald-600">{progress}%</span>
|
||||
</div>
|
||||
<div className="h-1.5 sm:h-2 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div className="h-2 bg-gray-100 rounded-full overflow-hidden">
|
||||
<motion.div
|
||||
initial={{ width: 0 }}
|
||||
animate={{ width: `${progress}%` }}
|
||||
transition={{ duration: 0.6, delay: 0.1, ease: [0.25, 0.46, 0.45, 0.94] }}
|
||||
className="h-full bg-gradient-to-r from-blue-500 to-cyan-500 rounded-full will-change-transform"
|
||||
className="h-full bg-gradient-to-r from-emerald-500 to-green-500 rounded-full will-change-transform"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -831,7 +842,7 @@ export function TextilePlanOverview() {
|
||||
</div>
|
||||
<div className="truncate">
|
||||
<span className="text-gray-400">进度:</span>
|
||||
<span className="ml-1 text-blue-600 font-medium">{progress}%</span>
|
||||
<span className="ml-1 text-emerald-600 font-medium">{progress}%</span>
|
||||
</div>
|
||||
<div className="truncate">
|
||||
<span className="text-gray-400">创建:</span>
|
||||
@ -882,8 +893,8 @@ export function TextilePlanOverview() {
|
||||
<h5 className="text-sm font-medium text-gray-900 mb-2">纱线名称/占比</h5>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{plan.yarnRatios.map((yarn, index) => (
|
||||
<span key={index} className="inline-flex items-center px-2 py-1 bg-blue-50 text-blue-700 text-xs rounded-lg">
|
||||
{yarnTypeLabels[yarn.yarn_type || 'all']}/{yarn.yarn_name}:{yarn.ratio}%
|
||||
<span key={index} className="inline-flex items-center px-2 py-1 bg-emerald-50 text-emerald-700 text-xs rounded-lg">
|
||||
{yarnTypeLabels[yarn.yarn_type || 'all']}/{yarn.yarn_name}:{Number(yarn.ratio).toFixed(0)}%
|
||||
{yarn.amount_per_meter && `(${yarn.amount_per_meter}g/m)`}
|
||||
</span>
|
||||
))}
|
||||
@ -900,7 +911,7 @@ export function TextilePlanOverview() {
|
||||
{plan.processSteps.length > 0 && (
|
||||
<div className="border-t border-gray-200 pt-3">
|
||||
<h5 className="font-medium mb-3 sm:mb-4 flex items-center gap-2 text-sm">
|
||||
<Check className="w-4 h-4 text-blue-600" />
|
||||
<Check className="w-4 h-4 text-emerald-600" />
|
||||
生产记录
|
||||
</h5>
|
||||
<ProcessFlow
|
||||
@ -914,6 +925,9 @@ export function TextilePlanOverview() {
|
||||
isPlanLocked={plan.status === 'completed'}
|
||||
lastInventoryTime={plan.inventoryRecords[0]?.time}
|
||||
inventoryRecords={plan.inventoryRecords}
|
||||
yarnRatios={plan.yarnRatios}
|
||||
companyId={auth.company?.id}
|
||||
planId={plan.id}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -1,17 +1,31 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ArrowLeft, Plus, AlertTriangle } from 'lucide-react';
|
||||
import { ArrowLeft, Plus, AlertTriangle, ArrowDownLeft, ArrowUpRight } from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { supabase } from '../../supabase/client';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import type { YarnStock, Warehouse } from '../../types';
|
||||
|
||||
interface YarnStockRecord {
|
||||
id: string;
|
||||
yarn_stock_id: string | null;
|
||||
plan_id: string | null;
|
||||
record_type: 'inbound' | 'outbound';
|
||||
quantity: number;
|
||||
unit: string;
|
||||
batch_no: string | null;
|
||||
notes: string | null;
|
||||
created_at: string;
|
||||
yarn_name?: string;
|
||||
}
|
||||
|
||||
export function YarnWarehouse() {
|
||||
const navigate = useNavigate();
|
||||
const { auth } = useAuth();
|
||||
const [activeTab, setActiveTab] = useState<'stock' | 'record'>('stock');
|
||||
const [yarns, setYarns] = useState<YarnStock[]>([]);
|
||||
const [warehouses, setWarehouses] = useState<Warehouse[]>([]);
|
||||
const [records, setRecords] = useState<YarnStockRecord[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showAddModal, setShowAddModal] = useState(false);
|
||||
const [newYarn, setNewYarn] = useState({ name: '', spec: '', quantity: 0, minStock: 0, unit: 'kg' });
|
||||
@ -21,6 +35,12 @@ export function YarnWarehouse() {
|
||||
fetchData();
|
||||
}, [auth.company]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab === 'record' && auth.company) {
|
||||
fetchRecords();
|
||||
}
|
||||
}, [activeTab, auth.company]);
|
||||
|
||||
const fetchData = async () => {
|
||||
const { data: yarnData } = await supabase
|
||||
.from('yarn_stock')
|
||||
@ -39,10 +59,43 @@ export function YarnWarehouse() {
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const fetchRecords = async () => {
|
||||
setLoading(true);
|
||||
const { data: recordsData } = await supabase
|
||||
.from('yarn_stock_records')
|
||||
.select('*')
|
||||
.eq('company_id', auth.company!.id)
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
// 获取纱线名称
|
||||
const yarnIds = recordsData?.map(r => r.yarn_stock_id).filter((id): id is string => !!id) || [];
|
||||
const { data: yarnsData } = await supabase
|
||||
.from('yarn_stock')
|
||||
.select('id, name')
|
||||
.in('id', yarnIds.length > 0 ? yarnIds : ['00000000-0000-0000-0000-000000000000']);
|
||||
|
||||
const yarnMap = new Map(yarnsData?.map(y => [y.id, y.name]) || []);
|
||||
|
||||
const recordsWithNames: YarnStockRecord[] = recordsData?.map(r => ({
|
||||
...r,
|
||||
yarn_name: r.yarn_stock_id ? yarnMap.get(r.yarn_stock_id) || '未知纱线' : '未知纱线',
|
||||
record_type: r.record_type as 'inbound' | 'outbound'
|
||||
})) || [];
|
||||
|
||||
setRecords(recordsWithNames);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handleAddYarn = async () => {
|
||||
if (!newYarn.name) return;
|
||||
const yarnWarehouse = warehouses[0]; // 使用第一个纱线仓库
|
||||
const { error } = await supabase
|
||||
|
||||
// 获取当前用户ID
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
const operatorId = user?.id;
|
||||
|
||||
// 插入纱线库存
|
||||
const { data: newYarnData, error } = await supabase
|
||||
.from('yarn_stock')
|
||||
.insert({
|
||||
company_id: auth.company!.id,
|
||||
@ -52,11 +105,24 @@ export function YarnWarehouse() {
|
||||
quantity: newYarn.quantity,
|
||||
min_stock: newYarn.minStock,
|
||||
unit: newYarn.unit
|
||||
});
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
console.error('添加失败:', error);
|
||||
} else {
|
||||
// 记录入库
|
||||
await supabase.from('yarn_stock_records').insert({
|
||||
company_id: auth.company!.id,
|
||||
yarn_stock_id: newYarnData.id,
|
||||
record_type: 'inbound',
|
||||
quantity: newYarn.quantity,
|
||||
unit: newYarn.unit,
|
||||
notes: `纱线入库 - ${newYarn.name}`,
|
||||
operator_id: operatorId
|
||||
});
|
||||
|
||||
setShowAddModal(false);
|
||||
setNewYarn({ name: '', spec: '', quantity: 0, minStock: 0, unit: 'kg' });
|
||||
await fetchData();
|
||||
@ -84,7 +150,7 @@ export function YarnWarehouse() {
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onClick={() => setActiveTab('stock')}
|
||||
className={`flex-1 py-2 rounded-lg text-xs sm:text-sm font-medium transition-colors will-change-transform ${
|
||||
activeTab === 'stock' ? 'bg-blue-600 text-white' : 'bg-white text-gray-600 hover:bg-gray-50'
|
||||
activeTab === 'stock' ? 'bg-emerald-600 text-white' : 'bg-white text-gray-600 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
库存列表
|
||||
@ -93,10 +159,10 @@ export function YarnWarehouse() {
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onClick={() => setActiveTab('record')}
|
||||
className={`flex-1 py-2 rounded-lg text-xs sm:text-sm font-medium transition-colors will-change-transform ${
|
||||
activeTab === 'record' ? 'bg-blue-600 text-white' : 'bg-white text-gray-600 hover:bg-gray-50'
|
||||
activeTab === 'record' ? 'bg-emerald-600 text-white' : 'bg-white text-gray-600 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
入库记录
|
||||
出入库记录
|
||||
</motion.button>
|
||||
</div>
|
||||
|
||||
@ -105,7 +171,7 @@ export function YarnWarehouse() {
|
||||
<motion.div
|
||||
animate={{ rotate: 360 }}
|
||||
transition={{ duration: 1, repeat: Infinity, ease: 'linear' }}
|
||||
className="w-6 h-6 sm:w-8 sm:h-8 border-2 border-blue-500 border-t-transparent rounded-full mb-3"
|
||||
className="w-6 h-6 sm:w-8 sm:h-8 border-2 border-emerald-500 border-t-transparent rounded-full mb-3"
|
||||
/>
|
||||
<p className="text-gray-400 text-sm">加载中...</p>
|
||||
</div>
|
||||
@ -121,7 +187,7 @@ export function YarnWarehouse() {
|
||||
<motion.button
|
||||
whileTap={{ scale: 0.97 }}
|
||||
onClick={() => setShowAddModal(true)}
|
||||
className="flex items-center gap-1 text-xs sm:text-sm text-blue-600 will-change-transform"
|
||||
className="flex items-center gap-1 text-xs sm:text-sm text-emerald-600 will-change-transform"
|
||||
>
|
||||
<Plus className="w-4 h-4" /> 入库
|
||||
</motion.button>
|
||||
@ -169,9 +235,67 @@ export function YarnWarehouse() {
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="bg-white rounded-lg shadow-sm p-4"
|
||||
className="bg-white rounded-lg shadow-sm"
|
||||
>
|
||||
<p className="text-center text-gray-400 py-6 sm:py-8 text-sm">暂无入库记录</p>
|
||||
<div className="p-3 sm:p-4 border-b">
|
||||
<h2 className="font-medium text-sm sm:text-base">出入库记录</h2>
|
||||
</div>
|
||||
{records.length === 0 ? (
|
||||
<p className="text-center text-gray-400 py-6 sm:py-8 text-sm">暂无出入库记录</p>
|
||||
) : (
|
||||
<div className="divide-y">
|
||||
{records.map((record, index) => (
|
||||
<motion.div
|
||||
key={record.id}
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: index * 0.05, duration: 0.25 }}
|
||||
className="p-3 sm:p-4 hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-sm sm:text-base">{record.yarn_name}</span>
|
||||
<span className={`text-[10px] sm:text-xs px-1.5 py-0.5 rounded flex-shrink-0 ${
|
||||
record.record_type === 'inbound'
|
||||
? 'bg-emerald-100 text-emerald-600'
|
||||
: 'bg-amber-100 text-amber-600'
|
||||
}`}>
|
||||
{record.record_type === 'inbound' ? (
|
||||
<span className="flex items-center gap-1">
|
||||
<ArrowDownLeft className="w-3 h-3" /> 入库
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center gap-1">
|
||||
<ArrowUpRight className="w-3 h-3" /> 出库
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs sm:text-sm text-gray-500 mt-1">
|
||||
{record.notes || '-'}
|
||||
</p>
|
||||
{record.plan_id && (
|
||||
<p className="text-[10px] sm:text-xs text-gray-400 mt-0.5">
|
||||
关联计划: {record.plan_id.slice(0, 8)}...
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-right flex-shrink-0 ml-2">
|
||||
<p className={`font-semibold text-sm sm:text-base ${
|
||||
record.record_type === 'inbound' ? 'text-emerald-600' : 'text-amber-600'
|
||||
}`}>
|
||||
{record.record_type === 'inbound' ? '+' : '-'}{record.quantity} {record.unit}
|
||||
</p>
|
||||
<p className="text-[10px] sm:text-xs text-gray-400">
|
||||
{new Date(record.created_at).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
@ -245,7 +369,7 @@ export function YarnWarehouse() {
|
||||
<motion.button
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onClick={handleAddYarn}
|
||||
className="flex-1 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-sm will-change-transform"
|
||||
className="flex-1 py-2 bg-emerald-600 text-white rounded-lg hover:bg-emerald-700 text-sm will-change-transform"
|
||||
>
|
||||
确认入库
|
||||
</motion.button>
|
||||
|
||||
@ -93,19 +93,19 @@ export function CompletedFabric() {
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-green-50 to-emerald-50">
|
||||
{/* Header */}
|
||||
<header className="bg-gradient-to-r from-green-500 to-emerald-500 shadow-lg">
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-purple-50/30 to-violet-50/30">
|
||||
{/* Header - 与 Dashboard 一致 */}
|
||||
<header className="bg-white/80 backdrop-blur-xl border-b border-gray-200/50 sticky top-0 z-10">
|
||||
<div className="max-w-7xl mx-auto px-4 md:px-8 py-4 flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => navigate('/washing')}
|
||||
className="p-2 hover:bg-white/20 rounded-lg transition-colors"
|
||||
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
||||
>
|
||||
<ArrowLeft className="w-5 h-5 text-white" />
|
||||
<ArrowLeft className="w-5 h-5 text-gray-600" />
|
||||
</button>
|
||||
<div>
|
||||
<h1 className="text-lg font-bold text-white">已完成坯布</h1>
|
||||
<p className="text-xs text-white/70">查看已完成水洗的坯布记录</p>
|
||||
<h1 className="text-lg font-bold text-gray-900">已完成坯布</h1>
|
||||
<p className="text-xs text-gray-500">查看已完成水洗的坯布记录</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@ -117,9 +117,9 @@ export function CompletedFabric() {
|
||||
<p className="text-xs text-gray-500 mb-1">已完成计划</p>
|
||||
<p className="text-2xl font-bold text-gray-800">{items.length}</p>
|
||||
</div>
|
||||
<div className="bg-blue-50 rounded-xl p-4 shadow-sm">
|
||||
<p className="text-xs text-blue-600 mb-1">水洗前米数</p>
|
||||
<p className="text-2xl font-bold text-blue-700">{totalBefore.toFixed(2)}</p>
|
||||
<div className="bg-purple-50 rounded-xl p-4 shadow-sm">
|
||||
<p className="text-xs text-purple-600 mb-1">水洗前米数</p>
|
||||
<p className="text-2xl font-bold text-purple-700">{totalBefore.toFixed(2)}</p>
|
||||
</div>
|
||||
<div className="bg-green-50 rounded-xl p-4 shadow-sm">
|
||||
<p className="text-xs text-green-600 mb-1">水洗后米数</p>
|
||||
|
||||
@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { supabase } from '../../supabase/client';
|
||||
import { motion } from 'framer-motion';
|
||||
import { Import, Warehouse, CreditCard, ArrowLeft, Users, Droplets, Package, CheckCircle, Clock, LogOut, HelpCircle } from 'lucide-react';
|
||||
import { Import, Warehouse, CreditCard, ArrowLeft, Users, Droplets, Package, CheckCircle, Clock, LogOut, HelpCircle, X, BookOpen, MessageSquare, Info, Camera, FileText, Plus, TrendingUp, Building2 } from 'lucide-react';
|
||||
import { useResponsive } from '../../hooks/useResponsive';
|
||||
import { OnboardingGuide, shouldShowOnboarding } from '../../components/OnboardingGuide';
|
||||
import { NotificationCenter } from '../../components/NotificationCenter';
|
||||
@ -14,48 +14,34 @@ import { UserManual } from '../../components/UserManual';
|
||||
import { CrashReporter } from '../../components/CrashReporter';
|
||||
import type { WashingPlan } from '../../types';
|
||||
|
||||
const statusMap: Record<string, { label: string; color: string; bgColor: string; icon: any }> = {
|
||||
pending: { label: '待处理', color: 'text-amber-600', bgColor: 'bg-amber-50', icon: Clock },
|
||||
processing: { label: '处理中', color: 'text-violet-500', bgColor: 'bg-violet-50', icon: TrendingUp },
|
||||
completed: { label: '已完成', color: 'text-emerald-600', bgColor: 'bg-emerald-50', icon: Package }
|
||||
};
|
||||
|
||||
const getQuickActions = (isMaster: boolean) => {
|
||||
const actions = [
|
||||
{ id: 'plans', icon: Import, label: '计划总览', path: '/washing/plans', gradient: 'from-purple-500 to-violet-500' },
|
||||
{ id: 'pending', icon: Package, label: '待处理坯布', path: '/washing/pending', gradient: 'from-amber-500 to-orange-500' },
|
||||
{ id: 'completed', icon: CheckCircle, label: '已完成坯布', path: '/washing/completed', gradient: 'from-green-500 to-emerald-500' },
|
||||
{ id: 'finished', icon: Warehouse, label: '成品仓库', path: '/washing/finished-warehouse', gradient: 'from-blue-500 to-cyan-500' },
|
||||
{ id: 'payment', icon: CreditCard, label: '待结款', path: '/washing/payments', gradient: 'from-rose-500 to-pink-500' }
|
||||
{ id: 'plans', icon: FileText, label: '计划总览', path: '/washing/plans', gradient: 'from-violet-400 to-fuchsia-400', description: '查看所有计划' },
|
||||
{ id: 'pending', icon: Package, label: '待处理坯布', path: '/washing/pending', gradient: 'from-amber-500 to-orange-500', description: '处理待水洗坯布' },
|
||||
{ id: 'completed', icon: CheckCircle, label: '已完成坯布', path: '/washing/completed', gradient: 'from-emerald-500 to-green-500', description: '查看已完成水洗' },
|
||||
{ id: 'finished', icon: Warehouse, label: '成品仓库', path: '/washing/finished-warehouse', gradient: 'from-violet-400 to-fuchsia-400', description: '管理成品库存' },
|
||||
{ id: 'payment', icon: CreditCard, label: '待结款', path: '/washing/payments', gradient: 'from-rose-500 to-pink-500', description: '查看待结款项' }
|
||||
];
|
||||
if (isMaster) {
|
||||
actions.push({ id: 'members', icon: Users, label: '账号管理', path: '/members', gradient: 'from-indigo-500 to-purple-500' });
|
||||
actions.push({ id: 'members', icon: Users, label: '账号管理', path: '/members', gradient: 'from-fuchsia-400 to-purple-400', description: '管理团队账号' });
|
||||
}
|
||||
return actions;
|
||||
};
|
||||
|
||||
const statusMap: Record<string, { label: string; color: string }> = {
|
||||
pending: { label: '待处理', color: 'bg-amber-100 text-amber-700' },
|
||||
processing: { label: '处理中', color: 'bg-blue-100 text-blue-700' },
|
||||
completed: { label: '已完成', color: 'bg-green-100 text-green-700' }
|
||||
};
|
||||
|
||||
const containerVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.08,
|
||||
delayChildren: 0.05
|
||||
}
|
||||
}
|
||||
visible: { opacity: 1, transition: { staggerChildren: 0.1 } }
|
||||
};
|
||||
|
||||
const itemVariants = {
|
||||
hidden: { opacity: 0, y: 15, scale: 0.98 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
scale: 1,
|
||||
transition: {
|
||||
duration: 0.35,
|
||||
ease: [0.25, 0.46, 0.45, 0.94]
|
||||
}
|
||||
}
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
visible: { opacity: 1, y: 0, transition: { duration: 0.4 } }
|
||||
};
|
||||
|
||||
export function WashingDashboard() {
|
||||
@ -64,8 +50,6 @@ export function WashingDashboard() {
|
||||
const { isMobile } = useResponsive();
|
||||
const [plans, setPlans] = useState<WashingPlan[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [pendingCount, setPendingCount] = useState(0);
|
||||
const [completedCount, setCompletedCount] = useState(0);
|
||||
|
||||
// 帮助中心状态
|
||||
const [showHelp, setShowHelp] = useState(false);
|
||||
@ -77,6 +61,12 @@ export function WashingDashboard() {
|
||||
// 版本更新检查
|
||||
const { showUpdate, setShowUpdate } = useVersionCheck();
|
||||
|
||||
// 账户卡片弹窗状态
|
||||
const [showAccountCard, setShowAccountCard] = useState(false);
|
||||
|
||||
// 最近计划轮播索引
|
||||
const [currentPlanIndex, setCurrentPlanIndex] = useState(0);
|
||||
|
||||
// 检查是否显示新手引导
|
||||
useEffect(() => {
|
||||
if (shouldShowOnboarding('washing')) {
|
||||
@ -96,10 +86,7 @@ export function WashingDashboard() {
|
||||
.eq('washing_factory_id', auth.company!.id)
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
const plans = washingPlans || [];
|
||||
setPlans(plans);
|
||||
setPendingCount(plans.filter(p => p.status === 'pending' || p.status === 'processing').length);
|
||||
setCompletedCount(plans.filter(p => p.status === 'completed').length);
|
||||
setPlans(washingPlans || []);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
@ -109,205 +96,240 @@ export function WashingDashboard() {
|
||||
completed: plans.filter(p => p.status === 'completed').length
|
||||
};
|
||||
|
||||
const recentPlans = plans.slice(0, 5);
|
||||
// 获取进行中的计划用于轮播
|
||||
const processingPlans = plans.filter(p => p.status === 'processing').slice(0, 5);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-purple-50 to-violet-50">
|
||||
<header className="bg-gradient-to-r from-purple-600 to-violet-600 shadow-lg">
|
||||
<div className="max-w-7xl mx-auto px-4 md:px-8 py-4 md:py-5 flex justify-between items-center">
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-violet-50/40 to-fuchsia-50/30">
|
||||
{/* Header - 与采购商一致 */}
|
||||
<header className="bg-white/80 backdrop-blur-xl border-b border-gray-200/50 sticky top-0 z-10">
|
||||
<div className="max-w-7xl mx-auto px-3 sm:px-4 md:px-8 py-3 sm:py-4 flex justify-between items-center">
|
||||
<div className="flex items-center gap-2 sm:gap-3">
|
||||
<div className="w-8 h-8 sm:w-10 sm:h-10 bg-white/20 rounded-xl flex items-center justify-center">
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={() => setShowAccountCard(true)}
|
||||
className="w-8 h-8 sm:w-10 sm:h-10 bg-gradient-to-br from-violet-400 to-fuchsia-400 rounded-xl flex items-center justify-center shadow-lg shadow-violet-400/20 cursor-pointer overflow-hidden"
|
||||
>
|
||||
{(auth.user as any)?.image_url ? (
|
||||
<img
|
||||
src={(auth.user as any).image_url}
|
||||
alt="头像"
|
||||
className="w-full h-full object-cover"
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Droplets className="w-4 h-4 sm:w-5 sm:h-5 text-white" />
|
||||
</div>
|
||||
)}
|
||||
</motion.button>
|
||||
<div>
|
||||
<h1 className="text-sm sm:text-lg font-bold text-white flex items-center gap-2 whitespace-nowrap">
|
||||
{auth.company?.name || '水洗厂'}工作台
|
||||
<span className="text-[10px] sm:text-xs bg-white/20 text-white px-1.5 sm:px-2 py-0.5 rounded-full font-medium whitespace-nowrap">限时免费</span>
|
||||
<h1 className="text-sm sm:text-lg font-bold text-gray-900 flex items-center gap-2 whitespace-nowrap">
|
||||
<span className="sm:hidden truncate max-w-[80px]">{(auth.user as any)?.display_name || auth.user?.username || '用户'}</span>
|
||||
<span className="hidden sm:inline">{auth.company?.name || '水洗厂'}工作台</span>
|
||||
<span className="text-[10px] sm:text-xs bg-gradient-to-r from-violet-400 to-fuchsia-400 text-white px-1.5 sm:px-2 py-0.5 rounded-full font-medium whitespace-nowrap">限时免费</span>
|
||||
</h1>
|
||||
<p className="text-xs text-white/70 hidden sm:block">{(auth.user as any)?.display_name || auth.user?.username || '用户'},欢迎回来</p>
|
||||
<p className="text-xs text-gray-500 hidden sm:block truncate max-w-[150px]">{(auth.user as any)?.display_name || auth.user?.username || '用户'},欢迎回来</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<HelpButton onClick={() => setShowHelp(true)} />
|
||||
<NotificationCenter theme="purple" />
|
||||
<NotificationCenter theme="violet" />
|
||||
<button
|
||||
onClick={async () => { await logout(); navigate('/login'); }}
|
||||
className="p-2 sm:px-3 sm:py-1 text-white/70 hover:text-white hover:bg-white/10 rounded-lg transition-colors"
|
||||
className="p-2 sm:px-3 sm:py-1.5 text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors"
|
||||
title="退出登录"
|
||||
>
|
||||
<LogOut className="w-5 h-5 sm:hidden" />
|
||||
<span className="hidden sm:inline text-xs md:text-sm">退出登录</span>
|
||||
<span className="hidden sm:inline text-xs sm:text-sm">退出</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="p-4 md:p-8 max-w-7xl mx-auto">
|
||||
<div className="p-3 sm:p-4 md:p-8 max-w-7xl mx-auto">
|
||||
{loading ? (
|
||||
<div className="text-center py-12 text-gray-400">加载中...</div>
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<div className="w-8 h-8 border-2 border-violet-400 border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
<motion.div variants={containerVariants} initial="hidden" animate="visible">
|
||||
<div className="grid grid-cols-3 gap-3 md:gap-6 mb-6 md:mb-8">
|
||||
<motion.div variants={itemVariants} className="bg-gradient-to-br from-purple-500 to-violet-500 rounded-xl md:rounded-2xl shadow-lg shadow-purple-500/20 p-4 md:p-6 text-white">
|
||||
<p className="text-xs md:text-sm text-purple-100 mb-1 md:mb-2">所有计划</p>
|
||||
<p className="text-2xl md:text-4xl font-bold">{stats.all}</p>
|
||||
<motion.div variants={containerVariants} initial="hidden" animate="visible" className="space-y-4 sm:space-y-6">
|
||||
{/* 统计卡片 - 与采购商一致 */}
|
||||
<div className="grid grid-cols-3 gap-2 sm:gap-4">
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
whileHover={isMobile ? {} : { scale: 1.02, y: -2 }}
|
||||
onClick={() => navigate('/washing/plans')}
|
||||
className="bg-white rounded-xl sm:rounded-2xl p-3 sm:p-5 shadow-sm border border-gray-100 cursor-pointer hover:border-violet-300 hover:shadow-md transition-all"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2 sm:mb-3">
|
||||
<p className="text-xs sm:text-sm text-gray-500">所有计划</p>
|
||||
<div className="w-6 h-6 sm:w-8 sm:h-8 bg-violet-50 rounded-lg flex items-center justify-center">
|
||||
<FileText className="w-3 h-3 sm:w-4 sm:h-4 text-violet-500" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xl sm:text-3xl font-bold text-gray-900">{stats.all}</p>
|
||||
<p className="text-xs text-gray-400 mt-0.5 sm:mt-1">累计接收</p>
|
||||
</motion.div>
|
||||
<motion.div variants={itemVariants} className="bg-gradient-to-br from-blue-500 to-cyan-500 rounded-xl md:rounded-2xl shadow-lg shadow-blue-500/20 p-4 md:p-6 text-white">
|
||||
<p className="text-xs md:text-sm text-blue-100 mb-1 md:mb-2">进行中</p>
|
||||
<p className="text-2xl md:text-4xl font-bold">{stats.producing}</p>
|
||||
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
whileHover={isMobile ? {} : { scale: 1.02, y: -2 }}
|
||||
onClick={() => navigate('/washing/plans?filter=processing')}
|
||||
className="bg-white rounded-xl sm:rounded-2xl p-3 sm:p-5 shadow-sm border border-gray-100 cursor-pointer hover:border-violet-300 hover:shadow-md transition-all"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2 sm:mb-3">
|
||||
<p className="text-xs sm:text-sm text-gray-500">进行中</p>
|
||||
<div className="w-6 h-6 sm:w-8 sm:h-8 bg-violet-50 rounded-lg flex items-center justify-center">
|
||||
<TrendingUp className="w-3 h-3 sm:w-4 sm:h-4 text-violet-500" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xl sm:text-3xl font-bold text-gray-900">{stats.producing}</p>
|
||||
<p className="text-xs text-gray-400 mt-0.5 sm:mt-1">正在水洗</p>
|
||||
</motion.div>
|
||||
<motion.div variants={itemVariants} className="bg-gradient-to-br from-green-500 to-emerald-500 rounded-xl md:rounded-2xl shadow-lg shadow-green-500/20 p-4 md:p-6 text-white">
|
||||
<p className="text-xs md:text-sm text-green-100 mb-1 md:mb-2">已完成</p>
|
||||
<p className="text-2xl md:text-4xl font-bold">{stats.completed}</p>
|
||||
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
whileHover={isMobile ? {} : { scale: 1.02, y: -2 }}
|
||||
onClick={() => navigate('/washing/plans?filter=completed')}
|
||||
className="bg-white rounded-xl sm:rounded-2xl p-3 sm:p-5 shadow-sm border border-gray-100 cursor-pointer hover:border-violet-300 hover:shadow-md transition-all"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2 sm:mb-3">
|
||||
<p className="text-xs sm:text-sm text-gray-500">已完成</p>
|
||||
<div className="w-6 h-6 sm:w-8 sm:h-8 bg-emerald-50 rounded-lg flex items-center justify-center">
|
||||
<Package className="w-3 h-3 sm:w-4 sm:h-4 text-emerald-600" />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xl sm:text-3xl font-bold text-gray-900">{stats.completed}</p>
|
||||
<p className="text-xs text-gray-400 mt-0.5 sm:mt-1">已完工</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<motion.div variants={itemVariants} className="bg-white/80 backdrop-blur rounded-2xl shadow-sm p-4 md:p-6 mb-6 md:mb-8 border border-white/50">
|
||||
<h2 className="text-base md:text-lg font-semibold text-gray-800 mb-4 md:mb-6">快捷操作</h2>
|
||||
<div className="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-6 gap-3 md:gap-4">
|
||||
{/* 最近计划 - 左右滑动展示 */}
|
||||
<motion.div variants={itemVariants} className="bg-white rounded-xl sm:rounded-2xl shadow-sm border border-gray-100 p-3 sm:p-4 md:p-6">
|
||||
<div className="flex justify-between items-center mb-3 sm:mb-4">
|
||||
<h2 className="text-sm sm:text-base font-semibold text-gray-900 whitespace-nowrap">最近计划</h2>
|
||||
<button
|
||||
onClick={() => navigate('/washing/plans')}
|
||||
className="text-xs sm:text-sm text-violet-500 hover:text-violet-600 font-medium flex items-center gap-1 whitespace-nowrap"
|
||||
>
|
||||
查看全部
|
||||
<ArrowLeft className="w-3 h-3 sm:w-4 sm:h-4 rotate-180" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{processingPlans.length === 0 ? (
|
||||
<div className="text-center py-8 sm:py-12 text-gray-400">
|
||||
<Package className="w-10 h-10 sm:w-12 sm:h-12 mx-auto mb-3 text-gray-300" />
|
||||
<p className="text-sm">暂无进行中的计划</p>
|
||||
<button
|
||||
onClick={() => navigate('/washing/plans')}
|
||||
className="mt-3 px-4 py-2 bg-violet-400 text-white rounded-lg text-sm hover:bg-violet-500 transition-colors"
|
||||
>
|
||||
查看计划
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative">
|
||||
{/* 滑动容器 */}
|
||||
<div className="overflow-hidden">
|
||||
<motion.div
|
||||
className="flex"
|
||||
animate={{ x: -currentPlanIndex * 100 + '%' }}
|
||||
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
|
||||
>
|
||||
{processingPlans.map((plan, i) => {
|
||||
const status = statusMap[plan.status] || statusMap.pending;
|
||||
const StatusIcon = status.icon;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={plan.id}
|
||||
className="w-full flex-shrink-0 px-1"
|
||||
onClick={() => navigate(`/washing/plans?id=${plan.id}`)}
|
||||
>
|
||||
<div className="group p-3 sm:p-4 rounded-xl border border-gray-100 hover:border-purple-200 hover:bg-purple-50/30 cursor-pointer transition-all">
|
||||
{/* 计划标题行 */}
|
||||
<div className="flex items-center gap-2 sm:gap-3">
|
||||
<div className={`w-8 h-8 sm:w-10 sm:h-10 rounded-lg flex items-center justify-center ${status.bgColor}`}>
|
||||
<StatusIcon className={`w-4 h-4 sm:w-5 sm:h-5 ${status.color}`} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-1.5 sm:gap-2">
|
||||
<h3 className="font-medium text-gray-900 text-sm sm:text-base truncate">{plan.plan_code}</h3>
|
||||
<span className={`px-1.5 py-0.5 text-xs rounded-full whitespace-nowrap ${status.bgColor} ${status.color}`}>
|
||||
{status.label}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">{plan.planned_meters}米</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* 左右滑动按钮 */}
|
||||
{processingPlans.length > 1 && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setCurrentPlanIndex(prev => Math.max(0, prev - 1))}
|
||||
disabled={currentPlanIndex === 0}
|
||||
className="absolute left-0 top-1/2 -translate-y-1/2 -translate-x-2 w-8 h-8 bg-white shadow-lg rounded-full flex items-center justify-center disabled:opacity-30 disabled:cursor-not-allowed hover:bg-gray-50 transition-colors z-10"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 text-gray-600" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentPlanIndex(prev => Math.min(processingPlans.length - 1, prev + 1))}
|
||||
disabled={currentPlanIndex === processingPlans.length - 1}
|
||||
className="absolute right-0 top-1/2 -translate-y-1/2 translate-x-2 w-8 h-8 bg-white shadow-lg rounded-full flex items-center justify-center disabled:opacity-30 disabled:cursor-not-allowed hover:bg-gray-50 transition-colors z-10"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 text-gray-600 rotate-180" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 指示器 */}
|
||||
{processingPlans.length > 1 && (
|
||||
<div className="flex items-center justify-center gap-1.5 mt-4">
|
||||
{processingPlans.map((_, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => setCurrentPlanIndex(i)}
|
||||
className={`w-2 h-2 rounded-full transition-all ${
|
||||
i === currentPlanIndex ? 'bg-purple-500 w-4' : 'bg-gray-300 hover:bg-gray-400'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
{/* 快捷操作 - 与采购商一致 */}
|
||||
<motion.div variants={itemVariants} className="bg-white rounded-xl sm:rounded-2xl shadow-sm border border-gray-100 p-3 sm:p-4 md:p-6">
|
||||
<h2 className="text-sm sm:text-base font-semibold text-gray-900 mb-3 sm:mb-4 whitespace-nowrap">快捷操作</h2>
|
||||
<div className="grid grid-cols-3 sm:grid-cols-4 gap-2 sm:gap-3">
|
||||
{getQuickActions(auth.user?.is_master || false).map((action, index) => (
|
||||
<motion.button
|
||||
key={action.id}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.1 + index * 0.05, duration: 0.3 }}
|
||||
whileHover={isMobile ? {} : { scale: 1.03, y: -3 }}
|
||||
whileHover={isMobile ? {} : { scale: 1.03, y: -2 }}
|
||||
whileTap={{ scale: 0.97 }}
|
||||
onClick={() => navigate(action.path)}
|
||||
className="flex flex-col items-center justify-center p-3 md:p-4 rounded-xl border border-gray-200 hover:border-transparent hover:shadow-md transition-all duration-200 group will-change-transform"
|
||||
className="group flex flex-col items-center p-2 sm:p-3 rounded-lg sm:rounded-xl border border-gray-100 hover:border-transparent hover:shadow-md transition-all bg-gray-50/50 hover:bg-white"
|
||||
>
|
||||
<div className={`w-10 h-10 md:w-12 md:h-12 rounded-xl flex items-center justify-center mb-2 md:mb-3 bg-gradient-to-br ${action.gradient} shadow-lg transition-shadow duration-200 group-hover:shadow-xl`}>
|
||||
<action.icon className="w-5 h-5 md:w-6 md:h-6 text-white" />
|
||||
<div className={`w-8 h-8 sm:w-10 sm:h-10 rounded-lg sm:rounded-xl flex items-center justify-center mb-1 sm:mb-2 bg-gradient-to-br ${action.gradient} shadow-sm group-hover:shadow-md transition-shadow`}>
|
||||
<action.icon className="w-4 h-4 sm:w-5 sm:h-5 text-white" />
|
||||
</div>
|
||||
<span className="text-xs md:text-sm text-gray-700 group-hover:text-gray-900 transition-colors text-center">{action.label}</span>
|
||||
<span className="text-[10px] sm:text-xs font-medium text-gray-700 group-hover:text-gray-900 text-center whitespace-nowrap">{action.label}</span>
|
||||
</motion.button>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-6">
|
||||
<motion.div variants={itemVariants} className="bg-white/80 backdrop-blur rounded-2xl shadow-sm p-4 md:p-6 border border-white/50">
|
||||
<div className="flex justify-between items-center mb-4 md:mb-6">
|
||||
<h2 className="text-base md:text-lg font-semibold text-gray-800">待处理坯布</h2>
|
||||
<button onClick={() => navigate('/washing/pending')} className="text-xs md:text-sm text-amber-600 hover:text-amber-700 font-medium">查看全部</button>
|
||||
</div>
|
||||
{pendingCount === 0 ? (
|
||||
<p className="text-center text-gray-400 py-6 md:py-8">暂无待处理坯布</p>
|
||||
) : (
|
||||
<div className="space-y-3 md:space-y-4">
|
||||
{plans
|
||||
.filter(p => p.status === 'pending' || p.status === 'processing')
|
||||
.slice(0, 3)
|
||||
.map((plan, i) => (
|
||||
<motion.div
|
||||
key={plan.id}
|
||||
initial={{ opacity: 0, x: -15 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: i * 0.08, duration: 0.35, ease: [0.25, 0.46, 0.45, 0.94] }}
|
||||
whileHover={isMobile ? {} : { x: 4, backgroundColor: 'rgba(254, 243, 199, 0.5)' }}
|
||||
whileTap={{ scale: 0.99 }}
|
||||
onClick={() => navigate(`/washing/plans?id=${plan.id}`)}
|
||||
className="border border-gray-200 rounded-xl p-3 md:p-4 hover:border-amber-300 cursor-pointer transition-all duration-200 will-change-transform"
|
||||
>
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<div>
|
||||
<p className="text-xs text-gray-500">{plan.plan_code}</p>
|
||||
<p className="text-sm font-medium text-gray-800 mt-1">{plan.planned_meters}米</p>
|
||||
</div>
|
||||
<span className={`px-2 py-1 rounded-lg text-xs font-medium ${statusMap[plan.status]?.color || 'bg-gray-100'}`}>
|
||||
{statusMap[plan.status]?.label || plan.status}
|
||||
</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
<motion.div variants={itemVariants} className="bg-white/80 backdrop-blur rounded-2xl shadow-sm p-4 md:p-6 border border-white/50">
|
||||
<div className="flex justify-between items-center mb-4 md:mb-6">
|
||||
<h2 className="text-base md:text-lg font-semibold text-gray-800">已完成坯布</h2>
|
||||
<button onClick={() => navigate('/washing/completed')} className="text-xs md:text-sm text-green-600 hover:text-green-700 font-medium">查看全部</button>
|
||||
</div>
|
||||
{completedCount === 0 ? (
|
||||
<p className="text-center text-gray-400 py-6 md:py-8">暂无已完成坯布</p>
|
||||
) : (
|
||||
<div className="space-y-3 md:space-y-4">
|
||||
{plans
|
||||
.filter(p => p.status === 'completed')
|
||||
.slice(0, 3)
|
||||
.map((plan, i) => (
|
||||
<motion.div
|
||||
key={plan.id}
|
||||
initial={{ opacity: 0, x: -15 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: i * 0.08, duration: 0.35, ease: [0.25, 0.46, 0.45, 0.94] }}
|
||||
whileHover={isMobile ? {} : { x: 4, backgroundColor: 'rgba(209, 250, 229, 0.5)' }}
|
||||
whileTap={{ scale: 0.99 }}
|
||||
onClick={() => navigate(`/washing/plans?id=${plan.id}`)}
|
||||
className="border border-gray-200 rounded-xl p-3 md:p-4 hover:border-green-300 cursor-pointer transition-all duration-200 will-change-transform"
|
||||
>
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<div>
|
||||
<p className="text-xs text-gray-500">{plan.plan_code}</p>
|
||||
<p className="text-sm font-medium text-gray-800 mt-1">
|
||||
洗前: {plan.planned_meters}米 → 洗后: {plan.actual_washed_meters || plan.estimated_washed_meters}米
|
||||
</p>
|
||||
</div>
|
||||
<span className="px-2 py-1 rounded-lg text-xs font-medium bg-green-100 text-green-700">
|
||||
已完成
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">
|
||||
缩水率: {plan.actual_shrinkage_rate || plan.estimated_shrinkage_rate}%
|
||||
</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<motion.div variants={itemVariants} className="bg-white/80 backdrop-blur rounded-2xl shadow-sm p-4 md:p-6 mt-4 md:mt-6 border border-white/50">
|
||||
<div className="flex justify-between items-center mb-4 md:mb-6">
|
||||
<h2 className="text-base md:text-lg font-semibold text-gray-800">最近计划</h2>
|
||||
<button onClick={() => navigate('/washing/plans')} className="text-xs md:text-sm text-purple-600 hover:text-purple-700 font-medium">查看全部</button>
|
||||
</div>
|
||||
{recentPlans.length === 0 ? (
|
||||
<p className="text-center text-gray-400 py-6 md:py-8">暂无关联计划</p>
|
||||
) : (
|
||||
<div className="space-y-3 md:space-y-4">
|
||||
{recentPlans.map((plan, i) => (
|
||||
<motion.div
|
||||
key={plan.id}
|
||||
initial={{ opacity: 0, x: -15 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: i * 0.08, duration: 0.35, ease: [0.25, 0.46, 0.45, 0.94] }}
|
||||
whileHover={isMobile ? {} : { x: 4, backgroundColor: 'rgba(243, 232, 255, 0.8)' }}
|
||||
whileTap={{ scale: 0.99 }}
|
||||
onClick={() => navigate(`/washing/plans?id=${plan.id}`)}
|
||||
className="border border-gray-200 rounded-xl p-4 md:p-5 hover:border-purple-300 cursor-pointer transition-all duration-200 will-change-transform"
|
||||
>
|
||||
<div className="flex justify-between items-start mb-2 md:mb-3">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">{plan.plan_code}</p>
|
||||
<p className="text-xs text-gray-400 mt-1">计划米数: {plan.planned_meters}米</p>
|
||||
</div>
|
||||
<span className={`px-2 py-1 md:px-3 md:py-1.5 rounded-lg text-xs md:text-sm font-medium ${statusMap[plan.status]?.color || 'bg-gray-100'}`}>
|
||||
{statusMap[plan.status]?.label || plan.status}
|
||||
</span>
|
||||
</div>
|
||||
{plan.status === 'completed' && (
|
||||
<div className="flex items-center gap-2 md:gap-4 text-xs md:text-sm text-gray-600">
|
||||
<span>洗后: {plan.actual_washed_meters}米</span>
|
||||
<span>缩水率: {plan.actual_shrinkage_rate}%</span>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
@ -339,6 +361,160 @@ export function WashingDashboard() {
|
||||
|
||||
{/* 崩溃报告 */}
|
||||
<CrashReporter />
|
||||
|
||||
{/* 账户卡片弹窗 */}
|
||||
{showAccountCard && (
|
||||
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4" onClick={() => setShowAccountCard(false)}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
className="bg-white rounded-2xl w-full max-w-sm shadow-2xl overflow-hidden"
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
{/* 头部 */}
|
||||
<div className="bg-gradient-to-r from-violet-400 to-fuchsia-400 p-6 text-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xl font-bold">账户信息</h2>
|
||||
<button onClick={() => setShowAccountCard(false)} className="p-2 hover:bg-white/20 rounded-lg transition-colors">
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 账户详情 */}
|
||||
<div className="p-6 space-y-4">
|
||||
{/* 用户信息 */}
|
||||
<div className="flex items-center gap-4">
|
||||
<label className="w-16 h-16 bg-gradient-to-br from-violet-400 to-fuchsia-400 rounded-xl flex items-center justify-center shadow-lg cursor-pointer hover:opacity-90 transition-opacity overflow-hidden relative group">
|
||||
{(auth.user as any)?.image_url ? (
|
||||
<img src={(auth.user as any).image_url} alt="头像" className="w-full h-full object-cover relative z-10" />
|
||||
) : (
|
||||
<Droplets className="w-8 h-8 text-white relative z-10" />
|
||||
)}
|
||||
<div className="absolute inset-0 bg-black/30 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity z-20">
|
||||
<Camera className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={async (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) return;
|
||||
try {
|
||||
const fileExt = file.name.split('.').pop();
|
||||
const fileName = `${user.id}-${Date.now()}.${fileExt}`;
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const { error: uploadError } = await supabase.storage
|
||||
.from('avatars')
|
||||
.upload(fileName, arrayBuffer, {
|
||||
contentType: file.type,
|
||||
upsert: true
|
||||
});
|
||||
if (uploadError) {
|
||||
console.error('头像上传失败:', uploadError);
|
||||
alert('头像上传失败: ' + uploadError.message);
|
||||
return;
|
||||
}
|
||||
const { data: { publicUrl } } = supabase.storage
|
||||
.from('avatars')
|
||||
.getPublicUrl(fileName);
|
||||
const { error: updateError } = await supabase
|
||||
.from('profiles')
|
||||
.update({ image_url: publicUrl } as any)
|
||||
.eq('id', user.id);
|
||||
if (updateError) {
|
||||
console.error('头像更新失败:', updateError);
|
||||
alert('头像更新失败: ' + updateError.message);
|
||||
} else {
|
||||
window.location.reload();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('头像上传出错:', err);
|
||||
alert('头像上传出错,请重试');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900 text-lg truncate max-w-[200px]">{(auth.user as any)?.display_name || auth.user?.username || '用户'}</p>
|
||||
<p className="text-sm text-gray-500">{auth.company?.name || '水洗厂'}</p>
|
||||
<span className="inline-flex items-center gap-1 text-xs bg-violet-100 text-violet-600 px-2 py-0.5 rounded-full mt-1">
|
||||
{auth.user?.is_master ? '主账号' : '子账号'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 功能菜单 */}
|
||||
<div className="space-y-2 pt-4 border-t border-gray-100">
|
||||
<button
|
||||
onClick={() => { setShowAccountCard(false); navigate('/members'); }}
|
||||
className="w-full flex items-center gap-3 p-3 rounded-xl hover:bg-gray-50 transition-colors text-left"
|
||||
>
|
||||
<div className="w-10 h-10 bg-violet-100 rounded-lg flex items-center justify-center">
|
||||
<Users className="w-5 h-5 text-violet-500" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">账号管理</p>
|
||||
<p className="text-xs text-gray-500">管理团队成员和账户设置</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => { setShowAccountCard(false); setShowUserManual(true); }}
|
||||
className="w-full flex items-center gap-3 p-3 rounded-xl hover:bg-gray-50 transition-colors text-left"
|
||||
>
|
||||
<div className="w-10 h-10 bg-violet-100 rounded-lg flex items-center justify-center">
|
||||
<BookOpen className="w-5 h-5 text-violet-500" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">用户手册</p>
|
||||
<p className="text-xs text-gray-500">查看详细使用指南</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => { setShowAccountCard(false); setShowFeedback(true); }}
|
||||
className="w-full flex items-center gap-3 p-3 rounded-xl hover:bg-gray-50 transition-colors text-left"
|
||||
>
|
||||
<div className="w-10 h-10 bg-violet-100 rounded-lg flex items-center justify-center">
|
||||
<MessageSquare className="w-5 h-5 text-violet-500" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">用户反馈</p>
|
||||
<p className="text-xs text-gray-500">提交问题或建议</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => { setShowAccountCard(false); setShowAbout(true); }}
|
||||
className="w-full flex items-center gap-3 p-3 rounded-xl hover:bg-gray-50 transition-colors text-left"
|
||||
>
|
||||
<div className="w-10 h-10 bg-gray-100 rounded-lg flex items-center justify-center">
|
||||
<Info className="w-5 h-5 text-gray-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">关于</p>
|
||||
<p className="text-xs text-gray-500">版本信息与版权</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 退出登录 */}
|
||||
<button
|
||||
onClick={async () => { await logout(); navigate('/login'); }}
|
||||
className="w-full py-3 bg-red-50 hover:bg-red-100 text-red-600 rounded-xl transition-colors flex items-center justify-center gap-2 font-medium"
|
||||
>
|
||||
<LogOut className="w-5 h-5" />
|
||||
退出登录
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -101,19 +101,19 @@ export function FinishedWarehouse() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-purple-50 to-violet-50">
|
||||
{/* Header */}
|
||||
<header className="bg-gradient-to-r from-purple-600 to-violet-600 shadow-lg">
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-purple-50/30 to-violet-50/30">
|
||||
{/* Header - 与 Dashboard 一致 */}
|
||||
<header className="bg-white/80 backdrop-blur-xl border-b border-gray-200/50 sticky top-0 z-10">
|
||||
<div className="max-w-7xl mx-auto px-4 md:px-8 py-4 flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => navigate('/washing')}
|
||||
className="p-2 hover:bg-white/20 rounded-lg transition-colors"
|
||||
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
||||
>
|
||||
<ArrowLeft className="w-5 h-5 text-white" />
|
||||
<ArrowLeft className="w-5 h-5 text-gray-600" />
|
||||
</button>
|
||||
<div>
|
||||
<h1 className="text-lg font-bold text-white">成品仓库</h1>
|
||||
<p className="text-xs text-white/70">管理水洗后的成品库存</p>
|
||||
<h1 className="text-lg font-bold text-gray-900">成品仓库</h1>
|
||||
<p className="text-xs text-gray-500">管理水洗后的成品库存</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@ -131,7 +131,7 @@ export function FinishedWarehouse() {
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-4 shadow-sm">
|
||||
<p className="text-xs text-gray-500 mb-1">库存匹/件数</p>
|
||||
<p className="text-2xl font-bold text-blue-600">{stats.totalStockRolls}</p>
|
||||
<p className="text-2xl font-bold text-purple-600">{stats.totalStockRolls}</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-4 shadow-sm">
|
||||
<p className="text-xs text-gray-500 mb-1">在库品种</p>
|
||||
|
||||
@ -21,7 +21,7 @@ interface PendingFabricItem {
|
||||
|
||||
const statusMap: Record<string, { label: string; color: string; bgColor: string }> = {
|
||||
pending: { label: '待确认', color: 'text-amber-600', bgColor: 'bg-amber-50' },
|
||||
processing: { label: '处理中', color: 'text-blue-600', bgColor: 'bg-blue-50' }
|
||||
processing: { label: '处理中', color: 'text-purple-600', bgColor: 'bg-purple-50' }
|
||||
};
|
||||
|
||||
export function PendingFabric() {
|
||||
@ -72,19 +72,19 @@ export function PendingFabric() {
|
||||
const totalMeters = items.reduce((sum, item) => sum + item.planned_meters, 0);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-amber-50 to-orange-50">
|
||||
{/* Header */}
|
||||
<header className="bg-gradient-to-r from-amber-500 to-orange-500 shadow-lg">
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-purple-50/30 to-violet-50/30">
|
||||
{/* Header - 与 Dashboard 一致 */}
|
||||
<header className="bg-white/80 backdrop-blur-xl border-b border-gray-200/50 sticky top-0 z-10">
|
||||
<div className="max-w-7xl mx-auto px-4 md:px-8 py-4 flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => navigate('/washing')}
|
||||
className="p-2 hover:bg-white/20 rounded-lg transition-colors"
|
||||
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
||||
>
|
||||
<ArrowLeft className="w-5 h-5 text-white" />
|
||||
<ArrowLeft className="w-5 h-5 text-gray-600" />
|
||||
</button>
|
||||
<div>
|
||||
<h1 className="text-lg font-bold text-white">待处理坯布</h1>
|
||||
<p className="text-xs text-white/70">查看待处理的水洗坯布</p>
|
||||
<h1 className="text-lg font-bold text-gray-900">待处理坯布</h1>
|
||||
<p className="text-xs text-gray-500">查看待处理的水洗坯布</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@ -23,7 +23,7 @@ interface PlanGroup {
|
||||
|
||||
const statusMap: Record<string, { label: string; color: string }> = {
|
||||
pending: { label: '待处理', color: 'bg-amber-100 text-amber-700' },
|
||||
processing: { label: '处理中', color: 'bg-blue-100 text-blue-700' },
|
||||
processing: { label: '处理中', color: 'bg-purple-100 text-purple-700' },
|
||||
completed: { label: '已完成', color: 'bg-green-100 text-green-700' }
|
||||
};
|
||||
|
||||
@ -171,24 +171,27 @@ export function WashingPlanOverview() {
|
||||
return {
|
||||
token: url.searchParams.get('token'),
|
||||
plan: url.searchParams.get('plan'),
|
||||
type: url.searchParams.get('type')
|
||||
type: url.searchParams.get('type'),
|
||||
shortCode: url.searchParams.get('s')
|
||||
};
|
||||
} catch {
|
||||
return { token: null, plan: null, type: null };
|
||||
return { token: null, plan: null, type: null, shortCode: null };
|
||||
}
|
||||
};
|
||||
|
||||
const handleImportFromLink = () => {
|
||||
if (!importLink.trim()) return;
|
||||
|
||||
const { token, plan, type } = extractParamsFromLink(importLink);
|
||||
const { token, plan, type, shortCode } = extractParamsFromLink(importLink);
|
||||
|
||||
if (token) {
|
||||
if (shortCode) {
|
||||
navigate(`/import?s=${shortCode}`);
|
||||
} else if (token) {
|
||||
navigate(`/import?token=${token}`);
|
||||
} else if (plan && type) {
|
||||
navigate(`/import?plan=${plan}&type=${type}`);
|
||||
} else {
|
||||
alert('无效的链接格式,请确保链接包含 token 或 plan 参数');
|
||||
alert('无效的链接格式,请确保链接包含 token、s 或 plan 参数');
|
||||
}
|
||||
};
|
||||
|
||||
@ -197,9 +200,11 @@ export function WashingPlanOverview() {
|
||||
const text = await navigator.clipboard.readText();
|
||||
if (text.includes('/import?') || text.includes('#/import?')) {
|
||||
setImportLink(text);
|
||||
const { token, plan, type } = extractParamsFromLink(text);
|
||||
const { token, plan, type, shortCode } = extractParamsFromLink(text);
|
||||
|
||||
if (token) {
|
||||
if (shortCode) {
|
||||
navigate(`/import?s=${shortCode}`);
|
||||
} else if (token) {
|
||||
navigate(`/import?token=${token}`);
|
||||
} else if (plan && type) {
|
||||
navigate(`/import?plan=${plan}&type=${type}`);
|
||||
@ -327,13 +332,13 @@ export function WashingPlanOverview() {
|
||||
<div key={stepType} className="flex items-start gap-3">
|
||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 ${
|
||||
isCompleted ? 'bg-green-500 text-white' :
|
||||
isActive ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-400'
|
||||
isActive ? 'bg-purple-500 text-white' : 'bg-gray-200 text-gray-400'
|
||||
}`}>
|
||||
{isCompleted ? <Check className="w-4 h-4" /> : index + 1}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className={`font-medium ${isCompleted ? 'text-green-700' : isActive ? 'text-blue-700' : 'text-gray-500'}`}>
|
||||
<p className={`font-medium ${isCompleted ? 'text-green-700' : isActive ? 'text-purple-700' : 'text-gray-500'}`}>
|
||||
{stepNames[stepType]}
|
||||
</p>
|
||||
{step?.timestamp && (
|
||||
@ -351,7 +356,7 @@ export function WashingPlanOverview() {
|
||||
<button
|
||||
onClick={() => handleConfirmStep(step.id, stepType)}
|
||||
disabled={confirming === step.id}
|
||||
className="mt-2 px-3 py-1 bg-blue-500 text-white text-xs rounded-lg hover:bg-blue-600 disabled:opacity-50"
|
||||
className="mt-2 px-3 py-1 bg-purple-500 text-white text-xs rounded-lg hover:bg-purple-600 disabled:opacity-50"
|
||||
>
|
||||
{confirming === step.id ? '确认中...' : '确认'}
|
||||
</button>
|
||||
@ -373,18 +378,24 @@ export function WashingPlanOverview() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 p-3 sm:p-4 md:p-6">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<div className="flex items-center gap-2 sm:gap-3 md:gap-4 mb-4 sm:mb-5 md:mb-6">
|
||||
<motion.button
|
||||
whileTap={{ scale: 0.95 }}
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-purple-50/30 to-violet-50/30">
|
||||
{/* Header - 与 Dashboard 一致 */}
|
||||
<header className="bg-white/80 backdrop-blur-xl border-b border-gray-200/50 sticky top-0 z-10">
|
||||
<div className="max-w-7xl mx-auto px-4 md:px-8 py-4 flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => navigate('/washing')}
|
||||
className="p-2 hover:bg-gray-200 rounded-full transition-colors will-change-transform"
|
||||
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
||||
>
|
||||
<ArrowLeft className="w-5 h-5 sm:w-6 sm:h-6 text-gray-600" />
|
||||
</motion.button>
|
||||
<h1 className="text-lg sm:text-xl md:text-2xl font-bold text-gray-800">计划总览</h1>
|
||||
<ArrowLeft className="w-5 h-5 text-gray-600" />
|
||||
</button>
|
||||
<div>
|
||||
<h1 className="text-lg font-bold text-gray-900">计划总览</h1>
|
||||
<p className="text-xs text-gray-500">查看和管理水洗计划</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="p-3 sm:p-4 md:p-6 max-w-6xl mx-auto">
|
||||
|
||||
{/* 导入链接区域 */}
|
||||
<motion.div
|
||||
|
||||
@ -1157,6 +1157,7 @@ export type Database = {
|
||||
created_at: string
|
||||
display_name: string | null
|
||||
id: string
|
||||
image_url: string | null
|
||||
is_master: boolean
|
||||
master_id: string | null
|
||||
phone: string | null
|
||||
@ -1167,6 +1168,7 @@ export type Database = {
|
||||
created_at?: string
|
||||
display_name?: string | null
|
||||
id: string
|
||||
image_url?: string | null
|
||||
is_master?: boolean
|
||||
master_id?: string | null
|
||||
phone?: string | null
|
||||
@ -1177,6 +1179,7 @@ export type Database = {
|
||||
created_at?: string
|
||||
display_name?: string | null
|
||||
id?: string
|
||||
image_url?: string | null
|
||||
is_master?: boolean
|
||||
master_id?: string | null
|
||||
phone?: string | null
|
||||
@ -1191,6 +1194,39 @@ export type Database = {
|
||||
},
|
||||
]
|
||||
}
|
||||
share_links: {
|
||||
Row: {
|
||||
click_count: number | null
|
||||
created_at: string
|
||||
created_by: string
|
||||
expires_at: string | null
|
||||
factory_type: string
|
||||
id: string
|
||||
plan_id: string
|
||||
short_code: string
|
||||
}
|
||||
Insert: {
|
||||
click_count?: number | null
|
||||
created_at?: string
|
||||
created_by: string
|
||||
expires_at?: string | null
|
||||
factory_type: string
|
||||
id?: string
|
||||
plan_id: string
|
||||
short_code: string
|
||||
}
|
||||
Update: {
|
||||
click_count?: number | null
|
||||
created_at?: string
|
||||
created_by?: string
|
||||
expires_at?: string | null
|
||||
factory_type?: string
|
||||
id?: string
|
||||
plan_id?: string
|
||||
short_code?: string
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
user_feedback: {
|
||||
Row: {
|
||||
contact: string | null
|
||||
@ -1596,6 +1632,67 @@ export type Database = {
|
||||
},
|
||||
]
|
||||
}
|
||||
yarn_stock_records: {
|
||||
Row: {
|
||||
batch_no: string | null
|
||||
company_id: string
|
||||
created_at: string
|
||||
id: string
|
||||
notes: string | null
|
||||
operator_id: string | null
|
||||
plan_id: string | null
|
||||
quantity: number
|
||||
record_type: string
|
||||
unit: string
|
||||
yarn_stock_id: string | null
|
||||
}
|
||||
Insert: {
|
||||
batch_no?: string | null
|
||||
company_id: string
|
||||
created_at?: string
|
||||
id?: string
|
||||
notes?: string | null
|
||||
operator_id?: string | null
|
||||
plan_id?: string | null
|
||||
quantity?: number
|
||||
record_type: string
|
||||
unit?: string
|
||||
yarn_stock_id?: string | null
|
||||
}
|
||||
Update: {
|
||||
batch_no?: string | null
|
||||
company_id?: string
|
||||
created_at?: string
|
||||
id?: string
|
||||
notes?: string | null
|
||||
operator_id?: string | null
|
||||
plan_id?: string | null
|
||||
quantity?: number
|
||||
record_type?: string
|
||||
unit?: string
|
||||
yarn_stock_id?: string | null
|
||||
}
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "yarn_stock_records_company_id_fkey"
|
||||
columns: ["company_id"]
|
||||
referencedRelation: "companies"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
{
|
||||
foreignKeyName: "yarn_stock_records_plan_id_fkey"
|
||||
columns: ["plan_id"]
|
||||
referencedRelation: "production_plans"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
{
|
||||
foreignKeyName: "yarn_stock_records_yarn_stock_id_fkey"
|
||||
columns: ["yarn_stock_id"]
|
||||
referencedRelation: "yarn_stock"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
Views: {
|
||||
[_ in never]: never
|
||||
@ -1649,6 +1746,10 @@ export type Database = {
|
||||
Args: { "": unknown }
|
||||
Returns: unknown
|
||||
}
|
||||
increment_share_link_clicks: {
|
||||
Args: { code: string }
|
||||
Returns: undefined
|
||||
}
|
||||
ivfflat_bit_support: {
|
||||
Args: { "": unknown }
|
||||
Returns: unknown
|
||||
|
||||
@ -37,26 +37,27 @@ export const stepOrder = ['confirm', 'yarn_purchase', 'dyeing', 'machine_start',
|
||||
|
||||
// 动画配置
|
||||
export const animationConfig = {
|
||||
// 标准过渡
|
||||
// 标准过渡 - 用于展开/收起,使用更平滑的缓动函数避免抖动
|
||||
transition: {
|
||||
duration: 0.3,
|
||||
ease: [0.25, 0.46, 0.45, 0.94]
|
||||
duration: 0.25,
|
||||
ease: [0.4, 0, 0.2, 1] // 使用 ease-out 缓动,更平滑
|
||||
},
|
||||
// 快速过渡
|
||||
fastTransition: {
|
||||
duration: 0.2,
|
||||
ease: [0.25, 0.46, 0.45, 0.94]
|
||||
ease: [0.4, 0, 0.2, 1]
|
||||
},
|
||||
// 慢速过渡
|
||||
slowTransition: {
|
||||
duration: 0.5,
|
||||
ease: [0.25, 0.46, 0.45, 0.94]
|
||||
duration: 0.4,
|
||||
ease: [0.4, 0, 0.2, 1]
|
||||
},
|
||||
// 弹簧动画
|
||||
// 弹簧动画 - 用于交互反馈,降低弹性避免抖动
|
||||
spring: {
|
||||
type: 'spring',
|
||||
stiffness: 300,
|
||||
damping: 30
|
||||
stiffness: 200,
|
||||
damping: 25,
|
||||
mass: 0.8
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user