parent
377d1b03fa
commit
d901407d10
@ -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,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 (
|
||||||
|
<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">
|
||||||
|
更新花卉
|
||||||
|
</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>单价 ¥{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', // 浅粉红色,作为背景色
|
||||||
|
},
|
||||||
|
};
|
Loading…
Reference in new issue