2025-07-06 Line 訂單系統-B 階段:商品與訂單管理 API + Vue3 後台管理 UI(初版

2025-07-06 Line 訂單系統-B 階段:商品與訂單管理 API + Vue3 後台管理 UI(初版

完成 A 階段(Line webhook + MySQL + Node.js base)
現在進入下一階段 —— 📗 B 階段:商品與訂單管理 API + Vue3 後台管理 UI(初版)

這個階段的目標是讓你可以在電腦上建立一個簡單可操作的訂單管理後台
可以用瀏覽器新增商品、查看訂單、確認訂單。
LINE 用戶未來送出訂單時,這些商品與訂單會連動到這裡。


🧭 B 階段整體結構

📁 line-order-system

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
line-order-system/
├─ backend/ ← 已完成 (A階段)
│ ├─ routes/
│ │ ├─ line.js
│ │ ├─ products.js ← 新增
│ │ └─ orders.js ← 新增
│ ├─ db_init.sql ← 新增商品/訂單表
│ └─ ...
└─ frontend/ ← 新增 Vue3 (Vite) 專案
├─ src/
│ ├─ pages/
│ │ ├─ Products.vue
│ │ ├─ Orders.vue
│ │ └─ Dashboard.vue
│ ├─ components/
│ │ ├─ ProductForm.vue
│ │ └─ OrderTable.vue
│ ├─ App.vue
│ └─ main.js
└─ package.json

🧩 後端設計(Node.js / Express)

🧱 1️⃣ 資料庫 — db_init.sql 新增

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
USE line_order_db;

CREATE TABLE IF NOT EXISTS products (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
price DECIMAL(10,2) NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS orders (
id INT AUTO_INCREMENT PRIMARY KEY,
product_id INT NOT NULL,
quantity INT DEFAULT 1,
customer_name VARCHAR(100),
status ENUM('pending', 'confirmed', 'cancelled') DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (product_id) REFERENCES products(id)
);

🧱 2️⃣ 新增 backend/routes/products.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import express from "express";
import { pool } from "../db.js";

const router = express.Router();

// 取得所有商品
router.get("/", async (req, res) => {
const [rows] = await pool.query("SELECT * FROM products ORDER BY id DESC");
res.json(rows);
});

// 新增商品
router.post("/", async (req, res) => {
const { name, price, description } = req.body;
await pool.query(
"INSERT INTO products (name, price, description) VALUES (?, ?, ?)",
[name, price, description]
);
res.json({ message: "Product added" });
});

// 刪除商品
router.delete("/:id", async (req, res) => {
await pool.query("DELETE FROM products WHERE id = ?", [req.params.id]);
res.json({ message: "Product deleted" });
});

export default router;

🧱 3️⃣ 新增 backend/routes/orders.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import express from "express";
import { pool } from "../db.js";

const router = express.Router();

// 取得所有訂單
router.get("/", async (req, res) => {
const [rows] = await pool.query(
`SELECT o.*, p.name AS product_name
FROM orders o
JOIN products p ON o.product_id = p.id
ORDER BY o.id DESC`
);
res.json(rows);
});

// 新增訂單(未來 LINE 也會用這個)
router.post("/", async (req, res) => {
const { product_id, quantity, customer_name } = req.body;
await pool.query(
"INSERT INTO orders (product_id, quantity, customer_name) VALUES (?, ?, ?)",
[product_id, quantity, customer_name]
);
res.json({ message: "Order created" });
});

// 更新訂單狀態
router.put("/:id/status", async (req, res) => {
const { status } = req.body;
await pool.query("UPDATE orders SET status = ? WHERE id = ?", [
status,
req.params.id,
]);
res.json({ message: "Status updated" });
});

export default router;

🧱 4️⃣ 修改 backend/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import express from "express";
import cors from "cors";
import dotenv from "dotenv";
import lineRouter from "./routes/line.js";
import productRouter from "./routes/products.js";
import orderRouter from "./routes/orders.js";

dotenv.config();

const app = express();
app.use(cors());
app.use(express.json());

app.get("/", (req, res) => res.send("Line Order Backend - B Stage Ready"));
app.use("/line", lineRouter);
app.use("/products", productRouter);
app.use("/orders", orderRouter);

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`🚀 Server running on port ${PORT}`));

🎨 前端(Vue3 + Vite)

🧱 1️⃣ 建立專案(在 MacAir #1)

1
2
3
4
5
cd line-order-system
npm create vite@latest frontend -- --template vue
cd frontend
npm install axios
npm run dev

🧱 2️⃣ src/main.js

1
2
3
import { createApp } from "vue";
import App from "./App.vue";
createApp(App).mount("#app");

🧱 3️⃣ src/App.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<template>
<div class="p-4 font-sans">
<h1 class="text-2xl font-bold mb-4">📦 Line Order System - Admin</h1>
<div class="space-x-4 mb-4">
<button @click="page = 'products'">商品管理</button>
<button @click="page = 'orders'">訂單管理</button>
</div>
<Products v-if="page === 'products'" />
<Orders v-else />
</div>
</template>

<script setup>
import { ref } from "vue";
import Products from "./pages/Products.vue";
import Orders from "./pages/Orders.vue";

const page = ref("products");
</script>

<style>
button {
background: #007bff;
color: white;
border: none;
padding: 6px 10px;
border-radius: 6px;
cursor: pointer;
}
button:hover {
background: #0056b3;
}
</style>

🧱 4️⃣ src/pages/Products.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<template>
<div>
<h2>商品管理</h2>
<form @submit.prevent="addProduct" class="mb-3">
<input v-model="name" placeholder="商品名稱" />
<input v-model.number="price" placeholder="價格" type="number" />
<input v-model="description" placeholder="描述" />
<button>新增商品</button>
</form>

<table border="1" cellspacing="0" cellpadding="5">
<tr>
<th>ID</th><th>名稱</th><th>價格</th><th>描述</th><th>操作</th>
</tr>
<tr v-for="p in products" :key="p.id">
<td>{{ p.id }}</td>
<td>{{ p.name }}</td>
<td>{{ p.price }}</td>
<td>{{ p.description }}</td>
<td><button @click="removeProduct(p.id)">刪除</button></td>
</tr>
</table>
</div>
</template>

<script setup>
import { ref, onMounted } from "vue";
import axios from "axios";
const api = "http://localhost:3000/products";

const products = ref([]);
const name = ref("");
const price = ref("");
const description = ref("");

async function loadProducts() {
const res = await axios.get(api);
products.value = res.data;
}

async function addProduct() {
await axios.post(api, { name: name.value, price: price.value, description: description.value });
name.value = price.value = description.value = "";
await loadProducts();
}

async function removeProduct(id) {
await axios.delete(`${api}/${id}`);
await loadProducts();
}

onMounted(loadProducts);
</script>

🧱 5️⃣ src/pages/Orders.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<template>
<div>
<h2>訂單管理</h2>
<table border="1" cellspacing="0" cellpadding="5">
<tr>
<th>ID</th><th>商品</th><th>數量</th><th>顧客</th><th>狀態</th><th>操作</th>
</tr>
<tr v-for="o in orders" :key="o.id">
<td>{{ o.id }}</td>
<td>{{ o.product_name }}</td>
<td>{{ o.quantity }}</td>
<td>{{ o.customer_name }}</td>
<td>{{ o.status }}</td>
<td>
<select v-model="o.status" @change="updateStatus(o.id, o.status)">
<option value="pending">待處理</option>
<option value="confirmed">已確認</option>
<option value="cancelled">已取消</option>
</select>
</td>
</tr>
</table>
</div>
</template>

<script setup>
import { ref, onMounted } from "vue";
import axios from "axios";
const api = "http://localhost:3000/orders";

const orders = ref([]);

async function loadOrders() {
const res = await axios.get(api);
orders.value = res.data;
}

async function updateStatus(id, status) {
await axios.put(`${api}/${id}/status`, { status });
await loadOrders();
}

onMounted(loadOrders);
</script>

🚀 B 階段完成後,你能做到:

✅ 用 Vue3 後台新增 / 刪除商品
✅ 查看 / 修改訂單狀態
✅ 前後端資料同步於 MySQL
✅ 為未來「LINE 用戶下單」功能做好準備(C 階段)


📘 下一步(C 階段預告)

「C 階段 — LINE 訂單整合」
讓 LINE 用戶在對話中輸入 “我要買 XX”,
→ LINE Bot 自動建立 orders 資料
→ 後台即時出現訂單。


是否要我接著為你撰寫「📘 C 階段 — LINE 訂單整合(前端+後端連動)」的詳細設計與實作?
這會讓整個流程從「LINE 對話 → MySQL → Vue 後台」完整串接起來。