!DOCTYPE html
html lang=zh-TW
head
meta charset=UTF-8
meta name=viewport content=width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no
title購物狂的專屬祕書 - Pink Strawberry Y2Ktitle
link rel=icon type=imagesvg+xml href=dataimagesvg+xml,%3Csvg xmlns='httpwww.w3.org2000svg' viewBox='0 0 100 100'%3E%3Cdefs%3E%3ClinearGradient id='grad' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' style='stop-color%23FFD1DC;stop-opacity1' %3E%3Cstop offset='100%25' style='stop-color%23FF8AAB;stop-opacity1' %3E%3ClinearGradient%3E%3Cdefs%3E%3Cpath d='M50 20 C65 20 85 30 85 55 C85 75 65 95 50 95 C35 95 15 75 15 55 C15 30 35 20 50 20' fill='url(%23grad)'%3E%3Cpath d='M40 25 Q50 5 60 25 L50 20 Z' fill='%2381C784'%3E%3Ccircle cx='35' cy='45' r='1.2' fill='%23AD1457' opacity='0.6'%3E%3Ccircle cx='65' cy='50' r='1.2' fill='%23AD1457' opacity='0.6'%3E%3Csvg%3E
link href=httpsfonts.googleapis.comcss2family=Zen+Maru+Gothicwght@900&display=swap rel=stylesheet
script src=httpscdn.tailwindcss.comscript
script src=httpsunpkg.comreact@18umdreact.production.min.jsscript
script src=httpsunpkg.comreact-dom@18umdreact-dom.production.min.jsscript
script src=httpsunpkg.com@babelstandalonebabel.min.jsscript
style
root {
--bg #FFFDF2;
--primary #D1C4E9;
--primary-dark #9575CD;
--mint #B2DFDB;
--text #4527A0;
--strawberry-pink #FFB7C5;
}
{
font-family 'Zen Maru Gothic', sans-serif !important;
font-weight 900 !important;
-webkit-tap-highlight-color transparent;
}
body {
background-color var(--bg);
color var(--text);
margin 0;
overflow-x hidden;
background-image url(dataimagesvg+xml,%3Csvg xmlns='httpwww.w3.org2000svg' width='120' height='120' viewBox='0 0 100 100'%3E%3Cpath d='M50 20 C65 20 85 30 85 55 C85 75 65 95 50 95 C35 95 15 75 15 55 C15 30 35 20 50 20' fill='%23FFB7C5' opacity='0.1'%3E%3Cpath d='M40 25 Q50 5 60 25 L50 20 Z' fill='%23B2DFDB' opacity='0.1'%3E%3Csvg%3E);
}
.hide-scrollbar-webkit-scrollbar { display none; }
.hide-scrollbar { -ms-overflow-style none; scrollbar-width none; }
input, select, textarea {
border-radius 1.2rem;
border 3px solid #EDE7F6;
padding 0.7rem 1rem;
background white;
outline none;
transition all 0.3s;
color var(--text);
}
inputfocus {
border-color var(--primary-dark);
box-shadow 0 0 0 4px rgba(149, 117, 205, 0.1);
}
.y2k-card {
background white;
border 3px solid #EDE7F6;
border-radius 2rem;
box-shadow 8px 8px 0px #EDE7F6;
transition all 0.2s;
}
.y2k-cardactive {
transform translate(2px, 2px);
box-shadow 4px 4px 0px #EDE7F6;
}
.status-tag {
padding 4px 12px;
border-radius 999px;
font-size 11px;
color white;
}
.bg-ordered { background #B39DDB; }
.bg-shipped { background var(--primary-dark); }
.bg-received { background var(--mint); color #00695C; }
.modal-overlay {
background rgba(255, 253, 242, 0.9);
backdrop-filter blur(12px);
z-index 4000;
position fixed;
inset 0;
display flex;
align-items center;
justify-content center;
padding 1rem;
}
.toast {
position fixed;
bottom 110px;
left 50%;
transform translateX(-50%);
z-index 10000;
padding 12px 24px;
border-radius 2rem;
background var(--text);
color white;
font-size 14px;
box-shadow 0 10px 30px rgba(69, 39, 160, 0.3);
}
.suggestion-box {
position absolute;
top 105%;
left 0;
right 0;
background white;
border-radius 1.5rem;
border 3px solid var(--primary);
z-index 5000;
max-height 200px;
overflow-y auto;
}
.suggestion-item {
padding 12px 16px;
border-bottom 1px solid #EDE7F6;
cursor pointer;
color var(--text);
}
.active-tab {
background var(--primary-dark) !important;
color white !important;
box-shadow 0 4px 12px rgba(103, 58, 183, 0.3);
}
.strawberry-float {
animation float 3s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform translateY(0); }
50% { transform translateY(-8px); }
}
style
head
body
div id=rootdiv
script src=httpswww.gstatic.comfirebasejs11.1.0firebase-app-compat.jsscript
script src=httpswww.gstatic.comfirebasejs11.1.0firebase-firestore-compat.jsscript
script src=httpswww.gstatic.comfirebasejs11.1.0firebase-auth-compat.jsscript
script
const firebaseConfig = {
apiKey AIzaSyArZI5pK2bRLdicQsE11n_0_lRcX0J7JtE,
authDomain lina0750database.firebaseapp.com,
projectId lina0750database,
storageBucket lina0750database.firebasestorage.app,
messagingSenderId 323253664660,
appId 1323253664660webd82e7db4d329c112c6ed81
};
firebase.initializeApp(firebaseConfig);
window.db = firebase.firestore();
window.auth = firebase.auth();
script
script type=textbabel
const { useState, useEffect, useMemo, useRef } = React;
const appId = livestream-shopping;
const STATUS = { ORDERED '已下單', SHIPPED '已發貨', RECEIVED '已收到貨' };
const WAREHOUSES = ['漳州倉', '上海倉', '東莞倉'];
const PLATFORMS = ['抖音', '小紅書', '微信', '淘寶', '閒魚', '其他'];
const SecretaryIcon = ({ className = w-10 h-10 }) = (
svg viewBox=0 0 100 100 className={className}
defs
linearGradient id=pinkBerryGrad x1=0% y1=0% x2=100% y2=100%
stop offset=0% style={{stopColor #FFD1DC}}
stop offset=100% style={{stopColor #FF8AAB}}
linearGradient
filter id=shadow x=-10% y=-10% width=120% height=120%
feGaussianBlur in=SourceAlpha stdDeviation=2
feOffset dx=2 dy=2 result=offsetblur
feComponentTransferfeFuncA type=linear slope=0.3feComponentTransfer
feMergefeMergeNode feMergeNode in=SourceGraphic feMerge
filter
defs
g filter=url(#shadow)
path d=M50 18 C70 18 90 35 90 60 C90 85 70 98 50 98 C30 98 10 85 10 60 C10 35 30 18 50 18 fill=url(#pinkBerryGrad)
path d=M35 22 C45 22 50 5 50 5 C50 5 55 22 65 22 C70 15 80 18 80 18 C65 25 55 28 50 28 C45 28 35 25 20 18 C20 18 30 15 35 22 fill=#81C784
circle cx=35 cy=45 r=1.5 fill=#AD1457 opacity=0.6
circle cx=50 cy=50 r=1.5 fill=#AD1457 opacity=0.6
circle cx=65 cy=45 r=1.5 fill=#AD1457 opacity=0.6
circle cx=42 cy=65 r=1.5 fill=#AD1457 opacity=0.6
circle cx=58 cy=65 r=1.5 fill=#AD1457 opacity=0.6
circle cx=50 cy=80 r=1.5 fill=#AD1457 opacity=0.6
path d=M25 55 Q20 45 35 35 stroke=white strokeWidth=2 strokeLinecap=round fill=none opacity=0.5
g
svg
);
const SvgIcon = ({ name, size = 20, className = }) = {
const icons = {
x path d=M18 6L6 18M6 6l12 12 ,
search React.Fragmentcircle cx=11 cy=11 r=8 line x1=21 cy=21 x2=16.65 y2=16.65 React.Fragment,
edit path d=M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7M18.5 2.5a2.121 2.121 0 113 3L12 15l-4 1 1-4 9.5-9.5z ,
trash path d=M3 6h18m-2 0v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6m3 0V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2 ,
copy path d=M16 4h2a2 2 0 012 2v14a2 2 0 01-2 2H6a2 2 0 01-2-2V6a2 2 0 012-2h2M9 2h6v4H9z ,
check path d=M20 6L9 17l-5-5 ,
clipboard path d=M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2
};
return svg width={size} height={size} viewBox=0 0 24 24 fill=none stroke=currentColor strokeWidth=3 strokeLinecap=round strokeLinejoin=round className={className}{icons[name]}svg;
};
const showToast = (msg) = {
const t = document.createElement('div');
t.className = 'toast animate-bounce';
t.innerText = msg;
document.body.appendChild(t);
setTimeout(() = t.remove(), 2000);
};
const copyToClipboard = (text) = {
const el = document.createElement('textarea');
el.value = text;
document.body.appendChild(el);
el.select();
document.execCommand('copy');
document.body.removeChild(el);
showToast(複製成功 📋);
};
function TrackingModal({ onConfirm, onClose }) {
const [no, setNo] = useState();
return (
div className=modal-overlay
div className=bg-white p-8 rounded-[2.5rem] w-full max-w-xs border-4 border-[#FFB7C5] shadow-2xl text-center
h3 className=text-xl mb-4 text-[#AD1457]📦 填寫運輸單號h3
input autoFocus placeholder=請輸入單號 value={no} onChange={e = setNo(e.target.value)} className=w-full mb-6 text-center text-lg font-mono border-[#FFD1DC]
div className=flex flex-col gap-3
button onClick={() = onConfirm(no)} className=w-full py-4 bg-[#FF8AAB] text-white rounded-2xl shadow-lg activescale-95 transition-all確認發貨 ✨button
button onClick={onClose} className=text-gray-400 py-2取消button
div
div
div
);
}
function EZWayModal({ items, onClose }) {
const shippedItems = items.filter(i = i.status === STATUS.SHIPPED && i.trackingNo);
return (
div className=modal-overlay
div className=bg-white p-8 rounded-[3rem] w-full max-w-sm border-4 border-[#FFB7C5] shadow-2xl max-h-[80vh] overflow-y-auto hide-scrollbar
div className=flex justify-between items-center mb-6
h2 className=text-2xl text-[#AD1457]EZWay 填寫區 📝h2
button onClick={onClose} className=p-2 bg-gray-50 rounded-full text-gray-400SvgIcon name=x size={16}button
div
p className=text-xs text-gray-400 mb-6老闆,這裡彙整了所有「已發貨、待收貨」的單號:p
div className=space-y-4
{shippedItems.length 0 shippedItems.map(i = (
div key={i.id} className=p-5 bg-[#FFF0F3] rounded-3xl border-2 border-white shadow-sm flex flex-col gap-2
div className=flex justify-between items-start
div className=flex flex-col
span className=text-[10px] text-[#FF8AAB] opacity-70 font-bold品名樣式span
span className=text-sm line-clamp-1{i.styles}span
div
span className=text-[9px] bg-white50 px-2 py-0.5 rounded-full text-[#FF8AAB]{i.storeName}span
div
div className=flex justify-between items-center mt-1 pt-2 border-t border-white40
div className=flex flex-col
span className=text-[10px] text-[#FF8AAB] opacity-70 font-bold運輸單號span
span className=text-lg font-mono text-[#AD1457] tracking-wider{i.trackingNo}span
div
button
onClick={() = copyToClipboard(i.trackingNo)}
className=p-3 bg-white text-[#FF8AAB] rounded-2xl shadow-sm activescale-90 transition-all flex items-center gap-2 text-xs font-bold border border-[#FFE0E6]
SvgIcon name=clipboard size={14} 複製
button
div
div
)) div className=text-center py-10 opacity-30 text-sm目前沒有發貨中的寶貝唷div}
div
button onClick={onClose} className=w-full py-4 mt-8 bg-[#9575CD] text-white rounded-2xl shadow-lg activescale-95 transition-all辛苦了老闆 🍵button
div
div
);
}
function AddEditModal({ onClose, onSave, editingItem, items }) {
const [form, setForm] = useState(editingItem {
platform '抖音', storeName '', styles '', orderDate new Date().toISOString().split('T')[0],
trackingNo '', warehouse '漳州倉', status STATUS.ORDERED, price '', images [], note ''
});
const [suggests, setSuggests] = useState({ store [], styles [] });
圓紐名稱映射表
const platformMap = { '抖音' '抖', '小紅書' '紅', '微信' '微', '淘寶' '淘', '閒魚' '閒', '其他' '其' };
const handleInput = (field, val) = {
setForm(prev = ({ ...prev, [field] val }));
if (val.length 0) {
const pool = field === 'storeName' 'storeName' 'styles';
const unique = [...new Set(items.map(i = i[pool]).filter(s = s.toLowerCase().includes(val.toLowerCase()) && s !== val))].slice(0, 5);
setSuggests(prev = ({ ...prev, [field === 'storeName' 'store' 'styles'] unique }));
} else {
setSuggests(prev = ({ ...prev, [field === 'storeName' 'store' 'styles'] [] }));
}
};
const compressImage = (file) = {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = (e) = {
const img = new Image();
img.src = e.target.result;
img.onload = () = {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const MAX = 800;
let w = img.width, h = img.height;
if (w h) { if (w MAX) { h = MAXw; w = MAX; } }
else { if (h MAX) { w = MAXh; h = MAX; } }
canvas.width = w; canvas.height = h;
ctx.drawImage(img, 0, 0, w, h);
setForm(prev = ({ ...prev, images [canvas.toDataURL('imagejpeg', 0.6)] }));
};
};
};
return (
div className=modal-overlay
div className=bg-white p-8 rounded-[3rem] w-full max-w-lg border-4 border-[#EDE7F6] shadow-2xl max-h-[90vh] overflow-y-auto hide-scrollbar relative
div className=flex justify-between items-center mb-8
h2 className=text-2xl text-[#7E57C2]{editingItem '編輯寶貝' '新增登記'}h2
button onClick={onClose} className=p-2 bg-gray-50 rounded-full text-gray-400SvgIcon name=x size={18}button
div
div className=space-y-6
div className=flex flex-col items-center gap-4
div className=w-32 h-32 rounded-3xl bg-[#F3E5F5] border-4 border-dashed border-[#D1C4E9] flex items-center justify-center relative overflow-hidden
{form.images.[0] img src={form.images[0]} className=w-full h-full object-cover div className=text-center text-[#9575CD] text-xs📸 上傳照片div}
input type=file accept=image onChange={e = e.target.files[0] && compressImage(e.target.files[0])} className=absolute inset-0 opacity-0 cursor-pointer
div
div
{ 老闆,平台選擇也改成一個字的圓圓鈕囉 }
div className=flex gap-3 overflow-x-auto hide-scrollbar pb-2
{PLATFORMS.map(p = (
button
key={p}
onClick={() = setForm({...form, platform p})}
className={`w-10 h-10 shrink-0 rounded-full text-sm flex items-center justify-center border-2 transition-all ${form.platform === p 'bg-[#9575CD] border-[#9575CD] text-white shadow-md' 'border-purple-100 text-purple-300 bg-white'}`}
title={p}
{platformMap[p]}
button
))}
div
div className=relative
input placeholder=店家名稱 value={form.storeName} onChange={e = handleInput('storeName', e.target.value)} className=w-full text-lg
{suggests.store.length 0 && (
div className=suggestion-box
{suggests.store.map(s = div key={s} className=suggestion-item onClick={() = { setForm({...form, storeName s}); setSuggests({...suggests, store []}); }}🏢 {s}div)}
div
)}
div
div className=relative
input placeholder=品名樣式 value={form.styles} onChange={e = handleInput('styles', e.target.value)} className=w-full text-lg
{suggests.styles.length 0 && (
div className=suggestion-box
{suggests.styles.map(s = div key={s} className=suggestion-item onClick={() = { setForm({...form, styles s}); setSuggests({...suggests, styles []}); }}💎 {s}div)}
div
)}
div
div className=grid grid-cols-2 gap-4
div className=flex flex-col gap-1
span className=text-[10px] text-purple-300 ml-2 font-bold下單日期span
input type=date value={form.orderDate} onChange={e = setForm({...form, orderDate e.target.value})} className=w-full text-sm
div
div className=flex flex-col gap-1
span className=text-[10px] text-purple-300 ml-2 font-bold價格 (¥)span
input type=number placeholder=¥ 價格 value={form.price} onChange={e = setForm({...form, price e.target.value})} className=w-full text-sm
div
div
div className=grid grid-cols-2 gap-4
select value={form.warehouse} onChange={e = setForm({...form, warehouse e.target.value})} className=text-sm
{WAREHOUSES.map(w = option key={w} value={w}{w}option)}
select
select value={form.status} onChange={e = setForm({...form, status e.target.value})} className=text-sm
{Object.values(STATUS).map(s = option key={s} value={s}{s}option)}
select
div
input placeholder=運輸單號 (選填) value={form.trackingNo} onChange={e = setForm({...form, trackingNo e.target.value})} className=w-full text-sm font-mono tracking-wider
textarea placeholder=老闆,還有什麼要記下來的嗎? value={form.note} onChange={e = setForm({...form, note e.target.value})} className=w-full text-sm h-24
button onClick={() = onSave(form)} className=w-full py-5 bg-gradient-to-r from-[#9575CD] to-[#D1C4E9] text-white rounded-[1.5rem] text-xl shadow-lg activescale-95 transition-all確認存檔 ✨button
div
div
div
);
}
function App() {
const [items, setItems] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [activeTab, setActiveTab] = useState('list');
const [searchQuery, setSearchQuery] = useState();
const [isModalOpen, setIsModalOpen] = useState(false);
const [isEZWayOpen, setIsEZWayOpen] = useState(false);
const [editingItem, setEditingItem] = useState(null);
const [trackingTarget, setTrackingTarget] = useState(null);
const shoppingRef = window.db.collection('artifacts').doc(appId).collection('public').doc('data').collection('shopping_items');
useEffect(() = {
const init = async () = {
await window.auth.signInAnonymously();
shoppingRef.orderBy('createdAt', 'desc').onSnapshot(s = {
setItems(s.docs.map(d = ({ id d.id, ...d.data() })));
setIsLoading(false);
});
};
init();
}, []);
const isHistory = (item) = {
if (item.status !== STATUS.RECEIVED) return false;
const oneMonthAgo = Date.now() - (30 24 60 60 1000);
return (item.updatedAt item.createdAt) oneMonthAgo;
};
const filtered = useMemo(() = {
const q = searchQuery.toLowerCase().trim();
return items.filter(i = {
const matchSearch = !q
i.storeName.toLowerCase().includes(q)
i.styles.toLowerCase().includes(q)
i.trackingNo.toLowerCase().includes(q);
if (activeTab === 'history') return matchSearch && isHistory(i);
if (activeTab === 'list') return matchSearch && !isHistory(i);
if (activeTab === 'gallery') return matchSearch && i.images.[0];
if (activeTab === 'warehouse') return matchSearch && i.status !== STATUS.RECEIVED;
return matchSearch;
});
}, [items, activeTab, searchQuery]);
const groupedByDate = useMemo(() = {
return filtered.reduce((acc, i) = {
const d = i.orderDate 日期未定;
if (!acc[d]) acc[d] = [];
acc[d].push(i);
return acc;
}, {});
}, [filtered]);
const handleSave = async (data) = {
const { id, ...payload } = data;
const update = { ...payload, updatedAt Date.now() };
if (id) await shoppingRef.doc(id).update(update);
else await shoppingRef.add({ ...update, createdAt Date.now() });
setIsModalOpen(false);
setEditingItem(null);
showToast(祕書辦妥了 ✨);
};
const updateStatus = async (id, nextStatus) = {
if (nextStatus === STATUS.SHIPPED) {
setTrackingTarget(id);
} else {
await shoppingRef.doc(id).update({ status nextStatus, updatedAt Date.now() });
showToast(狀態更新囉 ✨);
}
};
const confirmShipped = async (no) = {
await shoppingRef.doc(trackingTarget).update({ status STATUS.SHIPPED, trackingNo no, updatedAt Date.now() });
setTrackingTarget(null);
showToast(發貨成功!已彙整至 EZWay);
};
return (
div className=min-h-screen pb-32
header className=sticky top-0 z-[1000] bg-[#FFFDF2]90 backdrop-blur-xl border-b-4 border-[#EDE7F6] p-4 mdp-6
div className=max-w-4xl mx-auto flex flex-col gap-4
div className=flex justify-between items-center
div className=flex items-center gap-3
SecretaryIcon
h1 className=text-xl mdtext-2xl text-[#7E57C2]購物狂的專屬祕書h1
div
div className=bg-white px-4 py-2 rounded-full border-2 border-[#EDE7F6] text-[10px] text-purple-400
{filtered.length} {items.length} 件
div
div
div className=relative
input placeholder=🔍 搜尋店家、品名、單號 (包含歷史)... value={searchQuery} onChange={e = setSearchQuery(e.target.value)} className=w-full pl-12 pr-4 py-3 text-sm shadow-inner
SvgIcon name=search className=absolute left-4 top-12 -translate-y-12 text-purple-200
div
div className=flex gap-2 overflow-x-auto hide-scrollbar py-1
{['list', 'gallery', 'warehouse', 'history'].map(t = (
button key={t} onClick={() = setActiveTab(t)} className={`px-6 py-2 rounded-full text-xs whitespace-nowrap bg-white border-2 border-[#EDE7F6] text-purple-300 transition-all ${activeTab === t 'active-tab border-transparent' ''}`}
{t==='list''全部清單't==='gallery''美照圖庫't==='warehouse''倉庫進度''歷史存摺'}
button
))}
div
div
header
main className=max-w-4xl mx-auto p-4 mdp-6
{isLoading div className=text-center py-20 opacity-20 animate-pulse正在為老闆準備筆記...div (
activeTab === 'gallery' (
div className=grid grid-cols-2 mdgrid-cols-4 gap-4
{filtered.map(i = (
div key={i.id} onClick={() = {setEditingItem(i); setIsModalOpen(true);}} className=aspect-square y2k-card overflow-hidden
img src={i.images[0]} className=w-full h-full object-cover
div
))}
div
) activeTab === 'warehouse' (
div className=space-y-6
{WAREHOUSES.map(wh = {
const whItems = filtered.filter(i = i.warehouse === wh);
return whItems.length 0 && (
div key={wh} className=space-y-3
h2 className=text-lg text-purple-300 ml-2 font-bold📍 {wh}h2
div className=grid grid-cols-1 mdgrid-cols-2 gap-4
{whItems.map(i = (
div key={i.id} onClick={() = {setEditingItem(i); setIsModalOpen(true);}} className=y2k-card p-4 flex gap-4 items-center
div className=w-16 h-16 rounded-2xl bg-gray-50 shrink-0 overflow-hidden
{i.images.[0] && img src={i.images[0]} className=w-full h-full object-cover }
div
div className=flex-1 min-w-0
div className=flex justify-between items-start
p className=text-sm truncate font-bold{i.styles}p
span className={`status-tag shrink-0 ${i.status === STATUS.ORDERED 'bg-ordered' i.status === STATUS.SHIPPED 'bg-shipped' 'bg-received'}`}{i.status}span
div
p className=text-[10px] text-purple-300 font-mono mt-1 font-bold{i.trackingNo '單號待填'}p
div
div
))}
div
div
);
})}
div
) (
div className=space-y-8
{Object.entries(groupedByDate).sort((a,b) = b[0].localeCompare(a[0])).map(([date, list]) = (
div key={date} className=space-y-4
h3 className=text-sm bg-white50 inline-block px-4 py-1 rounded-full text-purple-300 border border-[#EDE7F6] font-bold{date}h3
div className=grid grid-cols-1 mdgrid-cols-2 gap-5
{list.map(item = (
div key={item.id} onClick={() = {setEditingItem(item); setIsModalOpen(true);}} className=y2k-card p-5 relative group
div className=flex gap-4
div className=w-20 h-20 rounded-[1.5rem] bg-[#F3E5F5] border-2 border-purple-50 shrink-0 overflow-hidden
{item.images.[0] && img src={item.images[0]} className=w-full h-full object-cover }
div
div className=flex-1 min-w-0
div className=flex justify-between items-start mb-1
span className=text-[10px] opacity-40 uppercase tracking-tighter font-bold{item.platform}span
div className={`status-tag ${item.status === STATUS.ORDERED 'bg-ordered' item.status === STATUS.SHIPPED 'bg-shipped' 'bg-received'}`}{item.status}div
div
h4 className=text-lg leading-tight mb-1 truncate font-bold{item.styles}h4
p className=text-xs text-purple-300 font-bold{item.storeName}p
div
div
div className=mt-4 pt-4 border-t border-[#EDE7F6] flex justify-between items-center
span className=text-[11px] font-mono text-purple-200 tracking-wider font-bold{item.trackingNo 陸運單號未填}span
div className=flex gap-3
{item.status === STATUS.ORDERED && (
button
onClick={(e) = {e.stopPropagation(); updateStatus(item.id, STATUS.SHIPPED);}}
className=w-8 h-8 rounded-full bg-purple-50 text-[#7E57C2] flex items-center justify-center text-sm activescale-90 transition-all font-bold border border-purple-100 shadow-sm
title=發貨
發
button
)}
{item.status === STATUS.SHIPPED && (
button
onClick={(e) = {e.stopPropagation(); updateStatus(item.id, STATUS.RECEIVED);}}
className=w-8 h-8 rounded-full bg-mint20 text-[#00897B] flex items-center justify-center text-sm activescale-90 transition-all font-bold border border-[#B2DFDB] shadow-sm
title=簽收
收
button
)}
button onClick={(e) = {e.stopPropagation(); if(confirm('要丟掉這件寶貝嗎?')) shoppingRef.doc(item.id).delete();}} className=p-1.5 text-gray-200 hovertext-red-400 transition-colorsSvgIcon name=trash size={14}button
div
div
div
))}
div
div
))}
div
)
)}
main
div className=fixed bottom-8 right-8 flex flex-col gap-4 items-end z-[5000]
button onClick={() = setIsEZWayOpen(true)} className=strawberry-float w-14 h-14 bg-white rounded-full shadow-xl flex items-center justify-center border-4 border-[#FFD1DC] activescale-90 transition-all
SecretaryIcon className=w-8 h-8
button
button onClick={() = {setEditingItem(null); setIsModalOpen(true);}} className=w-16 h-16 bg-[#D1C4E9] rounded-full shadow-2xl text-[#4527A0] text-4xl flex items-center justify-center border-4 border-white activescale-90 transition-all font-bold
+
button
div
{isModalOpen && AddEditModal onClose={() = setIsModalOpen(false)} onSave={handleSave} editingItem={editingItem} items={items} }
{trackingTarget && TrackingModal onConfirm={confirmShipped} onClose={() = setTrackingTarget(null)} }
{isEZWayOpen && EZWayModal items={items} onClose={() = setIsEZWayOpen(false)} }
div
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(App );
script
body
html