@ -0,0 +1,23 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
@ -0,0 +1,70 @@
|
||||
# Getting Started with Create React App
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm start`
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
|
||||
|
||||
The page will reload when you make changes.\
|
||||
You may also see any lint errors in the console.
|
||||
|
||||
### `npm test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.\
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `npm run eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
|
||||
|
||||
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
|
||||
|
||||
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
|
||||
### Code Splitting
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
|
||||
|
||||
### Analyzing the Bundle Size
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
|
||||
|
||||
### Making a Progressive Web App
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
|
||||
|
||||
### Deployment
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
|
||||
|
||||
### `npm run build` fails to minify
|
||||
|
||||
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
|
@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "flower-trading-system",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"antd": "^5.20.6",
|
||||
"axios": "^1.7.7",
|
||||
"classnames": "^2.5.1",
|
||||
"https-browserify": "^1.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"stream-http": "^3.2.0",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"plugins": [
|
||||
[
|
||||
"import",
|
||||
{
|
||||
"libraryName": "antd",
|
||||
"libraryDirectory": "es",
|
||||
"style": "css"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
After Width: | Height: | Size: 824 KiB |
@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>花卉交易系统</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>使用JavaScript运行该系统</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
After Width: | Height: | Size: 5.2 KiB |
After Width: | Height: | Size: 9.4 KiB |
@ -0,0 +1,25 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
@ -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);
|
||||
}
|
||||
}
|
@ -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 (
|
||||
<ConfigProvider theme={theme}>
|
||||
<Router>
|
||||
<Layout className="layout" style={{ minHeight: '100vh' }}>
|
||||
<Header style={{ background: theme.token.colorPrimary }}>
|
||||
<div className="logo" />
|
||||
<Menu mode="horizontal" defaultSelectedKeys={['1']} style={{ background: '#fff', borderBottom: 'none' }}>
|
||||
<Menu.Item key="1" icon={<HomeOutlined />}><Link to="/">主页面</Link></Menu.Item>
|
||||
<Menu.Item key="2" icon={<SearchOutlined />}><Link to="/search">搜索</Link></Menu.Item>
|
||||
<Menu.Item key="3" icon={<OrderedListOutlined />}><Link to="/orders">我的订单</Link></Menu.Item>
|
||||
<Menu.Item key="5" icon={<UploadOutlined />}><Link to="/upload">上传花卉</Link></Menu.Item>
|
||||
<Menu.Item key="4" icon={<ScanOutlined />}><Link to="/scanner">识别花卉</Link></Menu.Item>
|
||||
</Menu>
|
||||
</Header>
|
||||
<Content style={{ padding: '0 50px', marginTop: 64 }}>
|
||||
<div className="site-layout-content" style={{ background: '#fff', padding: 24, minHeight: 380 }}>
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/search" element={<SearchPage />} />
|
||||
<Route path="/product/:id" element={<ProductDetailPage />} />
|
||||
<Route path="/order/:id" element={<OrderPage />} />
|
||||
<Route path="/orders" element={<OrderPage />} />
|
||||
<Route path="/scanner" element={<ScannerPage />} />
|
||||
<Route path="/upload" element={<UploadFlowerPage />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</Content>
|
||||
<Footer style={{ textAlign: 'center', background: theme.token.colorBgBase }}>
|
||||
Flower Trading System ©2024
|
||||
</Footer>
|
||||
</Layout>
|
||||
</Router>
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
@ -0,0 +1,8 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
@ -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 (
|
||||
<Modal
|
||||
title={`Edit ${product.name}`}
|
||||
visible={visible}
|
||||
onCancel={onCancel}
|
||||
footer={null}
|
||||
>
|
||||
<div className="p-4">
|
||||
<p>Name: <Input value={editedProduct.name} onChange={(e) => handleInputChange('name', e.target.value)} /></p>
|
||||
<p>Price: $<InputNumber
|
||||
value={editedProduct.price}
|
||||
onChange={(value) => handleInputChange('price', value)}
|
||||
min={0}
|
||||
step={0.01}
|
||||
/></p>
|
||||
<p>Available: <InputNumber
|
||||
value={editedProduct.quantity}
|
||||
onChange={(value) => handleInputChange('quantity', value)}
|
||||
min={0}
|
||||
/></p>
|
||||
<p>Description: <Input.TextArea
|
||||
value={editedProduct.description}
|
||||
onChange={(e) => handleInputChange('description', e.target.value)}
|
||||
/></p>
|
||||
<div className="flex justify-end mt-4">
|
||||
<Button onClick={onCancel} className="mr-2">取消</Button>
|
||||
<Button type="primary" onClick={handleUpdate}>完成</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditProductModal;
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
After Width: | Height: | Size: 175 KiB |
After Width: | Height: | Size: 644 KiB |
After Width: | Height: | Size: 20 KiB |
@ -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;
|
||||
}
|
@ -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(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
After Width: | Height: | Size: 2.6 KiB |
@ -0,0 +1,183 @@
|
||||
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('取消订单失败:', error);
|
||||
message.error('取消订单失败');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Tabs defaultActiveKey="1">
|
||||
<TabPane tab="我的上架" key="1">
|
||||
<List
|
||||
grid={{ gutter: 16, column: 3 }}
|
||||
dataSource={products}
|
||||
renderItem={product => (
|
||||
<List.Item>
|
||||
<Card
|
||||
title={product.name}
|
||||
extra={<Button onClick={() => handleEditProduct(product)}>编辑</Button>}
|
||||
actions={[
|
||||
<Button onClick={() => handleToggleProductStatus(product.id)}>
|
||||
{product.is_active ? '下架' : '上架'}
|
||||
</Button>
|
||||
]}
|
||||
>
|
||||
<p>价格: ¥{product.price}</p>
|
||||
<p>数量: {product.quantity}</p>
|
||||
<p>状态: {product.is_active ? '上架' : '未上架'}</p>
|
||||
</Card>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane tab="我的订单" key="2">
|
||||
<List
|
||||
dataSource={orders}
|
||||
renderItem={order => (
|
||||
<List.Item>
|
||||
<Card
|
||||
title={`订单 #${order.id} - ${order.flowerName}`}
|
||||
extra={
|
||||
order.status === 'pending' ?
|
||||
<Button onClick={() => handleCancelOrder(order.id)}>取消订单</Button> :
|
||||
null
|
||||
}
|
||||
>
|
||||
<p>数量: {order.quantity}</p>
|
||||
<p>总价: ¥{order.totalPrice}</p>
|
||||
<p>状态: {order.status}</p>
|
||||
<p>订单日期: {new Date(order.createdAt).toLocaleString()}</p>
|
||||
</Card>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
|
||||
<Modal
|
||||
title="编辑花卉信息"
|
||||
visible={isModalVisible}
|
||||
onCancel={() => {
|
||||
setIsModalVisible(false);
|
||||
setEditingProduct(null);
|
||||
form.resetFields();
|
||||
}}
|
||||
footer={null}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={handleUpdateProduct}
|
||||
layout="vertical"
|
||||
>
|
||||
<Form.Item name="name" label="花名" rules={[{ required: true }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="price" label="单价" rules={[{ required: true }]}>
|
||||
<InputNumber min={0} step={0.01} />
|
||||
</Form.Item>
|
||||
<Form.Item name="quantity" label="数量" rules={[{ required: true }]}>
|
||||
<InputNumber min={0} />
|
||||
</Form.Item>
|
||||
<Form.Item name="description" label="描述">
|
||||
<Input.TextArea />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Update Product
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrderPage;
|
@ -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 (
|
||||
<div>
|
||||
<h2>{product.name}</h2>
|
||||
<p>Price: ${product.price}</p>
|
||||
<p>{product.description}</p>
|
||||
<button onClick={handleOrder}>Place Order</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ProductDetailPage;
|
@ -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 (
|
||||
<Space direction="vertical" size="large" style={{ display: 'flex' }}>
|
||||
<Title level={2}>花卉识别</Title>
|
||||
<Text>上传一张花卉图片以供识别:</Text>
|
||||
<Upload
|
||||
name="image"
|
||||
action="http://localhost:5000/api/identify"
|
||||
onChange={handleUpload}
|
||||
accept="image/*"
|
||||
showUploadList={false}
|
||||
>
|
||||
<Button icon={<UploadOutlined />} loading={scanning}>
|
||||
{scanning ? '识别中' : '上传图片'}
|
||||
</Button>
|
||||
</Upload>
|
||||
|
||||
{result && (
|
||||
<Card title="识别结果:" style={{ width: 300 }}>
|
||||
{result.error ? (
|
||||
<Text type="danger">{result.error}</Text>
|
||||
) : (
|
||||
<>
|
||||
<img src={imageUrl} alt="花卉识别图片" width={250} height={200}></img>
|
||||
<Text strong>花名: </Text>
|
||||
<Text>{result.name}</Text>
|
||||
<br />
|
||||
{result.probability && (
|
||||
<>
|
||||
<Text strong>识别正确概率: </Text>
|
||||
<Text>{(result.probability * 100).toFixed(2)}%</Text>
|
||||
<br />
|
||||
</>
|
||||
)}
|
||||
{result.description && (
|
||||
<>
|
||||
<Text strong>描述: </Text>
|
||||
<Text>{result.description}</Text>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
</Space>
|
||||
);
|
||||
}
|
@ -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 (
|
||||
<div>
|
||||
<h1>上传花卉</h1>
|
||||
<Form form={form} layout="vertical" onFinish={onFinish}>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="花名"
|
||||
rules={[{ required: true, message: '请输入花名!' }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="description" label="描述">
|
||||
<Input.TextArea />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="price"
|
||||
label="单价"
|
||||
rules={[
|
||||
{ required: true, message: '请输入价格!' },
|
||||
{ type: 'number', min: 0, message: '价格需要是有效数字!' }
|
||||
]}
|
||||
>
|
||||
<InputNumber style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="quantity"
|
||||
label="数量"
|
||||
rules={[
|
||||
{ required: true, message: '请输入花卉数量!' },
|
||||
{ type: 'number', min: 1, message: '数量应该至少为1!' }
|
||||
]}
|
||||
>
|
||||
<InputNumber style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item label="图片">
|
||||
<Upload
|
||||
customRequest={handleImageUpload}
|
||||
listType="picture"
|
||||
maxCount={1}
|
||||
beforeUpload={(file) => {
|
||||
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
|
||||
if (!isJpgOrPng) {
|
||||
message.error('只能上传JPG/PNG文件!');
|
||||
}
|
||||
return isJpgOrPng;
|
||||
}}
|
||||
>
|
||||
<Button icon={<UploadOutlined />}>上传图片</Button>
|
||||
</Upload>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
上传花卉
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UploadFlowerPage;
|
@ -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;
|
@ -0,0 +1,12 @@
|
||||
export const theme = {
|
||||
token: {
|
||||
colorPrimary: '#FF69B4', // 热粉红色
|
||||
colorLink: '#FF1493', // 深粉红色
|
||||
colorSuccess: '#98FB98', // 淡绿色,与粉色搭配
|
||||
colorWarning: '#FFB6C1', // 浅粉红色
|
||||
colorError: '#FF69B4', // 热粉红色
|
||||
colorInfo: '#FFC0CB', // 粉红色
|
||||
colorTextBase: '#4B0082', // 靛青色,作为主要文字颜色
|
||||
colorBgBase: '#FFF0F5', // 浅粉红色,作为背景色
|
||||
},
|
||||
};
|