diff --git a/src/App.css b/src/App.css new file mode 100644 index 0000000..74b5e05 --- /dev/null +++ b/src/App.css @@ -0,0 +1,38 @@ +.App { + text-align: center; +} + +.App-logo { + height: 40vmin; + pointer-events: none; +} + +@media (prefers-reduced-motion: no-preference) { + .App-logo { + animation: App-logo-spin infinite 20s linear; + } +} + +.App-header { + background-color: #282c34; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-size: calc(10px + 2vmin); + color: white; +} + +.App-link { + color: #61dafb; +} + +@keyframes App-logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} diff --git a/src/App.js b/src/App.js new file mode 100644 index 0000000..1ff5877 --- /dev/null +++ b/src/App.js @@ -0,0 +1,52 @@ +import React from 'react'; +import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom'; +import { Layout, Menu, ConfigProvider } from 'antd'; +import { HomeOutlined, SearchOutlined, OrderedListOutlined, ScanOutlined, UploadOutlined } from '@ant-design/icons'; +import HomePage from './pages/HomePage'; +import SearchPage from './pages/SearchPage'; +import ProductDetailPage from './pages/ProductDetailPage'; +import OrderPage from './pages/OrderPage'; +import ScannerPage from './pages/ScannerPage'; +import UploadFlowerPage from './pages/UploadFlowerPage'; // 新增 +import { theme } from './theme'; + +const { Header, Content, Footer } = Layout; + +function App() { + return ( + + + +
+
+ + }>主页面 + }>搜索 + }>我的订单 + }>上传花卉 + }>识别花卉 + +
+ +
+ + } /> + } /> + } /> + } /> + } /> + } /> + } /> + +
+
+ +
+
+
+ ); +} + +export default App; \ No newline at end of file diff --git a/src/App.test.js b/src/App.test.js new file mode 100644 index 0000000..1f03afe --- /dev/null +++ b/src/App.test.js @@ -0,0 +1,8 @@ +import { render, screen } from '@testing-library/react'; +import App from './App'; + +test('renders learn react link', () => { + render(); + const linkElement = screen.getByText(/learn react/i); + expect(linkElement).toBeInTheDocument(); +}); diff --git a/src/Edit Product Modal.js b/src/Edit Product Modal.js new file mode 100644 index 0000000..c7560a5 --- /dev/null +++ b/src/Edit Product Modal.js @@ -0,0 +1,60 @@ +import React, { useState } from 'react'; +import { Modal, Input, InputNumber, Button, message } from 'antd'; +import axios from 'axios'; + +const EditProductModal = ({ visible, product, onCancel, onUpdate }) => { + const [editedProduct, setEditedProduct] = useState(product); + + const handleInputChange = (field, value) => { + setEditedProduct({ ...editedProduct, [field]: value }); + }; + + const handleUpdate = async () => { + try { + const response = await axios.put(`http://localhost:5000/api/products/${product.id}`, editedProduct); + if (response.data.message === "Product updated successfully") { + message.success('Product updated successfully'); + onUpdate(editedProduct); + } else { + throw new Error('Update failed'); + } + } catch (error) { + console.error('Error updating product:', error); + message.error('Failed to update product: ' + (error.response?.data?.error || error.message)); + } + }; + + return ( + +
+

Name: handleInputChange('name', e.target.value)} />

+

Price: $ handleInputChange('price', value)} + min={0} + step={0.01} + />

+

Available: handleInputChange('quantity', value)} + min={0} + />

+

Description: handleInputChange('description', e.target.value)} + />

