first commit

This commit is contained in:
x1ulan 2025-12-03 18:21:00 +08:00
commit 2934405992
25 changed files with 4804 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

15
backend_service/app.py Normal file
View 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)

View 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
}
]
}
]

View 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
View 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)

View 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
View 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"

View file

@ -0,0 +1,4 @@
.react-router
build
node_modules
README.md

7
frontend_service/.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
.DS_Store
.env
/node_modules/
# React Router
/.react-router/
/build/

View 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"]

View file

@ -0,0 +1,87 @@
# Welcome to React Router!
A modern, production-ready template for building full-stack React applications using React Router.
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](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.

View 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; /* 紅色 */
}

View 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>
);
}

View 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;

View file

@ -0,0 +1,8 @@
export default function NotFound(){
return (
<>
<h1>404</h1>
<p>The requested page could not be found.</p>
</>
)
}

View 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;

View 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>
)
}

View 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}/>
</>
)
}

View 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

File diff suppressed because it is too large Load diff

View 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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View 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;

View 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
}
}

View 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()],
});