feat: 商品详情页、XLS导入修复、分页选择器、导出功能

后端:
- 新增 product_images 表,支持每商品最多5张图(服务端压缩至1200px/JPEG85%)
- products 表新增 public_id(UUID)、description 字段
- 新增商品详情接口、二维码接口、公开商品接口(无鉴权)
- 修复 XLS 导入:OLE2 magic bytes 检测 + 临时文件解析,兼容 extrame/xls
- 修复商品/名称/系列/规格三张表导入数据为0(LastCol()=0 bug)
- 所有导入接口返回 total/imported/skipped 统计
- config 新增 StorageConfig,支持 STORAGE_* 环境变量覆盖
- 种子数据修复:products 补 public_id、新增 product_images TRUNCATE、schema.sql 表名修正

前端:
- 商品详情页:图片上传/删除、描述内联编辑、二维码弹窗、公开链接复制
- 公开商品页:无鉴权路由 /product/:public_id,Flutter Web SPA
- 商品详情列表(批次追踪)商品名超链接跳转详情页
- 导航「商品管理」改名「商品详情」
- 所有列表表格新增每页条数选择(10/20/50/100)
- 表格列头内嵌筛选(FilterableColumnHeader)
- 导出 Excel 功能(入库/出库/库存/财务/批次/往来单位)
- 网络恢复自动刷新 + 离线缓存展示

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-04-27 00:29:51 +08:00
parent 5dd7c07138
commit 393e227de5
70 changed files with 4993 additions and 1169 deletions
+76 -16
View File
@@ -6,10 +6,13 @@ fi
# dev.sh — 本地开发环境管理
#
# 启动:
# sh scripts/dev.sh run # 启动前后端(后端无改动则跳过重启
# sh scripts/dev.sh run # 启动前后端(macOS 桌面端
# sh scripts/dev.sh run --force # 强制重启后端
# sh scripts/dev.sh run --web # 启动前后端(Chrome Web 端)
# sh scripts/dev.sh run --all # 启动前后端(macOS + Web 同时)
# sh scripts/dev.sh --backend-only # 仅启动后端
# sh scripts/dev.sh --frontend-only # 仅启动前端
# sh scripts/dev.sh --frontend-only # 仅启动前端macOS
# sh scripts/dev.sh --web-only # 仅启动前端(Web)
#
# 数据库:
# sh scripts/dev.sh seed H001 # 清空并写入 H001 测试数据(SQL 文件)
@@ -27,6 +30,8 @@ LOG_DIR="$ROOT/scripts/.logs"
BACKEND_LOG="$LOG_DIR/backend.log"
BACKEND_PID_FILE="$LOG_DIR/backend.pid"
BACKEND_HASH_FILE="$LOG_DIR/backend.hash"
WEB_LOG="$LOG_DIR/web.log"
WEB_PID_FILE="$LOG_DIR/web.pid"
mkdir -p "$LOG_DIR"
@@ -86,11 +91,16 @@ error() { echo -e "${RED}[dev]${NC} $*"; }
COMMAND="${1:-run}"
FORCE=false
RUN_BACKEND=true
RUN_FRONTEND=true
RUN_FRONTEND=true # macOS
RUN_WEB=false
case "$COMMAND" in
run)
[ "${2:-}" = "--force" ] && FORCE=true
case "${2:-}" in
--force) FORCE=true ;;
--web) RUN_FRONTEND=false; RUN_WEB=true ;;
--all) RUN_WEB=true ;;
esac
;;
seed)
cmd_seed "${2:-}"
@@ -117,14 +127,23 @@ case "$COMMAND" in
COMMAND=run
RUN_BACKEND=false
;;
--web-only)
COMMAND=run
RUN_BACKEND=false
RUN_FRONTEND=false
RUN_WEB=true
;;
stop)
# 优先用 PID 文件
if [ -f "$BACKEND_PID_FILE" ]; then
pid=$(cat "$BACKEND_PID_FILE")
kill -9 "$pid" 2>/dev/null || true
rm -f "$BACKEND_PID_FILE"
fi
# 无论如何,再用 lsof 兜底清理端口(只杀监听 8080 的进程,不杀连接客户端)
if [ -f "$WEB_PID_FILE" ]; then
pid=$(cat "$WEB_PID_FILE")
kill -9 "$pid" 2>/dev/null || true
rm -f "$WEB_PID_FILE"
fi
lsof -ti TCP:8080 -s TCP:LISTEN 2>/dev/null | xargs kill -9 2>/dev/null || true
sleep 0.5
if lsof -ti TCP:8080 -s TCP:LISTEN >/dev/null 2>&1; then
@@ -136,7 +155,7 @@ case "$COMMAND" in
;;
*)
error "未知命令: $COMMAND"
echo "用法: sh scripts/dev.sh [run [--force] | seed <shop> | reset | clear | stop | --backend-only | --frontend-only]"
echo "用法: sh scripts/dev.sh [run [--force|--web|--all] | seed <shop> | reset | clear | stop | --backend-only | --frontend-only | --web-only]"
exit 1
;;
esac
@@ -145,8 +164,13 @@ esac
cleanup() {
echo ""
info "正在关闭服务..."
# 不杀后端:run 模式下后端是持久进程,Ctrl+C 只退出 flutter
info "后端继续在后台运行。如需停止:bash scripts/dev.sh stop"
if [ -f "$WEB_PID_FILE" ]; then
pid=$(cat "$WEB_PID_FILE")
kill "$pid" 2>/dev/null || true
rm -f "$WEB_PID_FILE"
info "Web 服务已停止"
fi
info "后端继续在后台运行。如需停止:sh scripts/dev.sh stop"
exit 0
}
trap cleanup INT TERM
@@ -246,6 +270,25 @@ start_backend() {
wait_backend
}
# ── 启动 Web 前端(后台) ─────────────────────────────────
start_web_bg() {
info "启动 Flutter Web (Chrome,后台)..."
flutter run -d chrome --web-port 8081 >>"$WEB_LOG" 2>&1 &
local pid=$!
echo "$pid" > "$WEB_PID_FILE"
# 等待 Chrome 打开(最多 15 秒)
local i=0
while [ $i -lt 15 ]; do
if grep -q "To hot reload\|Launching lib/main.dart" "$WEB_LOG" 2>/dev/null; then
success "Web 已启动 → http://localhost:8081 (日志: tail -f $WEB_LOG)"
return 0
fi
sleep 1
i=$((i + 1))
done
warn "Web 启动较慢,继续等待... (日志: tail -f $WEB_LOG)"
}
# ════════════════════════════════════════════════════════
check_deps
@@ -253,16 +296,33 @@ if [ "$RUN_BACKEND" = true ]; then
start_backend
fi
if [ "$RUN_FRONTEND" = true ]; then
info "启动 Flutter (macOS)..."
cd "$CLIENT_DIR"
flutter pub get
# --all 模式:先在后台启动 Web,再在前台启动 macOS
if [ "$RUN_WEB" = true ] && [ "$RUN_FRONTEND" = true ]; then
cd "$CLIENT_DIR" && flutter pub get
start_web_bg
echo ""
success "Flutter 启动中,窗口将自动弹出..."
echo -e "${CYAN}提示:关闭 Flutter 窗口或按 q 退出前端,后端继续运行${NC}"
echo -e "${CYAN} 再次执行 bash scripts/dev.sh run 可复用已有后端${NC}"
success "Flutter macOS 启动中..."
echo -e "${CYAN}提示:macOS 窗口 + Chrome 同时运行,Ctrl+C 退出时 Web 同步停止${NC}"
echo ""
flutter run -d macos
elif [ "$RUN_WEB" = true ]; then
cd "$CLIENT_DIR" && flutter pub get
echo ""
success "Chrome 启动中,稍候自动打开..."
echo -e "${CYAN}提示:按 r 热重载,按 q 退出,后端继续在后台运行${NC}"
echo ""
flutter run -d chrome --web-port 8081
elif [ "$RUN_FRONTEND" = true ]; then
cd "$CLIENT_DIR" && flutter pub get
echo ""
success "Flutter macOS 启动中,窗口将自动弹出..."
echo -e "${CYAN}提示:按 q 退出前端,后端继续运行${NC}"
echo -e "${CYAN} 再次执行 sh scripts/dev.sh run 可复用已有后端${NC}"
echo ""
flutter run -d macos
else
echo ""
success "后端运行中,按 Ctrl+C 退出此脚本(后端仍在后台)"