ljxg_dish.html
· 16 KiB · HTML
Raw
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>庐间菜品浏览器</title>
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
background: #f5f5f5;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
.header {
background: white;
padding: 24px;
border-radius: 8px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
h1 {
font-size: 28px;
margin-bottom: 20px;
color: #333;
}
.filters {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 16px;
}
.filter-group {
display: flex;
flex-direction: column;
gap: 8px;
}
label {
font-size: 14px;
color: #666;
font-weight: 500;
}
input, select {
padding: 10px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
transition: border-color 0.3s;
}
input:focus, select:focus {
outline: none;
border-color: #1890ff;
}
.stats {
display: flex;
gap: 24px;
padding-top: 16px;
border-top: 1px solid #eee;
font-size: 14px;
color: #666;
}
.stat-item {
display: flex;
align-items: center;
gap: 8px;
}
.stat-number {
font-size: 20px;
font-weight: bold;
color: #1890ff;
}
.dishes-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 12px;
}
.dish-card {
background: white;
border-radius: 6px;
overflow: hidden;
box-shadow: 0 1px 4px rgba(0,0,0,0.1);
transition: transform 0.2s, box-shadow 0.2s;
}
.dish-card:hover {
transform: translateY(-2px);
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
.dish-image {
width: 100%;
height: 140px;
object-fit: cover;
background: #f0f0f0;
}
.no-image {
width: 100%;
height: 140px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
font-size: 13px;
}
.dish-content {
padding: 10px;
}
.dish-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 8px;
margin-bottom: 6px;
}
.dish-name {
font-size: 14px;
font-weight: bold;
color: #333;
flex: 1;
line-height: 1.3;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.dish-image, .no-image {
cursor: pointer;
}
.dish-image:active, .no-image:active {
opacity: 0.8;
}
.toast {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 12px 24px;
border-radius: 4px;
font-size: 14px;
z-index: 1000;
animation: fadeInOut 2s ease-in-out;
}
@keyframes fadeInOut {
0% { opacity: 0; transform: translateX(-50%) translateY(-10px); }
10% { opacity: 1; transform: translateX(-50%) translateY(0); }
90% { opacity: 1; transform: translateX(-50%) translateY(0); }
100% { opacity: 0; transform: translateX(-50%) translateY(-10px); }
}
.dish-price {
font-size: 16px;
font-weight: bold;
color: #ff4d4f;
white-space: nowrap;
}
.dish-info {
display: flex;
gap: 8px;
font-size: 11px;
color: #999;
margin-bottom: 6px;
}
.dish-category {
display: inline-block;
padding: 2px 6px;
background: #f0f0f0;
border-radius: 3px;
font-size: 10px;
color: #666;
}
.dish-methods {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 6px;
}
.method-tag {
padding: 1px 6px;
background: #e6f7ff;
border: 1px solid #91d5ff;
border-radius: 3px;
font-size: 10px;
color: #1890ff;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #999;
}
.empty-state-icon {
font-size: 64px;
margin-bottom: 16px;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect, useMemo } = React;
// 内嵌数据 - 从Gitea raw URL加载
const REPO_BASE = 'https://git.kitchain.cn/liusijin/ljxg_dish/raw/branch/main';
const DATA_URL = `${REPO_BASE}/meituan_goods_dishes_only.json`;
function App() {
const [dishes, setDishes] = useState([]);
const [searchName, setSearchName] = useState('');
const [hasImage, setHasImage] = useState('all');
const [minPrice, setMinPrice] = useState('');
const [maxPrice, setMaxPrice] = useState('');
const [selectedCategory, setSelectedCategory] = useState('all');
const [toast, setToast] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(DATA_URL, {
credentials: 'include'
})
.then(res => res.json())
.then(data => {
setDishes(data);
setLoading(false);
})
.catch(err => {
console.error('加载数据失败:', err);
setLoading(false);
});
}, []);
const categories = useMemo(() => {
const cats = [...new Set(dishes.map(d => d.category))];
return cats.sort();
}, [dishes]);
const filteredDishes = useMemo(() => {
return dishes.filter(dish => {
if (searchName && !dish.name.toLowerCase().includes(searchName.toLowerCase())) {
return false;
}
if (hasImage === 'yes' && !dish.image) return false;
if (hasImage === 'no' && dish.image) return false;
const price = dish.specs[0]?.price || 0;
if (minPrice && price < parseFloat(minPrice)) return false;
if (maxPrice && price > parseFloat(maxPrice)) return false;
if (selectedCategory !== 'all' && dish.category !== selectedCategory) {
return false;
}
return true;
});
}, [dishes, searchName, hasImage, minPrice, maxPrice, selectedCategory]);
const stats = useMemo(() => {
return {
total: dishes.length,
withImage: dishes.filter(d => d.image).length,
filtered: filteredDishes.length
};
}, [dishes, filteredDishes]);
const showToast = (message) => {
setToast(message);
setTimeout(() => setToast(null), 2000);
};
if (loading) {
return (
<div className="container">
<div className="empty-state">
<div className="empty-state-icon">⏳</div>
<div>加载中...</div>
</div>
</div>
);
}
return (
<div className="container">
{toast && <div className="toast">{toast}</div>}
<div className="header">
<h1>🍽️ 庐间菜品浏览器</h1>
<div className="filters">
<div className="filter-group">
<label>菜品名称</label>
<input
type="text"
placeholder="搜索菜品..."
value={searchName}
onChange={(e) => setSearchName(e.target.value)}
/>
</div>
<div className="filter-group">
<label>分类</label>
<select
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
>
<option value="all">全部分类</option>
{categories.map(cat => (
<option key={cat} value={cat}>{cat}</option>
))}
</select>
</div>
<div className="filter-group">
<label>是否有图片</label>
<select
value={hasImage}
onChange={(e) => setHasImage(e.target.value)}
>
<option value="all">全部</option>
<option value="yes">有图片</option>
<option value="no">无图片</option>
</select>
</div>
<div className="filter-group">
<label>最低价格</label>
<input
type="number"
placeholder="¥0"
value={minPrice}
onChange={(e) => setMinPrice(e.target.value)}
/>
</div>
<div className="filter-group">
<label>最高价格</label>
<input
type="number"
placeholder="¥999"
value={maxPrice}
onChange={(e) => setMaxPrice(e.target.value)}
/>
</div>
</div>
<div className="stats">
<div className="stat-item">
<span>总菜品:</span>
<span className="stat-number">{stats.total}</span>
</div>
<div className="stat-item">
<span>有图片:</span>
<span className="stat-number">{stats.withImage}</span>
</div>
<div className="stat-item">
<span>当前显示:</span>
<span className="stat-number">{stats.filtered}</span>
</div>
</div>
</div>
{filteredDishes.length === 0 ? (
<div className="empty-state">
<div className="empty-state-icon">🔍</div>
<div>没有找到符合条件的菜品</div>
</div>
) : (
<div className="dishes-grid">
{filteredDishes.map(dish => (
<DishCard key={dish.id} dish={dish} onCopy={showToast} />
))}
</div>
)}
</div>
);
}
function DishCard({ dish, onCopy }) {
const imageUrl = dish.image
? `${REPO_BASE}/media/${dish.id}.${dish.image.endsWith('.jpg') || dish.image.endsWith('.jpeg') ? 'jpg' : 'png'}`
: null;
const price = dish.specs[0]?.price || 0;
const handleImageClick = () => {
const imageName = imageUrl ? imageUrl.split('/').pop() : `${dish.id}.png`;
navigator.clipboard.writeText(imageName).then(() => {
onCopy(`已复制: ${imageName}`);
}).catch(() => {
onCopy('复制失败');
});
};
return (
<div className="dish-card">
{imageUrl ? (
<img
src={imageUrl}
alt={dish.name}
className="dish-image"
onClick={handleImageClick}
onError={(e) => {
e.target.style.display = 'none';
e.target.nextSibling.style.display = 'flex';
}}
/>
) : null}
<div
className="no-image"
style={{ display: imageUrl ? 'none' : 'flex' }}
onClick={handleImageClick}
>
暂无图片
</div>
<div className="dish-content">
<div className="dish-header">
<div className="dish-name">{dish.name}</div>
<div className="dish-price">¥{price}</div>
</div>
<div className="dish-info">
<span className="dish-category">{dish.category}</span>
<span>{dish.unit}</span>
</div>
{dish.methods && dish.methods.length > 0 && (
<div className="dish-methods">
{dish.methods.map((method, idx) => (
<span key={idx} className="method-tag">
{method.name}
</span>
))}
</div>
)}
</div>
</div>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
</script>
</body>
</html>
| 1 | <!DOCTYPE html> |
| 2 | <html lang="zh-CN"> |
| 3 | <head> |
| 4 | <meta charset="UTF-8"> |
| 5 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| 6 | <title>庐间菜品浏览器</title> |
| 7 | <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script> |
| 8 | <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script> |
| 9 | <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> |
| 10 | <style> |
| 11 | * { |
| 12 | margin: 0; |
| 13 | padding: 0; |
| 14 | box-sizing: border-box; |
| 15 | } |
| 16 | |
| 17 | body { |
| 18 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif; |
| 19 | background: #f5f5f5; |
| 20 | padding: 20px; |
| 21 | } |
| 22 | |
| 23 | .container { |
| 24 | max-width: 1400px; |
| 25 | margin: 0 auto; |
| 26 | } |
| 27 | |
| 28 | .header { |
| 29 | background: white; |
| 30 | padding: 24px; |
| 31 | border-radius: 8px; |
| 32 | margin-bottom: 20px; |
| 33 | box-shadow: 0 2px 8px rgba(0,0,0,0.1); |
| 34 | } |
| 35 | |
| 36 | h1 { |
| 37 | font-size: 28px; |
| 38 | margin-bottom: 20px; |
| 39 | color: #333; |
| 40 | } |
| 41 | |
| 42 | .filters { |
| 43 | display: grid; |
| 44 | grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); |
| 45 | gap: 16px; |
| 46 | margin-bottom: 16px; |
| 47 | } |
| 48 | |
| 49 | .filter-group { |
| 50 | display: flex; |
| 51 | flex-direction: column; |
| 52 | gap: 8px; |
| 53 | } |
| 54 | |
| 55 | label { |
| 56 | font-size: 14px; |
| 57 | color: #666; |
| 58 | font-weight: 500; |
| 59 | } |
| 60 | |
| 61 | input, select { |
| 62 | padding: 10px 12px; |
| 63 | border: 1px solid #ddd; |
| 64 | border-radius: 4px; |
| 65 | font-size: 14px; |
| 66 | transition: border-color 0.3s; |
| 67 | } |
| 68 | |
| 69 | input:focus, select:focus { |
| 70 | outline: none; |
| 71 | border-color: #1890ff; |
| 72 | } |
| 73 | |
| 74 | .stats { |
| 75 | display: flex; |
| 76 | gap: 24px; |
| 77 | padding-top: 16px; |
| 78 | border-top: 1px solid #eee; |
| 79 | font-size: 14px; |
| 80 | color: #666; |
| 81 | } |
| 82 | |
| 83 | .stat-item { |
| 84 | display: flex; |
| 85 | align-items: center; |
| 86 | gap: 8px; |
| 87 | } |
| 88 | |
| 89 | .stat-number { |
| 90 | font-size: 20px; |
| 91 | font-weight: bold; |
| 92 | color: #1890ff; |
| 93 | } |
| 94 | |
| 95 | .dishes-grid { |
| 96 | display: grid; |
| 97 | grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); |
| 98 | gap: 12px; |
| 99 | } |
| 100 | |
| 101 | .dish-card { |
| 102 | background: white; |
| 103 | border-radius: 6px; |
| 104 | overflow: hidden; |
| 105 | box-shadow: 0 1px 4px rgba(0,0,0,0.1); |
| 106 | transition: transform 0.2s, box-shadow 0.2s; |
| 107 | } |
| 108 | |
| 109 | .dish-card:hover { |
| 110 | transform: translateY(-2px); |
| 111 | box-shadow: 0 2px 8px rgba(0,0,0,0.15); |
| 112 | } |
| 113 | |
| 114 | .dish-image { |
| 115 | width: 100%; |
| 116 | height: 140px; |
| 117 | object-fit: cover; |
| 118 | background: #f0f0f0; |
| 119 | } |
| 120 | |
| 121 | .no-image { |
| 122 | width: 100%; |
| 123 | height: 140px; |
| 124 | display: flex; |
| 125 | align-items: center; |
| 126 | justify-content: center; |
| 127 | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| 128 | color: white; |
| 129 | font-size: 13px; |
| 130 | } |
| 131 | |
| 132 | .dish-content { |
| 133 | padding: 10px; |
| 134 | } |
| 135 | |
| 136 | .dish-header { |
| 137 | display: flex; |
| 138 | justify-content: space-between; |
| 139 | align-items: flex-start; |
| 140 | gap: 8px; |
| 141 | margin-bottom: 6px; |
| 142 | } |
| 143 | |
| 144 | .dish-name { |
| 145 | font-size: 14px; |
| 146 | font-weight: bold; |
| 147 | color: #333; |
| 148 | flex: 1; |
| 149 | line-height: 1.3; |
| 150 | display: -webkit-box; |
| 151 | -webkit-line-clamp: 2; |
| 152 | -webkit-box-orient: vertical; |
| 153 | overflow: hidden; |
| 154 | text-overflow: ellipsis; |
| 155 | } |
| 156 | |
| 157 | .dish-image, .no-image { |
| 158 | cursor: pointer; |
| 159 | } |
| 160 | |
| 161 | .dish-image:active, .no-image:active { |
| 162 | opacity: 0.8; |
| 163 | } |
| 164 | |
| 165 | .toast { |
| 166 | position: fixed; |
| 167 | top: 20px; |
| 168 | left: 50%; |
| 169 | transform: translateX(-50%); |
| 170 | background: rgba(0, 0, 0, 0.8); |
| 171 | color: white; |
| 172 | padding: 12px 24px; |
| 173 | border-radius: 4px; |
| 174 | font-size: 14px; |
| 175 | z-index: 1000; |
| 176 | animation: fadeInOut 2s ease-in-out; |
| 177 | } |
| 178 | |
| 179 | @keyframes fadeInOut { |
| 180 | 0% { opacity: 0; transform: translateX(-50%) translateY(-10px); } |
| 181 | 10% { opacity: 1; transform: translateX(-50%) translateY(0); } |
| 182 | 90% { opacity: 1; transform: translateX(-50%) translateY(0); } |
| 183 | 100% { opacity: 0; transform: translateX(-50%) translateY(-10px); } |
| 184 | } |
| 185 | |
| 186 | .dish-price { |
| 187 | font-size: 16px; |
| 188 | font-weight: bold; |
| 189 | color: #ff4d4f; |
| 190 | white-space: nowrap; |
| 191 | } |
| 192 | |
| 193 | .dish-info { |
| 194 | display: flex; |
| 195 | gap: 8px; |
| 196 | font-size: 11px; |
| 197 | color: #999; |
| 198 | margin-bottom: 6px; |
| 199 | } |
| 200 | |
| 201 | .dish-category { |
| 202 | display: inline-block; |
| 203 | padding: 2px 6px; |
| 204 | background: #f0f0f0; |
| 205 | border-radius: 3px; |
| 206 | font-size: 10px; |
| 207 | color: #666; |
| 208 | } |
| 209 | |
| 210 | .dish-methods { |
| 211 | display: flex; |
| 212 | flex-wrap: wrap; |
| 213 | gap: 4px; |
| 214 | margin-top: 6px; |
| 215 | } |
| 216 | |
| 217 | .method-tag { |
| 218 | padding: 1px 6px; |
| 219 | background: #e6f7ff; |
| 220 | border: 1px solid #91d5ff; |
| 221 | border-radius: 3px; |
| 222 | font-size: 10px; |
| 223 | color: #1890ff; |
| 224 | } |
| 225 | |
| 226 | .empty-state { |
| 227 | text-align: center; |
| 228 | padding: 60px 20px; |
| 229 | color: #999; |
| 230 | } |
| 231 | |
| 232 | .empty-state-icon { |
| 233 | font-size: 64px; |
| 234 | margin-bottom: 16px; |
| 235 | } |
| 236 | </style> |
| 237 | </head> |
| 238 | <body> |
| 239 | <div id="root"></div> |
| 240 | |
| 241 | <script type="text/babel"> |
| 242 | const { useState, useEffect, useMemo } = React; |
| 243 | |
| 244 | // 内嵌数据 - 从Gitea raw URL加载 |
| 245 | const REPO_BASE = 'https://git.kitchain.cn/liusijin/ljxg_dish/raw/branch/main'; |
| 246 | const DATA_URL = `${REPO_BASE}/meituan_goods_dishes_only.json`; |
| 247 | |
| 248 | function App() { |
| 249 | const [dishes, setDishes] = useState([]); |
| 250 | const [searchName, setSearchName] = useState(''); |
| 251 | const [hasImage, setHasImage] = useState('all'); |
| 252 | const [minPrice, setMinPrice] = useState(''); |
| 253 | const [maxPrice, setMaxPrice] = useState(''); |
| 254 | const [selectedCategory, setSelectedCategory] = useState('all'); |
| 255 | const [toast, setToast] = useState(null); |
| 256 | const [loading, setLoading] = useState(true); |
| 257 | |
| 258 | useEffect(() => { |
| 259 | fetch(DATA_URL, { |
| 260 | credentials: 'include' |
| 261 | }) |
| 262 | .then(res => res.json()) |
| 263 | .then(data => { |
| 264 | setDishes(data); |
| 265 | setLoading(false); |
| 266 | }) |
| 267 | .catch(err => { |
| 268 | console.error('加载数据失败:', err); |
| 269 | setLoading(false); |
| 270 | }); |
| 271 | }, []); |
| 272 | |
| 273 | const categories = useMemo(() => { |
| 274 | const cats = [...new Set(dishes.map(d => d.category))]; |
| 275 | return cats.sort(); |
| 276 | }, [dishes]); |
| 277 | |
| 278 | const filteredDishes = useMemo(() => { |
| 279 | return dishes.filter(dish => { |
| 280 | if (searchName && !dish.name.toLowerCase().includes(searchName.toLowerCase())) { |
| 281 | return false; |
| 282 | } |
| 283 | |
| 284 | if (hasImage === 'yes' && !dish.image) return false; |
| 285 | if (hasImage === 'no' && dish.image) return false; |
| 286 | |
| 287 | const price = dish.specs[0]?.price || 0; |
| 288 | if (minPrice && price < parseFloat(minPrice)) return false; |
| 289 | if (maxPrice && price > parseFloat(maxPrice)) return false; |
| 290 | |
| 291 | if (selectedCategory !== 'all' && dish.category !== selectedCategory) { |
| 292 | return false; |
| 293 | } |
| 294 | |
| 295 | return true; |
| 296 | }); |
| 297 | }, [dishes, searchName, hasImage, minPrice, maxPrice, selectedCategory]); |
| 298 | |
| 299 | const stats = useMemo(() => { |
| 300 | return { |
| 301 | total: dishes.length, |
| 302 | withImage: dishes.filter(d => d.image).length, |
| 303 | filtered: filteredDishes.length |
| 304 | }; |
| 305 | }, [dishes, filteredDishes]); |
| 306 | |
| 307 | const showToast = (message) => { |
| 308 | setToast(message); |
| 309 | setTimeout(() => setToast(null), 2000); |
| 310 | }; |
| 311 | |
| 312 | if (loading) { |
| 313 | return ( |
| 314 | <div className="container"> |
| 315 | <div className="empty-state"> |
| 316 | <div className="empty-state-icon">⏳</div> |
| 317 | <div>加载中...</div> |
| 318 | </div> |
| 319 | </div> |
| 320 | ); |
| 321 | } |
| 322 | |
| 323 | return ( |
| 324 | <div className="container"> |
| 325 | {toast && <div className="toast">{toast}</div>} |
| 326 | <div className="header"> |
| 327 | <h1>🍽️ 庐间菜品浏览器</h1> |
| 328 | |
| 329 | <div className="filters"> |
| 330 | <div className="filter-group"> |
| 331 | <label>菜品名称</label> |
| 332 | <input |
| 333 | type="text" |
| 334 | placeholder="搜索菜品..." |
| 335 | value={searchName} |
| 336 | onChange={(e) => setSearchName(e.target.value)} |
| 337 | /> |
| 338 | </div> |
| 339 | |
| 340 | <div className="filter-group"> |
| 341 | <label>分类</label> |
| 342 | <select |
| 343 | value={selectedCategory} |
| 344 | onChange={(e) => setSelectedCategory(e.target.value)} |
| 345 | > |
| 346 | <option value="all">全部分类</option> |
| 347 | {categories.map(cat => ( |
| 348 | <option key={cat} value={cat}>{cat}</option> |
| 349 | ))} |
| 350 | </select> |
| 351 | </div> |
| 352 | |
| 353 | <div className="filter-group"> |
| 354 | <label>是否有图片</label> |
| 355 | <select |
| 356 | value={hasImage} |
| 357 | onChange={(e) => setHasImage(e.target.value)} |
| 358 | > |
| 359 | <option value="all">全部</option> |
| 360 | <option value="yes">有图片</option> |
| 361 | <option value="no">无图片</option> |
| 362 | </select> |
| 363 | </div> |
| 364 | |
| 365 | <div className="filter-group"> |
| 366 | <label>最低价格</label> |
| 367 | <input |
| 368 | type="number" |
| 369 | placeholder="¥0" |
| 370 | value={minPrice} |
| 371 | onChange={(e) => setMinPrice(e.target.value)} |
| 372 | /> |
| 373 | </div> |
| 374 | |
| 375 | <div className="filter-group"> |
| 376 | <label>最高价格</label> |
| 377 | <input |
| 378 | type="number" |
| 379 | placeholder="¥999" |
| 380 | value={maxPrice} |
| 381 | onChange={(e) => setMaxPrice(e.target.value)} |
| 382 | /> |
| 383 | </div> |
| 384 | </div> |
| 385 | |
| 386 | <div className="stats"> |
| 387 | <div className="stat-item"> |
| 388 | <span>总菜品:</span> |
| 389 | <span className="stat-number">{stats.total}</span> |
| 390 | </div> |
| 391 | <div className="stat-item"> |
| 392 | <span>有图片:</span> |
| 393 | <span className="stat-number">{stats.withImage}</span> |
| 394 | </div> |
| 395 | <div className="stat-item"> |
| 396 | <span>当前显示:</span> |
| 397 | <span className="stat-number">{stats.filtered}</span> |
| 398 | </div> |
| 399 | </div> |
| 400 | </div> |
| 401 | |
| 402 | {filteredDishes.length === 0 ? ( |
| 403 | <div className="empty-state"> |
| 404 | <div className="empty-state-icon">🔍</div> |
| 405 | <div>没有找到符合条件的菜品</div> |
| 406 | </div> |
| 407 | ) : ( |
| 408 | <div className="dishes-grid"> |
| 409 | {filteredDishes.map(dish => ( |
| 410 | <DishCard key={dish.id} dish={dish} onCopy={showToast} /> |
| 411 | ))} |
| 412 | </div> |
| 413 | )} |
| 414 | </div> |
| 415 | ); |
| 416 | } |
| 417 | |
| 418 | function DishCard({ dish, onCopy }) { |
| 419 | const imageUrl = dish.image |
| 420 | ? `${REPO_BASE}/media/${dish.id}.${dish.image.endsWith('.jpg') || dish.image.endsWith('.jpeg') ? 'jpg' : 'png'}` |
| 421 | : null; |
| 422 | |
| 423 | const price = dish.specs[0]?.price || 0; |
| 424 | |
| 425 | const handleImageClick = () => { |
| 426 | const imageName = imageUrl ? imageUrl.split('/').pop() : `${dish.id}.png`; |
| 427 | navigator.clipboard.writeText(imageName).then(() => { |
| 428 | onCopy(`已复制: ${imageName}`); |
| 429 | }).catch(() => { |
| 430 | onCopy('复制失败'); |
| 431 | }); |
| 432 | }; |
| 433 | |
| 434 | return ( |
| 435 | <div className="dish-card"> |
| 436 | {imageUrl ? ( |
| 437 | <img |
| 438 | src={imageUrl} |
| 439 | alt={dish.name} |
| 440 | className="dish-image" |
| 441 | onClick={handleImageClick} |
| 442 | onError={(e) => { |
| 443 | e.target.style.display = 'none'; |
| 444 | e.target.nextSibling.style.display = 'flex'; |
| 445 | }} |
| 446 | /> |
| 447 | ) : null} |
| 448 | <div |
| 449 | className="no-image" |
| 450 | style={{ display: imageUrl ? 'none' : 'flex' }} |
| 451 | onClick={handleImageClick} |
| 452 | > |
| 453 | 暂无图片 |
| 454 | </div> |
| 455 | |
| 456 | <div className="dish-content"> |
| 457 | <div className="dish-header"> |
| 458 | <div className="dish-name">{dish.name}</div> |
| 459 | <div className="dish-price">¥{price}</div> |
| 460 | </div> |
| 461 | |
| 462 | <div className="dish-info"> |
| 463 | <span className="dish-category">{dish.category}</span> |
| 464 | <span>{dish.unit}</span> |
| 465 | </div> |
| 466 | |
| 467 | {dish.methods && dish.methods.length > 0 && ( |
| 468 | <div className="dish-methods"> |
| 469 | {dish.methods.map((method, idx) => ( |
| 470 | <span key={idx} className="method-tag"> |
| 471 | {method.name} |
| 472 | </span> |
| 473 | ))} |
| 474 | </div> |
| 475 | )} |
| 476 | </div> |
| 477 | </div> |
| 478 | ); |
| 479 | } |
| 480 | |
| 481 | ReactDOM.render(<App />, document.getElementById('root')); |
| 482 | </script> |
| 483 | </body> |
| 484 | </html> |
| 485 |