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",
|
"id": "1",
|
||||||
|
"content": "创建短链映射表 share_links",
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"content": "纺织厂工作台集成帮助系统",
|
|
||||||
"priority": "high"
|
"priority": "high"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "2",
|
"id": "2",
|
||||||
|
"content": "修改分享链接生成逻辑,使用短链",
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"content": "水洗厂工作台集成帮助系统",
|
|
||||||
"priority": "high"
|
"priority": "high"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "3",
|
"id": "3",
|
||||||
|
"content": "修改导入页面,支持短链解析",
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"content": "通知中心按钮适配主题色",
|
|
||||||
"priority": "high"
|
"priority": "high"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "4",
|
"id": "4",
|
||||||
"status": "completed",
|
|
||||||
"content": "运行类型检查",
|
"content": "运行类型检查",
|
||||||
|
"status": "completed",
|
||||||
"priority": "medium"
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
353
AGENTS.md
353
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)
|
### 2025-05-27 (v1.0.2)
|
||||||
- **账号管理页面帮助系统集成**
|
- **账号管理页面帮助系统集成**
|
||||||
- 新增"帮助与支持"卡片区域
|
- 新增"帮助与支持"卡片区域
|
||||||
@ -2233,4 +2250,338 @@ class ErrorBoundary extends React.Component<
|
|||||||
- [ ] Lighthouse 评分 > 90
|
- [ ] Lighthouse 评分 > 90
|
||||||
- [ ] 移动端模拟器测试通过
|
- [ ] 移动端模拟器测试通过
|
||||||
- [ ] 类型检查通过 `pnpm run typecheck`
|
- [ ] 类型检查通过 `pnpm run typecheck`
|
||||||
- [ ] 代码规范检查通过 `pnpm run lint`
|
- [ ] 代码规范检查通过 `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%完成度自动提醒
|
- 实时进度可视化,支持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)
|
### 2025-05-27 (v1.0.2)
|
||||||
- **账号管理页面帮助系统集成**
|
- **账号管理页面帮助系统集成**
|
||||||
- 新增"帮助与支持"卡片区域,集成用户手册、用户反馈、关于窗口
|
- 新增"帮助与支持"卡片区域,集成用户手册、用户反馈、关于窗口
|
||||||
@ -254,7 +271,7 @@ import { InventoryRecordsModal } from '../../components/InventoryRecordsModal';
|
|||||||
|
|
||||||
### 2025-05-25
|
### 2025-05-25
|
||||||
- **产品图片上传功能**
|
- **产品图片上传功能**
|
||||||
- 为采购商原坯布仓库产品管理添加图片上传功能
|
- 为采购商产品信息管理添加图片上传功能
|
||||||
- 支持在产品创建和编辑时上传产品图片(JPG、PNG格式,最大5MB)
|
- 支持在产品创建和编辑时上传产品图片(JPG、PNG格式,最大5MB)
|
||||||
- 产品列表展示40x40px缩略图,无图片时显示默认占位图标
|
- 产品列表展示40x40px缩略图,无图片时显示默认占位图标
|
||||||
- 产品表单支持图片预览、重新上传和删除功能
|
- 产品表单支持图片预览、重新上传和删除功能
|
||||||
@ -265,7 +282,7 @@ import { InventoryRecordsModal } from '../../components/InventoryRecordsModal';
|
|||||||
|
|
||||||
### 2025-05-24
|
### 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 {
|
interface NotificationCenterProps {
|
||||||
theme?: 'amber' | 'emerald' | 'purple' | 'blue';
|
theme?: 'amber' | 'emerald' | 'purple' | 'blue' | 'violet';
|
||||||
}
|
}
|
||||||
|
|
||||||
const themeConfig = {
|
const themeConfig = {
|
||||||
@ -38,6 +38,7 @@ const themeConfig = {
|
|||||||
emerald: { bg: 'bg-emerald-100', hover: 'hover:bg-emerald-200', icon: 'text-emerald-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' },
|
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' },
|
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 = {}) {
|
export function NotificationCenter({ theme = 'amber' }: NotificationCenterProps = {}) {
|
||||||
@ -51,7 +52,11 @@ export function NotificationCenter({ theme = 'amber' }: NotificationCenterProps
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!auth.user) return;
|
if (!auth.user) return;
|
||||||
fetchNotifications();
|
fetchNotifications();
|
||||||
subscribeToNotifications();
|
const channel = subscribeToNotifications();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
channel.unsubscribe();
|
||||||
|
};
|
||||||
}, [auth.user]);
|
}, [auth.user]);
|
||||||
|
|
||||||
const fetchNotifications = async () => {
|
const fetchNotifications = async () => {
|
||||||
@ -79,8 +84,9 @@ export function NotificationCenter({ theme = 'amber' }: NotificationCenterProps
|
|||||||
};
|
};
|
||||||
|
|
||||||
const subscribeToNotifications = () => {
|
const subscribeToNotifications = () => {
|
||||||
supabase
|
const channel = supabase.channel('notifications');
|
||||||
.channel('notifications')
|
|
||||||
|
channel
|
||||||
.on('postgres_changes', {
|
.on('postgres_changes', {
|
||||||
event: 'INSERT',
|
event: 'INSERT',
|
||||||
schema: 'public',
|
schema: 'public',
|
||||||
@ -89,7 +95,13 @@ export function NotificationCenter({ theme = 'amber' }: NotificationCenterProps
|
|||||||
}, () => {
|
}, () => {
|
||||||
fetchNotifications();
|
fetchNotifications();
|
||||||
})
|
})
|
||||||
.subscribe();
|
.subscribe((status) => {
|
||||||
|
if (status === 'SUBSCRIBED') {
|
||||||
|
console.log('Successfully subscribed to notifications');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return channel;
|
||||||
};
|
};
|
||||||
|
|
||||||
const markAsRead = async (id: string) => {
|
const markAsRead = async (id: string) => {
|
||||||
|
|||||||
@ -39,7 +39,7 @@ const purchaserSteps: OnboardingStep[] = [
|
|||||||
{
|
{
|
||||||
id: 'manage-inventory',
|
id: 'manage-inventory',
|
||||||
title: '管理库存',
|
title: '管理库存',
|
||||||
description: '在"原坯布仓库"中查看和管理所有入库的坯布,支持出入库历史查询。',
|
description: '在"产品信息"中查看和管理所有入库的坯布,支持出入库历史查询。',
|
||||||
icon: Package,
|
icon: Package,
|
||||||
action: '去仓库管理',
|
action: '去仓库管理',
|
||||||
actionPath: '/purchaser/warehouse',
|
actionPath: '/purchaser/warehouse',
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { Check, Clock, Loader2, Package, AlertCircle, List } from 'lucide-react';
|
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 {
|
interface ProcessNode {
|
||||||
id: string;
|
id: string;
|
||||||
@ -23,6 +24,9 @@ interface ProcessFlowProps {
|
|||||||
isPlanLocked?: boolean;
|
isPlanLocked?: boolean;
|
||||||
lastInventoryTime?: string | null;
|
lastInventoryTime?: string | null;
|
||||||
inventoryRecords?: { time: string; quantity: number; rolls: number | null }[];
|
inventoryRecords?: { time: string; quantity: number; rolls: number | null }[];
|
||||||
|
yarnRatios?: YarnRatio[];
|
||||||
|
companyId?: string;
|
||||||
|
planId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const stepTypeConfig: Record<StepType, { name: string; icon: React.ReactNode }> = {
|
const stepTypeConfig: Record<StepType, { name: string; icon: React.ReactNode }> = {
|
||||||
@ -43,7 +47,10 @@ export function ProcessFlow({
|
|||||||
planCompletedQuantity = 0,
|
planCompletedQuantity = 0,
|
||||||
isPlanLocked = false,
|
isPlanLocked = false,
|
||||||
lastInventoryTime,
|
lastInventoryTime,
|
||||||
inventoryRecords = []
|
inventoryRecords = [],
|
||||||
|
yarnRatios = [],
|
||||||
|
companyId,
|
||||||
|
planId
|
||||||
}: ProcessFlowProps) {
|
}: ProcessFlowProps) {
|
||||||
const [inputQuantity, setInputQuantity] = useState<string>('');
|
const [inputQuantity, setInputQuantity] = useState<string>('');
|
||||||
const [inputRolls, setInputRolls] = useState<string>('');
|
const [inputRolls, setInputRolls] = useState<string>('');
|
||||||
@ -52,6 +59,8 @@ export function ProcessFlow({
|
|||||||
const [pendingQuantity, setPendingQuantity] = useState<number>(0);
|
const [pendingQuantity, setPendingQuantity] = useState<number>(0);
|
||||||
const [pendingRolls, setPendingRolls] = useState<number>(0);
|
const [pendingRolls, setPendingRolls] = useState<number>(0);
|
||||||
const [pendingNodeId, setPendingNodeId] = useState<string>('');
|
const [pendingNodeId, setPendingNodeId] = useState<string>('');
|
||||||
|
const [showYarnModal, setShowYarnModal] = useState(false);
|
||||||
|
const [pendingYarnStepId, setPendingYarnStepId] = useState<string>('');
|
||||||
|
|
||||||
// 按顺序排序节点
|
// 按顺序排序节点
|
||||||
const sortedNodes = React.useMemo(() => {
|
const sortedNodes = React.useMemo(() => {
|
||||||
@ -76,12 +85,27 @@ export function ProcessFlow({
|
|||||||
// 坯布入库需要输入数量
|
// 坯布入库需要输入数量
|
||||||
setShowInput(nodeId);
|
setShowInput(nodeId);
|
||||||
setInputQuantity('');
|
setInputQuantity('');
|
||||||
|
} else if (stepType === 'yarn_purchase') {
|
||||||
|
// 采纱环节需要检查纱线库存
|
||||||
|
if (yarnRatios.length > 0 && companyId) {
|
||||||
|
setPendingYarnStepId(nodeId);
|
||||||
|
setShowYarnModal(true);
|
||||||
|
} else {
|
||||||
|
// 没有纱线配比数据,直接确认
|
||||||
|
onConfirm?.(nodeId);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// 其他节点直接确认
|
// 其他节点直接确认
|
||||||
onConfirm?.(nodeId);
|
onConfirm?.(nodeId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleYarnAllocationConfirm = () => {
|
||||||
|
// 纱线分配确认后,继续确认采纱步骤
|
||||||
|
onConfirm?.(pendingYarnStepId);
|
||||||
|
setPendingYarnStepId('');
|
||||||
|
};
|
||||||
|
|
||||||
const handleWarehouseConfirm = (nodeId: string) => {
|
const handleWarehouseConfirm = (nodeId: string) => {
|
||||||
const quantity = parseFloat(inputQuantity);
|
const quantity = parseFloat(inputQuantity);
|
||||||
const rolls = parseInt(inputRolls) || 0;
|
const rolls = parseInt(inputRolls) || 0;
|
||||||
@ -377,6 +401,17 @@ export function ProcessFlow({
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{/* 纱线分配弹窗 */}
|
||||||
|
<YarnAllocationModal
|
||||||
|
isOpen={showYarnModal}
|
||||||
|
onClose={() => setShowYarnModal(false)}
|
||||||
|
onConfirm={handleYarnAllocationConfirm}
|
||||||
|
yarnRatios={yarnRatios}
|
||||||
|
companyId={companyId || ''}
|
||||||
|
planTargetQuantity={planTargetQuantity}
|
||||||
|
planId={planId}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,7 +12,7 @@ export const ProgressBar: React.FC<ProgressBarProps> = ({ progress, status }) =>
|
|||||||
case 'completed':
|
case 'completed':
|
||||||
return 'bg-green-500';
|
return 'bg-green-500';
|
||||||
case 'producing':
|
case 'producing':
|
||||||
return 'bg-blue-500';
|
return 'bg-amber-500';
|
||||||
default:
|
default:
|
||||||
return 'bg-gray-400';
|
return 'bg-gray-400';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,7 @@ interface StatusBadgeProps {
|
|||||||
|
|
||||||
const statusConfig: Record<string, { label: string; gradient: string }> = {
|
const statusConfig: Record<string, { label: string; gradient: string }> = {
|
||||||
pending: { label: '待确定', gradient: 'from-amber-400 to-orange-400' },
|
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' }
|
completed: { label: '已完成', gradient: 'from-green-400 to-emerald-400' }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
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 {
|
interface UserManualProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -27,7 +27,7 @@ const manualChapters = [
|
|||||||
{ title: '创建生产计划', content: '点击"新建计划"按钮,填写产品名称、颜色、坯布码、色号、计划产量等信息,配置纱线配比后提交。' },
|
{ title: '创建生产计划', content: '点击"新建计划"按钮,填写产品名称、颜色、坯布码、色号、计划产量等信息,配置纱线配比后提交。' },
|
||||||
{ title: '分享计划', content: '在计划总览页面,点击"分享计划"按钮,通过二次确认后将链接发送给纺织厂。纺织厂确认后即建立关联。' },
|
{ title: '分享计划', content: '在计划总览页面,点击"分享计划"按钮,通过二次确认后将链接发送给纺织厂。纺织厂确认后即建立关联。' },
|
||||||
{ 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 [activeChapter, setActiveChapter] = useState(manualChapters[0]);
|
||||||
const [expandedSection, setExpandedSection] = useState<number | null>(0);
|
const [expandedSection, setExpandedSection] = useState<number | null>(0);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [showChapterList, setShowChapterList] = useState(true);
|
||||||
|
|
||||||
// 搜索过滤
|
// 搜索过滤
|
||||||
const filteredChapters = searchQuery
|
const filteredChapters = searchQuery
|
||||||
@ -82,46 +83,57 @@ export function UserManual({ isOpen, onClose }: UserManualProps) {
|
|||||||
})).filter(ch => ch.sections.length > 0)
|
})).filter(ch => ch.sections.length > 0)
|
||||||
: manualChapters;
|
: manualChapters;
|
||||||
|
|
||||||
|
const handleChapterSelect = (chapter: typeof manualChapters[0]) => {
|
||||||
|
setActiveChapter(chapter);
|
||||||
|
setExpandedSection(0);
|
||||||
|
setShowChapterList(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBackToChapters = () => {
|
||||||
|
setShowChapterList(true);
|
||||||
|
};
|
||||||
|
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
return (
|
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
|
<motion.div
|
||||||
initial={{ opacity: 0, scale: 0.95 }}
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
exit={{ opacity: 0, scale: 0.95 }}
|
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 justify-between p-3 sm:p-4 border-b border-gray-100">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-2 sm:gap-3">
|
||||||
<div className="w-10 h-10 bg-blue-100 rounded-xl flex items-center justify-center">
|
<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-5 h-5 text-blue-600" />
|
<BookOpen className="w-4 h-4 sm:w-5 sm:h-5 text-blue-600" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold text-gray-900">用户手册</h2>
|
<h2 className="text-base sm:text-lg font-semibold text-gray-900">用户手册</h2>
|
||||||
<p className="text-xs text-gray-500">详细的使用指南和操作说明</p>
|
<p className="text-xs text-gray-500 hidden sm:block">详细的使用指南和操作说明</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-1 sm:gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => window.open('/manual.pdf', '_blank')}
|
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" />
|
<Download className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
|
||||||
下载PDF
|
<span className="hidden sm:inline">下载PDF</span>
|
||||||
|
<span className="sm:hidden">下载</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div className="relative">
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||||
<input
|
<input
|
||||||
@ -134,65 +146,111 @@ export function UserManual({ isOpen, onClose }: UserManualProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 内容区域 */}
|
{/* 移动端章节导航栏 */}
|
||||||
<div className="flex h-[60vh]">
|
{!showChapterList && (
|
||||||
{/* 左侧导航 */}
|
<div className="sm:hidden flex items-center gap-2 px-3 py-2 border-b border-gray-100 bg-gray-50">
|
||||||
<div className="w-64 border-r border-gray-100 overflow-y-auto bg-gray-50/50">
|
<button
|
||||||
{filteredChapters.map((chapter) => (
|
onClick={handleBackToChapters}
|
||||||
<button
|
className="flex items-center gap-1 text-sm text-blue-600 hover:text-blue-700"
|
||||||
key={chapter.id}
|
>
|
||||||
onClick={() => {
|
<ChevronLeft className="w-4 h-4" />
|
||||||
setActiveChapter(chapter);
|
返回目录
|
||||||
setExpandedSection(0);
|
</button>
|
||||||
}}
|
<span className="text-gray-400">|</span>
|
||||||
className={`w-full flex items-center gap-3 px-4 py-3 text-left transition-colors ${
|
<span className="text-sm font-medium text-gray-700 truncate">{activeChapter.title}</span>
|
||||||
activeChapter.id === chapter.id
|
|
||||||
? 'bg-blue-50 text-blue-700 border-r-2 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>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 右侧内容 */}
|
{/* 内容区域 */}
|
||||||
<div className="flex-1 overflow-y-auto p-6">
|
<div className="flex h-[calc(100vh-140px)] sm:h-[60vh]">
|
||||||
<h3 className="text-xl font-bold text-gray-900 mb-4">{activeChapter.title}</h3>
|
{/* 左侧章节列表 - 移动端全屏显示,桌面端固定宽度 */}
|
||||||
<div className="space-y-3">
|
<AnimatePresence mode="wait">
|
||||||
{activeChapter.sections.map((section, index) => (
|
{(showChapterList || window.innerWidth >= 640) && (
|
||||||
<div
|
<motion.div
|
||||||
key={index}
|
initial={{ x: -100, opacity: 0 }}
|
||||||
className="border border-gray-200 rounded-xl overflow-hidden"
|
animate={{ x: 0, opacity: 1 }}
|
||||||
>
|
exit={{ x: -100, opacity: 0 }}
|
||||||
<button
|
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`}
|
||||||
onClick={() => setExpandedSection(expandedSection === index ? null : index)}
|
>
|
||||||
className="w-full flex items-center justify-between p-4 text-left hover:bg-gray-50 transition-colors"
|
<div className="p-2 space-y-1">
|
||||||
>
|
{filteredChapters.map((chapter) => (
|
||||||
<span className="font-medium text-gray-900">{section.title}</span>
|
<button
|
||||||
<ChevronRight
|
key={chapter.id}
|
||||||
className={`w-4 h-4 text-gray-400 transition-transform ${
|
onClick={() => handleChapterSelect(chapter)}
|
||||||
expandedSection === index ? 'rotate-90' : ''
|
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-l-4 border-blue-500'
|
||||||
|
: 'text-gray-700 hover:bg-gray-100'
|
||||||
}`}
|
}`}
|
||||||
/>
|
>
|
||||||
</button>
|
<chapter.icon className={`w-5 h-5 shrink-0 ${activeChapter.id === chapter.id ? 'text-blue-600' : 'text-gray-400'}`} />
|
||||||
<AnimatePresence>
|
<div className="flex-1 min-w-0">
|
||||||
{expandedSection === index && (
|
<span className="text-sm font-medium block">{chapter.title}</span>
|
||||||
<motion.div
|
<span className="text-xs text-gray-400 block mt-0.5">{chapter.sections.length} 个章节</span>
|
||||||
initial={{ height: 0 }}
|
</div>
|
||||||
animate={{ height: 'auto' }}
|
<ChevronRight className={`w-4 h-4 shrink-0 ${activeChapter.id === chapter.id ? 'text-blue-500' : 'text-gray-300'}`} />
|
||||||
exit={{ height: 0 }}
|
</button>
|
||||||
className="overflow-hidden"
|
))}
|
||||||
>
|
|
||||||
<div className="p-4 pt-0 text-sm text-gray-600 leading-relaxed border-t border-gray-100">
|
|
||||||
{section.content}
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{/* 右侧内容详情 */}
|
||||||
|
<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 bg-white shadow-sm"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={() => setExpandedSection(expandedSection === index ? null : index)}
|
||||||
|
className="w-full flex items-center justify-between p-3 sm:p-4 text-left hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
<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 sm:w-5 sm:h-5 text-gray-400 transition-transform shrink-0 ${
|
||||||
|
expandedSection === index ? 'rotate-90' : ''
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<AnimatePresence>
|
||||||
|
{expandedSection === index && (
|
||||||
|
<motion.div
|
||||||
|
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-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>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -164,26 +164,9 @@ export function VersionUpdate({ isOpen, onClose, onUpdate }: VersionUpdateProps)
|
|||||||
<div className="flex gap-3 pt-2">
|
<div className="flex gap-3 pt-2">
|
||||||
<button
|
<button
|
||||||
onClick={handleSkip}
|
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>
|
</button>
|
||||||
</div>
|
</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">
|
<div className="flex flex-wrap gap-2">
|
||||||
{plan.yarn_ratios.map((yarn, index) => (
|
{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">
|
<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)`}
|
{yarn.amount_per_meter && `(${yarn.amount_per_meter}g/m)`}
|
||||||
</span>
|
</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 { motion } from 'framer-motion';
|
||||||
import { Share2, Copy, Check, X, AlertTriangle, Building2 } from 'lucide-react';
|
import { Share2, Copy, Check, X, AlertTriangle, Building2 } from 'lucide-react';
|
||||||
import { useResponsive } from '../../hooks/useResponsive';
|
import { useResponsive } from '../../hooks/useResponsive';
|
||||||
import { encryptShareParams } from '../../utils/shareCrypto';
|
import { encryptShareParamsSync } from '../../utils/shareCrypto';
|
||||||
import { supabase } from '../../supabase/client';
|
import { supabase } from '../../supabase/client';
|
||||||
|
|
||||||
interface ShareModalProps {
|
interface ShareModalProps {
|
||||||
@ -21,6 +21,8 @@ export function ShareModal({ isOpen, onClose, planId, factoryType, factoryName }
|
|||||||
const [confirmFactoryName, setConfirmFactoryName] = useState('');
|
const [confirmFactoryName, setConfirmFactoryName] = useState('');
|
||||||
const [confirmCompanyName, setConfirmCompanyName] = useState('');
|
const [confirmCompanyName, setConfirmCompanyName] = useState('');
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
|
const [shortCode, setShortCode] = useState<string>('');
|
||||||
|
const shortLinkGenerated = useRef(false);
|
||||||
|
|
||||||
// 获取当前用户的公司名称
|
// 获取当前用户的公司名称
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -46,14 +48,61 @@ export function ShareModal({ isOpen, onClose, planId, factoryType, factoryName }
|
|||||||
};
|
};
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
fetchCompanyName();
|
fetchCompanyName();
|
||||||
|
// 生成短链
|
||||||
|
if (!shortLinkGenerated.current) {
|
||||||
|
shortLinkGenerated.current = true;
|
||||||
|
generateShortLink();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 弹窗关闭时重置
|
||||||
|
shortLinkGenerated.current = false;
|
||||||
|
setShortCode('');
|
||||||
}
|
}
|
||||||
}, [isOpen]);
|
}, [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 generateShareLink = useCallback(() => {
|
||||||
const baseUrl = window.location.origin + window.location.pathname;
|
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}`;
|
return `${baseUrl}#/import?token=${token}`;
|
||||||
}, [planId, factoryType]);
|
}, [planId, factoryType, shortCode]);
|
||||||
|
|
||||||
const handleCopy = useCallback(async () => {
|
const handleCopy = useCallback(async () => {
|
||||||
const link = generateShareLink();
|
const link = generateShareLink();
|
||||||
@ -130,12 +179,6 @@ export function ShareModal({ isOpen, onClose, planId, factoryType, factoryName }
|
|||||||
>
|
>
|
||||||
关闭
|
关闭
|
||||||
</button>
|
</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
|
<button
|
||||||
onClick={handleCopy}
|
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 ${
|
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 => {
|
const generateNextFabricCode = async (companyId: string, prefix: string): Promise<string> => {
|
||||||
if (!currentCode) return '';
|
if (!prefix) return '';
|
||||||
// 匹配末尾的数字
|
|
||||||
const match = currentCode.match(/^(.*?)(\d+)$/);
|
try {
|
||||||
if (match) {
|
// 查询该公司所有以该前缀开头的产品码
|
||||||
const prefix = match[1];
|
const { data, error } = await supabase
|
||||||
const num = parseInt(match[2], 10);
|
.from('products')
|
||||||
return `${prefix}${num + 1}`;
|
.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 => {
|
const generateNextColorCode = async (
|
||||||
if (!currentCode) return '01';
|
companyId: string,
|
||||||
const num = parseInt(currentCode, 10);
|
productName: string,
|
||||||
const nextNum = num + 1;
|
totalWeight: number | null
|
||||||
// 保持两位数格式
|
): Promise<string> => {
|
||||||
return nextNum < 10 ? `0${nextNum}` : String(nextNum);
|
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] || [];
|
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 => ({
|
setFormData(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
product_name: suggestion.product_name,
|
product_name: suggestion.product_name,
|
||||||
@ -246,10 +320,10 @@ export function ProductForm({
|
|||||||
weft_weight: suggestion.weft_weight ? String(suggestion.weft_weight) : '',
|
weft_weight: suggestion.weft_weight ? String(suggestion.weft_weight) : '',
|
||||||
// 颜色需要用户自己输入,不自动填写
|
// 颜色需要用户自己输入,不自动填写
|
||||||
color: '',
|
color: '',
|
||||||
// 产品码自动生成(递增)
|
// 产品码自动生成(基于数据库最大值+1)
|
||||||
fabric_code: generateNextFabricCode(suggestion.fabric_code),
|
fabric_code: newFabricCode,
|
||||||
// 色号自动生成(递增)
|
// 色号自动生成(基于该产品名称-克重-颜色组合的最大值+1)
|
||||||
color_code: generateNextColorCode(suggestion.color_code),
|
color_code: newColorCode,
|
||||||
// 自动填写生产采购价
|
// 自动填写生产采购价
|
||||||
production_price: suggestion.production_price ? String(suggestion.production_price) : '',
|
production_price: suggestion.production_price ? String(suggestion.production_price) : '',
|
||||||
// 自动填写纱线配比
|
// 自动填写纱线配比
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
import React from 'react';
|
import React, { memo } from 'react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
|
||||||
import { ChevronDown, ChevronUp, List, Image as ImageIcon } from 'lucide-react';
|
import { ChevronDown, ChevronUp, List, Image as ImageIcon } from 'lucide-react';
|
||||||
import { useResponsive } from '../../hooks/useResponsive';
|
import { useResponsive } from '../../hooks/useResponsive';
|
||||||
import { animationConfig } from '../../utils/constants';
|
|
||||||
import type { ProductWithInventory, ProductYarnRatio, ProductInRecord } from '../../types';
|
import type { ProductWithInventory, ProductYarnRatio, ProductInRecord } from '../../types';
|
||||||
|
|
||||||
interface ProductListProps {
|
interface ProductListProps {
|
||||||
@ -30,14 +28,16 @@ export function ProductList({
|
|||||||
}: ProductListProps) {
|
}: ProductListProps) {
|
||||||
const { isMobile } = useResponsive();
|
const { isMobile } = useResponsive();
|
||||||
|
|
||||||
// 按名称和克重分组(优先使用 total_weight)
|
// 按名称和克重分组(优先使用 total_weight),无克重的产品直接按名称分组
|
||||||
const groupProductsByNameAndWeight = () => {
|
const groupProductsByNameAndWeight = () => {
|
||||||
const groups: Record<string, Record<string, ProductWithInventory[]>> = {};
|
const groups: Record<string, Record<string, ProductWithInventory[]>> = {};
|
||||||
products.forEach(product => {
|
products.forEach(product => {
|
||||||
if (!groups[product.product_name]) {
|
if (!groups[product.product_name]) {
|
||||||
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]) {
|
if (!groups[product.product_name][weightKey]) {
|
||||||
groups[product.product_name][weightKey] = [];
|
groups[product.product_name][weightKey] = [];
|
||||||
}
|
}
|
||||||
@ -46,15 +46,6 @@ export function ProductList({
|
|||||||
return groups;
|
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();
|
const productGroups = groupProductsByNameAndWeight();
|
||||||
|
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
@ -69,32 +60,27 @@ export function ProductList({
|
|||||||
{Object.entries(productGroups).map(([productName, weightGroups]) => {
|
{Object.entries(productGroups).map(([productName, weightGroups]) => {
|
||||||
const isNameExpanded = expandedGroups[productName];
|
const isNameExpanded = expandedGroups[productName];
|
||||||
const allProducts = Object.values(weightGroups).flat();
|
const allProducts = Object.values(weightGroups).flat();
|
||||||
// 计算总入库量(从入库记录)
|
|
||||||
const totalInRolls = allProducts.reduce((sum, p) => sum + (p.total_in_rolls || 0), 0);
|
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 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 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 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 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 availableMeters = allProducts.reduce((sum, p) => sum + (p.raw_fabric_meters || 0), 0);
|
||||||
const weightCount = Object.keys(weightGroups).length;
|
const weightCount = Object.keys(weightGroups).length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<div
|
||||||
key={productName}
|
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"
|
className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden"
|
||||||
>
|
>
|
||||||
{/* 一级分组头部 */}
|
{/* 一级分组头部 */}
|
||||||
<div
|
<div
|
||||||
onClick={() => onToggleGroup(productName)}
|
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="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'}`}>
|
<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-blue-600" /> : <ChevronDown size={18} className="text-gray-500" />}
|
{isNameExpanded ? <ChevronUp size={18} className="text-amber-600" /> : <ChevronDown size={18} className="text-gray-500" />}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-semibold text-gray-900">{productName}</h3>
|
<h3 className="font-semibold text-gray-900">{productName}</h3>
|
||||||
@ -110,83 +96,154 @@ export function ProductList({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 二级分组 */}
|
{/* 二级分组 - 使用CSS Grid实现平滑高度动画 */}
|
||||||
<AnimatePresence>
|
<div
|
||||||
{isNameExpanded && (
|
className="border-t border-gray-100 grid transition-all duration-300 ease-out"
|
||||||
<motion.div
|
style={{
|
||||||
initial={{ height: 0, opacity: 0 }}
|
gridTemplateRows: isNameExpanded ? '1fr' : '0fr',
|
||||||
animate={{ height: 'auto', opacity: 1 }}
|
opacity: isNameExpanded ? 1 : 0
|
||||||
exit={{ height: 0, opacity: 0 }}
|
}}
|
||||||
transition={animationConfig.transition}
|
>
|
||||||
className="border-t border-gray-100"
|
<div className="overflow-hidden">
|
||||||
>
|
<div className="p-4 space-y-3">
|
||||||
<div className="p-4 space-y-3">
|
{Object.entries(weightGroups).map(([weight, products]) => {
|
||||||
{Object.entries(weightGroups).map(([weight, products]) => {
|
const weightKey = `${productName}-${weight}`;
|
||||||
const weightKey = `${productName}-${weight}`;
|
const isWeightExpanded = expandedGroups[weightKey];
|
||||||
const isWeightExpanded = expandedGroups[weightKey];
|
const hasNoWeight = weight === '__no_weight__';
|
||||||
|
const isSingleWeight = Object.keys(weightGroups).length === 1;
|
||||||
|
|
||||||
return (
|
// 如果没有克重,直接显示产品列表
|
||||||
<div key={weight} className="bg-gray-50 rounded-xl overflow-hidden">
|
if (hasNoWeight) {
|
||||||
<div
|
return (
|
||||||
onClick={() => onToggleGroup(weightKey)}
|
<div key={weight} className="bg-gray-50 rounded-xl overflow-hidden">
|
||||||
className="flex items-center justify-between p-3 cursor-pointer hover:bg-gray-100 transition-colors"
|
<table className="w-full">
|
||||||
>
|
<thead className="bg-white/50 border-b border-gray-100">
|
||||||
<div className="flex items-center gap-2">
|
<tr>
|
||||||
<div className={`w-6 h-6 rounded flex items-center justify-center transition-colors ${isWeightExpanded ? 'bg-blue-100' : 'bg-white'}`}>
|
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500">颜色</th>
|
||||||
{isWeightExpanded ? <ChevronUp size={14} className="text-blue-600" /> : <ChevronDown size={14} className="text-gray-500" />}
|
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500">产品码</th>
|
||||||
</div>
|
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500">库存</th>
|
||||||
<span className="font-medium text-gray-800">{weight}g</span>
|
<th className="px-3 py-2 text-right text-xs font-medium text-gray-500">操作</th>
|
||||||
{products[0]?.warp_weight && products[0]?.weft_weight && (
|
</tr>
|
||||||
<span className="text-xs text-gray-400 ml-1">经{products[0].warp_weight}+纬{products[0].weft_weight}</span>
|
</thead>
|
||||||
)}
|
<tbody className="divide-y divide-gray-100">
|
||||||
<span className="text-xs text-gray-500">({products.length}个颜色)</span>
|
{products.map(product => (
|
||||||
</div>
|
<ProductRow
|
||||||
</div>
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
<AnimatePresence>
|
// 如果只有一种克重,直接显示
|
||||||
{isWeightExpanded && (
|
if (isSingleWeight) {
|
||||||
<motion.div
|
return (
|
||||||
initial={{ height: 0, opacity: 0 }}
|
<div key={weight} className="bg-gray-50 rounded-xl overflow-hidden">
|
||||||
animate={{ height: 'auto', opacity: 1 }}
|
<div className="flex items-center gap-2 p-3 bg-gray-100 border-b border-gray-200">
|
||||||
exit={{ height: 0, opacity: 0 }}
|
<span className="font-medium text-gray-800">{weight}g</span>
|
||||||
transition={animationConfig.fastTransition}
|
{products[0]?.warp_weight && products[0]?.weft_weight && (
|
||||||
className="border-t border-gray-200"
|
<span className="text-xs text-gray-500 ml-1">经{products[0].warp_weight}+纬{products[0].weft_weight}</span>
|
||||||
>
|
)}
|
||||||
<table className="w-full">
|
<span className="text-xs text-gray-500">({products.length}个颜色)</span>
|
||||||
<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>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
<table className="w-full">
|
||||||
})}
|
<thead className="bg-white/50 border-b border-gray-100">
|
||||||
</div>
|
<tr>
|
||||||
</motion.div>
|
<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>
|
||||||
</AnimatePresence>
|
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500">库存</th>
|
||||||
</motion.div>
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
@ -198,23 +255,20 @@ export function ProductList({
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{Object.entries(productGroups).map(([productName, weightGroups]) => {
|
{Object.entries(productGroups).map(([productName, weightGroups]) => {
|
||||||
const isNameExpanded = expandedGroups[productName];
|
const isNameExpanded = expandedGroups[productName];
|
||||||
const allProducts = Object.values(weightGroups).flat();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<div
|
||||||
key={productName}
|
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"
|
className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
onClick={() => onToggleGroup(productName)}
|
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 justify-between items-start">
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
<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'}`}>
|
<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-blue-600" /> : <ChevronDown size={16} className="text-gray-500" />}
|
{isNameExpanded ? <ChevronUp size={16} className="text-amber-600" /> : <ChevronDown size={16} className="text-gray-500" />}
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="font-medium text-gray-900 text-sm truncate">{productName}</p>
|
<p className="font-medium text-gray-900 text-sm truncate">{productName}</p>
|
||||||
@ -223,21 +277,46 @@ export function ProductList({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AnimatePresence>
|
{/* 移动端展开内容 - 使用CSS Grid实现平滑高度动画 */}
|
||||||
{isNameExpanded && (
|
<div
|
||||||
<motion.div
|
className="border-t border-gray-100 grid transition-all duration-300 ease-out"
|
||||||
initial={{ height: 0, opacity: 0 }}
|
style={{
|
||||||
animate={{ height: 'auto', opacity: 1 }}
|
gridTemplateRows: isNameExpanded ? '1fr' : '0fr',
|
||||||
exit={{ height: 0, opacity: 0 }}
|
opacity: isNameExpanded ? 1 : 0
|
||||||
className="border-t border-gray-100 bg-gray-50"
|
}}
|
||||||
>
|
>
|
||||||
<div className="p-3 space-y-2">
|
<div className="overflow-hidden bg-gray-50">
|
||||||
{Object.entries(weightGroups).map(([weight, products]) => (
|
<div className="p-3 space-y-2">
|
||||||
|
{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 key={weight} className="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||||
<div className="p-3">
|
<div className="p-3 bg-gray-50 border-b border-gray-100">
|
||||||
<span className="font-medium text-gray-800 text-sm">{weight}g</span>
|
<span className="font-medium text-gray-800 text-sm">{weight}g</span>
|
||||||
{products[0]?.warp_weight && products[0]?.weft_weight && (
|
{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 ml-1">经{products[0].warp_weight}+纬{products[0].weft_weight}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="p-2 space-y-2">
|
<div className="p-2 space-y-2">
|
||||||
@ -255,12 +334,38 @@ export function ProductList({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
</div>
|
}
|
||||||
</motion.div>
|
|
||||||
)}
|
return (
|
||||||
</AnimatePresence>
|
<div key={weight} className="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||||
</motion.div>
|
<div className="p-3">
|
||||||
|
<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-400 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>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
@ -281,7 +386,7 @@ interface ProductRowProps {
|
|||||||
|
|
||||||
function ProductRow({ product, ratios, onViewRecords, onViewPriceHistory, onViewStockHistory, onEdit, onDelete }: ProductRowProps) {
|
function ProductRow({ product, ratios, onViewRecords, onViewPriceHistory, onViewStockHistory, onEdit, onDelete }: ProductRowProps) {
|
||||||
return (
|
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">
|
<td className="px-3 py-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{product.image_url ? (
|
{product.image_url ? (
|
||||||
@ -316,7 +421,7 @@ function ProductRow({ product, ratios, onViewRecords, onViewPriceHistory, onView
|
|||||||
<div className="text-xs text-gray-400 mt-1">
|
<div className="text-xs text-gray-400 mt-1">
|
||||||
<button
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); onViewRecords(); }}
|
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" />
|
<List className="w-3 h-3" />
|
||||||
入库{product.in_records?.length || 0}条
|
入库{product.in_records?.length || 0}条
|
||||||
@ -328,7 +433,7 @@ function ProductRow({ product, ratios, onViewRecords, onViewPriceHistory, onView
|
|||||||
<td className="px-3 py-2 text-right">
|
<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={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={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>
|
<button onClick={onDelete} className="text-red-600 hover:text-red-800 text-sm font-medium">删除</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -378,7 +483,7 @@ function ProductCardMobile({ product, ratios, onViewRecords, onViewPriceHistory,
|
|||||||
<div className="text-xs text-gray-400 mb-1">
|
<div className="text-xs text-gray-400 mb-1">
|
||||||
<button
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); onViewRecords(); }}
|
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" />
|
<List className="w-3 h-3" />
|
||||||
入库{product.in_records?.length || 0}条
|
入库{product.in_records?.length || 0}条
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { useAuth } from '../contexts/AuthContext';
|
|||||||
import { supabase } from '../supabase/client';
|
import { supabase } from '../supabase/client';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { ArrowLeft, CheckCircle, AlertTriangle, Package, Droplet, Link2 } from 'lucide-react';
|
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 { useResponsive } from '../hooks/useResponsive';
|
||||||
import type { ProductionPlan, WashingPlan, YarnRatio, FactoryType } from '../types';
|
import type { ProductionPlan, WashingPlan, YarnRatio, FactoryType } from '../types';
|
||||||
|
|
||||||
@ -25,22 +25,47 @@ export function ImportPlanPage() {
|
|||||||
const { isMobile } = useResponsive();
|
const { isMobile } = useResponsive();
|
||||||
|
|
||||||
const token = searchParams.get('token') || '';
|
const token = searchParams.get('token') || '';
|
||||||
|
const shortCode = searchParams.get('s') || '';
|
||||||
const [planId, setPlanId] = useState('');
|
const [planId, setPlanId] = useState('');
|
||||||
const [factoryType, setFactoryType] = useState<FactoryType | null>(null);
|
const [factoryType, setFactoryType] = useState<FactoryType | null>(null);
|
||||||
|
const [decrypting, setDecrypting] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (token) {
|
const resolveLink = async () => {
|
||||||
decryptShareParams(token).then(decrypted => {
|
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) {
|
if (decrypted) {
|
||||||
setPlanId(decrypted.planId);
|
setPlanId(decrypted.planId);
|
||||||
setFactoryType(decrypted.factoryType as FactoryType);
|
setFactoryType(decrypted.factoryType as FactoryType);
|
||||||
}
|
}
|
||||||
});
|
setDecrypting(false);
|
||||||
} else {
|
} else {
|
||||||
setPlanId(searchParams.get('plan') || '');
|
setPlanId(searchParams.get('plan') || '');
|
||||||
setFactoryType(searchParams.get('type') as FactoryType | null);
|
setFactoryType(searchParams.get('type') as FactoryType | null);
|
||||||
}
|
setDecrypting(false);
|
||||||
}, [token]);
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
resolveLink();
|
||||||
|
}, [token, shortCode]);
|
||||||
|
|
||||||
const [plan, setPlan] = useState<ProductionPlan | WashingPlan | null>(null);
|
const [plan, setPlan] = useState<ProductionPlan | WashingPlan | null>(null);
|
||||||
const [yarns, setYarns] = useState<YarnRatio[]>([]);
|
const [yarns, setYarns] = useState<YarnRatio[]>([]);
|
||||||
@ -93,6 +118,11 @@ export function ImportPlanPage() {
|
|||||||
setLoading(false);
|
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一致
|
// 角色匹配:当前角色必须与链接中的factoryType一致
|
||||||
const roleMatches = auth.company && auth.currentRole === factoryType;
|
const roleMatches = auth.company && auth.currentRole === factoryType;
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { ComingSoonModal } from '../components/ComingSoonModal';
|
|||||||
const demoAccounts = [
|
const demoAccounts = [
|
||||||
{ username: 'purchaser', label: '采购商(布行)', color: 'from-blue-500 to-blue-600', icon: Building2, enabled: true },
|
{ 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: '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) {
|
if (!auth.user?.is_master) {
|
||||||
return (
|
return (
|
||||||
@ -319,7 +361,7 @@ export function MemberManage() {
|
|||||||
<p className="text-gray-500 mb-4">只有主账号可以管理账号</p>
|
<p className="text-gray-500 mb-4">只有主账号可以管理账号</p>
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate(-1)}
|
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>
|
</button>
|
||||||
@ -349,7 +391,7 @@ export function MemberManage() {
|
|||||||
whileHover={{ scale: isMobile ? 1 : 1.05 }}
|
whileHover={{ scale: isMobile ? 1 : 1.05 }}
|
||||||
whileTap={{ scale: 0.95 }}
|
whileTap={{ scale: 0.95 }}
|
||||||
onClick={handleOpenModal}
|
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} />
|
<Plus size={18} />
|
||||||
添加子账号
|
添加子账号
|
||||||
@ -357,14 +399,14 @@ export function MemberManage() {
|
|||||||
</div>
|
</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 justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-12 h-12 bg-white/20 rounded-full flex items-center justify-center">
|
<div className="w-12 h-12 bg-white/20 rounded-full flex items-center justify-center">
|
||||||
<Shield className="w-6 h-6" />
|
<Shield className="w-6 h-6" />
|
||||||
</div>
|
</div>
|
||||||
<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>
|
<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">
|
<span className="inline-flex items-center gap-1 text-xs bg-white/20 px-2 py-0.5 rounded mt-1">
|
||||||
<Shield size={12} />
|
<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="bg-white rounded-lg shadow-sm p-4 md:p-6 mb-6">
|
||||||
<div className="flex items-center gap-2 mb-4">
|
<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>
|
<h2 className="font-semibold text-gray-800">帮助与支持</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||||
@ -394,10 +436,10 @@ export function MemberManage() {
|
|||||||
whileHover={{ scale: 1.02 }}
|
whileHover={{ scale: 1.02 }}
|
||||||
whileTap={{ scale: 0.98 }}
|
whileTap={{ scale: 0.98 }}
|
||||||
onClick={() => setShowUserManual(true)}
|
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">
|
<div className={`w-10 h-10 ${theme.bg} rounded-lg flex items-center justify-center`}>
|
||||||
<BookOpen className="w-5 h-5 text-blue-600" />
|
<BookOpen className={`w-5 h-5 ${theme.text}`} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-gray-800">用户手册</p>
|
<p className="font-medium text-gray-800">用户手册</p>
|
||||||
@ -409,10 +451,10 @@ export function MemberManage() {
|
|||||||
whileHover={{ scale: 1.02 }}
|
whileHover={{ scale: 1.02 }}
|
||||||
whileTap={{ scale: 0.98 }}
|
whileTap={{ scale: 0.98 }}
|
||||||
onClick={() => setShowFeedback(true)}
|
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">
|
<div className={`w-10 h-10 ${theme.bg} rounded-lg flex items-center justify-center`}>
|
||||||
<MessageSquare className="w-5 h-5 text-emerald-600" />
|
<MessageSquare className={`w-5 h-5 ${theme.text}`} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-gray-800">用户反馈</p>
|
<p className="font-medium text-gray-800">用户反馈</p>
|
||||||
@ -424,10 +466,10 @@ export function MemberManage() {
|
|||||||
whileHover={{ scale: 1.02 }}
|
whileHover={{ scale: 1.02 }}
|
||||||
whileTap={{ scale: 0.98 }}
|
whileTap={{ scale: 0.98 }}
|
||||||
onClick={() => setShowAbout(true)}
|
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">
|
<div className={`w-10 h-10 ${theme.bg} rounded-lg flex items-center justify-center`}>
|
||||||
<Info className="w-5 h-5 text-purple-600" />
|
<Info className={`w-5 h-5 ${theme.text}`} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-gray-800">关于</p>
|
<p className="font-medium text-gray-800">关于</p>
|
||||||
@ -451,7 +493,7 @@ export function MemberManage() {
|
|||||||
whileHover={{ scale: isMobile ? 1 : 1.05 }}
|
whileHover={{ scale: isMobile ? 1 : 1.05 }}
|
||||||
whileTap={{ scale: 0.95 }}
|
whileTap={{ scale: 0.95 }}
|
||||||
onClick={handleOpenModal}
|
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>
|
</motion.button>
|
||||||
@ -535,7 +577,7 @@ export function MemberManage() {
|
|||||||
type="text"
|
type="text"
|
||||||
value={formData.username}
|
value={formData.username}
|
||||||
onChange={(e) => setFormData({ ...formData, username: e.target.value })}
|
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个字符)"
|
placeholder="请输入用户名(至少3个字符)"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@ -552,7 +594,7 @@ export function MemberManage() {
|
|||||||
type="text"
|
type="text"
|
||||||
value={formData.displayName}
|
value={formData.displayName}
|
||||||
onChange={(e) => setFormData({ ...formData, displayName: e.target.value })}
|
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="请输入姓名"
|
placeholder="请输入姓名"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@ -569,7 +611,7 @@ export function MemberManage() {
|
|||||||
type="tel"
|
type="tel"
|
||||||
value={formData.phone}
|
value={formData.phone}
|
||||||
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
|
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="请输入手机号"
|
placeholder="请输入手机号"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@ -584,7 +626,7 @@ export function MemberManage() {
|
|||||||
type="password"
|
type="password"
|
||||||
value={formData.password}
|
value={formData.password}
|
||||||
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
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个字符)"
|
placeholder="请输入密码(至少6个字符)"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@ -614,7 +656,7 @@ export function MemberManage() {
|
|||||||
whileTap={{ scale: 0.98 }}
|
whileTap={{ scale: 0.98 }}
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={submitting}
|
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 ? '创建中...' : '创建子账号'}
|
{submitting ? '创建中...' : '创建子账号'}
|
||||||
</motion.button>
|
</motion.button>
|
||||||
@ -705,14 +747,14 @@ export function MemberManage() {
|
|||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<User className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
|
<User className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={masterFormData.displayName}
|
value={masterFormData.displayName}
|
||||||
onChange={(e) => setMasterFormData({ ...masterFormData, displayName: e.target.value })}
|
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="请输入姓名"
|
placeholder="请输入姓名"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -723,14 +765,14 @@ export function MemberManage() {
|
|||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Phone className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
|
<Phone className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
|
||||||
<input
|
<input
|
||||||
type="tel"
|
type="tel"
|
||||||
value={masterFormData.phone}
|
value={masterFormData.phone}
|
||||||
onChange={(e) => setMasterFormData({ ...masterFormData, phone: e.target.value })}
|
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="请输入手机号"
|
placeholder="请输入手机号"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -743,7 +785,7 @@ export function MemberManage() {
|
|||||||
type="password"
|
type="password"
|
||||||
value={masterFormData.password || ''}
|
value={masterFormData.password || ''}
|
||||||
onChange={(e) => setMasterFormData({ ...masterFormData, password: e.target.value })}
|
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个字符)"
|
placeholder="请输入新密码(至少6个字符)"
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-gray-400 mt-1">留空则保持原密码不变</p>
|
<p className="text-xs text-gray-400 mt-1">留空则保持原密码不变</p>
|
||||||
@ -774,7 +816,7 @@ export function MemberManage() {
|
|||||||
whileTap={{ scale: 0.98 }}
|
whileTap={{ scale: 0.98 }}
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={editingMaster}
|
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 ? '保存中...' : '保存修改'}
|
{editingMaster ? '保存中...' : '保存修改'}
|
||||||
</motion.button>
|
</motion.button>
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import { useAuth } from '../../contexts/AuthContext';
|
import { useAuth } from '../../contexts/AuthContext';
|
||||||
import { supabase } from '../../supabase/client';
|
import { supabase } from '../../supabase/client';
|
||||||
import { motion } from 'framer-motion';
|
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 { useResponsive } from '../../hooks/useResponsive';
|
||||||
import { OnboardingGuide, shouldShowOnboarding } from '../../components/OnboardingGuide';
|
import { OnboardingGuide, shouldShowOnboarding } from '../../components/OnboardingGuide';
|
||||||
import { NotificationCenter } from '../../components/NotificationCenter';
|
import { NotificationCenter } from '../../components/NotificationCenter';
|
||||||
@ -14,25 +14,21 @@ import { UserManual } from '../../components/UserManual';
|
|||||||
import type { ProductionPlan } from '../../types';
|
import type { ProductionPlan } from '../../types';
|
||||||
|
|
||||||
const statusMap: Record<string, { label: string; color: string; bgColor: string; icon: any }> = {
|
const statusMap: Record<string, { label: string; color: string; bgColor: string; icon: any }> = {
|
||||||
pending: { label: '待确定', color: 'text-amber-600', bgColor: 'bg-amber-50', icon: Clock },
|
pending: { label: '待确定', color: 'text-amber-500', bgColor: 'bg-amber-50', icon: Clock },
|
||||||
producing: { label: '生产中', color: 'text-blue-600', bgColor: 'bg-blue-50', icon: TrendingUp },
|
producing: { label: '生产中', color: 'text-amber-500', bgColor: 'bg-amber-50', icon: TrendingUp },
|
||||||
completed: { label: '已完成', color: 'text-emerald-600', bgColor: 'bg-emerald-50', icon: Package }
|
completed: { label: '已完成', color: 'text-emerald-500', bgColor: 'bg-emerald-50', icon: Package }
|
||||||
};
|
};
|
||||||
|
|
||||||
const getQuickActions = (isMaster: boolean) => {
|
const getQuickActions = () => {
|
||||||
const actions = [
|
return [
|
||||||
{ id: 'plans', icon: FileText, label: '计划总览', path: '/purchaser/plans', gradient: 'from-blue-500 to-cyan-500', description: '查看所有计划' },
|
{ 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-emerald-500 to-green-500', 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-500 to-blue-500', 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-500 to-orange-500', 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-emerald-500 to-teal-500', 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-purple-500 to-violet-500', 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-500 to-pink-500', 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 = {
|
const containerVariants = {
|
||||||
@ -59,6 +55,9 @@ export function PurchaserDashboard() {
|
|||||||
const [showFeedback, setShowFeedback] = useState(false);
|
const [showFeedback, setShowFeedback] = useState(false);
|
||||||
const [showAbout, setShowAbout] = useState(false);
|
const [showAbout, setShowAbout] = useState(false);
|
||||||
|
|
||||||
|
// 账户卡片弹窗状态
|
||||||
|
const [showAccountCard, setShowAccountCard] = useState(false);
|
||||||
|
|
||||||
// 版本更新检查
|
// 版本更新检查
|
||||||
const { showUpdate, setShowUpdate } = useVersionCheck();
|
const { showUpdate, setShowUpdate } = useVersionCheck();
|
||||||
|
|
||||||
@ -107,7 +106,9 @@ export function PurchaserDashboard() {
|
|||||||
completed: plans.filter(p => p.status === 'completed').length
|
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 (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50/30 to-indigo-50/30">
|
<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">
|
<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="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="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
|
||||||
<Building2 className="w-4 h-4 sm:w-5 sm:h-5 text-white" />
|
whileHover={{ scale: 1.05 }}
|
||||||
</div>
|
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" />
|
||||||
|
)}
|
||||||
|
</motion.button>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-sm sm:text-lg font-bold text-gray-900 flex items-center gap-2 whitespace-nowrap">
|
<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>
|
<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>
|
</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>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@ -200,11 +218,118 @@ export function PurchaserDashboard() {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
</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">
|
<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>
|
<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">
|
<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
|
<motion.button
|
||||||
key={action.id}
|
key={action.id}
|
||||||
whileHover={isMobile ? {} : { scale: 1.03, y: -2 }}
|
whileHover={isMobile ? {} : { scale: 1.03, y: -2 }}
|
||||||
@ -220,88 +345,6 @@ export function PurchaserDashboard() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</motion.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>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -330,6 +373,173 @@ export function PurchaserDashboard() {
|
|||||||
isOpen={showUpdate}
|
isOpen={showUpdate}
|
||||||
onClose={() => setShowUpdate(false)}
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,7 @@ interface YarnInput {
|
|||||||
yarn_type?: 'warp' | 'weft' | 'all';
|
yarn_type?: 'warp' | 'weft' | 'all';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 产品类型定义(来自原坯布仓库)
|
// 产品类型定义(来自产品信息)
|
||||||
interface Product {
|
interface Product {
|
||||||
id: string;
|
id: string;
|
||||||
product_name: string;
|
product_name: string;
|
||||||
@ -49,7 +49,7 @@ export function NewPlan() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { auth } = useAuth();
|
const { auth } = useAuth();
|
||||||
|
|
||||||
// 从原坯布仓库选择的产品
|
// 从产品信息选择的产品
|
||||||
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
|
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
|
||||||
const [products, setProducts] = useState<Product[]>([]);
|
const [products, setProducts] = useState<Product[]>([]);
|
||||||
const [productRatios, setProductRatios] = useState<Record<string, ProductYarnRatio[]>>({});
|
const [productRatios, setProductRatios] = useState<Record<string, ProductYarnRatio[]>>({});
|
||||||
@ -78,7 +78,7 @@ export function NewPlan() {
|
|||||||
fetchProducts();
|
fetchProducts();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 从原坯布仓库获取产品数据
|
// 从产品信息获取产品数据
|
||||||
const fetchProducts = async () => {
|
const fetchProducts = async () => {
|
||||||
if (!auth.company) return;
|
if (!auth.company) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@ -288,10 +288,10 @@ export function NewPlan() {
|
|||||||
|
|
||||||
<div className="max-w-4xl mx-auto px-4 py-6">
|
<div className="max-w-4xl mx-auto px-4 py-6">
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
{/* 原坯布仓库产品选择 - 搜索+筛选形式 */}
|
{/* 产品信息选择 - 搜索+筛选形式 */}
|
||||||
<div className="bg-white rounded-lg shadow-sm p-6">
|
<div className="bg-white rounded-lg shadow-sm p-6">
|
||||||
<h2 className="text-base font-semibold mb-4 flex items-center gap-2">
|
<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>
|
</h2>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="text-center py-8 text-gray-400">加载中...</div>
|
<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>
|
<p className="text-xs text-gray-400 mt-1">自动从原坯布产品获取</p>
|
||||||
</div>
|
</div>
|
||||||
<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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={selectedProduct?.total_weight ? `${selectedProduct.total_weight}g/m` : (selectedProduct?.weight ? `${selectedProduct.weight}g/m` : '-')}
|
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>
|
<p className="text-xs text-gray-400 mt-1">自动从原坯布产品获取</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="col-span-2">
|
||||||
<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>
|
|
||||||
<label className="block text-sm text-gray-600 mb-1">纱线名称/占比</label>
|
<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) => (
|
{yarns.filter(y => y.name).map((yarn, idx) => (
|
||||||
<span key={yarn.id} className="inline-block mr-2">
|
<span key={yarn.id} className="inline-block whitespace-nowrap">
|
||||||
{yarnTypeLabels[yarn.yarn_type || 'all']}/{yarn.name}:{yarn.ratio}{idx < yarns.filter(y => y.name).length - 1 ? '、' : ''}
|
{yarnTypeLabels[yarn.yarn_type || 'all']}:{yarn.name} 占比:{Number(yarn.ratio).toFixed(0)}%
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
{!yarns.some(y => y.name) && <span className="text-gray-400">-</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">
|
<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
|
<button
|
||||||
onClick={() => setShowProductModal(false)}
|
onClick={() => setShowProductModal(false)}
|
||||||
className="p-2 hover:bg-gray-200 rounded-full transition-colors"
|
className="p-2 hover:bg-gray-200 rounded-full transition-colors"
|
||||||
|
|||||||
@ -36,7 +36,7 @@ export function NewWashingPlan() {
|
|||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
// 获取原坯布仓库产品(包含库存计算)
|
// 获取产品信息(包含库存计算)
|
||||||
const fetchProducts = useCallback(async () => {
|
const fetchProducts = useCallback(async () => {
|
||||||
if (!auth.company) return;
|
if (!auth.company) return;
|
||||||
|
|
||||||
@ -295,7 +295,7 @@ export function NewWashingPlan() {
|
|||||||
{products.length === 0 && (
|
{products.length === 0 && (
|
||||||
<p className="text-xs text-amber-600 mt-2 flex items-center gap-1">
|
<p className="text-xs text-amber-600 mt-2 flex items-center gap-1">
|
||||||
<AlertCircle className="w-3 h-3" />
|
<AlertCircle className="w-3 h-3" />
|
||||||
原坯布仓库中没有可用产品,请先创建产品并入库
|
产品信息中没有可用产品,请先创建产品并入库
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -169,6 +169,38 @@ export function WarehouseManage() {
|
|||||||
image_url: formData.image_url || null
|
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;
|
let productId: string;
|
||||||
|
|
||||||
if (editingProduct) {
|
if (editingProduct) {
|
||||||
@ -415,7 +447,7 @@ export function WarehouseManage() {
|
|||||||
>
|
>
|
||||||
<ArrowLeft className="w-5 h-5 text-gray-600" />
|
<ArrowLeft className="w-5 h-5 text-gray-600" />
|
||||||
</button>
|
</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>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import { useAuth } from '../../contexts/AuthContext';
|
import { useAuth } from '../../contexts/AuthContext';
|
||||||
import { supabase } from '../../supabase/client';
|
import { supabase } from '../../supabase/client';
|
||||||
import { motion } from 'framer-motion';
|
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 { useResponsive } from '../../hooks/useResponsive';
|
||||||
import { OnboardingGuide, shouldShowOnboarding } from '../../components/OnboardingGuide';
|
import { OnboardingGuide, shouldShowOnboarding } from '../../components/OnboardingGuide';
|
||||||
import { NotificationCenter } from '../../components/NotificationCenter';
|
import { NotificationCenter } from '../../components/NotificationCenter';
|
||||||
@ -14,60 +14,29 @@ import { UserManual } from '../../components/UserManual';
|
|||||||
import { CrashReporter } from '../../components/CrashReporter';
|
import { CrashReporter } from '../../components/CrashReporter';
|
||||||
import type { ProductionPlan } from '../../types';
|
import type { ProductionPlan } from '../../types';
|
||||||
|
|
||||||
const getQuickActions = (isMaster: boolean) => {
|
const statusMap: Record<string, { label: string; color: string; bgColor: string; icon: any }> = {
|
||||||
const actions = [
|
pending: { label: '待确定', color: 'text-emerald-500', bgColor: 'bg-emerald-50', icon: HelpCircle },
|
||||||
{ id: 'plans', icon: Link2, label: '计划总览', path: '/textile/plans', gradient: 'from-emerald-500 to-green-500' },
|
producing: { label: '生产中', color: 'text-emerald-500', bgColor: 'bg-emerald-50', icon: TrendingUp },
|
||||||
{ id: 'fabric', icon: Warehouse, label: '已生产坯布', path: '/textile/fabric-warehouse', gradient: 'from-indigo-500 to-purple-500' },
|
completed: { label: '已完成', color: 'text-teal-500', bgColor: 'bg-teal-50', icon: Package }
|
||||||
{ 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 }> = {
|
const getQuickActions = () => {
|
||||||
pending: { label: '待确定', color: 'bg-yellow-100 text-yellow-700' },
|
return [
|
||||||
producing: { label: '生产中', color: 'bg-blue-100 text-blue-700' },
|
{ id: 'plans', icon: FileText, label: '计划总览', path: '/textile/plans', gradient: 'from-emerald-400 to-teal-400', description: '查看所有计划' },
|
||||||
completed: { label: '已完成', color: 'bg-green-100 text-green-700' }
|
{ 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 = {
|
const containerVariants = {
|
||||||
hidden: { opacity: 0 },
|
hidden: { opacity: 0 },
|
||||||
visible: {
|
visible: { opacity: 1, transition: { staggerChildren: 0.1 } }
|
||||||
opacity: 1,
|
|
||||||
transition: {
|
|
||||||
staggerChildren: 0.08,
|
|
||||||
delayChildren: 0.05
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const itemVariants = {
|
const itemVariants = {
|
||||||
hidden: { opacity: 0, y: 15, scale: 0.98 },
|
hidden: { opacity: 0, y: 20 },
|
||||||
visible: {
|
visible: { opacity: 1, y: 0, transition: { duration: 0.4 } }
|
||||||
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 }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function TextileDashboard() {
|
export function TextileDashboard() {
|
||||||
@ -85,6 +54,9 @@ export function TextileDashboard() {
|
|||||||
const [showFeedback, setShowFeedback] = useState(false);
|
const [showFeedback, setShowFeedback] = useState(false);
|
||||||
const [showAbout, setShowAbout] = useState(false);
|
const [showAbout, setShowAbout] = useState(false);
|
||||||
|
|
||||||
|
// 账户卡片弹窗状态
|
||||||
|
const [showAccountCard, setShowAccountCard] = useState(false);
|
||||||
|
|
||||||
// 版本更新检查
|
// 版本更新检查
|
||||||
const { showUpdate, setShowUpdate } = useVersionCheck();
|
const { showUpdate, setShowUpdate } = useVersionCheck();
|
||||||
|
|
||||||
@ -152,23 +124,41 @@ export function TextileDashboard() {
|
|||||||
|
|
||||||
// 过滤掉被拒绝的计划
|
// 过滤掉被拒绝的计划
|
||||||
const filteredPlans = plans.filter(p => !rejectedPlanIds.has(p.id));
|
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 (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-green-50 to-emerald-50">
|
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-emerald-50/30 to-green-50/30">
|
||||||
{/* 新用户引导动画 - 已更新为弹窗模式 */}
|
{/* Header - 与采购商保持一致 */}
|
||||||
<header className="bg-gradient-to-r from-emerald-600 to-green-600 shadow-lg">
|
<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 md:py-5 flex justify-between items-center">
|
<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="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
|
||||||
<Factory className="w-4 h-4 sm:w-5 sm:h-5 text-white" />
|
whileHover={{ scale: 1.05 }}
|
||||||
</div>
|
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" />
|
||||||
|
)}
|
||||||
|
</motion.button>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-sm sm:text-lg font-bold text-white flex items-center gap-2 whitespace-nowrap">
|
<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="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>
|
<span className="hidden sm:inline">{auth.company?.name || '纺织厂'}工作台</span>
|
||||||
</h1>
|
<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>
|
||||||
<p className="text-xs text-white/70 hidden sm:block">{(auth.user as any)?.display_name || auth.user?.username || '用户'},欢迎回来</p>
|
</h1>
|
||||||
|
<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>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@ -176,130 +166,209 @@ export function TextileDashboard() {
|
|||||||
<NotificationCenter theme="emerald" />
|
<NotificationCenter theme="emerald" />
|
||||||
<button
|
<button
|
||||||
onClick={async () => { await logout(); navigate('/login'); }}
|
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="退出登录"
|
title="退出登录"
|
||||||
>
|
>
|
||||||
<LogOut className="w-5 h-5 sm:hidden" />
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</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 ? (
|
{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">
|
<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-3 md:gap-6 mb-4 sm:mb-6 md:mb-8">
|
{/* 统计卡片 - 与采购商保持一致 */}
|
||||||
|
<div className="grid grid-cols-3 gap-2 sm:gap-4">
|
||||||
<motion.div
|
<motion.div
|
||||||
variants={itemVariants}
|
variants={itemVariants}
|
||||||
whileHover={isMobile ? {} : { scale: 1.02, y: -2 }}
|
whileHover={isMobile ? {} : { scale: 1.02, y: -2 }}
|
||||||
whileTap={{ scale: 0.98 }}
|
onClick={() => navigate('/textile/plans')}
|
||||||
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"
|
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>
|
<div className="flex items-center justify-between mb-2 sm:mb-3">
|
||||||
<p className="text-xl sm:text-2xl md:text-4xl font-bold">{stats.all}</p>
|
<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>
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
variants={itemVariants}
|
variants={itemVariants}
|
||||||
whileHover={isMobile ? {} : { scale: 1.02, y: -2 }}
|
whileHover={isMobile ? {} : { scale: 1.02, y: -2 }}
|
||||||
whileTap={{ scale: 0.98 }}
|
onClick={() => navigate('/textile/plans?filter=producing')}
|
||||||
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"
|
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>
|
<div className="flex items-center justify-between mb-2 sm:mb-3">
|
||||||
<p className="text-xl sm:text-2xl md:text-4xl font-bold">{stats.producing}</p>
|
<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>
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
variants={itemVariants}
|
variants={itemVariants}
|
||||||
whileHover={isMobile ? {} : { scale: 1.02, y: -2 }}
|
whileHover={isMobile ? {} : { scale: 1.02, y: -2 }}
|
||||||
whileTap={{ scale: 0.98 }}
|
onClick={() => navigate('/textile/plans?filter=completed')}
|
||||||
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"
|
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>
|
<div className="flex items-center justify-between mb-2 sm:mb-3">
|
||||||
<p className="text-xl sm:text-2xl md:text-4xl font-bold">{stats.completed}</p>
|
<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>
|
</motion.div>
|
||||||
</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>
|
<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="grid grid-cols-3 gap-2 sm:gap-3 md:gap-6">
|
<div className="flex justify-between items-center mb-3 sm:mb-4">
|
||||||
{getQuickActions(auth.user?.is_master || false).map((action, index) => (
|
<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"
|
||||||
|
>
|
||||||
|
查看全部
|
||||||
|
<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>
|
||||||
|
<p className="text-xs text-gray-400 mt-1">等待采购商分配计划</p>
|
||||||
|
</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(`/textile/plans?id=${plan.id}`)}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<p className="text-xs text-gray-500">{plan.plan_code}</p>
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
|
</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
|
<motion.button
|
||||||
key={action.id}
|
key={action.id}
|
||||||
initial={{ opacity: 0, y: 10 }}
|
whileHover={isMobile ? {} : { scale: 1.03, y: -2 }}
|
||||||
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 }}
|
whileTap={{ scale: 0.97 }}
|
||||||
onClick={() => navigate(action.path)}
|
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"
|
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 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`}>
|
<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-5 h-5 sm:w-6 sm:h-6 md:w-7 md:h-7 text-white" />
|
<action.icon className="w-4 h-4 sm:w-5 sm:h-5 text-white" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs sm:text-sm md:text-base text-gray-700 group-hover:text-gray-900 transition-colors">{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>
|
</motion.button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</motion.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>
|
|
||||||
</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) => {
|
|
||||||
const progress = plan.target_quantity > 0
|
|
||||||
? Math.round((plan.completed_quantity / plan.target_quantity) * 100)
|
|
||||||
: 0;
|
|
||||||
return (
|
|
||||||
<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(236, 253, 245, 0.8)' }}
|
|
||||||
whileTap={{ scale: 0.99 }}
|
|
||||||
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>
|
|
||||||
<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}
|
|
||||||
</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>
|
|
||||||
</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>
|
|
||||||
<p className="text-[10px] sm:text-xs md:text-sm text-gray-500 mt-1 sm:mt-2">进度 {progress}%</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</motion.div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</motion.div>
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -331,6 +400,160 @@ export function TextileDashboard() {
|
|||||||
|
|
||||||
{/* 崩溃报告 */}
|
{/* 崩溃报告 */}
|
||||||
<CrashReporter />
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -258,7 +258,7 @@ export function FabricWarehouse() {
|
|||||||
<motion.div
|
<motion.div
|
||||||
animate={{ rotate: 360 }}
|
animate={{ rotate: 360 }}
|
||||||
transition={{ duration: 1, repeat: Infinity, ease: 'linear' }}
|
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>
|
<p className="text-gray-500 text-xs sm:text-sm">加载中...</p>
|
||||||
</div>
|
</div>
|
||||||
@ -290,30 +290,33 @@ export function FabricWarehouse() {
|
|||||||
transition={{ delay: groupIndex * 0.08, duration: 0.35, ease: [0.25, 0.46, 0.45, 0.94] }}
|
transition={{ delay: groupIndex * 0.08, duration: 0.35, ease: [0.25, 0.46, 0.45, 0.94] }}
|
||||||
className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden will-change-transform"
|
className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden will-change-transform"
|
||||||
>
|
>
|
||||||
{/* 分组头部 */}
|
{/* 分组头部 */}
|
||||||
<motion.div
|
<motion.div
|
||||||
onClick={() => toggleExpand(fabricCode)}
|
onClick={() => toggleExpand(fabricCode)}
|
||||||
whileTap={{ scale: 0.995 }}
|
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
|
<motion.div
|
||||||
animate={{ rotate: isExpanded ? 180 : 0 }}
|
animate={{ rotate: isExpanded ? 180 : 0 }}
|
||||||
transition={{ duration: 0.2 }}
|
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>
|
</motion.div>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="text-xs sm:text-sm text-gray-500 truncate">
|
<p className="text-sm font-medium text-gray-900 truncate">
|
||||||
坯布码: {fabricCode} · 已入库{totalInMeters}米 · 计划{totalTargetMeters}米
|
{fabricCode}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
<p className="text-xs text-gray-500 mt-0.5">
|
||||||
</div>
|
已入库 <span className="text-emerald-600 font-medium">{totalInMeters.toLocaleString()}</span> 米 · 计划 {totalTargetMeters.toLocaleString()} 米
|
||||||
<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">
|
</p>
|
||||||
{items.length}个计划
|
</div>
|
||||||
</span>
|
</div>
|
||||||
</motion.div>
|
<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>
|
||||||
|
|
||||||
{/* 展开的库存列表 - 移动端卡片式,桌面端表格 */}
|
{/* 展开的库存列表 - 移动端卡片式,桌面端表格 */}
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
@ -328,43 +331,49 @@ export function FabricWarehouse() {
|
|||||||
{/* 桌面端表格 */}
|
{/* 桌面端表格 */}
|
||||||
<div className="hidden sm:block overflow-x-auto">
|
<div className="hidden sm:block overflow-x-auto">
|
||||||
<table className="w-full min-w-[500px]">
|
<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>
|
<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-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">计划编号</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-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-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>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-50">
|
<tbody className="divide-y divide-gray-100">
|
||||||
{items.map((item, itemIndex) => (
|
{items.map((item, itemIndex) => (
|
||||||
<motion.tr
|
<motion.tr
|
||||||
key={item.plan_id}
|
key={item.plan_id}
|
||||||
initial={{ opacity: 0, x: -10 }}
|
initial={{ opacity: 0, x: -10 }}
|
||||||
animate={{ opacity: 1, x: 0 }}
|
animate={{ opacity: 1, x: 0 }}
|
||||||
transition={{ delay: itemIndex * 0.05, duration: 0.25 }}
|
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">
|
<td className="px-4 py-3">
|
||||||
<span className="px-2 py-1 bg-gray-100 rounded-lg text-xs sm:text-sm font-mono text-gray-700">
|
<span className="px-2.5 py-1 bg-emerald-50 text-emerald-700 rounded-lg text-xs font-mono font-medium">
|
||||||
{item.plan_code}
|
{item.plan_code}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 sm:px-4 py-2 sm:py-3">
|
<td className="px-4 py-3">
|
||||||
<p className="text-xs sm:text-sm text-gray-900">{item.weight}g</p>
|
<p className="text-sm text-gray-900 font-medium">{item.weight}g</p>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 sm:px-4 py-2 sm:py-3">
|
<td className="px-4 py-3">
|
||||||
<div className="text-xs sm:text-sm">
|
<div className="text-sm">
|
||||||
<span className="text-gray-600">{item.target_quantity}米</span>
|
<span className="text-gray-500">{item.target_quantity.toLocaleString()}米</span>
|
||||||
<span className="text-gray-400 mx-1">/</span>
|
<span className="text-gray-300 mx-2">/</span>
|
||||||
<span className="text-emerald-600 font-medium">{item.completed_quantity}米</span>
|
<span className="text-emerald-600 font-semibold">{item.completed_quantity.toLocaleString()}米</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 sm:px-4 py-2 sm:py-3">
|
<td className="px-4 py-3">
|
||||||
{item.in_records.length > 0 ? (
|
{item.in_records.length > 0 ? (
|
||||||
<div className="text-xs text-gray-500">
|
<div className="text-xs text-gray-600">
|
||||||
<span className="inline-block">
|
<span className="inline-flex items-center gap-1.5">
|
||||||
{new Date(item.in_records[0].time).toLocaleDateString('zh-CN')}({item.in_records[0].quantity}米/{item.in_records[0].rolls}匹/件)
|
<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>
|
</span>
|
||||||
{item.in_records.length > 1 && (
|
{item.in_records.length > 1 && (
|
||||||
<button
|
<button
|
||||||
@ -374,15 +383,20 @@ export function FabricWarehouse() {
|
|||||||
setSelectedPlanCode(item.plan_code);
|
setSelectedPlanCode(item.plan_code);
|
||||||
setRecordsModalOpen(true);
|
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" />
|
<List className="w-3.5 h-3.5" />
|
||||||
全部记录 ({item.in_records.length}条)
|
全部 ({item.in_records.length}条)
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
</td>
|
||||||
</motion.tr>
|
</motion.tr>
|
||||||
@ -392,36 +406,41 @@ export function FabricWarehouse() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 移动端卡片 */}
|
{/* 移动端卡片 */}
|
||||||
<div className="sm:hidden p-3 space-y-2">
|
<div className="sm:hidden p-4 space-y-3">
|
||||||
{items.map((item, itemIndex) => (
|
{items.map((item, itemIndex) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={item.plan_id}
|
key={item.plan_id}
|
||||||
initial={{ opacity: 0, y: 10 }}
|
initial={{ opacity: 0, y: 10 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ delay: itemIndex * 0.05, duration: 0.25 }}
|
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">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<span className="px-2 py-1 bg-gray-100 rounded-lg text-xs font-mono text-gray-700">
|
<span className="px-2.5 py-1 bg-emerald-50 text-emerald-700 rounded-lg text-xs font-mono font-medium">
|
||||||
{item.plan_code}
|
{item.plan_code}
|
||||||
</span>
|
</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>
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<div className="text-xs">
|
<div className="text-sm">
|
||||||
<span className="text-gray-500">计划:</span>
|
<span className="text-gray-400">计划</span>
|
||||||
<span className="text-gray-700 ml-1">{item.target_quantity}米</span>
|
<span className="text-gray-700 font-medium ml-1.5">{item.target_quantity.toLocaleString()}米</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs">
|
<div className="text-sm">
|
||||||
<span className="text-gray-500">已入库:</span>
|
<span className="text-gray-400">已入</span>
|
||||||
<span className="text-emerald-600 font-medium ml-1">{item.completed_quantity}米</span>
|
<span className="text-emerald-600 font-semibold ml-1.5">{item.completed_quantity.toLocaleString()}米</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{item.in_records.length > 0 ? (
|
{item.in_records.length > 0 ? (
|
||||||
<div className="flex items-center justify-between pt-2 border-t border-gray-50">
|
<div className="flex items-center justify-between pt-3 border-t border-gray-100">
|
||||||
<div className="text-xs text-gray-500">
|
<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>{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>
|
</div>
|
||||||
{item.in_records.length > 1 && (
|
{item.in_records.length > 1 && (
|
||||||
<button
|
<button
|
||||||
@ -431,15 +450,20 @@ export function FabricWarehouse() {
|
|||||||
setSelectedPlanCode(item.plan_code);
|
setSelectedPlanCode(item.plan_code);
|
||||||
setRecordsModalOpen(true);
|
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}条)
|
全部 ({item.in_records.length}条)
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -89,7 +89,7 @@ export function PaymentPending() {
|
|||||||
<motion.div
|
<motion.div
|
||||||
animate={{ rotate: 360 }}
|
animate={{ rotate: 360 }}
|
||||||
transition={{ duration: 1, repeat: Infinity, ease: 'linear' }}
|
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>
|
<p className="text-gray-400 text-sm">加载中...</p>
|
||||||
</div>
|
</div>
|
||||||
@ -99,12 +99,12 @@ export function PaymentPending() {
|
|||||||
initial={{ opacity: 0, y: 10 }}
|
initial={{ opacity: 0, y: 10 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.3 }}
|
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">
|
<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" />
|
<DollarSign className="w-6 h-6 sm:w-7 sm:h-7 md:w-8 md:h-8" />
|
||||||
<div>
|
<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>
|
<p className="text-2xl sm:text-3xl md:text-4xl font-bold">¥{totalAmount.toLocaleString()}</p>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<p className="text-xs sm:text-sm text-gray-500">已完成: {item.quantity}米 × ¥{item.price_per_meter}/米</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right flex-shrink-0">
|
<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>
|
</div>
|
||||||
<div className="mt-3 sm:mt-4 pt-3 sm:pt-4 border-t border-gray-100 flex justify-end">
|
<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';
|
import type { ProductionPlan, PlanProcessStep, Company, Profile, YarnRatio } from '../../types';
|
||||||
|
|
||||||
const statusMap: Record<string, { label: string; color: string }> = {
|
const statusMap: Record<string, { label: string; color: string }> = {
|
||||||
pending: { label: '待确定', color: 'bg-yellow-100 text-yellow-700' },
|
pending: { label: '待确定', color: 'bg-amber-100 text-amber-700' },
|
||||||
producing: { label: '生产中', color: 'bg-blue-100 text-blue-700' },
|
producing: { label: '生产中', color: 'bg-emerald-100 text-emerald-700' },
|
||||||
completed: { label: '已完成', color: 'bg-green-100 text-green-700' },
|
completed: { label: '已完成', color: 'bg-green-100 text-green-700' },
|
||||||
rejected: { label: '已拒绝', color: 'bg-gray-100 text-gray-600' }
|
rejected: { label: '已拒绝', color: 'bg-gray-100 text-gray-600' }
|
||||||
};
|
};
|
||||||
@ -236,7 +236,8 @@ export function TextilePlanOverview() {
|
|||||||
return {
|
return {
|
||||||
token: params.get('token'),
|
token: params.get('token'),
|
||||||
plan: params.get('plan'),
|
plan: params.get('plan'),
|
||||||
type: params.get('type')
|
type: params.get('type'),
|
||||||
|
shortCode: params.get('s')
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@ -244,24 +245,27 @@ export function TextilePlanOverview() {
|
|||||||
return {
|
return {
|
||||||
token: url.searchParams.get('token'),
|
token: url.searchParams.get('token'),
|
||||||
plan: url.searchParams.get('plan'),
|
plan: url.searchParams.get('plan'),
|
||||||
type: url.searchParams.get('type')
|
type: url.searchParams.get('type'),
|
||||||
|
shortCode: url.searchParams.get('s')
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return { token: null, plan: null, type: null };
|
return { token: null, plan: null, type: null, shortCode: null };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleImportFromLink = () => {
|
const handleImportFromLink = () => {
|
||||||
if (!importLink.trim()) return;
|
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}`);
|
navigate(`/import?token=${token}`);
|
||||||
} else if (plan && type) {
|
} else if (plan && type) {
|
||||||
navigate(`/import?plan=${plan}&type=${type}`);
|
navigate(`/import?plan=${plan}&type=${type}`);
|
||||||
} else {
|
} else {
|
||||||
alert('无效的链接格式,请确保链接包含 token 或 plan 参数');
|
alert('无效的链接格式,请确保链接包含 token、s 或 plan 参数');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -270,9 +274,11 @@ export function TextilePlanOverview() {
|
|||||||
const text = await navigator.clipboard.readText();
|
const text = await navigator.clipboard.readText();
|
||||||
if (text.includes('/import?') || text.includes('#/import?')) {
|
if (text.includes('/import?') || text.includes('#/import?')) {
|
||||||
setImportLink(text);
|
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}`);
|
navigate(`/import?token=${token}`);
|
||||||
} else if (plan && type) {
|
} else if (plan && type) {
|
||||||
navigate(`/import?plan=${plan}&type=${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">
|
<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">
|
<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>
|
</h2>
|
||||||
<motion.button
|
<motion.button
|
||||||
whileTap={{ scale: 0.97 }}
|
whileTap={{ scale: 0.97 }}
|
||||||
onClick={handlePasteFromClipboard}
|
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" />
|
<Clipboard className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
|
||||||
识别剪贴板
|
识别剪贴板
|
||||||
@ -665,13 +671,13 @@ export function TextilePlanOverview() {
|
|||||||
value={importLink}
|
value={importLink}
|
||||||
onChange={(e) => setImportLink(e.target.value)}
|
onChange={(e) => setImportLink(e.target.value)}
|
||||||
placeholder="粘贴采购商分享的链接..."
|
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
|
<motion.button
|
||||||
whileTap={{ scale: 0.97 }}
|
whileTap={{ scale: 0.97 }}
|
||||||
onClick={handleImportFromLink}
|
onClick={handleImportFromLink}
|
||||||
disabled={!importLink.trim()}
|
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>
|
</motion.button>
|
||||||
@ -683,7 +689,7 @@ export function TextilePlanOverview() {
|
|||||||
<motion.div
|
<motion.div
|
||||||
animate={{ rotate: 360 }}
|
animate={{ rotate: 360 }}
|
||||||
transition={{ duration: 1, repeat: Infinity, ease: 'linear' }}
|
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>
|
<p className="text-gray-400 text-sm">加载中...</p>
|
||||||
</div>
|
</div>
|
||||||
@ -747,45 +753,50 @@ export function TextilePlanOverview() {
|
|||||||
initial={{ opacity: 0, y: 10 }}
|
initial={{ opacity: 0, y: 10 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ delay: planIndex * 0.05, duration: 0.3 }}
|
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
|
<motion.div
|
||||||
onClick={() => !isRejected && setExpandedPlan(expandedPlan === plan.id ? null : plan.id)}
|
onClick={() => !isRejected && setExpandedPlan(expandedPlan === plan.id ? null : plan.id)}
|
||||||
whileTap={isRejected ? {} : { scale: 0.995 }}
|
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="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 ? (
|
{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>
|
||||||
) : (
|
) : (
|
||||||
<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}
|
{statusMap[plan.status]?.label || plan.status}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
<span className="text-xs text-gray-400 font-mono">{plan.plan_code}</span>
|
||||||
</div>
|
</div>
|
||||||
<p className={`text-xs sm:text-sm truncate ${isRejected ? 'text-gray-500' : 'text-gray-600'}`}>{plan.fabric_code}</p>
|
<p className={`text-sm font-medium truncate ${isRejected ? 'text-gray-500' : 'text-gray-800'}`}>{plan.fabric_code}</p>
|
||||||
<p className="text-[10px] sm:text-xs text-gray-400">{plan.plan_code}</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className={`text-right text-xs sm:text-sm flex-shrink-0 ${isRejected ? 'text-gray-500' : 'text-gray-600'}`}>
|
<div className={`text-right text-sm flex-shrink-0 ${isRejected ? 'text-gray-500' : 'text-gray-700'}`}>
|
||||||
<p>计划: {plan.target_quantity}米</p>
|
<p className="font-medium">{plan.target_quantity.toLocaleString()}<span className="text-xs text-gray-500 ml-1">米</span></p>
|
||||||
<p>已入库: {plan.completed_quantity}米</p>
|
<p className="text-xs text-emerald-600">已入 {plan.completed_quantity.toLocaleString()}米</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 sm:mt-3">
|
<div className="mt-4">
|
||||||
<div className="flex justify-between text-[10px] sm:text-xs text-gray-500 mb-1">
|
<div className="flex justify-between text-xs text-gray-500 mb-1.5">
|
||||||
<span>进度</span>
|
<span className="flex items-center gap-1">
|
||||||
<span>{progress}%</span>
|
<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>
|
||||||
<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
|
<motion.div
|
||||||
initial={{ width: 0 }}
|
initial={{ width: 0 }}
|
||||||
animate={{ width: `${progress}%` }}
|
animate={{ width: `${progress}%` }}
|
||||||
transition={{ duration: 0.6, delay: 0.1, ease: [0.25, 0.46, 0.45, 0.94] }}
|
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>
|
||||||
</div>
|
</div>
|
||||||
@ -831,7 +842,7 @@ export function TextilePlanOverview() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="truncate">
|
<div className="truncate">
|
||||||
<span className="text-gray-400">进度:</span>
|
<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>
|
||||||
<div className="truncate">
|
<div className="truncate">
|
||||||
<span className="text-gray-400">创建:</span>
|
<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>
|
<h5 className="text-sm font-medium text-gray-900 mb-2">纱线名称/占比</h5>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{plan.yarnRatios.map((yarn, index) => (
|
{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">
|
<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}:{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)`}
|
{yarn.amount_per_meter && `(${yarn.amount_per_meter}g/m)`}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
@ -900,7 +911,7 @@ export function TextilePlanOverview() {
|
|||||||
{plan.processSteps.length > 0 && (
|
{plan.processSteps.length > 0 && (
|
||||||
<div className="border-t border-gray-200 pt-3">
|
<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">
|
<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>
|
</h5>
|
||||||
<ProcessFlow
|
<ProcessFlow
|
||||||
@ -914,6 +925,9 @@ export function TextilePlanOverview() {
|
|||||||
isPlanLocked={plan.status === 'completed'}
|
isPlanLocked={plan.status === 'completed'}
|
||||||
lastInventoryTime={plan.inventoryRecords[0]?.time}
|
lastInventoryTime={plan.inventoryRecords[0]?.time}
|
||||||
inventoryRecords={plan.inventoryRecords}
|
inventoryRecords={plan.inventoryRecords}
|
||||||
|
yarnRatios={plan.yarnRatios}
|
||||||
|
companyId={auth.company?.id}
|
||||||
|
planId={plan.id}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,17 +1,31 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
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 { useNavigate } from 'react-router-dom';
|
||||||
import { useAuth } from '../../contexts/AuthContext';
|
import { useAuth } from '../../contexts/AuthContext';
|
||||||
import { supabase } from '../../supabase/client';
|
import { supabase } from '../../supabase/client';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import type { YarnStock, Warehouse } from '../../types';
|
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() {
|
export function YarnWarehouse() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { auth } = useAuth();
|
const { auth } = useAuth();
|
||||||
const [activeTab, setActiveTab] = useState<'stock' | 'record'>('stock');
|
const [activeTab, setActiveTab] = useState<'stock' | 'record'>('stock');
|
||||||
const [yarns, setYarns] = useState<YarnStock[]>([]);
|
const [yarns, setYarns] = useState<YarnStock[]>([]);
|
||||||
const [warehouses, setWarehouses] = useState<Warehouse[]>([]);
|
const [warehouses, setWarehouses] = useState<Warehouse[]>([]);
|
||||||
|
const [records, setRecords] = useState<YarnStockRecord[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [showAddModal, setShowAddModal] = useState(false);
|
const [showAddModal, setShowAddModal] = useState(false);
|
||||||
const [newYarn, setNewYarn] = useState({ name: '', spec: '', quantity: 0, minStock: 0, unit: 'kg' });
|
const [newYarn, setNewYarn] = useState({ name: '', spec: '', quantity: 0, minStock: 0, unit: 'kg' });
|
||||||
@ -21,6 +35,12 @@ export function YarnWarehouse() {
|
|||||||
fetchData();
|
fetchData();
|
||||||
}, [auth.company]);
|
}, [auth.company]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeTab === 'record' && auth.company) {
|
||||||
|
fetchRecords();
|
||||||
|
}
|
||||||
|
}, [activeTab, auth.company]);
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
const { data: yarnData } = await supabase
|
const { data: yarnData } = await supabase
|
||||||
.from('yarn_stock')
|
.from('yarn_stock')
|
||||||
@ -39,10 +59,43 @@ export function YarnWarehouse() {
|
|||||||
setLoading(false);
|
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 () => {
|
const handleAddYarn = async () => {
|
||||||
if (!newYarn.name) return;
|
if (!newYarn.name) return;
|
||||||
const yarnWarehouse = warehouses[0]; // 使用第一个纱线仓库
|
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')
|
.from('yarn_stock')
|
||||||
.insert({
|
.insert({
|
||||||
company_id: auth.company!.id,
|
company_id: auth.company!.id,
|
||||||
@ -52,11 +105,24 @@ export function YarnWarehouse() {
|
|||||||
quantity: newYarn.quantity,
|
quantity: newYarn.quantity,
|
||||||
min_stock: newYarn.minStock,
|
min_stock: newYarn.minStock,
|
||||||
unit: newYarn.unit
|
unit: newYarn.unit
|
||||||
});
|
})
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error('添加失败:', error);
|
console.error('添加失败:', error);
|
||||||
} else {
|
} 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);
|
setShowAddModal(false);
|
||||||
setNewYarn({ name: '', spec: '', quantity: 0, minStock: 0, unit: 'kg' });
|
setNewYarn({ name: '', spec: '', quantity: 0, minStock: 0, unit: 'kg' });
|
||||||
await fetchData();
|
await fetchData();
|
||||||
@ -84,7 +150,7 @@ export function YarnWarehouse() {
|
|||||||
whileTap={{ scale: 0.98 }}
|
whileTap={{ scale: 0.98 }}
|
||||||
onClick={() => setActiveTab('stock')}
|
onClick={() => setActiveTab('stock')}
|
||||||
className={`flex-1 py-2 rounded-lg text-xs sm:text-sm font-medium transition-colors will-change-transform ${
|
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 }}
|
whileTap={{ scale: 0.98 }}
|
||||||
onClick={() => setActiveTab('record')}
|
onClick={() => setActiveTab('record')}
|
||||||
className={`flex-1 py-2 rounded-lg text-xs sm:text-sm font-medium transition-colors will-change-transform ${
|
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>
|
</motion.button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -105,7 +171,7 @@ export function YarnWarehouse() {
|
|||||||
<motion.div
|
<motion.div
|
||||||
animate={{ rotate: 360 }}
|
animate={{ rotate: 360 }}
|
||||||
transition={{ duration: 1, repeat: Infinity, ease: 'linear' }}
|
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>
|
<p className="text-gray-400 text-sm">加载中...</p>
|
||||||
</div>
|
</div>
|
||||||
@ -121,7 +187,7 @@ export function YarnWarehouse() {
|
|||||||
<motion.button
|
<motion.button
|
||||||
whileTap={{ scale: 0.97 }}
|
whileTap={{ scale: 0.97 }}
|
||||||
onClick={() => setShowAddModal(true)}
|
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" /> 入库
|
<Plus className="w-4 h-4" /> 入库
|
||||||
</motion.button>
|
</motion.button>
|
||||||
@ -169,9 +235,67 @@ export function YarnWarehouse() {
|
|||||||
initial={{ opacity: 0, y: 10 }}
|
initial={{ opacity: 0, y: 10 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.3 }}
|
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>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -245,7 +369,7 @@ export function YarnWarehouse() {
|
|||||||
<motion.button
|
<motion.button
|
||||||
whileTap={{ scale: 0.98 }}
|
whileTap={{ scale: 0.98 }}
|
||||||
onClick={handleAddYarn}
|
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>
|
</motion.button>
|
||||||
|
|||||||
@ -93,19 +93,19 @@ export function CompletedFabric() {
|
|||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-green-50 to-emerald-50">
|
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-purple-50/30 to-violet-50/30">
|
||||||
{/* Header */}
|
{/* Header - 与 Dashboard 一致 */}
|
||||||
<header className="bg-gradient-to-r from-green-500 to-emerald-500 shadow-lg">
|
<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">
|
<div className="max-w-7xl mx-auto px-4 md:px-8 py-4 flex items-center gap-3">
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/washing')}
|
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>
|
</button>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-lg font-bold text-white">已完成坯布</h1>
|
<h1 className="text-lg font-bold text-gray-900">已完成坯布</h1>
|
||||||
<p className="text-xs text-white/70">查看已完成水洗的坯布记录</p>
|
<p className="text-xs text-gray-500">查看已完成水洗的坯布记录</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@ -117,9 +117,9 @@ export function CompletedFabric() {
|
|||||||
<p className="text-xs text-gray-500 mb-1">已完成计划</p>
|
<p className="text-xs text-gray-500 mb-1">已完成计划</p>
|
||||||
<p className="text-2xl font-bold text-gray-800">{items.length}</p>
|
<p className="text-2xl font-bold text-gray-800">{items.length}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-blue-50 rounded-xl p-4 shadow-sm">
|
<div className="bg-purple-50 rounded-xl p-4 shadow-sm">
|
||||||
<p className="text-xs text-blue-600 mb-1">水洗前米数</p>
|
<p className="text-xs text-purple-600 mb-1">水洗前米数</p>
|
||||||
<p className="text-2xl font-bold text-blue-700">{totalBefore.toFixed(2)}</p>
|
<p className="text-2xl font-bold text-purple-700">{totalBefore.toFixed(2)}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-green-50 rounded-xl p-4 shadow-sm">
|
<div className="bg-green-50 rounded-xl p-4 shadow-sm">
|
||||||
<p className="text-xs text-green-600 mb-1">水洗后米数</p>
|
<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 { useAuth } from '../../contexts/AuthContext';
|
||||||
import { supabase } from '../../supabase/client';
|
import { supabase } from '../../supabase/client';
|
||||||
import { motion } from 'framer-motion';
|
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 { useResponsive } from '../../hooks/useResponsive';
|
||||||
import { OnboardingGuide, shouldShowOnboarding } from '../../components/OnboardingGuide';
|
import { OnboardingGuide, shouldShowOnboarding } from '../../components/OnboardingGuide';
|
||||||
import { NotificationCenter } from '../../components/NotificationCenter';
|
import { NotificationCenter } from '../../components/NotificationCenter';
|
||||||
@ -14,48 +14,34 @@ import { UserManual } from '../../components/UserManual';
|
|||||||
import { CrashReporter } from '../../components/CrashReporter';
|
import { CrashReporter } from '../../components/CrashReporter';
|
||||||
import type { WashingPlan } from '../../types';
|
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 getQuickActions = (isMaster: boolean) => {
|
||||||
const actions = [
|
const actions = [
|
||||||
{ id: 'plans', icon: Import, label: '计划总览', path: '/washing/plans', gradient: 'from-purple-500 to-violet-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' },
|
{ 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-green-500 to-emerald-500' },
|
{ 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-blue-500 to-cyan-500' },
|
{ 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' }
|
{ id: 'payment', icon: CreditCard, label: '待结款', path: '/washing/payments', gradient: 'from-rose-500 to-pink-500', description: '查看待结款项' }
|
||||||
];
|
];
|
||||||
if (isMaster) {
|
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;
|
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 = {
|
const containerVariants = {
|
||||||
hidden: { opacity: 0 },
|
hidden: { opacity: 0 },
|
||||||
visible: {
|
visible: { opacity: 1, transition: { staggerChildren: 0.1 } }
|
||||||
opacity: 1,
|
|
||||||
transition: {
|
|
||||||
staggerChildren: 0.08,
|
|
||||||
delayChildren: 0.05
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const itemVariants = {
|
const itemVariants = {
|
||||||
hidden: { opacity: 0, y: 15, scale: 0.98 },
|
hidden: { opacity: 0, y: 20 },
|
||||||
visible: {
|
visible: { opacity: 1, y: 0, transition: { duration: 0.4 } }
|
||||||
opacity: 1,
|
|
||||||
y: 0,
|
|
||||||
scale: 1,
|
|
||||||
transition: {
|
|
||||||
duration: 0.35,
|
|
||||||
ease: [0.25, 0.46, 0.45, 0.94]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function WashingDashboard() {
|
export function WashingDashboard() {
|
||||||
@ -64,8 +50,6 @@ export function WashingDashboard() {
|
|||||||
const { isMobile } = useResponsive();
|
const { isMobile } = useResponsive();
|
||||||
const [plans, setPlans] = useState<WashingPlan[]>([]);
|
const [plans, setPlans] = useState<WashingPlan[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [pendingCount, setPendingCount] = useState(0);
|
|
||||||
const [completedCount, setCompletedCount] = useState(0);
|
|
||||||
|
|
||||||
// 帮助中心状态
|
// 帮助中心状态
|
||||||
const [showHelp, setShowHelp] = useState(false);
|
const [showHelp, setShowHelp] = useState(false);
|
||||||
@ -77,6 +61,12 @@ export function WashingDashboard() {
|
|||||||
// 版本更新检查
|
// 版本更新检查
|
||||||
const { showUpdate, setShowUpdate } = useVersionCheck();
|
const { showUpdate, setShowUpdate } = useVersionCheck();
|
||||||
|
|
||||||
|
// 账户卡片弹窗状态
|
||||||
|
const [showAccountCard, setShowAccountCard] = useState(false);
|
||||||
|
|
||||||
|
// 最近计划轮播索引
|
||||||
|
const [currentPlanIndex, setCurrentPlanIndex] = useState(0);
|
||||||
|
|
||||||
// 检查是否显示新手引导
|
// 检查是否显示新手引导
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (shouldShowOnboarding('washing')) {
|
if (shouldShowOnboarding('washing')) {
|
||||||
@ -96,10 +86,7 @@ export function WashingDashboard() {
|
|||||||
.eq('washing_factory_id', auth.company!.id)
|
.eq('washing_factory_id', auth.company!.id)
|
||||||
.order('created_at', { ascending: false });
|
.order('created_at', { ascending: false });
|
||||||
|
|
||||||
const plans = washingPlans || [];
|
setPlans(washingPlans || []);
|
||||||
setPlans(plans);
|
|
||||||
setPendingCount(plans.filter(p => p.status === 'pending' || p.status === 'processing').length);
|
|
||||||
setCompletedCount(plans.filter(p => p.status === 'completed').length);
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -109,205 +96,240 @@ export function WashingDashboard() {
|
|||||||
completed: plans.filter(p => p.status === 'completed').length
|
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 (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-purple-50 to-violet-50">
|
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-violet-50/40 to-fuchsia-50/30">
|
||||||
<header className="bg-gradient-to-r from-purple-600 to-violet-600 shadow-lg">
|
{/* Header - 与采购商一致 */}
|
||||||
<div className="max-w-7xl mx-auto px-4 md:px-8 py-4 md:py-5 flex justify-between items-center">
|
<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="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
|
||||||
<Droplets className="w-4 h-4 sm:w-5 sm:h-5 text-white" />
|
whileHover={{ scale: 1.05 }}
|
||||||
</div>
|
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" />
|
||||||
|
)}
|
||||||
|
</motion.button>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-sm sm:text-lg font-bold text-white flex items-center gap-2 whitespace-nowrap">
|
<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="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>
|
<span className="hidden sm:inline">{auth.company?.name || '水洗厂'}工作台</span>
|
||||||
</h1>
|
<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>
|
||||||
<p className="text-xs text-white/70 hidden sm:block">{(auth.user as any)?.display_name || auth.user?.username || '用户'},欢迎回来</p>
|
</h1>
|
||||||
|
<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>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<HelpButton onClick={() => setShowHelp(true)} />
|
<HelpButton onClick={() => setShowHelp(true)} />
|
||||||
<NotificationCenter theme="purple" />
|
<NotificationCenter theme="violet" />
|
||||||
<button
|
<button
|
||||||
onClick={async () => { await logout(); navigate('/login'); }}
|
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="退出登录"
|
title="退出登录"
|
||||||
>
|
>
|
||||||
<LogOut className="w-5 h-5 sm:hidden" />
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</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 ? (
|
{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">
|
<motion.div variants={containerVariants} initial="hidden" animate="visible" className="space-y-4 sm:space-y-6">
|
||||||
<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">
|
<div className="grid grid-cols-3 gap-2 sm:gap-4">
|
||||||
<p className="text-xs md:text-sm text-purple-100 mb-1 md:mb-2">所有计划</p>
|
<motion.div
|
||||||
<p className="text-2xl md:text-4xl font-bold">{stats.all}</p>
|
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>
|
||||||
<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>
|
<motion.div
|
||||||
<p className="text-2xl md:text-4xl font-bold">{stats.producing}</p>
|
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>
|
||||||
<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>
|
<motion.div
|
||||||
<p className="text-2xl md:text-4xl font-bold">{stats.completed}</p>
|
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>
|
</motion.div>
|
||||||
</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>
|
<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="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-6 gap-3 md:gap-4">
|
<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) => (
|
{getQuickActions(auth.user?.is_master || false).map((action, index) => (
|
||||||
<motion.button
|
<motion.button
|
||||||
key={action.id}
|
key={action.id}
|
||||||
initial={{ opacity: 0, y: 10 }}
|
whileHover={isMobile ? {} : { scale: 1.03, y: -2 }}
|
||||||
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 }}
|
whileTap={{ scale: 0.97 }}
|
||||||
onClick={() => navigate(action.path)}
|
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`}>
|
<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-5 h-5 md:w-6 md:h-6 text-white" />
|
<action.icon className="w-4 h-4 sm:w-5 sm:h-5 text-white" />
|
||||||
</div>
|
</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>
|
</motion.button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</motion.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>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -339,6 +361,160 @@ export function WashingDashboard() {
|
|||||||
|
|
||||||
{/* 崩溃报告 */}
|
{/* 崩溃报告 */}
|
||||||
<CrashReporter />
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -101,19 +101,19 @@ export function FinishedWarehouse() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-purple-50 to-violet-50">
|
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-purple-50/30 to-violet-50/30">
|
||||||
{/* Header */}
|
{/* Header - 与 Dashboard 一致 */}
|
||||||
<header className="bg-gradient-to-r from-purple-600 to-violet-600 shadow-lg">
|
<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">
|
<div className="max-w-7xl mx-auto px-4 md:px-8 py-4 flex items-center gap-3">
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/washing')}
|
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>
|
</button>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-lg font-bold text-white">成品仓库</h1>
|
<h1 className="text-lg font-bold text-gray-900">成品仓库</h1>
|
||||||
<p className="text-xs text-white/70">管理水洗后的成品库存</p>
|
<p className="text-xs text-gray-500">管理水洗后的成品库存</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@ -131,7 +131,7 @@ export function FinishedWarehouse() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="bg-white rounded-xl p-4 shadow-sm">
|
<div className="bg-white rounded-xl p-4 shadow-sm">
|
||||||
<p className="text-xs text-gray-500 mb-1">库存匹/件数</p>
|
<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>
|
||||||
<div className="bg-white rounded-xl p-4 shadow-sm">
|
<div className="bg-white rounded-xl p-4 shadow-sm">
|
||||||
<p className="text-xs text-gray-500 mb-1">在库品种</p>
|
<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 }> = {
|
const statusMap: Record<string, { label: string; color: string; bgColor: string }> = {
|
||||||
pending: { label: '待确认', color: 'text-amber-600', bgColor: 'bg-amber-50' },
|
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() {
|
export function PendingFabric() {
|
||||||
@ -72,19 +72,19 @@ export function PendingFabric() {
|
|||||||
const totalMeters = items.reduce((sum, item) => sum + item.planned_meters, 0);
|
const totalMeters = items.reduce((sum, item) => sum + item.planned_meters, 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-amber-50 to-orange-50">
|
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-purple-50/30 to-violet-50/30">
|
||||||
{/* Header */}
|
{/* Header - 与 Dashboard 一致 */}
|
||||||
<header className="bg-gradient-to-r from-amber-500 to-orange-500 shadow-lg">
|
<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">
|
<div className="max-w-7xl mx-auto px-4 md:px-8 py-4 flex items-center gap-3">
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/washing')}
|
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>
|
</button>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-lg font-bold text-white">待处理坯布</h1>
|
<h1 className="text-lg font-bold text-gray-900">待处理坯布</h1>
|
||||||
<p className="text-xs text-white/70">查看待处理的水洗坯布</p>
|
<p className="text-xs text-gray-500">查看待处理的水洗坯布</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@ -23,7 +23,7 @@ interface PlanGroup {
|
|||||||
|
|
||||||
const statusMap: Record<string, { label: string; color: string }> = {
|
const statusMap: Record<string, { label: string; color: string }> = {
|
||||||
pending: { label: '待处理', color: 'bg-amber-100 text-amber-700' },
|
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' }
|
completed: { label: '已完成', color: 'bg-green-100 text-green-700' }
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -171,24 +171,27 @@ export function WashingPlanOverview() {
|
|||||||
return {
|
return {
|
||||||
token: url.searchParams.get('token'),
|
token: url.searchParams.get('token'),
|
||||||
plan: url.searchParams.get('plan'),
|
plan: url.searchParams.get('plan'),
|
||||||
type: url.searchParams.get('type')
|
type: url.searchParams.get('type'),
|
||||||
|
shortCode: url.searchParams.get('s')
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return { token: null, plan: null, type: null };
|
return { token: null, plan: null, type: null, shortCode: null };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleImportFromLink = () => {
|
const handleImportFromLink = () => {
|
||||||
if (!importLink.trim()) return;
|
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}`);
|
navigate(`/import?token=${token}`);
|
||||||
} else if (plan && type) {
|
} else if (plan && type) {
|
||||||
navigate(`/import?plan=${plan}&type=${type}`);
|
navigate(`/import?plan=${plan}&type=${type}`);
|
||||||
} else {
|
} else {
|
||||||
alert('无效的链接格式,请确保链接包含 token 或 plan 参数');
|
alert('无效的链接格式,请确保链接包含 token、s 或 plan 参数');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -197,9 +200,11 @@ export function WashingPlanOverview() {
|
|||||||
const text = await navigator.clipboard.readText();
|
const text = await navigator.clipboard.readText();
|
||||||
if (text.includes('/import?') || text.includes('#/import?')) {
|
if (text.includes('/import?') || text.includes('#/import?')) {
|
||||||
setImportLink(text);
|
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}`);
|
navigate(`/import?token=${token}`);
|
||||||
} else if (plan && type) {
|
} else if (plan && type) {
|
||||||
navigate(`/import?plan=${plan}&type=${type}`);
|
navigate(`/import?plan=${plan}&type=${type}`);
|
||||||
@ -326,14 +331,14 @@ export function WashingPlanOverview() {
|
|||||||
return (
|
return (
|
||||||
<div key={stepType} className="flex items-start gap-3">
|
<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 ${
|
<div className={`w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 ${
|
||||||
isCompleted ? 'bg-green-500 text-white' :
|
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}
|
{isCompleted ? <Check className="w-4 h-4" /> : index + 1}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center justify-between">
|
<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]}
|
{stepNames[stepType]}
|
||||||
</p>
|
</p>
|
||||||
{step?.timestamp && (
|
{step?.timestamp && (
|
||||||
@ -351,7 +356,7 @@ export function WashingPlanOverview() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => handleConfirmStep(step.id, stepType)}
|
onClick={() => handleConfirmStep(step.id, stepType)}
|
||||||
disabled={confirming === step.id}
|
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 ? '确认中...' : '确认'}
|
{confirming === step.id ? '确认中...' : '确认'}
|
||||||
</button>
|
</button>
|
||||||
@ -373,18 +378,24 @@ export function WashingPlanOverview() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 p-3 sm:p-4 md:p-6">
|
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-purple-50/30 to-violet-50/30">
|
||||||
<div className="max-w-6xl mx-auto">
|
{/* Header - 与 Dashboard 一致 */}
|
||||||
<div className="flex items-center gap-2 sm:gap-3 md:gap-4 mb-4 sm:mb-5 md:mb-6">
|
<header className="bg-white/80 backdrop-blur-xl border-b border-gray-200/50 sticky top-0 z-10">
|
||||||
<motion.button
|
<div className="max-w-7xl mx-auto px-4 md:px-8 py-4 flex items-center gap-3">
|
||||||
whileTap={{ scale: 0.95 }}
|
<button
|
||||||
onClick={() => navigate('/washing')}
|
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" />
|
<ArrowLeft className="w-5 h-5 text-gray-600" />
|
||||||
</motion.button>
|
</button>
|
||||||
<h1 className="text-lg sm:text-xl md:text-2xl font-bold text-gray-800">计划总览</h1>
|
<div>
|
||||||
|
<h1 className="text-lg font-bold text-gray-900">计划总览</h1>
|
||||||
|
<p className="text-xs text-gray-500">查看和管理水洗计划</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="p-3 sm:p-4 md:p-6 max-w-6xl mx-auto">
|
||||||
|
|
||||||
{/* 导入链接区域 */}
|
{/* 导入链接区域 */}
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|||||||
@ -1157,6 +1157,7 @@ export type Database = {
|
|||||||
created_at: string
|
created_at: string
|
||||||
display_name: string | null
|
display_name: string | null
|
||||||
id: string
|
id: string
|
||||||
|
image_url: string | null
|
||||||
is_master: boolean
|
is_master: boolean
|
||||||
master_id: string | null
|
master_id: string | null
|
||||||
phone: string | null
|
phone: string | null
|
||||||
@ -1167,6 +1168,7 @@ export type Database = {
|
|||||||
created_at?: string
|
created_at?: string
|
||||||
display_name?: string | null
|
display_name?: string | null
|
||||||
id: string
|
id: string
|
||||||
|
image_url?: string | null
|
||||||
is_master?: boolean
|
is_master?: boolean
|
||||||
master_id?: string | null
|
master_id?: string | null
|
||||||
phone?: string | null
|
phone?: string | null
|
||||||
@ -1177,6 +1179,7 @@ export type Database = {
|
|||||||
created_at?: string
|
created_at?: string
|
||||||
display_name?: string | null
|
display_name?: string | null
|
||||||
id?: string
|
id?: string
|
||||||
|
image_url?: string | null
|
||||||
is_master?: boolean
|
is_master?: boolean
|
||||||
master_id?: string | null
|
master_id?: string | null
|
||||||
phone?: 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: {
|
user_feedback: {
|
||||||
Row: {
|
Row: {
|
||||||
contact: string | null
|
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: {
|
Views: {
|
||||||
[_ in never]: never
|
[_ in never]: never
|
||||||
@ -1649,6 +1746,10 @@ export type Database = {
|
|||||||
Args: { "": unknown }
|
Args: { "": unknown }
|
||||||
Returns: unknown
|
Returns: unknown
|
||||||
}
|
}
|
||||||
|
increment_share_link_clicks: {
|
||||||
|
Args: { code: string }
|
||||||
|
Returns: undefined
|
||||||
|
}
|
||||||
ivfflat_bit_support: {
|
ivfflat_bit_support: {
|
||||||
Args: { "": unknown }
|
Args: { "": unknown }
|
||||||
Returns: unknown
|
Returns: unknown
|
||||||
|
|||||||
@ -37,26 +37,27 @@ export const stepOrder = ['confirm', 'yarn_purchase', 'dyeing', 'machine_start',
|
|||||||
|
|
||||||
// 动画配置
|
// 动画配置
|
||||||
export const animationConfig = {
|
export const animationConfig = {
|
||||||
// 标准过渡
|
// 标准过渡 - 用于展开/收起,使用更平滑的缓动函数避免抖动
|
||||||
transition: {
|
transition: {
|
||||||
duration: 0.3,
|
duration: 0.25,
|
||||||
ease: [0.25, 0.46, 0.45, 0.94]
|
ease: [0.4, 0, 0.2, 1] // 使用 ease-out 缓动,更平滑
|
||||||
},
|
},
|
||||||
// 快速过渡
|
// 快速过渡
|
||||||
fastTransition: {
|
fastTransition: {
|
||||||
duration: 0.2,
|
duration: 0.2,
|
||||||
ease: [0.25, 0.46, 0.45, 0.94]
|
ease: [0.4, 0, 0.2, 1]
|
||||||
},
|
},
|
||||||
// 慢速过渡
|
// 慢速过渡
|
||||||
slowTransition: {
|
slowTransition: {
|
||||||
duration: 0.5,
|
duration: 0.4,
|
||||||
ease: [0.25, 0.46, 0.45, 0.94]
|
ease: [0.4, 0, 0.2, 1]
|
||||||
},
|
},
|
||||||
// 弹簧动画
|
// 弹簧动画 - 用于交互反馈,降低弹性避免抖动
|
||||||
spring: {
|
spring: {
|
||||||
type: 'spring',
|
type: 'spring',
|
||||||
stiffness: 300,
|
stiffness: 200,
|
||||||
damping: 30
|
damping: 25,
|
||||||
|
mass: 0.8
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user