first commit
This commit is contained in:
commit
2934405992
25 changed files with 4804 additions and 0 deletions
BIN
.DS_Store
vendored
Normal file
BIN
.DS_Store
vendored
Normal file
Binary file not shown.
15
backend_service/app.py
Normal file
15
backend_service/app.py
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import json
|
||||||
|
from flask import Flask, jsonify
|
||||||
|
from flask_cors import CORS
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
CORS(app)
|
||||||
|
|
||||||
|
with open('config.json', mode='r') as f:
|
||||||
|
datas = json.loads(f.read())
|
||||||
|
|
||||||
|
@app.route('/list')
|
||||||
|
def list():
|
||||||
|
return jsonify(datas)
|
||||||
|
|
||||||
|
app.run('0.0.0.0', port=80)
|
||||||
44
backend_service/config.json
Normal file
44
backend_service/config.json
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"last_online": 1764740658,
|
||||||
|
"peer_id": 1,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"name": "tag1",
|
||||||
|
"price": 1,
|
||||||
|
"sale": 11
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tag2",
|
||||||
|
"price": 2,
|
||||||
|
"sale": 12
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tag3",
|
||||||
|
"price": 3,
|
||||||
|
"sale": 13
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"last_online": 1764750653,
|
||||||
|
"peer_id": 2,
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"name": "tag4",
|
||||||
|
"price": 4,
|
||||||
|
"sale": 14
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tag5",
|
||||||
|
"price": 5,
|
||||||
|
"sale": 15
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tag6",
|
||||||
|
"price": 6,
|
||||||
|
"sale": 16
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
9
backend_service/dockerfile
Normal file
9
backend_service/dockerfile
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
FROM python:3.9
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN pip3 install flask flask-cors
|
||||||
|
|
||||||
|
CMD ["python3", "app.py"]
|
||||||
44
bitmap_service/app.py
Normal file
44
bitmap_service/app.py
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
from flask import Flask, request, jsonify
|
||||||
|
from flask_cors import CORS
|
||||||
|
from PIL import Image
|
||||||
|
import requests
|
||||||
|
import io
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
CORS(app)
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def root():
|
||||||
|
return 'ok'
|
||||||
|
|
||||||
|
@app.route('/api/bitmap', methods=['POST'])
|
||||||
|
def bitmap():
|
||||||
|
|
||||||
|
if 'file' not in request.files:
|
||||||
|
return jsonify({"error": "no file"}), 400
|
||||||
|
|
||||||
|
file = request.files['file']
|
||||||
|
|
||||||
|
if file.filename == '':
|
||||||
|
return jsonify({"error": "no file"}), 400
|
||||||
|
|
||||||
|
if not file.filename.lower().endswith('.png'):
|
||||||
|
return jsonify({"error": "invalid file format"}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
image_stream = io.BytesIO(file.read())
|
||||||
|
img = Image.open(image_stream)
|
||||||
|
grayscale_img = img.convert('L')
|
||||||
|
bitmap_data = list(grayscale_img.getdata())
|
||||||
|
|
||||||
|
payload = ''.join([chr(i) for i in bitmap_data])
|
||||||
|
print(payload)
|
||||||
|
req = requests.post('http://10.141.142.75/api/tag', data=payload)
|
||||||
|
|
||||||
|
return jsonify({'code': req.status_code})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": e}), 500
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run('0.0.0.0', port=80)
|
||||||
9
bitmap_service/dockerfile
Normal file
9
bitmap_service/dockerfile
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
FROM python:3.9
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN pip3 install pillow flask flask-cors requests
|
||||||
|
|
||||||
|
CMD ["python3", "app.py"]
|
||||||
16
docker-compose.yml
Normal file
16
docker-compose.yml
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
services:
|
||||||
|
frontend_service:
|
||||||
|
build: ./frontend_service
|
||||||
|
container_name: frontend_service
|
||||||
|
ports:
|
||||||
|
- "12000:3000"
|
||||||
|
backend_service:
|
||||||
|
build: ./backend_service
|
||||||
|
container_name: backend_service
|
||||||
|
ports:
|
||||||
|
- "12001:80"
|
||||||
|
bitmap_service:
|
||||||
|
build: ./bitmap_service
|
||||||
|
container_name: bitmap_service
|
||||||
|
ports:
|
||||||
|
- "12002:80"
|
||||||
4
frontend_service/.dockerignore
Normal file
4
frontend_service/.dockerignore
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
.react-router
|
||||||
|
build
|
||||||
|
node_modules
|
||||||
|
README.md
|
||||||
7
frontend_service/.gitignore
vendored
Normal file
7
frontend_service/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
.DS_Store
|
||||||
|
.env
|
||||||
|
/node_modules/
|
||||||
|
|
||||||
|
# React Router
|
||||||
|
/.react-router/
|
||||||
|
/build/
|
||||||
22
frontend_service/Dockerfile
Normal file
22
frontend_service/Dockerfile
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
FROM node:20-alpine AS development-dependencies-env
|
||||||
|
COPY . /app
|
||||||
|
WORKDIR /app
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
FROM node:20-alpine AS production-dependencies-env
|
||||||
|
COPY ./package.json package-lock.json /app/
|
||||||
|
WORKDIR /app
|
||||||
|
RUN npm ci --omit=dev
|
||||||
|
|
||||||
|
FROM node:20-alpine AS build-env
|
||||||
|
COPY . /app/
|
||||||
|
COPY --from=development-dependencies-env /app/node_modules /app/node_modules
|
||||||
|
WORKDIR /app
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM node:20-alpine
|
||||||
|
COPY ./package.json package-lock.json /app/
|
||||||
|
COPY --from=production-dependencies-env /app/node_modules /app/node_modules
|
||||||
|
COPY --from=build-env /app/build /app/build
|
||||||
|
WORKDIR /app
|
||||||
|
CMD ["npm", "run", "start"]
|
||||||
87
frontend_service/README.md
Normal file
87
frontend_service/README.md
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
# Welcome to React Router!
|
||||||
|
|
||||||
|
A modern, production-ready template for building full-stack React applications using React Router.
|
||||||
|
|
||||||
|
[](https://stackblitz.com/github/remix-run/react-router-templates/tree/main/default)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 🚀 Server-side rendering
|
||||||
|
- ⚡️ Hot Module Replacement (HMR)
|
||||||
|
- 📦 Asset bundling and optimization
|
||||||
|
- 🔄 Data loading and mutations
|
||||||
|
- 🔒 TypeScript by default
|
||||||
|
- 🎉 TailwindCSS for styling
|
||||||
|
- 📖 [React Router docs](https://reactrouter.com/)
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
Install the dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
Start the development server with HMR:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Your application will be available at `http://localhost:5173`.
|
||||||
|
|
||||||
|
## Building for Production
|
||||||
|
|
||||||
|
Create a production build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
### Docker Deployment
|
||||||
|
|
||||||
|
To build and run using Docker:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t my-app .
|
||||||
|
|
||||||
|
# Run the container
|
||||||
|
docker run -p 3000:3000 my-app
|
||||||
|
```
|
||||||
|
|
||||||
|
The containerized application can be deployed to any platform that supports Docker, including:
|
||||||
|
|
||||||
|
- AWS ECS
|
||||||
|
- Google Cloud Run
|
||||||
|
- Azure Container Apps
|
||||||
|
- Digital Ocean App Platform
|
||||||
|
- Fly.io
|
||||||
|
- Railway
|
||||||
|
|
||||||
|
### DIY Deployment
|
||||||
|
|
||||||
|
If you're familiar with deploying Node applications, the built-in app server is production-ready.
|
||||||
|
|
||||||
|
Make sure to deploy the output of `npm run build`
|
||||||
|
|
||||||
|
```
|
||||||
|
├── package.json
|
||||||
|
├── package-lock.json (or pnpm-lock.yaml, or bun.lockb)
|
||||||
|
├── build/
|
||||||
|
│ ├── client/ # Static assets
|
||||||
|
│ └── server/ # Server-side code
|
||||||
|
```
|
||||||
|
|
||||||
|
## Styling
|
||||||
|
|
||||||
|
This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Built with ❤️ using React Router.
|
||||||
64
frontend_service/app/app.css
Normal file
64
frontend_service/app/app.css
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
body {
|
||||||
|
font-family: 'Noto Sans TC', 'Heiti TC', sans-serif;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
color: #333;
|
||||||
|
margin-left: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2.5em;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 機器列表佈局 (使用 Flexbox) */
|
||||||
|
.machine-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap; /* 允許卡片換行 */
|
||||||
|
gap: 20px; /* 卡片之間的間距 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.machine-card {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 25px;
|
||||||
|
width: 280px; /* 卡片固定寬度,可根據需求調整 */
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); /* 輕微陰影,增加立體感 */
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.machine-card:hover {
|
||||||
|
transform: translateY(-5px); /* 鼠標懸停時輕微上移 */
|
||||||
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15); /* 陰影加深 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-name {
|
||||||
|
font-size: 1.2em;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-bottom: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 50%;
|
||||||
|
align-self: flex-start; /* 將圓點放置在卡片底部左側 */
|
||||||
|
border: 3px solid rgba(0, 0, 0, 0.05); /* 輕微邊框,使其更清晰 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator.running {
|
||||||
|
background-color: #4CAF50; /* 綠色 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator.error {
|
||||||
|
background-color: #F44336; /* 紅色 */
|
||||||
|
}
|
||||||
75
frontend_service/app/root.tsx
Normal file
75
frontend_service/app/root.tsx
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
import {
|
||||||
|
isRouteErrorResponse,
|
||||||
|
Links,
|
||||||
|
Meta,
|
||||||
|
Outlet,
|
||||||
|
Scripts,
|
||||||
|
ScrollRestoration,
|
||||||
|
} from "react-router";
|
||||||
|
|
||||||
|
import type { Route } from "./+types/root";
|
||||||
|
import "./app.css";
|
||||||
|
|
||||||
|
export const links: Route.LinksFunction = () => [
|
||||||
|
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
|
||||||
|
{
|
||||||
|
rel: "preconnect",
|
||||||
|
href: "https://fonts.gstatic.com",
|
||||||
|
crossOrigin: "anonymous",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rel: "stylesheet",
|
||||||
|
href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function Layout({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charSet="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<Meta />
|
||||||
|
<Links />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{children}
|
||||||
|
<ScrollRestoration />
|
||||||
|
<Scripts />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
return <Outlet />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
||||||
|
let message = "Oops!";
|
||||||
|
let details = "An unexpected error occurred.";
|
||||||
|
let stack: string | undefined;
|
||||||
|
|
||||||
|
if (isRouteErrorResponse(error)) {
|
||||||
|
message = error.status === 404 ? "404" : "Error";
|
||||||
|
details =
|
||||||
|
error.status === 404
|
||||||
|
? "The requested page could not be found."
|
||||||
|
: error.statusText || details;
|
||||||
|
} else if (import.meta.env.DEV && error && error instanceof Error) {
|
||||||
|
details = error.message;
|
||||||
|
stack = error.stack;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="pt-16 p-4 container mx-auto">
|
||||||
|
<h1>{message}</h1>
|
||||||
|
<p>{details}</p>
|
||||||
|
{stack && (
|
||||||
|
<pre className="w-full p-4 overflow-x-auto">
|
||||||
|
<code>{stack}</code>
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
7
frontend_service/app/routes.ts
Normal file
7
frontend_service/app/routes.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { type RouteConfig, index, route, } from "@react-router/dev/routes";
|
||||||
|
|
||||||
|
export default [
|
||||||
|
index("routes/home.tsx"),
|
||||||
|
route("details/:name", "routes/detail.tsx"),
|
||||||
|
route("*", "routes/404.tsx")
|
||||||
|
] satisfies RouteConfig;
|
||||||
8
frontend_service/app/routes/404.tsx
Normal file
8
frontend_service/app/routes/404.tsx
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
export default function NotFound(){
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>404</h1>
|
||||||
|
<p>The requested page could not be found.</p>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
41
frontend_service/app/routes/components/canvas.tsx
Normal file
41
frontend_service/app/routes/components/canvas.tsx
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import React, { useEffect, forwardRef } from "react";
|
||||||
|
|
||||||
|
interface CanvaProps {
|
||||||
|
name: string;
|
||||||
|
price: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Canva = forwardRef<HTMLCanvasElement, CanvaProps>((props, ref) => {
|
||||||
|
useEffect(()=>{
|
||||||
|
const canvas = (ref as React.RefObject<HTMLCanvasElement> | null)?.current;
|
||||||
|
|
||||||
|
if (!canvas) return;
|
||||||
|
const context = canvas.getContext("2d")
|
||||||
|
if (!context) return;
|
||||||
|
|
||||||
|
canvas.width = 296
|
||||||
|
canvas.height = 152
|
||||||
|
context.clearRect(0, 0, canvas.width, canvas.height)
|
||||||
|
context.font = "24px 'Inter', Arial"
|
||||||
|
context.textAlign = "center"
|
||||||
|
context.fillStyle = "#374151"
|
||||||
|
context.fillText(props.name, canvas.width/2, 50)
|
||||||
|
context.font = "48px 'Inter', Arial";
|
||||||
|
context.fillStyle = "#111827";
|
||||||
|
context.fillText(`$${props.price}`, canvas.width / 2, 110);
|
||||||
|
|
||||||
|
}, [props.name, props.price, ref]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
marginTop: '20px'
|
||||||
|
}}>
|
||||||
|
<canvas ref={ref} style={{
|
||||||
|
border: '1px solid black'
|
||||||
|
}}></canvas>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Canva;
|
||||||
15
frontend_service/app/routes/components/card.tsx
Normal file
15
frontend_service/app/routes/components/card.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { Link } from "react-router"
|
||||||
|
|
||||||
|
export default function(props: any){
|
||||||
|
let s = `status-indicator ${['error', 'running'][props.status]}`
|
||||||
|
return(
|
||||||
|
<Link to={'/details/'+props.metadata.name}
|
||||||
|
style={{textDecoration: 'none'}}
|
||||||
|
state={{metadata: props.metadata}}>
|
||||||
|
<div className="machine-card">
|
||||||
|
<span className="label-name">{props.metadata.name}</span>
|
||||||
|
<div className={s}></div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
101
frontend_service/app/routes/detail.tsx
Normal file
101
frontend_service/app/routes/detail.tsx
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
import Canva from './components/canvas';
|
||||||
|
import type { Route } from "./+types/home";
|
||||||
|
import { useState, useEffect, useRef } from 'react';
|
||||||
|
import { Link, Navigate, useLocation } from 'react-router';
|
||||||
|
|
||||||
|
export function meta({}: Route.MetaArgs) {
|
||||||
|
return [
|
||||||
|
{ title: "detail" }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default function Defail(){
|
||||||
|
const DivStyle = {
|
||||||
|
padding: '10px'
|
||||||
|
}
|
||||||
|
const BtnStyle = {
|
||||||
|
padding: '10px',
|
||||||
|
fontSize: '16px',
|
||||||
|
borderRadius: '5px',
|
||||||
|
border: '1px solid #ccc',
|
||||||
|
}
|
||||||
|
|
||||||
|
const [name, setName] = useState('')
|
||||||
|
const [price, setPrice] = useState('')
|
||||||
|
const [sale, setSale] = useState('')
|
||||||
|
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||||
|
|
||||||
|
const location = useLocation()
|
||||||
|
const metadata = location.state?.metadata
|
||||||
|
|
||||||
|
if(!metadata){
|
||||||
|
return <Navigate to="/" replace/>
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(()=>{
|
||||||
|
if(metadata){
|
||||||
|
setName(metadata.name)
|
||||||
|
setPrice(metadata.price)
|
||||||
|
setSale(metadata.sale)
|
||||||
|
}
|
||||||
|
}, [metadata])
|
||||||
|
|
||||||
|
async function handleClick(){
|
||||||
|
const canvas = canvasRef.current
|
||||||
|
if(!canvas){
|
||||||
|
console.error("Canvas 元素尚未準備好。")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob: Blob | null = await new Promise(resolve=>{
|
||||||
|
if (canvas instanceof HTMLCanvasElement && typeof canvas.toBlob === 'function') {
|
||||||
|
canvas.toBlob((blobResult)=>{
|
||||||
|
resolve(blobResult)
|
||||||
|
}, 'image/png')
|
||||||
|
} else {
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if(!blob){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const formdata = new FormData()
|
||||||
|
|
||||||
|
formdata.append('file', blob, '哈哈哈哈屁眼派對.png')
|
||||||
|
formdata.append('name', name)
|
||||||
|
formdata.append('price', price)
|
||||||
|
formdata.append('sale', sale)
|
||||||
|
|
||||||
|
const req = await fetch("https://bitmap-service.docker.orb.local/api/bitmap", {
|
||||||
|
method: 'POST',
|
||||||
|
body: formdata,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Link to="/">Home</Link>
|
||||||
|
<h1>Tag詳細資訊</h1>
|
||||||
|
<div style={DivStyle}>
|
||||||
|
<label>名稱: </label>
|
||||||
|
<input type="text" style={BtnStyle} value={name} onChange={e=>{setName(e.target.value)}} />
|
||||||
|
</div>
|
||||||
|
<div style={DivStyle}>
|
||||||
|
<label>售價: </label>
|
||||||
|
<input type="number" style={BtnStyle} value={price} onChange={e=>{setPrice(e.target.value)}}/>
|
||||||
|
</div>
|
||||||
|
<div style={DivStyle}>
|
||||||
|
<label>折扣: </label>
|
||||||
|
<input type="text" style={BtnStyle} value={sale} onChange={e=>{setSale(e.target.value)}}/>
|
||||||
|
</div>
|
||||||
|
{/* Canva 組件必須使用 forwardRef 才能接收 ref */}
|
||||||
|
<Canva name={name} price={price} ref={canvasRef}/>
|
||||||
|
<br />
|
||||||
|
<input type="button" value="送出" onClick={handleClick}/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
65
frontend_service/app/routes/home.tsx
Normal file
65
frontend_service/app/routes/home.tsx
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
import type { Route } from "./+types/home";
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import Card from './components/card'
|
||||||
|
|
||||||
|
export function meta({}: Route.MetaArgs) {
|
||||||
|
return [
|
||||||
|
{ title: "Index" }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TagInfo{
|
||||||
|
name: string,
|
||||||
|
price: number,
|
||||||
|
sale: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Peer{
|
||||||
|
peer_id: number,
|
||||||
|
last_online: number,
|
||||||
|
tags: [TagInfo]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default function Home(){
|
||||||
|
const [Peers, setPeers] = useState<Peer[]>([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
useEffect(()=>{
|
||||||
|
const fetchData = async () => {
|
||||||
|
try{
|
||||||
|
const response = await fetch('https://backend-service.docker.orb.local/list')
|
||||||
|
if(!response.ok){
|
||||||
|
throw new Error('Network error')
|
||||||
|
}
|
||||||
|
const data = await response.json()
|
||||||
|
setPeers(data)
|
||||||
|
}catch(error){
|
||||||
|
if(error instanceof Error){
|
||||||
|
setError(error.message)
|
||||||
|
}else{
|
||||||
|
setError('Unknown error')
|
||||||
|
}
|
||||||
|
}finally{
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetchData()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>機器列表</h1>
|
||||||
|
<div className="machine-list">
|
||||||
|
{loading && <p>Loading...</p>}
|
||||||
|
{error && <p>Error: {error}</p>}
|
||||||
|
{!loading && !error && Peers.map(Peer=>{
|
||||||
|
return Peer.tags.map(Tag=>{
|
||||||
|
return <Card key={Tag.name} name={Tag.name} status='1' metadata={Tag}/>
|
||||||
|
})
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
4099
frontend_service/package-lock.json
generated
Normal file
4099
frontend_service/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
30
frontend_service/package.json
Normal file
30
frontend_service/package.json
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"name": "newnew",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "react-router build",
|
||||||
|
"dev": "react-router dev",
|
||||||
|
"start": "react-router-serve ./build/server/index.js",
|
||||||
|
"typecheck": "react-router typegen && tsc"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@react-router/node": "^7.9.2",
|
||||||
|
"@react-router/serve": "^7.9.2",
|
||||||
|
"isbot": "^5.1.31",
|
||||||
|
"react": "^19.1.1",
|
||||||
|
"react-dom": "^19.1.1",
|
||||||
|
"react-router": "^7.9.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@react-router/dev": "^7.9.2",
|
||||||
|
"@tailwindcss/vite": "^4.1.13",
|
||||||
|
"@types/node": "^22",
|
||||||
|
"@types/react": "^19.1.13",
|
||||||
|
"@types/react-dom": "^19.1.9",
|
||||||
|
"tailwindcss": "^4.1.13",
|
||||||
|
"typescript": "^5.9.2",
|
||||||
|
"vite": "^7.1.7",
|
||||||
|
"vite-tsconfig-paths": "^5.1.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
frontend_service/public/favicon.ico
Normal file
BIN
frontend_service/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
7
frontend_service/react-router.config.ts
Normal file
7
frontend_service/react-router.config.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import type { Config } from "@react-router/dev/config";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
// Config options...
|
||||||
|
// Server-side render by default, to enable SPA mode set this to `false`
|
||||||
|
ssr: true,
|
||||||
|
} satisfies Config;
|
||||||
27
frontend_service/tsconfig.json
Normal file
27
frontend_service/tsconfig.json
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"include": [
|
||||||
|
"**/*",
|
||||||
|
"**/.server/**/*",
|
||||||
|
"**/.client/**/*",
|
||||||
|
".react-router/types/**/*"
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||||
|
"types": ["node", "vite/client"],
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ES2022",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"rootDirs": [".", "./.react-router/types"],
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"~/*": ["./app/*"]
|
||||||
|
},
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true
|
||||||
|
}
|
||||||
|
}
|
||||||
8
frontend_service/vite.config.ts
Normal file
8
frontend_service/vite.config.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { reactRouter } from "@react-router/dev/vite";
|
||||||
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
import tsconfigPaths from "vite-tsconfig-paths";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [tailwindcss(), reactRouter(), tsconfigPaths()],
|
||||||
|
});
|
||||||
Loading…
Add table
Reference in a new issue