add image flow
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
from typing import Generator
|
||||
|
||||
from sqlalchemy import create_engine, event
|
||||
from sqlalchemy import create_engine, event, text
|
||||
from sqlalchemy.engine import make_url
|
||||
from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker
|
||||
|
||||
@@ -68,3 +68,41 @@ def init_db(database_url: str | None = None) -> None:
|
||||
configure_database(database_url)
|
||||
|
||||
Base.metadata.create_all(bind=engine)
|
||||
_sync_sqlite_image_columns()
|
||||
|
||||
|
||||
def _sync_sqlite_image_columns() -> None:
|
||||
if engine is None or engine.dialect.name != "sqlite":
|
||||
return
|
||||
|
||||
image_columns = {
|
||||
"boxes": {
|
||||
"image_blob": "BLOB",
|
||||
"image_mime_type": "VARCHAR(50)",
|
||||
"image_width": "INTEGER",
|
||||
"image_height": "INTEGER",
|
||||
},
|
||||
"items": {
|
||||
"image_blob": "BLOB",
|
||||
"image_mime_type": "VARCHAR(50)",
|
||||
"image_width": "INTEGER",
|
||||
"image_height": "INTEGER",
|
||||
},
|
||||
"subitems": {
|
||||
"image_blob": "BLOB",
|
||||
"image_mime_type": "VARCHAR(50)",
|
||||
"image_width": "INTEGER",
|
||||
"image_height": "INTEGER",
|
||||
},
|
||||
}
|
||||
|
||||
with engine.begin() as connection:
|
||||
for table_name, columns in image_columns.items():
|
||||
existing_columns = {
|
||||
row[1] for row in connection.execute(text(f"PRAGMA table_info({table_name})"))
|
||||
}
|
||||
for column_name, column_type in columns.items():
|
||||
if column_name not in existing_columns:
|
||||
connection.execute(
|
||||
text(f"ALTER TABLE {table_name} ADD COLUMN {column_name} {column_type}")
|
||||
)
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
from dataclasses import dataclass
|
||||
from io import BytesIO
|
||||
|
||||
from fastapi import HTTPException, UploadFile
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
|
||||
|
||||
MAX_IMAGE_SIDE = 1600
|
||||
JPEG_QUALITY = 80
|
||||
JPEG_MIME_TYPE = "image/jpeg"
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ProcessedImage:
|
||||
blob: bytes
|
||||
mime_type: str
|
||||
width: int
|
||||
height: int
|
||||
|
||||
|
||||
def process_upload(file: UploadFile | None) -> ProcessedImage | None:
|
||||
if file is None or not file.filename:
|
||||
return None
|
||||
|
||||
try:
|
||||
raw_bytes = file.file.read()
|
||||
if not raw_bytes:
|
||||
raise HTTPException(status_code=400, detail="上传的图片内容为空")
|
||||
|
||||
with Image.open(BytesIO(raw_bytes)) as source_image:
|
||||
processed_image = _prepare_image(source_image)
|
||||
except UnidentifiedImageError as exc:
|
||||
raise HTTPException(status_code=400, detail="上传的文件不是合法图片") from exc
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=400, detail="图片处理失败,请尝试更换图片") from exc
|
||||
finally:
|
||||
file.file.close()
|
||||
|
||||
return processed_image
|
||||
|
||||
|
||||
def _prepare_image(source_image: Image.Image) -> ProcessedImage:
|
||||
prepared = _strip_metadata_and_convert(source_image)
|
||||
prepared.thumbnail((MAX_IMAGE_SIDE, MAX_IMAGE_SIDE))
|
||||
|
||||
output = BytesIO()
|
||||
prepared.save(output, format="JPEG", quality=JPEG_QUALITY, optimize=True)
|
||||
blob = output.getvalue()
|
||||
|
||||
return ProcessedImage(
|
||||
blob=blob,
|
||||
mime_type=JPEG_MIME_TYPE,
|
||||
width=prepared.width,
|
||||
height=prepared.height,
|
||||
)
|
||||
|
||||
|
||||
def _strip_metadata_and_convert(source_image: Image.Image) -> Image.Image:
|
||||
if source_image.mode in ("RGBA", "LA"):
|
||||
background = Image.new("RGB", source_image.size, (255, 255, 255))
|
||||
alpha = source_image.getchannel("A")
|
||||
background.paste(source_image.convert("RGBA"), mask=alpha)
|
||||
return background
|
||||
|
||||
if source_image.mode == "P":
|
||||
return source_image.convert("RGB")
|
||||
|
||||
if source_image.mode != "RGB":
|
||||
return source_image.convert("RGB")
|
||||
|
||||
return source_image.copy()
|
||||
+72
-2
@@ -1,12 +1,13 @@
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import Depends, FastAPI, Form, HTTPException, Request, status
|
||||
from fastapi.responses import RedirectResponse
|
||||
from fastapi import Depends, FastAPI, File, Form, HTTPException, Request, UploadFile, status
|
||||
from fastapi.responses import RedirectResponse, Response
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db import get_db, init_db
|
||||
from app.images import process_upload
|
||||
from app.models import Box, Item, SubItem
|
||||
|
||||
templates = Jinja2Templates(directory="app/templates")
|
||||
@@ -56,6 +57,30 @@ def _require_container_item(item: Item) -> None:
|
||||
raise HTTPException(status_code=400, detail="Only container items can have sub-items")
|
||||
|
||||
|
||||
def _set_image_fields(target, processed_image) -> None:
|
||||
if processed_image is None:
|
||||
return
|
||||
|
||||
target.image_blob = processed_image.blob
|
||||
target.image_mime_type = processed_image.mime_type
|
||||
target.image_width = processed_image.width
|
||||
target.image_height = processed_image.height
|
||||
|
||||
|
||||
def _clear_image_fields(target) -> None:
|
||||
target.image_blob = None
|
||||
target.image_mime_type = None
|
||||
target.image_width = None
|
||||
target.image_height = None
|
||||
|
||||
|
||||
def _image_response_or_404(target) -> Response:
|
||||
if target.image_blob is None or target.image_mime_type is None:
|
||||
raise HTTPException(status_code=404, detail="Image not found")
|
||||
|
||||
return Response(content=target.image_blob, media_type=target.image_mime_type)
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
@@ -97,6 +122,7 @@ def create_app() -> FastAPI:
|
||||
note: str | None = Form(default=None),
|
||||
room: str | None = Form(default=None),
|
||||
status_text: str | None = Form(default=None, alias="status"),
|
||||
image_file: UploadFile | None = File(default=None),
|
||||
db: Session = Depends(get_db),
|
||||
) -> RedirectResponse:
|
||||
box = Box(
|
||||
@@ -105,6 +131,7 @@ def create_app() -> FastAPI:
|
||||
room=_clean_text(room),
|
||||
status=_clean_text(status_text),
|
||||
)
|
||||
_set_image_fields(box, process_upload(image_file))
|
||||
db.add(box)
|
||||
db.commit()
|
||||
db.refresh(box)
|
||||
@@ -119,6 +146,10 @@ def create_app() -> FastAPI:
|
||||
context={"page_title": box.name, "box": box},
|
||||
)
|
||||
|
||||
@app.get("/boxes/{box_id}/image")
|
||||
def get_box_image(box_id: int, db: Session = Depends(get_db)) -> Response:
|
||||
return _image_response_or_404(_get_box_or_404(db, box_id))
|
||||
|
||||
@app.get("/boxes/{box_id}/edit")
|
||||
def edit_box_page(box_id: int, request: Request, db: Session = Depends(get_db)):
|
||||
box = _get_box_or_404(db, box_id)
|
||||
@@ -140,6 +171,7 @@ def create_app() -> FastAPI:
|
||||
note: str | None = Form(default=None),
|
||||
room: str | None = Form(default=None),
|
||||
status_text: str | None = Form(default=None, alias="status"),
|
||||
image_file: UploadFile | None = File(default=None),
|
||||
db: Session = Depends(get_db),
|
||||
) -> RedirectResponse:
|
||||
box = _get_box_or_404(db, box_id)
|
||||
@@ -147,9 +179,17 @@ def create_app() -> FastAPI:
|
||||
box.note = _clean_text(note)
|
||||
box.room = _clean_text(room)
|
||||
box.status = _clean_text(status_text)
|
||||
_set_image_fields(box, process_upload(image_file))
|
||||
db.commit()
|
||||
return RedirectResponse(url=f"/boxes/{box.id}", status_code=status.HTTP_303_SEE_OTHER)
|
||||
|
||||
@app.post("/boxes/{box_id}/image/delete")
|
||||
def delete_box_image(box_id: int, db: Session = Depends(get_db)) -> RedirectResponse:
|
||||
box = _get_box_or_404(db, box_id)
|
||||
_clear_image_fields(box)
|
||||
db.commit()
|
||||
return RedirectResponse(url=f"/boxes/{box.id}/edit", status_code=status.HTTP_303_SEE_OTHER)
|
||||
|
||||
@app.post("/boxes/{box_id}/delete")
|
||||
def delete_box(box_id: int, db: Session = Depends(get_db)) -> RedirectResponse:
|
||||
box = _get_box_or_404(db, box_id)
|
||||
@@ -179,6 +219,7 @@ def create_app() -> FastAPI:
|
||||
note: str | None = Form(default=None),
|
||||
quantity: str | None = Form(default=None),
|
||||
is_container: str | None = Form(default=None),
|
||||
image_file: UploadFile | None = File(default=None),
|
||||
db: Session = Depends(get_db),
|
||||
) -> RedirectResponse:
|
||||
box = _get_box_or_404(db, box_id)
|
||||
@@ -189,6 +230,7 @@ def create_app() -> FastAPI:
|
||||
quantity=_parse_quantity(quantity),
|
||||
is_container=_is_checked(is_container),
|
||||
)
|
||||
_set_image_fields(item, process_upload(image_file))
|
||||
db.add(item)
|
||||
db.commit()
|
||||
db.refresh(item)
|
||||
@@ -203,6 +245,10 @@ def create_app() -> FastAPI:
|
||||
context={"page_title": item.name, "item": item},
|
||||
)
|
||||
|
||||
@app.get("/items/{item_id}/image")
|
||||
def get_item_image(item_id: int, db: Session = Depends(get_db)) -> Response:
|
||||
return _image_response_or_404(_get_item_or_404(db, item_id))
|
||||
|
||||
@app.get("/items/{item_id}/edit")
|
||||
def edit_item_page(item_id: int, request: Request, db: Session = Depends(get_db)):
|
||||
item = _get_item_or_404(db, item_id)
|
||||
@@ -225,6 +271,7 @@ def create_app() -> FastAPI:
|
||||
note: str | None = Form(default=None),
|
||||
quantity: str | None = Form(default=None),
|
||||
is_container: str | None = Form(default=None),
|
||||
image_file: UploadFile | None = File(default=None),
|
||||
db: Session = Depends(get_db),
|
||||
) -> RedirectResponse:
|
||||
item = _get_item_or_404(db, item_id)
|
||||
@@ -234,9 +281,17 @@ def create_app() -> FastAPI:
|
||||
item.is_container = _is_checked(is_container)
|
||||
if not item.is_container:
|
||||
item.subitems.clear()
|
||||
_set_image_fields(item, process_upload(image_file))
|
||||
db.commit()
|
||||
return RedirectResponse(url=f"/items/{item.id}", status_code=status.HTTP_303_SEE_OTHER)
|
||||
|
||||
@app.post("/items/{item_id}/image/delete")
|
||||
def delete_item_image(item_id: int, db: Session = Depends(get_db)) -> RedirectResponse:
|
||||
item = _get_item_or_404(db, item_id)
|
||||
_clear_image_fields(item)
|
||||
db.commit()
|
||||
return RedirectResponse(url=f"/items/{item.id}/edit", status_code=status.HTTP_303_SEE_OTHER)
|
||||
|
||||
@app.post("/items/{item_id}/delete")
|
||||
def delete_item(item_id: int, db: Session = Depends(get_db)) -> RedirectResponse:
|
||||
item = _get_item_or_404(db, item_id)
|
||||
@@ -267,6 +322,7 @@ def create_app() -> FastAPI:
|
||||
name: str = Form(...),
|
||||
note: str | None = Form(default=None),
|
||||
quantity: str | None = Form(default=None),
|
||||
image_file: UploadFile | None = File(default=None),
|
||||
db: Session = Depends(get_db),
|
||||
) -> RedirectResponse:
|
||||
item = _get_item_or_404(db, item_id)
|
||||
@@ -277,11 +333,16 @@ def create_app() -> FastAPI:
|
||||
note=_clean_text(note),
|
||||
quantity=_parse_quantity(quantity),
|
||||
)
|
||||
_set_image_fields(subitem, process_upload(image_file))
|
||||
db.add(subitem)
|
||||
db.commit()
|
||||
db.refresh(subitem)
|
||||
return RedirectResponse(url=f"/items/{item.id}", status_code=status.HTTP_303_SEE_OTHER)
|
||||
|
||||
@app.get("/subitems/{subitem_id}/image")
|
||||
def get_subitem_image(subitem_id: int, db: Session = Depends(get_db)) -> Response:
|
||||
return _image_response_or_404(_get_subitem_or_404(db, subitem_id))
|
||||
|
||||
@app.get("/subitems/{subitem_id}/edit")
|
||||
def edit_subitem_page(subitem_id: int, request: Request, db: Session = Depends(get_db)):
|
||||
subitem = _get_subitem_or_404(db, subitem_id)
|
||||
@@ -303,18 +364,27 @@ def create_app() -> FastAPI:
|
||||
name: str = Form(...),
|
||||
note: str | None = Form(default=None),
|
||||
quantity: str | None = Form(default=None),
|
||||
image_file: UploadFile | None = File(default=None),
|
||||
db: Session = Depends(get_db),
|
||||
) -> RedirectResponse:
|
||||
subitem = _get_subitem_or_404(db, subitem_id)
|
||||
subitem.name = name.strip()
|
||||
subitem.note = _clean_text(note)
|
||||
subitem.quantity = _parse_quantity(quantity)
|
||||
_set_image_fields(subitem, process_upload(image_file))
|
||||
db.commit()
|
||||
return RedirectResponse(
|
||||
url=f"/items/{subitem.parent_item_id}",
|
||||
status_code=status.HTTP_303_SEE_OTHER,
|
||||
)
|
||||
|
||||
@app.post("/subitems/{subitem_id}/image/delete")
|
||||
def delete_subitem_image(subitem_id: int, db: Session = Depends(get_db)) -> RedirectResponse:
|
||||
subitem = _get_subitem_or_404(db, subitem_id)
|
||||
_clear_image_fields(subitem)
|
||||
db.commit()
|
||||
return RedirectResponse(url=f"/subitems/{subitem.id}/edit", status_code=status.HTTP_303_SEE_OTHER)
|
||||
|
||||
@app.post("/subitems/{subitem_id}/delete")
|
||||
def delete_subitem(subitem_id: int, db: Session = Depends(get_db)) -> RedirectResponse:
|
||||
subitem = _get_subitem_or_404(db, subitem_id)
|
||||
|
||||
+13
-1
@@ -1,6 +1,6 @@
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Text
|
||||
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, LargeBinary, String, Text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.db import Base
|
||||
@@ -18,6 +18,10 @@ class Box(Base):
|
||||
note: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
room: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||
status: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
||||
image_blob: Mapped[bytes | None] = mapped_column(LargeBinary, nullable=True)
|
||||
image_mime_type: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
||||
image_width: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
image_height: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, nullable=False)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
@@ -42,6 +46,10 @@ class Item(Base):
|
||||
note: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
quantity: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
is_container: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||||
image_blob: Mapped[bytes | None] = mapped_column(LargeBinary, nullable=True)
|
||||
image_mime_type: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
||||
image_width: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
image_height: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, nullable=False)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
@@ -69,6 +77,10 @@ class SubItem(Base):
|
||||
name: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
note: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
quantity: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
image_blob: Mapped[bytes | None] = mapped_column(LargeBinary, nullable=True)
|
||||
image_mime_type: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
||||
image_width: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
image_height: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, nullable=False)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True),
|
||||
|
||||
@@ -92,6 +92,25 @@ button:hover {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.detail-image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.thumb-image {
|
||||
display: block;
|
||||
width: 120px;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.meta,
|
||||
.muted {
|
||||
color: #666;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<a href="/boxes">返回箱子列表</a>
|
||||
</div>
|
||||
|
||||
<form method="post" action="{{ form_action }}" class="stack">
|
||||
<form method="post" action="{{ form_action }}" class="stack" enctype="multipart/form-data">
|
||||
<label>
|
||||
名称
|
||||
<input type="text" name="name" value="{{ box.name if box else '' }}" required>
|
||||
@@ -23,6 +23,24 @@
|
||||
备注
|
||||
<textarea name="note" rows="4">{{ box.note if box and box.note else '' }}</textarea>
|
||||
</label>
|
||||
<label>
|
||||
图片
|
||||
<input type="file" name="image_file" accept="image/*">
|
||||
</label>
|
||||
{% if box and box.image_blob %}
|
||||
<section class="card">
|
||||
<p><strong>当前图片:</strong></p>
|
||||
<img src="/boxes/{{ box.id }}/image" alt="{{ box.name }}" class="detail-image">
|
||||
<button
|
||||
type="submit"
|
||||
class="link-button"
|
||||
formaction="/boxes/{{ box.id }}/image/delete"
|
||||
formmethod="post"
|
||||
>
|
||||
删除当前图片
|
||||
</button>
|
||||
</section>
|
||||
{% endif %}
|
||||
<button type="submit">{{ submit_label }}</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
</div>
|
||||
|
||||
<section class="card">
|
||||
{% if box.image_blob %}
|
||||
<img src="/boxes/{{ box.id }}/image" alt="{{ box.name }}" class="detail-image">
|
||||
{% endif %}
|
||||
<p><strong>房间:</strong> {{ box.room or '-' }}</p>
|
||||
<p><strong>状态:</strong> {{ box.status or '-' }}</p>
|
||||
<p><strong>备注:</strong> {{ box.note or '-' }}</p>
|
||||
@@ -29,6 +32,9 @@
|
||||
{% if box.items %}
|
||||
{% for item in box.items %}
|
||||
<article class="card">
|
||||
{% if item.image_blob %}
|
||||
<img src="/items/{{ item.id }}/image" alt="{{ item.name }}" class="thumb-image">
|
||||
{% endif %}
|
||||
<h3><a href="/items/{{ item.id }}">{{ item.name }}</a></h3>
|
||||
<p><strong>是否容器:</strong> {{ "是" if item.is_container else "否" }}</p>
|
||||
{% if item.quantity is not none %}<p><strong>数量:</strong> {{ item.quantity }}</p>{% endif %}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<a href="/boxes/{{ box.id }}">返回箱子</a>
|
||||
</div>
|
||||
|
||||
<form method="post" action="{{ form_action }}" class="stack">
|
||||
<form method="post" action="{{ form_action }}" class="stack" enctype="multipart/form-data">
|
||||
<label>
|
||||
名称
|
||||
<input type="text" name="name" value="{{ item.name if item else '' }}" required>
|
||||
@@ -26,6 +26,24 @@
|
||||
备注
|
||||
<textarea name="note" rows="4">{{ item.note if item and item.note else '' }}</textarea>
|
||||
</label>
|
||||
<label>
|
||||
图片
|
||||
<input type="file" name="image_file" accept="image/*">
|
||||
</label>
|
||||
{% if item and item.image_blob %}
|
||||
<section class="card">
|
||||
<p><strong>当前图片:</strong></p>
|
||||
<img src="/items/{{ item.id }}/image" alt="{{ item.name }}" class="detail-image">
|
||||
<button
|
||||
type="submit"
|
||||
class="link-button"
|
||||
formaction="/items/{{ item.id }}/image/delete"
|
||||
formmethod="post"
|
||||
>
|
||||
删除当前图片
|
||||
</button>
|
||||
</section>
|
||||
{% endif %}
|
||||
<button type="submit">{{ submit_label }}</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
</div>
|
||||
|
||||
<section class="card">
|
||||
{% if item.image_blob %}
|
||||
<img src="/items/{{ item.id }}/image" alt="{{ item.name }}" class="detail-image">
|
||||
{% endif %}
|
||||
<p><strong>是否容器:</strong> {{ "是" if item.is_container else "否" }}</p>
|
||||
<p><strong>数量:</strong> {{ item.quantity if item.quantity is not none else '-' }}</p>
|
||||
<p><strong>备注:</strong> {{ item.note or '-' }}</p>
|
||||
@@ -32,6 +35,9 @@
|
||||
{% if item.subitems %}
|
||||
{% for subitem in item.subitems %}
|
||||
<article class="card">
|
||||
{% if subitem.image_blob %}
|
||||
<img src="/subitems/{{ subitem.id }}/image" alt="{{ subitem.name }}" class="thumb-image">
|
||||
{% endif %}
|
||||
<h3>{{ subitem.name }}</h3>
|
||||
{% if subitem.quantity is not none %}<p><strong>数量:</strong> {{ subitem.quantity }}</p>{% endif %}
|
||||
{% if subitem.note %}<p><strong>备注:</strong> {{ subitem.note }}</p>{% endif %}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<a href="/items/{{ item.id }}">返回物品</a>
|
||||
</div>
|
||||
|
||||
<form method="post" action="{{ form_action }}" class="stack">
|
||||
<form method="post" action="{{ form_action }}" class="stack" enctype="multipart/form-data">
|
||||
<label>
|
||||
名称
|
||||
<input type="text" name="name" value="{{ subitem.name if subitem else '' }}" required>
|
||||
@@ -22,6 +22,24 @@
|
||||
备注
|
||||
<textarea name="note" rows="4">{{ subitem.note if subitem and subitem.note else '' }}</textarea>
|
||||
</label>
|
||||
<label>
|
||||
图片
|
||||
<input type="file" name="image_file" accept="image/*">
|
||||
</label>
|
||||
{% if subitem and subitem.image_blob %}
|
||||
<section class="card">
|
||||
<p><strong>当前图片:</strong></p>
|
||||
<img src="/subitems/{{ subitem.id }}/image" alt="{{ subitem.name }}" class="detail-image">
|
||||
<button
|
||||
type="submit"
|
||||
class="link-button"
|
||||
formaction="/subitems/{{ subitem.id }}/image/delete"
|
||||
formmethod="post"
|
||||
>
|
||||
删除当前图片
|
||||
</button>
|
||||
</section>
|
||||
{% endif %}
|
||||
<button type="submit">{{ submit_label }}</button>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user