From 23b958753deed773b5a595d985abb2e6a33e9902 Mon Sep 17 00:00:00 2001 From: 24kycj <1637269896@qq.com> Date: Fri, 12 Jun 2026 11:53:18 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 5 +- docker/docker-compose.yml | 5 +- docker/php/Dockerfile.dev | 8 ++ docker/php/entrypoint-dev.sh | 12 +- frontend-web/js/design1/core.js | 121 ++++++++++++------ scripts/start.bat | 218 ++++++++++++++++---------------- 6 files changed, 209 insertions(+), 160 deletions(-) create mode 100644 docker/php/Dockerfile.dev diff --git a/README.md b/README.md index 3fa81bb..45c1ad3 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,12 @@ ```bat docker\sync-config.ps1 :: 同步 local.js 与 backend local.php(改端口后必跑) -scripts\start.bat :: 启动 +scripts\start.bat :: 启动(不销毁已有容器,适合日常开发) scripts\start.bat stop :: 停止(保留数据库) scripts\start.bat reset :: 停止并清空数据库卷 +scripts\start.bat rebuild :: 重建 API 镜像并重启 API 容器 +scripts\start.bat status :: 查看容器与 /health +scripts\start.bat logs :: 跟踪 API 日志 ``` 生产环境编辑 `frontend-web/config/local.js`(从 `config/local.js.example` 复制);全量更新 frontend 时不要覆盖该文件。本地开发由 `docker/sync-config.ps1` 生成 `local.js`。 diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 7c83417..7f023bb 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -24,7 +24,10 @@ services: start_period: 30s api: - image: php:8.2-cli-bookworm + build: + context: ./php + dockerfile: Dockerfile.dev + image: soondesign-api:dev container_name: soondesign-api restart: unless-stopped ports: diff --git a/docker/php/Dockerfile.dev b/docker/php/Dockerfile.dev new file mode 100644 index 0000000..ec19c7f --- /dev/null +++ b/docker/php/Dockerfile.dev @@ -0,0 +1,8 @@ +FROM php:8.2-cli-bookworm + +RUN apt-get update -qq \ + && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends \ + libmariadb-dev-compat $PHPIZE_DEPS \ + && docker-php-ext-install -j"$(nproc)" pdo_mysql \ + && apt-get purge -y --auto-remove $PHPIZE_DEPS \ + && rm -rf /var/lib/apt/lists/* diff --git a/docker/php/entrypoint-dev.sh b/docker/php/entrypoint-dev.sh index 0219d9c..c4bdbea 100644 --- a/docker/php/entrypoint-dev.sh +++ b/docker/php/entrypoint-dev.sh @@ -1,22 +1,13 @@ #!/bin/sh set -e -if ! php -m 2>/dev/null | grep -q pdo_mysql; then - echo "[api] installing pdo_mysql..." - apt-get update -qq - DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends \ - libmariadb-dev-compat $PHPIZE_DEPS - docker-php-ext-install -j"$(nproc)" pdo_mysql - apt-get purge -y --auto-remove $PHPIZE_DEPS - rm -rf /var/lib/apt/lists/* -fi - host="${SOON_DB_HOST:-mysql}" port="${SOON_DB_PORT:-3306}" user="${SOON_DB_USER:-soondesign}" pass="${SOON_DB_PASS:-soondesign}" db="${SOON_DB_NAME:-soondesign}" +echo "[api] waiting for mysql..." i=0 while [ "$i" -lt 60 ]; do if php -r " @@ -41,4 +32,5 @@ done php /docker/php/migrate-dev-schema.php || true php /docker/php/seed-dev-admin.php || true +echo "[api] listening on :8080" exec php -S 0.0.0.0:8080 -t /var/www/public /docker/php/router-dev.php diff --git a/frontend-web/js/design1/core.js b/frontend-web/js/design1/core.js index 11ad56f..91a6ee1 100644 --- a/frontend-web/js/design1/core.js +++ b/frontend-web/js/design1/core.js @@ -409,6 +409,7 @@ function addBackground() { function finishBack(backImg) { if (backImg) { + backImg.objectCaching = true; soonPlaceBgOnCanvas(backImg, '#canvas2'); background_image2 = backImg; window.background_image2 = backImg; @@ -426,6 +427,7 @@ function addBackground() { if (!loadBg) return; loadBg('bg_front_1.png', function (frontImg) { if (!frontImg) return; + frontImg.objectCaching = true; frontImg.scale(1, 1); soonPlaceBgOnCanvas(frontImg, '#canvas1'); background_image1 = frontImg; @@ -436,10 +438,10 @@ function addBackground() { canvas.renderAll(); recordObjs1.push(JSON.stringify(objs1)); recordJson1.push(canvas1.toJSON(TO_JSON_PROPERTIES)); - loadBg('bg_back.png', finishBack, function () { loadBg('bg_back.png', finishBack); }); + loadBg('bg_back.png', finishBack, function () { loadBg('bg_back.png', finishBack, null, 2, true); }, 2, true); }, function () { setTimeout(startLoad, 200); - }); + }, 2, true); } whenReady('#canvas1', startLoad); @@ -1515,7 +1517,20 @@ function handleObjectMoving(e) { obj.set("angle", angleDeg); obj.set("flipped", flipped); } - updateControls(); + soonScheduleUpdateControls(); +} + +var _soonUpdateControlsRaf = 0; +function soonScheduleUpdateControls() { + if (_soonUpdateControlsRaf) return; + _soonUpdateControlsRaf = requestAnimationFrame(function () { + _soonUpdateControlsRaf = 0; + window.updateControls(); + }); +} + +function soonDesign1SetInteracting(on) { + window._soonDesign1Interacting = !!on; } // 辅助函数:检查对象是否被锁定 @@ -1623,17 +1638,28 @@ function initCanvasEvents() { _canvasEventsInitialized = true; [canvas1, canvas2].forEach(currentCanvas => { + var _mouseMoveRaf = 0; + var _mouseMovePending = null; // 1. 鼠标移动 currentCanvas.on("mouse:move", (e) => { - let offsetX = 0, offsetY = 0; - if (currentCanvas === canvas2 && typeof background_image !== 'undefined') { - offsetX = background_image.left; - offsetY = background_image.top; - } - if (e.pointer) { - $("#posText").text("X: " + (e.pointer.x - offsetX).toFixed(0) + " Y: " + (e.pointer.y - offsetY).toFixed(0)); - } - updateRulerGuides(e.e); + _mouseMovePending = { e: e, canvas: currentCanvas }; + if (_mouseMoveRaf) return; + _mouseMoveRaf = requestAnimationFrame(function () { + _mouseMoveRaf = 0; + var payload = _mouseMovePending; + if (!payload) return; + var moveEvent = payload.e; + var moveCanvas = payload.canvas; + let offsetX = 0, offsetY = 0; + if (moveCanvas === canvas2 && typeof background_image !== 'undefined') { + offsetX = background_image.left; + offsetY = background_image.top; + } + if (moveEvent.pointer) { + $("#posText").text("X: " + (moveEvent.pointer.x - offsetX).toFixed(0) + " Y: " + (moveEvent.pointer.y - offsetY).toFixed(0)); + } + updateRulerGuides(moveEvent.e); + }); }); // 2. 鼠标移出 @@ -1726,12 +1752,19 @@ function initCanvasEvents() { return false; } } + soonDesign1SetInteracting(true); handleObjectMoving(e); }); // 6. 变换事件 - currentCanvas.on("object:rotating", updateControls); - currentCanvas.on("object:scaling", updateControls); + currentCanvas.on("object:rotating", function () { + soonDesign1SetInteracting(true); + soonScheduleUpdateControls(); + }); + currentCanvas.on("object:scaling", function () { + soonDesign1SetInteracting(true); + soonScheduleUpdateControls(); + }); currentCanvas.on("object:rotated", () => { window.recordState(); }); currentCanvas.on('text:editing:entered', () => { _textEdit = true; }); currentCanvas.on('text:editing:exited', () => { _textEdit = false; }); @@ -1739,38 +1772,41 @@ function initCanvasEvents() { // 7. 对象修改后 currentCanvas.on('object:modified', function (event) { - if (currentCanvas.getActiveObjects().length > 1) return; - let idx = getIndex(event.target); - if (idx <= 0) return; - let objData = objs[idx - 1]; - // 安全检查:确保 objData 存在且有 type 属性 - if (!objData || typeof objData.type === 'undefined') { + try { + if (currentCanvas.getActiveObjects().length > 1) return; + let idx = getIndex(event.target); + if (idx <= 0) return; + let objData = objs[idx - 1]; + // 安全检查:确保 objData 存在且有 type 属性 + if (!objData || typeof objData.type === 'undefined') { + // 更新对象列表以同步锁状态 + if (typeof window.updateList === 'function') { + window.updateList(); + } + return; + } + let target = event.target; + + if ([3, 4, 10].includes(objData.type)) { + target.fontSize = (target.fontSize * target.scaleX).toFixed(0); + target.scaleX = 1; target.scaleY = 1; + target._clearCache(); + if (objData.type == 3) $("#text_size").val(target.fontSize); + if (objData.type == 4) $("#out_text_size").val(target.fontSize); + if (objData.type == 10) $("#count_size").val(target.fontSize); + currentCanvas.renderAll(); + } else if (objData.type == 5) { + target.width *= target.scaleX; target.height *= target.scaleY; + target.scaleX = 1; target.scaleY = 1; + } + window.recordState(); // 更新对象列表以同步锁状态 if (typeof window.updateList === 'function') { window.updateList(); } - return; - } - let target = event.target; - - if ([3, 4, 10].includes(objData.type)) { - target.fontSize = (target.fontSize * target.scaleX).toFixed(0); - target.scaleX = 1; target.scaleY = 1; - target._clearCache(); - if (objData.type == 3) $("#text_size").val(target.fontSize); - if (objData.type == 4) $("#out_text_size").val(target.fontSize); - if (objData.type == 10) $("#count_size").val(target.fontSize); - currentCanvas.renderAll(); + } finally { + soonDesign1SetInteracting(false); updateControls(); - } else if (objData.type == 5) { - target.width *= target.scaleX; target.height *= target.scaleY; - target.scaleX = 1; target.scaleY = 1; - updateControls(); - } - window.recordState(); - // 更新对象列表以同步锁状态 - if (typeof window.updateList === 'function') { - window.updateList(); } }); // ================================================== @@ -1828,6 +1864,8 @@ function initCanvasEvents() { ctx.fill(); ctx.restore(); } + + if (window._soonDesign1Interacting) return; // 在遮罩之后重新绘制辅助线,确保辅助线在最上层 const objects = currentCanvas.getObjects(); @@ -2042,6 +2080,7 @@ function initRulers() { // --- 3. 绘制标尺 (核心绘制循环) --- function updateAndDraw() { + if (window._soonDesign1Interacting) return; // 每次重新获取尺寸,确保使用最新值 // 使用多种方法尝试获取实际尺寸,确保准确 let currentCanvasWidth = canvasWrapper.width() || canvasWrapper.outerWidth() || canvasWrapper[0].getBoundingClientRect().width || 800; diff --git a/scripts/start.bat b/scripts/start.bat index 0420319..9ac16b3 100644 --- a/scripts/start.bat +++ b/scripts/start.bat @@ -1,29 +1,34 @@ @echo off setlocal EnableDelayedExpansion -rem Double-click (cmd /c): re-launch under cmd /k so the window stays open -if /i not "%~1"=="__run__" ( +set "SELF=%~f0" +if /i "%~1"=="__run__" ( + set "ACTION=%~2" +) else ( + set "ACTION=%~1" echo %CMDCMDLINE% | find /i " /c " >nul if not errorlevel 1 ( - cmd /k "%~f0" __run__ %* + cmd /k "%SELF%" __run__ %* exit /b ) ) -shift set "ROOT=%~dp0.." +for %%I in ("%ROOT%") do set "ROOT=%%~fI" cd /d "%ROOT%" || goto :fail_cd set "COMPOSE=docker\docker-compose.yml" set "ENV_FILE=docker\.env" set "COMPOSE_PROJECT=soondesign" -set "ACTION=%~1" set "WEB_PORT=8100" set "ELECTRON_PORT=8101" set "BACKEND_PORT=8102" if /i "%ACTION%"=="stop" goto :do_stop if /i "%ACTION%"=="reset" goto :do_reset +if /i "%ACTION%"=="rebuild" goto :do_rebuild +if /i "%ACTION%"=="logs" goto :do_logs +if /i "%ACTION%"=="status" goto :do_status goto :do_start :compose @@ -34,6 +39,66 @@ exit /b !ERRORLEVEL! docker compose -p "%COMPOSE_PROJECT%" -f "%COMPOSE%" %* exit /b !ERRORLEVEL! +:load_ports +for /f "usebackq eol=# tokens=1,* delims==" %%A in ("%ENV_FILE%") do ( + if /i "%%A"=="WEB_PORT" set "WEB_PORT=%%B" + if /i "%%A"=="ELECTRON_PORT" set "ELECTRON_PORT=%%B" + if /i "%%A"=="BACKEND_PORT" set "BACKEND_PORT=%%B" +) +exit /b 0 + +:ensure_env +if not exist "%ROOT%\docker\docker-compose.yml" ( + echo [ERROR] Missing docker\docker-compose.yml under %ROOT% + exit /b 1 +) +if not exist "%ROOT%\soonModels\" mkdir "%ROOT%\soonModels" +if not exist "%ENV_FILE%" ( + if not exist "docker\.env.example" ( + echo [ERROR] Missing docker\.env.example + exit /b 1 + ) + copy /Y "docker\.env.example" "%ENV_FILE%" >nul +) +powershell -NoProfile -ExecutionPolicy Bypass -File "docker\sync-config.ps1" -EnvFile "%ENV_FILE%" +if errorlevel 1 ( + echo [ERROR] Config sync failed. + exit /b 1 +) +call :load_ports +exit /b 0 + +:wait_api +echo [INFO] Waiting for API http://localhost:%BACKEND_PORT%/health ... +set /a RETRY=0 +:wait_health +set /a RETRY+=1 +curl.exe -sf "http://localhost:%BACKEND_PORT%/health" >nul 2>&1 +if not errorlevel 1 exit /b 0 +if !RETRY! GEQ 90 ( + echo [WARN] API not ready after ~3 min. Run: scripts\start.bat logs + exit /b 1 +) +set /a MOD=!RETRY! %% 5 +if !MOD!==0 echo ... still waiting (!RETRY!/90) +ping -n 3 127.0.0.1 >nul +goto :wait_health + +:print_urls +echo. +echo ======================================== +echo Project: %COMPOSE_PROJECT% +echo Web: http://localhost:%WEB_PORT%/pages/index.web.html +echo Admin: http://localhost:%WEB_PORT%/pages/admin/index.html +echo Electron: http://localhost:%ELECTRON_PORT%/pages/index.html +echo API: http://localhost:%BACKEND_PORT%/health +echo ======================================== +echo Login: admin@local.test / admin123 +echo Desktop: cd frontend-electron ^&^& npm start +echo Stop: scripts\start.bat stop +echo Logs: scripts\start.bat logs +exit /b 0 + :do_stop if exist "%ENV_FILE%" (call :compose down) else (call :compose_noenv down) echo. @@ -46,6 +111,33 @@ echo. echo [OK] Project %COMPOSE_PROJECT% stopped. DB volume removed. exit /b 0 +:do_logs +docker logs -f --tail 80 soondesign-api +exit /b 0 + +:do_status +call :ensure_env +if errorlevel 1 goto :fail +docker ps --filter name=soondesign --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" +echo. +curl.exe -sf "http://localhost:%BACKEND_PORT%/health" >nul 2>&1 +if errorlevel 1 ( + echo [WARN] API health check failed. +) else ( + echo [OK] API health check passed. +) +exit /b 0 + +:do_rebuild +echo [SoonDesign] Rebuilding API image and restarting... +call :ensure_env +if errorlevel 1 goto :fail +if exist "%ENV_FILE%" (call :compose up -d --build --force-recreate api) else (call :compose_noenv up -d --build --force-recreate api) +if errorlevel 1 goto :fail +call :wait_api +call :print_urls +goto :done_ok + :do_start echo [SoonDesign] Starting local Docker environment... echo [INFO] Compose project: %COMPOSE_PROJECT% @@ -57,121 +149,33 @@ if errorlevel 1 ( goto :fail ) -if not exist "soonModels\" mkdir soonModels - -if not exist "docker\docker-compose.yml" ( - echo [ERROR] Missing docker\docker-compose.yml - goto :fail -) - -if not exist "%ENV_FILE%" ( - if not exist "docker\.env.example" ( - echo [ERROR] Missing docker\.env.example - goto :fail - ) - copy /Y "docker\.env.example" "%ENV_FILE%" >nul -) - -powershell -NoProfile -ExecutionPolicy Bypass -File "docker\sync-config.ps1" -EnvFile "%ENV_FILE%" -if errorlevel 1 ( - echo [ERROR] Config sync failed. - goto :fail -) - -for /f "usebackq eol=# tokens=1,* delims==" %%A in ("%ENV_FILE%") do ( - if /i "%%A"=="WEB_PORT" set "WEB_PORT=%%B" - if /i "%%A"=="ELECTRON_PORT" set "ELECTRON_PORT=%%B" - if /i "%%A"=="BACKEND_PORT" set "BACKEND_PORT=%%B" -) - -echo [INFO] Removing legacy compose project "docker" if present... -docker compose -p docker -f "%COMPOSE%" --env-file "%ENV_FILE%" down >nul 2>&1 -docker network rm docker_default >nul 2>&1 -call :compose down >nul 2>&1 +call :ensure_env +if errorlevel 1 goto :fail echo [INFO] Ports: Web=%WEB_PORT% Electron=%ELECTRON_PORT% API=%BACKEND_PORT% -echo [INFO] Starting containers - project %COMPOSE_PROJECT%... -call :compose up -d -set "DC_EXIT=!ERRORLEVEL!" -if not "!DC_EXIT!"=="0" ( - echo. - echo [ERROR] docker compose failed with exit code !DC_EXIT!. - echo Ensure Docker Desktop is running. - echo First API start may install pdo_mysql for about 1 minute. +echo [INFO] docker compose up -d (keeps running containers; use rebuild for fresh API image) +if exist "%ENV_FILE%" (call :compose up -d) else (call :compose_noenv up -d) +if errorlevel 1 ( + echo [ERROR] docker compose failed. Is Docker Desktop running? goto :fail ) -echo [INFO] Waiting for API - first run may take up to 2 min for pdo_mysql install... -set /a RETRY=0 -:wait_health -set /a RETRY+=1 -curl.exe -sf "http://localhost:%BACKEND_PORT%/health" >nul 2>&1 -if not errorlevel 1 goto :ready -if !RETRY! GEQ 60 goto :show_urls -ping -n 3 127.0.0.1 >nul -goto :wait_health +call :wait_api +if errorlevel 1 goto :show_urls_warn -:ready echo [OK] Backend is ready. - -:show_urls -echo. -echo ======================================== -echo Project: %COMPOSE_PROJECT% -echo Web: http://localhost:%WEB_PORT%/pages/index.web.html -echo Admin: http://localhost:%WEB_PORT%/pages/admin/index.html -echo Electron: http://localhost:%ELECTRON_PORT%/pages/index.html -echo API: http://localhost:%BACKEND_PORT%/health -echo ======================================== -echo Login: admin@local.test / admin123 -echo Stop: scripts\start.bat stop -echo. - -curl.exe -sf "http://localhost:%BACKEND_PORT%/health" >nul 2>&1 -if errorlevel 1 ( - echo [WARN] API not ready yet. Check: docker logs soondesign-api - goto :done_ok -) - curl.exe -sf "http://localhost:%BACKEND_PORT%/api/v1/settings" >nul 2>&1 if errorlevel 1 ( - echo [WARN] API routes not responding. Check: docker logs soondesign-api + echo [WARN] API routes not responding. Run: scripts\start.bat logs ) else ( echo [OK] API routes verified. ) - -start "" "http://localhost:%WEB_PORT%/pages/index.web.html" -start "" "http://localhost:%WEB_PORT%/pages/admin/index.html" -call :launch_electron +call :print_urls goto :done_ok -:launch_electron -where node >nul 2>&1 -if errorlevel 1 ( - echo [WARN] Node.js not found. Open Electron preview in browser instead. - start "" "http://localhost:%ELECTRON_PORT%/pages/index.html" - exit /b 0 -) -if not exist "frontend-electron\package.json" ( - echo [WARN] frontend-electron not found. Opening browser preview. - start "" "http://localhost:%ELECTRON_PORT%/pages/index.html" - exit /b 0 -) -if not exist "frontend-electron\node_modules\electron" ( - echo [INFO] Installing Electron dependencies - first run only... - pushd frontend-electron - call npm install --no-audit --no-fund - if errorlevel 1 ( - popd - echo [WARN] npm install failed. Opening browser preview. - start "" "http://localhost:%ELECTRON_PORT%/pages/index.html" - exit /b 0 - ) - popd -) -echo [INFO] Launching Electron desktop app... -start "SoonDesign Desktop" /D "%CD%\frontend-electron" cmd /c "npm start" -exit /b 0 +:show_urls_warn +call :print_urls +goto :done_ok :fail_cd echo [ERROR] Cannot cd to project root: %ROOT% @@ -184,5 +188,5 @@ exit /b 1 :done_ok echo. -echo [DONE] SoonDesign is running under Docker project %COMPOSE_PROJECT%. +echo [DONE] SoonDesign dev stack is up (project %COMPOSE_PROJECT%). exit /b 0