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 (
+
+
+
+
美丽的玫瑰
+
+
+
优雅的郁金香
+
+
+
异国情调的兰花
+
+
+
+
+ 欢迎来到花卉交易系统
+
+ 探索我们美丽的花朵并开始交易!我们提供适合各种场合的各种鲜花。
+
+
+ } size="large">
+ 搜索花卉
+
+ } size="large" style={{ background: theme.token.colorInfo, borderColor: theme.token.colorInfo }}>
+ 识别花卉
+
+
+
+
+
+
+
+
+
![玫瑰]({rosesImage})
+
+
+
+
+
+
+
+
![郁金香]({tulipsImage})
+
+
+
+
+
+
+
+
![兰花]({orchidsImage})
+
+
+
+
+
+
+
+ 今天就开始您的花卉之旅吧!
+ } size="large">
+ 开始购物
+
+
+
+ );
+}
+
+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 (
+
+ 花卉识别
+ 上传一张花卉图片以供识别:
+
+ } loading={scanning}>
+ {scanning ? '识别中' : '上传图片'}
+
+
+
+ {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