+
+ + +
+
+
+ ); +}; + +export default EditProductModal; \ No newline at end of file diff --git a/src/api.js b/src/api.js new file mode 100644 index 0000000..1ba4019 --- /dev/null +++ b/src/api.js @@ -0,0 +1,27 @@ +import axios from 'axios'; + +const API_URL = 'http://localhost:5000/api'; + +export const flowerApi = { + async getAllFlowers(query = '') { + try { + const response = await axios.get(`${API_URL}/flowers`, { + params: { query } + }); + return response.data; + } catch (error) { + console.error('Error fetching flowers:', error); + throw error; + } + }, + + async addFlower(flowerData) { + try { + const response = await axios.post(`${API_URL}/flowers`, flowerData); + return response.data; + } catch (error) { + console.error('Error adding flower:', error); + throw error; + } + } +}; \ No newline at end of file diff --git a/src/images/orchids.jpg b/src/images/orchids.jpg new file mode 100644 index 0000000..3dee223 Binary files /dev/null and b/src/images/orchids.jpg differ diff --git a/src/images/rose.jpg b/src/images/rose.jpg new file mode 100644 index 0000000..86862f9 Binary files /dev/null and b/src/images/rose.jpg differ diff --git a/src/images/tulips.jpg b/src/images/tulips.jpg new file mode 100644 index 0000000..951bdfb Binary files /dev/null and b/src/images/tulips.jpg differ diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..2ad5247 --- /dev/null +++ b/src/index.css @@ -0,0 +1,51 @@ +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} + +.App { + text-align: center; +} + +nav ul { + list-style-type: none; + padding: 0; +} + +nav ul li { + display: inline; + margin: 0 10px; +} + +nav ul li a { + text-decoration: none; + color: #333; +} + +button { + background-color: #4CAF50; + border: none; + color: white; + padding: 15px 32px; + text-align: center; + text-decoration: none; + display: inline-block; + font-size: 16px; + margin: 4px 2px; + cursor: pointer; +} + +input[type="text"] { + padding: 10px; + margin: 10px 0; + width: 300px; +} \ No newline at end of file diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..1675893 --- /dev/null +++ b/src/index.js @@ -0,0 +1,11 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import './index.css'; +import App from './App'; + +const root = ReactDOM.createRoot(document.getElementById('root')); +root.render( + + + +); \ No newline at end of file diff --git a/src/logo.svg b/src/logo.svg new file mode 100644 index 0000000..9dfc1c0 --- /dev/null +++ b/src/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/pages/HomePage.js b/src/pages/HomePage.js new file mode 100644 index 0000000..b377c64 --- /dev/null +++ b/src/pages/HomePage.js @@ -0,0 +1,115 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { Typography, Button, Space, Card, Row, Col, Carousel } from 'antd'; +import { SearchOutlined, ScanOutlined, ShoppingOutlined } from '@ant-design/icons'; +import { theme } from '../theme'; + +// 导入图片 +import rosesImage from '../images/rose.jpg'; +import tulipsImage from '../images/tulips.jpg'; +import orchidsImage from '../images/orchids.jpg'; + +const { Title, Paragraph } = Typography; + +// 样式定义 +const styles = { + page: { + padding: '20px', + background: theme.token.colorBgBase, + minHeight: '100vh', + }, + carousel: { + height: '300px', + lineHeight: '300px', + textAlign: 'center', + background: theme.token.colorPrimary, + color: '#fff', + }, + card: { + height: '100%', + display: 'flex', + flexDirection: 'column', + }, + imageContainer: { + height: '200px', + overflow: 'hidden', + }, + image: { + width: '100%', + height: '100%', + objectFit: 'cover', + }, + footer: { + marginTop: '20px', + textAlign: 'center', + }, +}; + +function HomePage() { + return ( +
+ +
+

美丽的玫瑰

+
+
+

优雅的郁金香

+
+
+

异国情调的兰花

+
+
+ + + 欢迎来到花卉交易系统 + + 探索我们美丽的花朵并开始交易!我们提供适合各种场合的各种鲜花。 + + + + + + + + + + +
+ 玫瑰 +
+ +
+ + + +
+ 郁金香 +
+ +
+ + + +
+ 兰花 +
+ +
+ +
+ + + 今天就开始您的花卉之旅吧! + + +
+ ); +} + +export default HomePage; \ No newline at end of file diff --git a/src/pages/OrderPage.js b/src/pages/OrderPage.js new file mode 100644 index 0000000..663adc1 --- /dev/null +++ b/src/pages/OrderPage.js @@ -0,0 +1,182 @@ +import React, { useState, useEffect } from 'react'; +import { Tabs, List, Card, Button, message, Modal, Form, Input, InputNumber } from 'antd'; +import axios from 'axios'; + +const { TabPane } = Tabs; + +const OrderPage = () => { + const [products, setProducts] = useState([]); + const [orders, setOrders] = useState([]); + const [editingProduct, setEditingProduct] = useState(null); + const [isModalVisible, setIsModalVisible] = useState(false); + const [form] = Form.useForm(); + + useEffect(() => { + fetchProducts(); + fetchOrders(); + }, []); + + const fetchProducts = async () => { + try { + const response = await axios.get('http://localhost:5000/api/my-products'); + setProducts(response.data); + } catch (error) { + console.error('获取花卉信息失败:', error); + message.error('获取花卉信息失败'); + } + }; + + const fetchOrders = async () => { + try { + const response = await axios.get('http://localhost:5000/api/orders'); + setOrders(response.data); + } catch (error) { + console.error('获取订单信息失败:', error); + message.error('获取订单信息失败'); + } + }; + + const handleEditProduct = (product) => { + console.log('Edit button clicked for product:', product); + setEditingProduct(product); + form.setFieldsValue(product); + setIsModalVisible(true); + }; + + const handleUpdateProduct = async (values) => { + try { + const response = await axios.put(`http://localhost:5000/api/products/${editingProduct.id}`, values); + if (response.data.message === "Product updated successfully") { + message.success('Product updated successfully'); + setIsModalVisible(false); + setEditingProduct(null); + form.resetFields(); + fetchProducts(); // Refresh the product list + } else { + throw new Error('Update failed'); + } + } catch (error) { + console.error('Error updating product:', error); + message.error('Failed to update product: ' + (error.response?.data?.error || error.message)); + } + }; + + const handleToggleProductStatus = async (productId) => { + try { + const response = await axios.post(`http://localhost:5000/api/products/${productId}/toggle-status`); + if (response.data.message.includes('successfully')) { + message.success(response.data.message); + fetchProducts(); + } else { + throw new Error('Toggle status failed'); + } + } catch (error) { + console.error('Error toggling product status:', error); + message.error('Failed to update product status'); + } + }; + + const handleCancelOrder = async (orderId) => { + try { + const response = await axios.post(`http://localhost:5000/api/orders/${orderId}/cancel`); + if (response.data.message === "Order cancelled successfully") { + message.success('成功取消订单'); + fetchOrders(); + } else { + throw new Error('取消订单失败'); + } + } catch (error) { + console.error('Failed to cancel order:', error); + message.error('取消订单失败'); + } + }; + + return ( +
+ + + ( + + handleEditProduct(product)}>编辑} + actions={[ + + ]} + > +

价格: ¥{product.price}

+

数量: {product.quantity}

+

状态: {product.is_active ? '上架' : '未上架'}

+
+
+ )} + /> +
+ + ( + + handleCancelOrder(order.id)}>取消订单 : + null + } + > +

