feat: 完善文档和界面,增加盒子类型支持及批量新增功能
This commit is contained in:
93
README-DEPLOY.md
Normal file
93
README-DEPLOY.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# README-DEPLOY
|
||||
|
||||
仅保留部署命令,按顺序执行。
|
||||
|
||||
## 1. 首次部署(服务器)
|
||||
|
||||
```bash
|
||||
cd /www/wwwroot
|
||||
git clone <your_repo_url> inventory
|
||||
cd inventory
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
python init_db.py
|
||||
```
|
||||
|
||||
## 2. 启动(临时测试)
|
||||
|
||||
```bash
|
||||
cd /www/wwwroot/inventory
|
||||
source venv/bin/activate
|
||||
python app.py
|
||||
```
|
||||
|
||||
访问:`http://127.0.0.1:5000`
|
||||
|
||||
## 3. 生产启动(Gunicorn)
|
||||
|
||||
先安装:
|
||||
|
||||
```bash
|
||||
cd /www/wwwroot/inventory
|
||||
source venv/bin/activate
|
||||
pip install gunicorn
|
||||
```
|
||||
|
||||
启动:
|
||||
|
||||
```bash
|
||||
cd /www/wwwroot/inventory
|
||||
source venv/bin/activate
|
||||
gunicorn -w 2 -b 127.0.0.1:5000 app:app
|
||||
```
|
||||
|
||||
说明:建议只监听 `127.0.0.1`,由 Nginx/宝塔反向代理。
|
||||
|
||||
## 4. 每次发布更新
|
||||
|
||||
### 本地
|
||||
|
||||
```bash
|
||||
cd C:/Users/BeihongWang/Desktop/inventory
|
||||
git add .
|
||||
git commit -m "feat: your change"
|
||||
git push origin main
|
||||
```
|
||||
|
||||
### 服务器
|
||||
|
||||
```bash
|
||||
cd /www/wwwroot/inventory
|
||||
git pull origin main
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
然后在宝塔中重启 Python 项目(或重启 Gunicorn 进程)。
|
||||
|
||||
## 5. 数据库备份
|
||||
|
||||
```bash
|
||||
cp /www/wwwroot/inventory/data/inventory.db /www/backup/inventory_$(date +%F).db
|
||||
```
|
||||
|
||||
## 6. 快速排查
|
||||
|
||||
查看服务端口是否监听:
|
||||
|
||||
```bash
|
||||
ss -lntp | grep 5000
|
||||
```
|
||||
|
||||
本机探活:
|
||||
|
||||
```bash
|
||||
curl http://127.0.0.1:5000
|
||||
```
|
||||
|
||||
若域名打不开,优先检查:
|
||||
|
||||
1. Python 项目是否运行中
|
||||
2. 域名解析是否生效
|
||||
3. Nginx/宝塔反向代理是否指向 `127.0.0.1:5000`
|
||||
162
README.md
162
README.md
@@ -0,0 +1,162 @@
|
||||
# 电子元件库存管理系统 v1.0
|
||||
|
||||
一个基于 `Flask + SQLite` 的轻量库存系统,适合先本地开发,后续部署到宝塔服务器。
|
||||
|
||||
当前支持三类容器:
|
||||
|
||||
- `28格小盒大盒`:常见 4 连排小盒(竖向 7 排)。
|
||||
- `14格中盒大盒`:中等盒子,无固定摆放图。
|
||||
- `袋装清单`:防静电袋列表模式(每行一个袋位,支持批量新增)。
|
||||
|
||||
## 1. 项目结构
|
||||
|
||||
```text
|
||||
inventory/
|
||||
├── app.py
|
||||
├── requirements.txt
|
||||
├── init_db.py
|
||||
├── data/
|
||||
│ └── inventory.db # 首次初始化后生成
|
||||
├── templates/
|
||||
│ ├── index.html
|
||||
│ ├── box.html
|
||||
│ ├── edit.html
|
||||
│ └── scan.html
|
||||
└── static/
|
||||
├── css/
|
||||
│ └── style.css
|
||||
└── js/
|
||||
└── scanner.js
|
||||
```
|
||||
|
||||
## 2. 本地运行
|
||||
|
||||
### 2.1 安装依赖
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 2.2 初始化数据库
|
||||
|
||||
```bash
|
||||
python init_db.py
|
||||
```
|
||||
|
||||
初始化后会生成:`data/inventory.db`
|
||||
|
||||
### 2.3 启动服务
|
||||
|
||||
```bash
|
||||
python app.py
|
||||
```
|
||||
|
||||
默认访问:`http://127.0.0.1:5000`
|
||||
|
||||
## 3. 页面说明
|
||||
|
||||
### 3.1 首页 `/`
|
||||
|
||||
- 按容器类型显示 3 个列表。
|
||||
- 每个列表都可新增盒子。
|
||||
|
||||
### 3.2 盒子详情 `/box/<box_id>`
|
||||
|
||||
- `28格/14格`:格子视图,点格子进入编辑。
|
||||
- `袋装清单`:表格视图,支持单条新增和批量新增。
|
||||
|
||||
### 3.3 编辑页 `/edit/<box_id>/<slot>`
|
||||
|
||||
- 编辑料号、名称、规格、数量、位置备注、备注。
|
||||
- 可删除当前格子记录。
|
||||
|
||||
### 3.4 扫码/搜索 `/scan`
|
||||
|
||||
- 可按料号或名称搜索。
|
||||
- 支持扫码枪输入后回车触发搜索。
|
||||
|
||||
## 4. 袋装批量新增格式
|
||||
|
||||
在袋装清单页面的批量输入框里,每行一条,可用英文逗号或 Tab 分隔:
|
||||
|
||||
```text
|
||||
料号, 名称, 数量, 规格, 位置备注, 备注
|
||||
```
|
||||
|
||||
示例:
|
||||
|
||||
```text
|
||||
10K-0603, 贴片电阻10K, 500, 0603, A袋, 常用
|
||||
100nF-0603, 电容100nF, 300, 0603, B袋, X7R
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `料号`、`名称` 必填。
|
||||
- `数量` 需为大于等于 0 的整数(留空按 0)。
|
||||
- 无效行会跳过并提示。
|
||||
|
||||
## 5. 数据库说明
|
||||
|
||||
- 使用 SQLite,文件路径:`data/inventory.db`
|
||||
- 首次发布执行一次 `python init_db.py`
|
||||
- 后续通常不需要重复初始化
|
||||
|
||||
## 6. 服务器部署(宝塔)
|
||||
|
||||
建议流程:
|
||||
|
||||
1. 上传代码或 `git clone` 到服务器。
|
||||
2. 创建并启用虚拟环境。
|
||||
3. `pip install -r requirements.txt`
|
||||
4. `python init_db.py`
|
||||
5. 用 Gunicorn 启动:`app:app`
|
||||
6. 宝塔/Nginx 反向代理到 Gunicorn 端口
|
||||
7. 域名解析 + SSL
|
||||
|
||||
建议 Gunicorn 仅监听内网:`127.0.0.1:5000`
|
||||
|
||||
## 7. 日常发布流程
|
||||
|
||||
本地开发后:
|
||||
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "feat: xxx"
|
||||
git push origin main
|
||||
```
|
||||
|
||||
服务器更新:
|
||||
|
||||
```bash
|
||||
cd /www/wwwroot/inventory
|
||||
git pull origin main
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
最后在宝塔里重启 Python 项目。
|
||||
|
||||
## 8. 备份建议
|
||||
|
||||
重点备份:`data/inventory.db`
|
||||
|
||||
可按天备份,例如:
|
||||
|
||||
```bash
|
||||
cp /www/wwwroot/inventory/data/inventory.db /www/backup/inventory_$(date +%F).db
|
||||
```
|
||||
|
||||
## 9. 常见问题
|
||||
|
||||
### Q1: VS Code 提示无法解析 `flask` 导入
|
||||
|
||||
通常是编辑器没选到正确虚拟环境,不代表代码不能运行。切换解释器到项目 venv 即可。
|
||||
|
||||
### Q2: 为什么线上不能用 `python app.py` 长期跑
|
||||
|
||||
`python app.py` 是开发模式。生产请使用 Gunicorn(或其他 WSGI)并由宝塔托管。
|
||||
|
||||
### Q3: 本地和服务器数据库要实时同步吗
|
||||
|
||||
不建议 SQLite 双向实时同步。建议以服务器库为主,本地用于测试。
|
||||
|
||||
270
app.py
270
app.py
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
from flask import Flask, redirect, render_template, request, url_for
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
@@ -15,12 +16,33 @@ app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
|
||||
db = SQLAlchemy(app)
|
||||
|
||||
|
||||
BOX_TYPES = {
|
||||
"small_28": {
|
||||
"label": "28格小盒大盒",
|
||||
"default_capacity": 28,
|
||||
"default_desc": "4连排小盒,常见摆放为竖向7排",
|
||||
},
|
||||
"medium_14": {
|
||||
"label": "14格中盒大盒",
|
||||
"default_capacity": 14,
|
||||
"default_desc": "装十四个中等盒子(尺寸不固定)",
|
||||
},
|
||||
"bag": {
|
||||
"label": "袋装清单",
|
||||
"default_capacity": 1,
|
||||
"default_desc": "用于较小防静电袋存放",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class Box(db.Model):
|
||||
__tablename__ = "boxes"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(100), nullable=False, unique=True)
|
||||
description = db.Column(db.String(255), nullable=True)
|
||||
box_type = db.Column(db.String(30), nullable=False, default="small_28")
|
||||
slot_capacity = db.Column(db.Integer, nullable=False, default=28)
|
||||
|
||||
|
||||
class Component(db.Model):
|
||||
@@ -45,44 +67,250 @@ class Component(db.Model):
|
||||
|
||||
|
||||
def ensure_default_box() -> None:
|
||||
if not Box.query.first():
|
||||
default_box = Box(name="默认大盒", description="每盒 28 个格子")
|
||||
db.session.add(default_box)
|
||||
db.session.commit()
|
||||
defaults = [
|
||||
("默认28格大盒", "small_28"),
|
||||
("默认14格中盒", "medium_14"),
|
||||
("默认袋装清单", "bag"),
|
||||
]
|
||||
for box_name, box_type in defaults:
|
||||
exists = Box.query.filter_by(name=box_name).first()
|
||||
if exists:
|
||||
continue
|
||||
meta = BOX_TYPES[box_type]
|
||||
db.session.add(
|
||||
Box(
|
||||
name=box_name,
|
||||
description=meta["default_desc"],
|
||||
box_type=box_type,
|
||||
slot_capacity=meta["default_capacity"],
|
||||
)
|
||||
)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def slot_data_for_box(box_id: int):
|
||||
components = Component.query.filter_by(box_id=box_id).all()
|
||||
def slot_data_for_box(box: Box):
|
||||
components = Component.query.filter_by(box_id=box.id).all()
|
||||
slot_map = {c.slot_index: c for c in components}
|
||||
slots = []
|
||||
for slot in range(1, 29):
|
||||
for slot in range(1, box.slot_capacity + 1):
|
||||
slots.append({"slot": slot, "component": slot_map.get(slot)})
|
||||
return slots
|
||||
|
||||
|
||||
def bag_items_for_box(box: Box):
|
||||
return (
|
||||
Component.query.filter_by(box_id=box.id)
|
||||
.order_by(Component.slot_index.asc())
|
||||
.all()
|
||||
)
|
||||
|
||||
|
||||
def _parse_non_negative_int(raw_value: str, default_value: int = 0) -> int:
|
||||
raw = (raw_value or "").strip()
|
||||
if raw == "":
|
||||
return default_value
|
||||
value = int(raw)
|
||||
if value < 0:
|
||||
raise ValueError
|
||||
return value
|
||||
|
||||
|
||||
def _add_bag_item(
|
||||
box: Box,
|
||||
part_no: str,
|
||||
name: str,
|
||||
quantity: int,
|
||||
specification: str,
|
||||
location: str,
|
||||
note: str,
|
||||
) -> None:
|
||||
next_slot = (
|
||||
db.session.query(db.func.max(Component.slot_index))
|
||||
.filter(Component.box_id == box.id)
|
||||
.scalar()
|
||||
or 0
|
||||
) + 1
|
||||
|
||||
item = Component(
|
||||
box_id=box.id,
|
||||
slot_index=next_slot,
|
||||
part_no=part_no,
|
||||
name=name,
|
||||
quantity=quantity,
|
||||
specification=specification or None,
|
||||
location=location or None,
|
||||
note=note or None,
|
||||
)
|
||||
db.session.add(item)
|
||||
|
||||
if next_slot > box.slot_capacity:
|
||||
box.slot_capacity = next_slot
|
||||
|
||||
|
||||
def render_box_page(box: Box, error: str = "", notice: str = ""):
|
||||
slots = slot_data_for_box(box)
|
||||
bag_items = bag_items_for_box(box) if box.box_type == "bag" else []
|
||||
return render_template(
|
||||
"box.html",
|
||||
box=box,
|
||||
slots=slots,
|
||||
bag_items=bag_items,
|
||||
box_types=BOX_TYPES,
|
||||
error=error,
|
||||
notice=notice,
|
||||
)
|
||||
|
||||
|
||||
def ensure_box_schema() -> None:
|
||||
columns = {
|
||||
row[1]
|
||||
for row in db.session.execute(db.text("PRAGMA table_info(boxes)")).fetchall()
|
||||
}
|
||||
if "box_type" not in columns:
|
||||
db.session.execute(
|
||||
db.text(
|
||||
"ALTER TABLE boxes ADD COLUMN box_type VARCHAR(30) NOT NULL DEFAULT 'small_28'"
|
||||
)
|
||||
)
|
||||
if "slot_capacity" not in columns:
|
||||
db.session.execute(
|
||||
db.text(
|
||||
"ALTER TABLE boxes ADD COLUMN slot_capacity INTEGER NOT NULL DEFAULT 28"
|
||||
)
|
||||
)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
boxes = Box.query.order_by(Box.id.asc()).all()
|
||||
box_cards = []
|
||||
groups = {key: [] for key in BOX_TYPES.keys()}
|
||||
for box in boxes:
|
||||
used_count = Component.query.filter_by(box_id=box.id).count()
|
||||
box_cards.append({"box": box, "used_count": used_count})
|
||||
return render_template("index.html", box_cards=box_cards)
|
||||
box_type = box.box_type if box.box_type in BOX_TYPES else "small_28"
|
||||
groups[box_type].append({"box": box, "used_count": used_count})
|
||||
return render_template("index.html", groups=groups, box_types=BOX_TYPES)
|
||||
|
||||
|
||||
@app.route("/boxes/create", methods=["POST"])
|
||||
def create_box():
|
||||
box_type = request.form.get("box_type", "small_28").strip()
|
||||
name = request.form.get("name", "").strip()
|
||||
description = request.form.get("description", "").strip()
|
||||
|
||||
if box_type not in BOX_TYPES:
|
||||
return "无效盒子类型", 400
|
||||
if not name:
|
||||
return "盒子名称不能为空", 400
|
||||
|
||||
if Box.query.filter_by(name=name).first():
|
||||
return "盒子名称已存在,请更换", 400
|
||||
|
||||
meta = BOX_TYPES[box_type]
|
||||
box = Box(
|
||||
name=name,
|
||||
description=description or meta["default_desc"],
|
||||
box_type=box_type,
|
||||
slot_capacity=meta["default_capacity"],
|
||||
)
|
||||
db.session.add(box)
|
||||
db.session.commit()
|
||||
return redirect(url_for("index"))
|
||||
|
||||
|
||||
@app.route("/box/<int:box_id>")
|
||||
def view_box(box_id: int):
|
||||
box = Box.query.get_or_404(box_id)
|
||||
slots = slot_data_for_box(box.id)
|
||||
return render_template("box.html", box=box, slots=slots)
|
||||
return render_box_page(box)
|
||||
|
||||
|
||||
@app.route("/box/<int:box_id>/bags/add", methods=["POST"])
|
||||
def add_bag_item(box_id: int):
|
||||
box = Box.query.get_or_404(box_id)
|
||||
if box.box_type != "bag":
|
||||
return "当前盒子不是袋装清单", 400
|
||||
|
||||
part_no = request.form.get("part_no", "").strip()
|
||||
name = request.form.get("name", "").strip()
|
||||
specification = request.form.get("specification", "").strip()
|
||||
location = request.form.get("location", "").strip()
|
||||
note = request.form.get("note", "").strip()
|
||||
|
||||
if not part_no or not name:
|
||||
return render_box_page(box, error="袋装新增失败: 料号和名称不能为空")
|
||||
|
||||
try:
|
||||
quantity = _parse_non_negative_int(request.form.get("quantity", "0"), 0)
|
||||
except ValueError:
|
||||
return render_box_page(box, error="袋装新增失败: 数量必须是大于等于 0 的整数")
|
||||
|
||||
_add_bag_item(box, part_no, name, quantity, specification, location, note)
|
||||
db.session.commit()
|
||||
return render_box_page(box, notice="已新增 1 条袋装记录")
|
||||
|
||||
|
||||
@app.route("/box/<int:box_id>/bags/batch", methods=["POST"])
|
||||
def add_bag_items_batch(box_id: int):
|
||||
box = Box.query.get_or_404(box_id)
|
||||
if box.box_type != "bag":
|
||||
return "当前盒子不是袋装清单", 400
|
||||
|
||||
raw_lines = request.form.get("lines", "")
|
||||
lines = [line.strip() for line in raw_lines.splitlines() if line.strip()]
|
||||
if not lines:
|
||||
return render_box_page(box, error="批量新增失败: 请至少输入一行")
|
||||
|
||||
invalid_lines = []
|
||||
added_count = 0
|
||||
for line_no, line in enumerate(lines, start=1):
|
||||
parts = [p.strip() for p in re.split(r"[,\t]", line)]
|
||||
while len(parts) < 6:
|
||||
parts.append("")
|
||||
|
||||
part_no = parts[0]
|
||||
name = parts[1]
|
||||
quantity_raw = parts[2]
|
||||
specification = parts[3]
|
||||
location = parts[4]
|
||||
note = parts[5]
|
||||
|
||||
if not part_no or not name:
|
||||
invalid_lines.append(f"第 {line_no} 行")
|
||||
continue
|
||||
|
||||
try:
|
||||
quantity = _parse_non_negative_int(quantity_raw, 0)
|
||||
except ValueError:
|
||||
invalid_lines.append(f"第 {line_no} 行")
|
||||
continue
|
||||
|
||||
_add_bag_item(box, part_no, name, quantity, specification, location, note)
|
||||
added_count += 1
|
||||
|
||||
if added_count:
|
||||
db.session.commit()
|
||||
|
||||
if invalid_lines and added_count == 0:
|
||||
return render_box_page(
|
||||
box,
|
||||
error="批量新增失败: 数据格式不正确(请检查: " + ", ".join(invalid_lines) + ")",
|
||||
)
|
||||
|
||||
if invalid_lines:
|
||||
return render_box_page(
|
||||
box,
|
||||
notice=f"已新增 {added_count} 条,以下行被跳过: " + ", ".join(invalid_lines),
|
||||
)
|
||||
|
||||
return render_box_page(box, notice=f"批量新增成功,共 {added_count} 条")
|
||||
|
||||
|
||||
@app.route("/edit/<int:box_id>/<int:slot>", methods=["GET", "POST"])
|
||||
def edit_component(box_id: int, slot: int):
|
||||
if slot < 1 or slot > 28:
|
||||
box = Box.query.get_or_404(box_id)
|
||||
if slot < 1 or slot > box.slot_capacity:
|
||||
return "无效的格子编号", 400
|
||||
|
||||
box = Box.query.get_or_404(box_id)
|
||||
component = Component.query.filter_by(box_id=box.id, slot_index=slot).first()
|
||||
|
||||
if request.method == "POST":
|
||||
@@ -186,9 +414,21 @@ def scan_api():
|
||||
def bootstrap() -> None:
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
ensure_box_schema()
|
||||
db.session.execute(
|
||||
db.text(
|
||||
"UPDATE boxes SET box_type = 'small_28' WHERE box_type IS NULL OR box_type = ''"
|
||||
)
|
||||
)
|
||||
db.session.execute(
|
||||
db.text("UPDATE boxes SET slot_capacity = 28 WHERE slot_capacity IS NULL")
|
||||
)
|
||||
db.session.commit()
|
||||
ensure_default_box()
|
||||
|
||||
|
||||
bootstrap()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
bootstrap()
|
||||
app.run(host="0.0.0.0", port=5000, debug=True)
|
||||
|
||||
@@ -76,6 +76,35 @@ body {
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.group-panel {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.group-title-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.group-title-row h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.group-desc {
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.new-box-form {
|
||||
display: grid;
|
||||
grid-template-columns: 1.2fr 1fr auto;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.box-card,
|
||||
.panel {
|
||||
background: var(--card);
|
||||
@@ -89,12 +118,24 @@ body {
|
||||
margin: 0 0 6px;
|
||||
}
|
||||
|
||||
.grid-28 {
|
||||
.box-card h4 {
|
||||
margin: 0 0 6px;
|
||||
}
|
||||
|
||||
.slot-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, minmax(90px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.slot-grid-14 {
|
||||
grid-template-columns: repeat(7, minmax(90px, 1fr));
|
||||
}
|
||||
|
||||
.slot-grid-bag {
|
||||
grid-template-columns: repeat(2, minmax(120px, 1fr));
|
||||
}
|
||||
|
||||
.slot {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -156,6 +197,14 @@ textarea {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.notice {
|
||||
background: #ecfeff;
|
||||
border: 1px solid #a5f3fc;
|
||||
color: #155e75;
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.search-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
@@ -187,16 +236,28 @@ td {
|
||||
margin: 10px 0 0;
|
||||
}
|
||||
|
||||
.batch-input {
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.hero {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.grid-28 {
|
||||
.slot-grid,
|
||||
.slot-grid-14,
|
||||
.slot-grid-bag {
|
||||
grid-template-columns: repeat(4, minmax(70px, 1fr));
|
||||
}
|
||||
|
||||
.new-box-form {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ box.name }} - 28宫格</title>
|
||||
<title>{{ box.name }} - 容器详情</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<header class="hero slim">
|
||||
<h1>{{ box.name }} - 28 宫格</h1>
|
||||
<h1>{{ box.name }} - {{ box_types.get(box.box_type, box_types['small_28']).label }}</h1>
|
||||
<nav>
|
||||
<a class="btn btn-light" href="{{ url_for('index') }}">返回首页</a>
|
||||
<a class="btn" href="{{ url_for('scan_page') }}">扫码/搜索</a>
|
||||
@@ -16,7 +16,97 @@
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
<section class="grid-28">
|
||||
{% if error %}
|
||||
<p class="alert">{{ error }}</p>
|
||||
{% endif %}
|
||||
{% if notice %}
|
||||
<p class="notice">{{ notice }}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if box.box_type == 'bag' %}
|
||||
<section class="panel">
|
||||
<h2>袋装记录</h2>
|
||||
<p class="group-desc">每行一个袋位,按序号自动编号</p>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>袋位</th>
|
||||
<th>料号</th>
|
||||
<th>名称</th>
|
||||
<th>数量</th>
|
||||
<th>规格</th>
|
||||
<th>位置备注</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for c in bag_items %}
|
||||
<tr>
|
||||
<td>#{{ c.slot_index }}</td>
|
||||
<td>{{ c.part_no }}</td>
|
||||
<td>{{ c.name }}</td>
|
||||
<td>{{ c.quantity }}</td>
|
||||
<td>{{ c.specification or '-' }}</td>
|
||||
<td>{{ c.location or '-' }}</td>
|
||||
<td><a href="{{ url_for('edit_component', box_id=box.id, slot=c.slot_index) }}">编辑</a></td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="7">当前没有袋装记录,请先新增。</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2>新增单条</h2>
|
||||
<form class="form-grid" method="post" action="{{ url_for('add_bag_item', box_id=box.id) }}">
|
||||
<label>
|
||||
料号 *
|
||||
<input type="text" name="part_no" required>
|
||||
</label>
|
||||
<label>
|
||||
名称 *
|
||||
<input type="text" name="name" required>
|
||||
</label>
|
||||
<label>
|
||||
数量
|
||||
<input type="number" min="0" name="quantity" value="0">
|
||||
</label>
|
||||
<label>
|
||||
规格
|
||||
<input type="text" name="specification">
|
||||
</label>
|
||||
<label>
|
||||
位置备注
|
||||
<input type="text" name="location">
|
||||
</label>
|
||||
<label class="full">
|
||||
备注
|
||||
<input type="text" name="note">
|
||||
</label>
|
||||
<div class="actions full">
|
||||
<button class="btn" type="submit">新增记录</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2>批量新增</h2>
|
||||
<p class="hint">每行一条, 格式: 料号, 名称, 数量, 规格, 位置备注, 备注。可用英文逗号或 Tab 分隔。</p>
|
||||
<form method="post" action="{{ url_for('add_bag_items_batch', box_id=box.id) }}">
|
||||
<textarea class="batch-input" name="lines" rows="8" placeholder="10K-0603, 贴片电阻10K, 500, 0603, A袋, 常用\n100nF-0603, 电容100nF, 300, 0603, B袋, X7R"></textarea>
|
||||
<div class="actions">
|
||||
<button class="btn" type="submit">批量导入</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
{% else %}
|
||||
<p class="group-desc">容量: {{ box.slot_capacity }} 位</p>
|
||||
<section class="slot-grid{% if box.slot_capacity <= 14 %} slot-grid-14{% endif %}{% if box.slot_capacity <= 4 %} slot-grid-bag{% endif %}">
|
||||
{% for item in slots %}
|
||||
<a class="slot {% if item.component %}filled{% endif %}" href="{{ url_for('edit_component', box_id=box.id, slot=item.slot) }}">
|
||||
<span class="slot-no">#{{ item.slot }}</span>
|
||||
@@ -30,6 +120,7 @@
|
||||
</a>
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% endif %}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -13,19 +13,36 @@
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
<h2>大盒列表</h2>
|
||||
<section class="box-list">
|
||||
{% for item in box_cards %}
|
||||
<article class="box-card">
|
||||
<h3>{{ item.box.name }}</h3>
|
||||
<p>{{ item.box.description or '暂无描述' }}</p>
|
||||
<p>已使用: {{ item.used_count }}/28</p>
|
||||
<a class="btn" href="{{ url_for('view_box', box_id=item.box.id) }}">进入 28 宫格</a>
|
||||
</article>
|
||||
{% else %}
|
||||
<p>暂无大盒数据,请先初始化数据库。</p>
|
||||
{% endfor %}
|
||||
<h2>容器列表</h2>
|
||||
|
||||
{% for key, meta in box_types.items() %}
|
||||
<section class="group-panel panel">
|
||||
<div class="group-title-row">
|
||||
<h3>{{ meta.label }}</h3>
|
||||
<span class="group-desc">{{ meta.default_desc }}</span>
|
||||
</div>
|
||||
|
||||
<form class="new-box-form" method="post" action="{{ url_for('create_box') }}">
|
||||
<input type="hidden" name="box_type" value="{{ key }}">
|
||||
<input type="text" name="name" placeholder="新增盒子名称" required>
|
||||
<input type="text" name="description" placeholder="备注(可选)">
|
||||
<button class="btn" type="submit">新增盒子</button>
|
||||
</form>
|
||||
|
||||
<section class="box-list">
|
||||
{% for item in groups[key] %}
|
||||
<article class="box-card">
|
||||
<h4>{{ item.box.name }}</h4>
|
||||
<p>{{ item.box.description or '暂无描述' }}</p>
|
||||
<p>已使用: {{ item.used_count }}/{{ item.box.slot_capacity }}</p>
|
||||
<a class="btn" href="{{ url_for('view_box', box_id=item.box.id) }}">进入列表</a>
|
||||
</article>
|
||||
{% else %}
|
||||
<p>当前分类还没有盒子,先新增一个。</p>
|
||||
{% endfor %}
|
||||
</section>
|
||||
</section>
|
||||
{% endfor %}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user