Last active 2 weeks ago

liusijin revised this gist 2 weeks ago. Go to revision

1 file changed, 482 insertions, 3 deletions

ljxg_dish.html

@@ -1,3 +1,482 @@
1 - <html>
2 - <body>hello</body>
3 - </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 + .then(res => res.json())
261 + .then(data => {
262 + setDishes(data);
263 + setLoading(false);
264 + })
265 + .catch(err => {
266 + console.error('加载数据失败:', err);
267 + setLoading(false);
268 + });
269 + }, []);
270 +
271 + const categories = useMemo(() => {
272 + const cats = [...new Set(dishes.map(d => d.category))];
273 + return cats.sort();
274 + }, [dishes]);
275 +
276 + const filteredDishes = useMemo(() => {
277 + return dishes.filter(dish => {
278 + if (searchName && !dish.name.toLowerCase().includes(searchName.toLowerCase())) {
279 + return false;
280 + }
281 +
282 + if (hasImage === 'yes' && !dish.image) return false;
283 + if (hasImage === 'no' && dish.image) return false;
284 +
285 + const price = dish.specs[0]?.price || 0;
286 + if (minPrice && price < parseFloat(minPrice)) return false;
287 + if (maxPrice && price > parseFloat(maxPrice)) return false;
288 +
289 + if (selectedCategory !== 'all' && dish.category !== selectedCategory) {
290 + return false;
291 + }
292 +
293 + return true;
294 + });
295 + }, [dishes, searchName, hasImage, minPrice, maxPrice, selectedCategory]);
296 +
297 + const stats = useMemo(() => {
298 + return {
299 + total: dishes.length,
300 + withImage: dishes.filter(d => d.image).length,
301 + filtered: filteredDishes.length
302 + };
303 + }, [dishes, filteredDishes]);
304 +
305 + const showToast = (message) => {
306 + setToast(message);
307 + setTimeout(() => setToast(null), 2000);
308 + };
309 +
310 + if (loading) {
311 + return (
312 + <div className="container">
313 + <div className="empty-state">
314 + <div className="empty-state-icon">⏳</div>
315 + <div>加载中...</div>
316 + </div>
317 + </div>
318 + );
319 + }
320 +
321 + return (
322 + <div className="container">
323 + {toast && <div className="toast">{toast}</div>}
324 + <div className="header">
325 + <h1>🍽️ 庐间菜品浏览器</h1>
326 +
327 + <div className="filters">
328 + <div className="filter-group">
329 + <label>菜品名称</label>
330 + <input
331 + type="text"
332 + placeholder="搜索菜品..."
333 + value={searchName}
334 + onChange={(e) => setSearchName(e.target.value)}
335 + />
336 + </div>
337 +
338 + <div className="filter-group">
339 + <label>分类</label>
340 + <select
341 + value={selectedCategory}
342 + onChange={(e) => setSelectedCategory(e.target.value)}
343 + >
344 + <option value="all">全部分类</option>
345 + {categories.map(cat => (
346 + <option key={cat} value={cat}>{cat}</option>
347 + ))}
348 + </select>
349 + </div>
350 +
351 + <div className="filter-group">
352 + <label>是否有图片</label>
353 + <select
354 + value={hasImage}
355 + onChange={(e) => setHasImage(e.target.value)}
356 + >
357 + <option value="all">全部</option>
358 + <option value="yes">有图片</option>
359 + <option value="no">无图片</option>
360 + </select>
361 + </div>
362 +
363 + <div className="filter-group">
364 + <label>最低价格</label>
365 + <input
366 + type="number"
367 + placeholder="¥0"
368 + value={minPrice}
369 + onChange={(e) => setMinPrice(e.target.value)}
370 + />
371 + </div>
372 +
373 + <div className="filter-group">
374 + <label>最高价格</label>
375 + <input
376 + type="number"
377 + placeholder="¥999"
378 + value={maxPrice}
379 + onChange={(e) => setMaxPrice(e.target.value)}
380 + />
381 + </div>
382 + </div>
383 +
384 + <div className="stats">
385 + <div className="stat-item">
386 + <span>总菜品:</span>
387 + <span className="stat-number">{stats.total}</span>
388 + </div>
389 + <div className="stat-item">
390 + <span>有图片:</span>
391 + <span className="stat-number">{stats.withImage}</span>
392 + </div>
393 + <div className="stat-item">
394 + <span>当前显示:</span>
395 + <span className="stat-number">{stats.filtered}</span>
396 + </div>
397 + </div>
398 + </div>
399 +
400 + {filteredDishes.length === 0 ? (
401 + <div className="empty-state">
402 + <div className="empty-state-icon">🔍</div>
403 + <div>没有找到符合条件的菜品</div>
404 + </div>
405 + ) : (
406 + <div className="dishes-grid">
407 + {filteredDishes.map(dish => (
408 + <DishCard key={dish.id} dish={dish} onCopy={showToast} />
409 + ))}
410 + </div>
411 + )}
412 + </div>
413 + );
414 + }
415 +
416 + function DishCard({ dish, onCopy }) {
417 + const imageUrl = dish.image
418 + ? `${REPO_BASE}/media/${dish.id}.${dish.image.endsWith('.jpg') || dish.image.endsWith('.jpeg') ? 'jpg' : 'png'}`
419 + : null;
420 +
421 + const price = dish.specs[0]?.price || 0;
422 +
423 + const handleImageClick = () => {
424 + const imageName = imageUrl ? imageUrl.split('/').pop() : `${dish.id}.png`;
425 + navigator.clipboard.writeText(imageName).then(() => {
426 + onCopy(`已复制: ${imageName}`);
427 + }).catch(() => {
428 + onCopy('复制失败');
429 + });
430 + };
431 +
432 + return (
433 + <div className="dish-card">
434 + {imageUrl ? (
435 + <img
436 + src={imageUrl}
437 + alt={dish.name}
438 + className="dish-image"
439 + onClick={handleImageClick}
440 + onError={(e) => {
441 + e.target.style.display = 'none';
442 + e.target.nextSibling.style.display = 'flex';
443 + }}
444 + />
445 + ) : null}
446 + <div
447 + className="no-image"
448 + style={{ display: imageUrl ? 'none' : 'flex' }}
449 + onClick={handleImageClick}
450 + >
451 + 暂无图片
452 + </div>
453 +
454 + <div className="dish-content">
455 + <div className="dish-header">
456 + <div className="dish-name">{dish.name}</div>
457 + <div className="dish-price">¥{price}</div>
458 + </div>
459 +
460 + <div className="dish-info">
461 + <span className="dish-category">{dish.category}</span>
462 + <span>{dish.unit}</span>
463 + </div>
464 +
465 + {dish.methods && dish.methods.length > 0 && (
466 + <div className="dish-methods">
467 + {dish.methods.map((method, idx) => (
468 + <span key={idx} className="method-tag">
469 + {method.name}
470 + </span>
471 + ))}
472 + </div>
473 + )}
474 + </div>
475 + </div>
476 + );
477 + }
478 +
479 + ReactDOM.render(<App />, document.getElementById('root'));
480 + </script>
481 + </body>
482 + </html>

liusijin revised this gist 2 weeks ago. Go to revision

1 file changed, 3 insertions

ljxg_dish.html(file created)

@@ -0,0 +1,3 @@
1 + <html>
2 + <body>hello</body>
3 + </html>
Newer Older