数量: {order.quantity}

+

总价: ¥{order.totalPrice}

+

状态: {order.status}

+

订单日期: {new Date(order.createdAt).toLocaleString()}

+
+
+ )} + /> +
+
+ + { + setIsModalVisible(false); + setEditingProduct(null); + form.resetFields(); + }} + footer={null} + > +
+ + + + + + + + + + + + + + + +
+
+
+ ); +}; + +export default OrderPage; \ No newline at end of file diff --git a/src/pages/ProductDetailPage.js b/src/pages/ProductDetailPage.js new file mode 100644 index 0000000..6e9d4a4 --- /dev/null +++ b/src/pages/ProductDetailPage.js @@ -0,0 +1,27 @@ +import React from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; + +function ProductDetailPage() { + const { id } = useParams(); + const navigate = useNavigate(); + + // 模拟产品数据 + const product = { id, name: 'Sample Flower', price: 10, description: 'A beautiful flower' }; + + const handleOrder = () => { + // 模拟下单过程 + const orderId = Math.floor(Math.random() * 1000); + navigate(`/order/${orderId}`); + }; + + return ( +
+

{product.name}

+

单价 ¥{product.price}

+

{product.description}

+ +
+ ); +} + +export default ProductDetailPage; \ No newline at end of file diff --git a/src/pages/ScannerPage.js b/src/pages/ScannerPage.js new file mode 100644 index 0000000..ae4774a --- /dev/null +++ b/src/pages/ScannerPage.js @@ -0,0 +1,87 @@ +import React, { useState } from 'react'; +import { Upload, Button, Card, Typography, Space } from 'antd'; +import { UploadOutlined } from '@ant-design/icons'; +import axios from 'axios'; + +const { Title, Text } = Typography; + +export default function FlowerScanner() { + const [imageUrl, setImageUrl] = useState(''); + const [scanning, setScanning] = useState(false); + const [result, setResult] = useState(null); + + const handleUpload = async (info) => { + const { status } = info.file; + + if (status === 'done') { + setScanning(true); + const formData = new FormData(); + formData.append('image', info.file.originFileObj); + + try { + const response = await axios.post('http://localhost:5000/api/identify', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + + const url = URL.createObjectURL(info.file.originFileObj); + setImageUrl(url); + setScanning(false); + setResult(response.data); + } catch (error) { + console.error('识别期间出现问题:', error); + setScanning(false); + setResult({ error: '识别失败,请重新尝试' }); + } + } else if (status === 'error') { + setResult({ error: `${info.file.name} file upload failed.` }); + } + }; + + return ( + + 花卉识别 + 上传一张花卉图片以供识别: + + + + + {result && ( + + {result.error ? ( + {result.error} + ) : ( + <> + 花卉识别图片 + 花名: + {result.name} +
+ {result.probability && ( + <> + 识别正确概率: + {(result.probability * 100).toFixed(2)}% +
+ + )} + {result.description && ( + <> + 描述: + {result.description} + + )} + + )} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage.js new file mode 100644 index 0000000..8c0096e --- /dev/null +++ b/src/pages/SearchPage.js @@ -0,0 +1,131 @@ +import React, { useState, useEffect } from 'react'; +import { Link } from 'react-router-dom'; +import { Input, Button, List, Card, message, Tag, Modal, InputNumber } from 'antd'; +import axios from 'axios'; + +const { Search } = Input; + +function SearchPage() { + const [flowers, setFlowers] = useState([]); + const [filteredFlowers, setFilteredFlowers] = useState([]); + const [loading, setLoading] = useState(false); + const [modalVisible, setModalVisible] = useState(false); + const [selectedFlower, setSelectedFlower] = useState(null); + const [purchaseQuantity, setPurchaseQuantity] = useState(1); + + useEffect(() => { + fetchFlowers(); + }, []); + + const fetchFlowers = async () => { + setLoading(true); + try { + const response = await axios.get('http://localhost:5000/api/flowers'); + const availableFlowers = response.data.filter(flower => flower.quantity > 0); + setFlowers(availableFlowers); + setFilteredFlowers(availableFlowers); + } catch (error) { + console.error('Error fetching flowers:', error); + message.error('Failed to load flowers. Please try again later.'); + } + setLoading(false); + }; + + const handleSearch = (value) => { + const filtered = flowers.filter(flower => + flower.name.toLowerCase().includes(value.toLowerCase()) + ); + setFilteredFlowers(filtered); + }; + + const showPurchaseModal = (flower) => { + setSelectedFlower(flower); + setPurchaseQuantity(1); + setModalVisible(true); + }; + + const handlePurchase = async () => { + try { + await axios.post('http://localhost:5000/api/orders', { + flowerId: selectedFlower.id, + quantity: purchaseQuantity + }); + message.success('下单成功!'); + setModalVisible(false); + fetchFlowers(); // Refresh the flower list to update quantities + } catch (error) { + console.error('Error placing order:', error); + message.error('Failed to place order. Please try again.'); + } + }; + + return ( +
+

花卉市场

+ + ( + + : null} + actions={[ + + ]} + > + {flower.name}} + description={ + <> +

¥{flower.price.toFixed(2)}

+

{flower.description}

+ 剩余(支): {flower.quantity} + + } + /> +
+
+ )} + /> + setModalVisible(false)} + > +

价格: ¥{selectedFlower?.price.toFixed(2)}

+

是否有余: {selectedFlower?.quantity}支

+

+ 数量(支): + setPurchaseQuantity(value)} + /> +

+

总价: ¥{((selectedFlower?.price || 0) * purchaseQuantity).toFixed(2)}

+
+
+ ); +} + +export default SearchPage; \ No newline at end of file diff --git a/src/pages/UploadFlowerPage.js b/src/pages/UploadFlowerPage.js new file mode 100644 index 0000000..2e325af --- /dev/null +++ b/src/pages/UploadFlowerPage.js @@ -0,0 +1,108 @@ +import React, { useState } from 'react'; +import { Form, Input, InputNumber, Button, message, Upload } from 'antd'; +import { UploadOutlined } from '@ant-design/icons'; +import { useNavigate } from 'react-router-dom'; +import axios from 'axios'; + +const UploadFlowerPage = () => { + const [form] = Form.useForm(); + const [imageFile, setImageFile] = useState(null); + const navigate = useNavigate(); + + const onFinish = async (values) => { + try { + const formData = new FormData(); + formData.append('name', values.name.trim()); + formData.append('description', (values.description || '').trim()); + formData.append('price', values.price.toString()); + formData.append('quantity', values.quantity.toString()); + if (imageFile) { + formData.append('image', imageFile); + } + + const response = await axios.post('http://localhost:5000/api/flowers', formData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }); + + message.success('成功上传花卉!'); + form.resetFields(); + setImageFile(null); + navigate('/search'); + } catch (error) { + console.error('上传花卉时出现错误:', error); + if (error.response && error.response.data && error.response.data.error) { + message.error(`上传花卉失败: ¥{error.response.data.error}`); + } else { + message.error('上传失败,请重新上传'); + } + } + }; + + const handleImageUpload = ({ file, onSuccess }) => { + setImageFile(file); + onSuccess("好"); + }; + + return ( +
+

上传花卉

+
+ + + + + + + + + + + + + + { + const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png'; + if (!isJpgOrPng) { + message.error('只能上传JPG/PNG文件!'); + } + return isJpgOrPng; + }} + > + + + + + + +
+
+ ); +}; + +export default UploadFlowerPage; \ No newline at end of file diff --git a/src/reportWebVitals.js b/src/reportWebVitals.js new file mode 100644 index 0000000..5253d3a --- /dev/null +++ b/src/reportWebVitals.js @@ -0,0 +1,13 @@ +const reportWebVitals = onPerfEntry => { + if (onPerfEntry && onPerfEntry instanceof Function) { + import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { + getCLS(onPerfEntry); + getFID(onPerfEntry); + getFCP(onPerfEntry); + getLCP(onPerfEntry); + getTTFB(onPerfEntry); + }); + } +}; + +export default reportWebVitals; diff --git a/src/theme.js b/src/theme.js new file mode 100644 index 0000000..27cd01c --- /dev/null +++ b/src/theme.js @@ -0,0 +1,12 @@ +export const theme = { + token: { + colorPrimary: '#FF69B4', // 热粉红色 + colorLink: '#FF1493', // 深粉红色 + colorSuccess: '#98FB98', // 淡绿色,与粉色搭配 + colorWarning: '#FFB6C1', // 浅粉红色 + colorError: '#FF69B4', // 热粉红色 + colorInfo: '#FFC0CB', // 粉红色 + colorTextBase: '#4B0082', // 靛青色,作为主要文字颜色 + colorBgBase: '#FFF0F5', // 浅粉红色,作为背景色 + }, + }; \ No newline at end of file