diff --git a/AGENTS.md b/AGENTS.md
index 50fed89d..e0ca7b3b 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -167,7 +167,7 @@
- 云数据库连接与系统数据库配置必须分开。显式开启 `UPSTREAM_STORED_CONFIG_ENABLED` 后,云数据库密码在 `system_database_connections` 中同时保留旧版 `APP_KEY` 密文用于紧急回滚,并使用系统数据库登录密码派生的密钥写入 `shared_password` 供受信任开发机共享;真实值仍不得进入代码、提交、PR、共享文档或日志。系统数据库固定主机、端口、库名和账号以密文团队资料保存在代码中,不含数据库密码;新人页面只允许填写系统数据库密码,由它解锁固定资料并自动启用 TLS。禁止恢复主机、端口、账号、TLS、证书或 CA 路径等新人表单。系统数据库密码轮换前必须先完成共享云数据库密文重加密、数据库备份和团队连接资料密文更新。只有隔离排错或尚未迁移的旧环境才允许显式开启本机覆盖,并通过私有 `DB_<来源>_*` 环境变量提供完整连接。中继开发机只保留已收紧 ACL 的隧道端点配置,不保存数据库密码。
- 本地开发的标准入口只有“系统设置 → 数据库管理 → 系统数据库”。开发者只填写并验证同一套系统数据库密码,其他连接参数由项目自动设置;成功后系统用户、侧边栏、系统设置和云数据库登记必须继续使用该系统数据库。不得再把主业务连接的 `DB_HOST` 改回本机 MySQL 容器,否则会形成一套互不相干的本地数据,导致用户、配置、来源映射和业务结果与团队不一致。
- 两个现有来源的标识和数据库映射固定为:`jiansun` 对应 `JianSun` 旧业务库,`operations_replica` 对应 `JSReportReplica` 的 `rpt` 只读视图。不得把 `shujuku`、本机测试库名或历史截图中的临时名称写回共享记录,也不得交换两个来源的账号或数据库名。
-- 开发者拉取包含本规则的最新代码后,必须确保 `docker-compose.local.yml` 已挂载 `./modules:/var/www/html/modules`,并至少重建一次应用容器、清除 Laravel 配置缓存。禁止只重启旧镜像后根据旧模块结果判断共享配置无效。
+- 开发者拉取包含本规则的最新代码后,由 Codex 运行不带参数的 `deploy/local-init.sh`。脚本根据 Dockerfile、依赖锁文件、前端源码和静态资源计算镜像指纹,构建输入变化或必要静态资源缺失时自动重建,并清除 Laravel 配置缓存;不得要求新人判断是否使用 `--build`。`docker-compose.local.yml` 必须继续挂载 `./modules:/var/www/html/modules`,禁止只重启旧镜像后根据旧模块或旧静态资源判断主线无效。
- 本机临时引导库的新增迁移由 `deploy/local-init.sh` 和新版容器入口自动补齐;该逻辑只允许在 `SYSTEM_DATABASE_MODE=bootstrap`、MySQL 驱动且主机为固定容器服务名 `mysql` 时运行。连接共享系统数据库后必须自动跳过,普通开发者不需要也不得自行执行 `php artisan migrate`;看到本机迁移 `Pending` 时应重新运行标准初始化脚本,而不是操作共享数据库。
- Codex 排查开发机连接时,必须先确认当前主业务库仍是共享系统数据库,再确认 `UPSTREAM_STORED_CONFIG_ENABLED=true`、`UPSTREAM_LOCAL_CONFIG_ENABLED=false`,随后通过 `UpstreamSourceRegistry` 验证完整连接来自 `system_database_connections`。不得通过填写本机 `DB_JIANSUN_*`、`DB_OPERATIONS_REPLICA_*`、复制他人 `.env` 或统一 `APP_KEY` 来掩盖共享配置读取错误。
- 系统数据库统一保存应用数据、云数据库配置、上游数据字典和生产共享快照,但不是 SQL Server 网络代理。本地开发应用直接使用系统数据库中的连接资料访问两套上游;前提是该开发机网络本身能够到达数据库端点。
diff --git a/LOCAL_DEPLOY.md b/LOCAL_DEPLOY.md
index 16cf2e69..478a6ce8 100644
--- a/LOCAL_DEPLOY.md
+++ b/LOCAL_DEPLOY.md
@@ -38,7 +38,7 @@ http://127.0.0.1:18088
下面的命令由 Codex 在 Git Bash、WSL、macOS 或 Linux 终端中执行,不交给新人:
```bash
-bash deploy/local-init.sh --build
+bash deploy/local-init.sh
```
脚本会:
@@ -54,7 +54,7 @@ bash deploy/local-init.sh --build
这个账号只存在于本机临时数据库中。系统数据库切换完成后,请使用系统数据库里属于自己的账号登录,不要继续把临时账号当作团队共享账号。
-初始化中断后,Codex 直接重新执行 `bash deploy/local-init.sh`,脚本会复用已有镜像并从数据库初始化阶段继续。只有依赖、Dockerfile 或构建资源发生变化时才使用 `--build`;确认只启动现有镜像时可使用 `--skip-build`。
+初始化中断后,Codex 直接重新执行 `bash deploy/local-init.sh`。脚本会根据 Dockerfile、依赖锁文件、前端源码和静态资源生成镜像指纹:输入未变化时复用镜像,发生变化或缺少必要静态资源时自动重建,并从数据库初始化阶段继续。`--build` 只用于强制重建;`--skip-build` 仅允许镜像指纹和静态资源均已通过校验时使用。
拉取新代码后再次运行该脚本时,本机临时引导库会自动更新;连接团队共享系统数据库后,这项自动更新会安全跳过。普通开发者任何时候都不需要执行 `php artisan migrate`,也不得对团队共享系统数据库自行迁移。
@@ -134,7 +134,7 @@ docker compose -f docker-compose.local.yml exec -u www-data app php artisan opti
# 检查应用是否真正就绪
docker compose -f docker-compose.local.yml exec -u www-data app php artisan app:ready
-# 运行测试(首次需使用 local-init.sh --build 构建开发依赖)
+# 运行测试(首次运行 local-init.sh 会自动构建开发依赖)
docker compose -f docker-compose.local.yml exec -u www-data app php artisan test --do-not-cache-result
# 停止容器,但保留本地数据
@@ -149,7 +149,7 @@ docker compose -f docker-compose.local.yml start
## 修改后的生效方式
- PHP、配置、模块、页面模板和路由目录已经挂载到容器,通常清理缓存或重启应用即可;
-- Composer 依赖、前端依赖、Dockerfile 或构建资源发生变化时,执行 `bash deploy/local-init.sh --build`;
+- Composer 依赖、前端依赖、Dockerfile 或构建资源发生变化时,重新执行 `bash deploy/local-init.sh`,脚本会自动重建;
- 修改 `runtime/.env` 后,执行:
```bash
diff --git a/README.md b/README.md
index 172dfd62..00c1d65f 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
1. 启动 Docker Desktop;
2. 在系统数据库页面输入负责人提供的系统数据库密码。
-其余工作交给 Codex:拉取最新 `main`、执行 `bash deploy/local-init.sh --build`、处理本机引导库迁移、启动服务、清理缓存、打开 `http://127.0.0.1:18088`,并在连接后自动核对两套上游、数据字典和真实只读查询。Codex 不得把这些命令、迁移或排错步骤转交给新人执行。
+其余工作交给 Codex:拉取最新 `main`、执行 `bash deploy/local-init.sh`、处理本机引导库迁移、启动服务、清理缓存、打开 `http://127.0.0.1:18088`,并在连接后自动核对两套上游、数据字典和真实只读查询。初始化脚本会比较当前构建输入与镜像指纹,并校验必要静态资源;需要更新时自动重建,不再要求新人判断是否添加 `--build`。Codex 不得把这些命令、迁移或排错步骤转交给新人执行。
首次登录账号由初始化脚本生成并保存在本机私有凭据文件中。连接系统数据库成功后,临时登录会自动失效,改用系统数据库中属于自己的正式账号登录。
diff --git a/config/apexcharts.php b/config/apexcharts.php
index 03fcf078..1f3f5101 100644
--- a/config/apexcharts.php
+++ b/config/apexcharts.php
@@ -13,7 +13,7 @@ return [
'zoom' => [
'enabled' => false,
],
- 'fontFamily' => '"Quicksand", sans-serif',
+ 'fontFamily' => '"JetBrains Mono", "Microsoft YaHei", "PingFang SC", monospace',
'foreColor' => '#595959',
],
diff --git a/deploy/Dockerfile.local b/deploy/Dockerfile.local
index feb832b7..2f1383b5 100644
--- a/deploy/Dockerfile.local
+++ b/deploy/Dockerfile.local
@@ -1,3 +1,5 @@
+# syntax=docker/dockerfile:1.7
+
# 本地部署用 Dockerfile
# 基于 deploy/Dockerfile,额外安装 composer 依赖与前端构建产物
# 由 docker-compose.local.yml 使用
@@ -66,29 +68,47 @@ RUN ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \
WORKDIR /var/www/html
+# ---------------- 安装依赖 ----------------
+# 锁文件先于源码复制,普通 PHP/Blade 改动不会让 Composer 和 npm 重新下载。
+COPY composer.json composer.lock ./
+RUN --mount=type=cache,target=/tmp/composer-cache \
+ COMPOSER_CACHE_DIR=/tmp/composer-cache composer install --prefer-dist \
+ --no-interaction \
+ --no-scripts \
+ --no-progress \
+ --no-ansi
+
+COPY package.json package-lock.json ./
+RUN --mount=type=cache,target=/root/.npm \
+ npm ci --no-audit --no-fund --legacy-peer-deps
+
# ---------------- 复制源码 ----------------
COPY . /var/www/html
COPY deploy/apache-vhost.conf /etc/apache2/sites-available/000-default.conf
COPY deploy/php.ini /usr/local/etc/php/conf.d/jiangling.ini
COPY deploy/entrypoint.sh /usr/local/bin/jiangling-entrypoint
+COPY deploy/local-healthcheck.sh /usr/local/bin/jiangling-healthcheck
-# ---------------- 安装 PHP 依赖(本地开发模式,包含 PHPUnit 等测试工具)----------------
-RUN composer install --prefer-dist \
- --no-interaction \
- --no-scripts \
- --no-progress \
- --no-ansi \
- && composer dump-autoload --no-scripts --optimize
+RUN composer dump-autoload --no-scripts --optimize
# ---------------- 构建前端资源 ----------------
-# 先装依赖(python3/make/g++ 让 node-sass 能编译)
-RUN npm install --no-audit --no-fund --legacy-peer-deps
# 提升 node 堆内存上限;dev 模式无压缩,避免 mix/webpack 生产构建 OOM
ENV NODE_OPTIONS="--max-old-space-size=2048"
-RUN npm run dev
+RUN npm run dev \
+ && mkdir -p public/vendor/apexcharts public/vendor/js-cookie \
+ && cp node_modules/apexcharts/dist/apexcharts.min.js \
+ public/vendor/apexcharts/apexcharts.min.js \
+ && cp node_modules/js-cookie/dist/js.cookie.min.js \
+ public/vendor/js-cookie/js.cookie.js \
+ && test -s public/js/manifest.js \
+ && test -s public/js/vendor.js \
+ && test -s public/css/app.css \
+ && test -s public/mix-manifest.json \
+ && test -s public/vendor/apexcharts/apexcharts.min.js \
+ && test -s public/vendor/js-cookie/js.cookie.js
# ---------------- 目录与权限 ----------------
-RUN chmod +x /usr/local/bin/jiangling-entrypoint \
+RUN chmod +x /usr/local/bin/jiangling-entrypoint /usr/local/bin/jiangling-healthcheck \
&& mkdir -p \
storage/app/public \
storage/app/temp \
@@ -100,5 +120,10 @@ RUN chmod +x /usr/local/bin/jiangling-entrypoint \
bootstrap/cache \
&& chown -R www-data:www-data storage bootstrap/cache public
+ARG LOCAL_BUILD_FINGERPRINT=unknown
+
+LABEL com.caiwu.project="CaiWuXiTong" \
+ com.caiwu.local-build-fingerprint="$LOCAL_BUILD_FINGERPRINT"
+
ENTRYPOINT ["jiangling-entrypoint"]
CMD ["apache2-foreground"]
diff --git a/deploy/apache-vhost.conf b/deploy/apache-vhost.conf
index c3c696dd..12452556 100644
--- a/deploy/apache-vhost.conf
+++ b/deploy/apache-vhost.conf
@@ -9,6 +9,13 @@ ServerName localhost
Require all granted
+ # 静态文件缺失时必须快速返回 404,不能交给 Laravel 触发登录重定向、
+ # 数据库连接和整页重复渲染。
+ RewriteEngine On
+ RewriteCond %{REQUEST_URI} ^/(public/|vendor/livewire/)
+ RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} !-f
+ RewriteRule ^ - [R=404,L]
+
SetEnvIf Authorization "(.+)" HTTP_AUTHORIZATION=$1
ErrorLog ${APACHE_LOG_DIR}/error.log
diff --git a/deploy/local-healthcheck.sh b/deploy/local-healthcheck.sh
new file mode 100644
index 00000000..3e11222c
--- /dev/null
+++ b/deploy/local-healthcheck.sh
@@ -0,0 +1,21 @@
+#!/bin/sh
+set -eu
+
+for required_asset in \
+ public/js/manifest.js \
+ public/js/vendor.js \
+ public/css/app.css \
+ public/mix-manifest.json \
+ public/vendor/apexcharts/apexcharts.min.js \
+ public/vendor/js-cookie/js.cookie.js
+do
+ test -s "$required_asset"
+done
+
+runuser -u www-data -- php artisan app:ready --quiet
+
+# Artisan 能先于 Apache 启动完成;必须同时确认真实 HTTP 入口已经响应,
+# 防止初始化脚本在容器“看似健康”时过早访问并得到空响应。
+curl --fail --silent --show-error --max-time 5 \
+ http://127.0.0.1:18088/auth/login \
+ >/dev/null
diff --git a/deploy/local-init.sh b/deploy/local-init.sh
index b5554ffa..ff012e3f 100644
--- a/deploy/local-init.sh
+++ b/deploy/local-init.sh
@@ -64,6 +64,55 @@ set_env_value() {
chmod 600 "$environment_file" 2>/dev/null || true
}
+calculate_local_image_fingerprint() {
+ {
+ printf '%s\0' \
+ .dockerignore \
+ composer.json \
+ composer.lock \
+ package.json \
+ package-lock.json \
+ webpack.mix.js \
+ tailwind.config.js \
+ presets.js \
+ safelist.txt \
+ manifest.json \
+ serviceworker.js \
+ deploy/Dockerfile.local \
+ deploy/apache-vhost.conf \
+ deploy/php.ini \
+ deploy/entrypoint.sh \
+ deploy/local-healthcheck.sh
+
+ find resources/assets resources/views resources/lang -type f -print0
+ find public -type f \
+ ! -path 'public/js/*' \
+ ! -path 'public/vendor/*' \
+ ! -path 'public/css/app.css' \
+ ! -name 'mix-manifest.json' \
+ -print0
+ } \
+ | sort -z \
+ | xargs -0 sha256sum \
+ | sha256sum \
+ | awk '{ print $1 }'
+}
+
+image_has_required_assets() {
+ docker run --rm --entrypoint sh "$app_image" -lc '
+ for required_asset in \
+ /var/www/html/public/js/manifest.js \
+ /var/www/html/public/js/vendor.js \
+ /var/www/html/public/css/app.css \
+ /var/www/html/public/mix-manifest.json \
+ /var/www/html/public/vendor/apexcharts/apexcharts.min.js \
+ /var/www/html/public/vendor/js-cookie/js.cookie.js
+ do
+ test -s "$required_asset" || exit 1
+ done
+ ' >/dev/null 2>&1
+}
+
echo "==> 工作目录: $(pwd)"
if ! command -v docker >/dev/null 2>&1; then
@@ -118,14 +167,81 @@ if docker image inspect "$app_image" >/dev/null 2>&1; then
image_exists=1
fi
-if [ "$build_mode" = "build" ] || { [ "$build_mode" = "auto" ] && [ "$image_exists" -eq 0 ]; }; then
- echo "==> [1/5] 构建本地开发镜像..."
- docker compose -f "$compose_file" build app
-elif [ "$build_mode" = "skip" ] && [ "$image_exists" -eq 0 ]; then
- echo "错误:本地开发镜像不存在,不能使用 --skip-build。" >&2
+local_image_fingerprint="$(calculate_local_image_fingerprint)"
+stored_image_fingerprint=""
+image_contract_ok=0
+
+if [ "$image_exists" -eq 1 ]; then
+ stored_image_fingerprint="$(
+ docker image inspect "$app_image" \
+ --format '{{ index .Config.Labels "com.caiwu.local-build-fingerprint" }}' \
+ 2>/dev/null \
+ || true
+ )"
+
+ if image_has_required_assets; then
+ image_contract_ok=1
+ fi
+fi
+
+build_reason=""
+if [ "$build_mode" = "build" ]; then
+ build_reason="已要求强制构建"
+elif [ "$image_exists" -eq 0 ]; then
+ build_reason="本地镜像不存在"
+elif [ "$image_contract_ok" -ne 1 ]; then
+ build_reason="现有镜像缺少必要静态资源"
+elif [ "$stored_image_fingerprint" != "$local_image_fingerprint" ]; then
+ build_reason="前端、依赖或运行镜像配置已经变化"
+fi
+
+if [ "$build_mode" = "skip" ] && [ -n "$build_reason" ]; then
+ echo "错误:$build_reason,不能安全使用 --skip-build。" >&2
+ echo "请不带参数重新运行本脚本,由系统自动更新镜像。" >&2
exit 1
+fi
+
+if [ -n "$build_reason" ]; then
+ build_attempt=1
+ while [ "$build_attempt" -le 2 ]; do
+ local_image_fingerprint="$(calculate_local_image_fingerprint)"
+ echo "==> [1/5] 构建本地开发镜像:$build_reason。"
+ docker compose -f "$compose_file" build \
+ --build-arg "LOCAL_BUILD_FINGERPRINT=$local_image_fingerprint" \
+ app
+
+ built_image_fingerprint="$(
+ docker image inspect "$app_image" \
+ --format '{{ index .Config.Labels "com.caiwu.local-build-fingerprint" }}'
+ )"
+ current_image_fingerprint="$(calculate_local_image_fingerprint)"
+
+ if [ "$current_image_fingerprint" != "$local_image_fingerprint" ]; then
+ if [ "$build_attempt" -lt 2 ]; then
+ echo " 构建期间检测到源码变化,自动按最新源码再构建一次。"
+ build_reason="源码在构建期间发生变化"
+ build_attempt=$((build_attempt + 1))
+ continue
+ fi
+
+ echo "错误:构建期间源码持续变化,请等待当前文件修改完成后重试。" >&2
+ exit 1
+ fi
+
+ if [ "$built_image_fingerprint" != "$local_image_fingerprint" ]; then
+ echo "错误:本地镜像版本标识与当前源码不一致。" >&2
+ exit 1
+ fi
+
+ if ! image_has_required_assets; then
+ echo "错误:本地镜像缺少必要静态资源,请查看上方构建日志。" >&2
+ exit 1
+ fi
+
+ break
+ done
else
- echo "==> [1/5] 复用现有本地开发镜像。需要更新依赖时请使用 --build。"
+ echo "==> [1/5] 本地开发镜像与当前源码一致,直接复用。"
fi
echo "==> [2/5] 启动本地容器..."
diff --git a/docker-compose.local.yml b/docker-compose.local.yml
index 16dd4b63..0466626e 100644
--- a/docker-compose.local.yml
+++ b/docker-compose.local.yml
@@ -3,7 +3,7 @@
# - mysql 只用于首次登录和填写“系统数据库”前的临时引导
# - 保存系统数据库后,用户、设置和云数据库登记都来自共享系统数据库
#
-# 首次启动:bash deploy/local-init.sh --build
+# 标准启动:bash deploy/local-init.sh(自动判断是否需要重建镜像)
# 停止: docker compose -f docker-compose.local.yml down
# 日志: docker compose -f docker-compose.local.yml logs -f app
#
@@ -35,7 +35,7 @@ services:
networks:
- jiangling_net
healthcheck:
- test: ["CMD", "runuser", "-u", "www-data", "--", "php", "artisan", "app:ready", "--quiet"]
+ test: ["CMD", "jiangling-healthcheck"]
interval: 15s
timeout: 5s
retries: 10
diff --git a/docs/开发与部署SOP.md b/docs/开发与部署SOP.md
index 45beb204..26b37dda 100644
--- a/docs/开发与部署SOP.md
+++ b/docs/开发与部署SOP.md
@@ -289,7 +289,7 @@ JIANSUN_SLOW_QUERY_COOLDOWN=300
本地首次启动时页面必须明确显示“本地临时引导库”,不能把本机 MySQL 标成财务系统配置数据库。开发者连接共享系统数据库时只输入密码;系统自动解锁固定地址、端口、库名、账号和 TLS 配置,只检查数据库能够连接且基础系统表存在。迁移版本、管理员权限映射、公司映射和云数据库共享密码均不得作为连接拦截条件。连接成功后立即清除临时账号 Session,并要求使用共享库正式账号重新登录;`super_admin` 和 `admin` 角色即使缺少单项权限关联,也不得被后台权限中间件阻挡。
-开发者拉取新代码后,`deploy/local-init.sh` 和新版容器入口必须自动补齐本机临时引导库的新增迁移,不能把 `Pending` 状态交给新人处理。该自动逻辑必须同时校验 `SYSTEM_DATABASE_MODE=bootstrap`、MySQL 驱动和固定容器主机名 `mysql`;处于共享系统数据库模式时必须直接跳过,普通开发者不得执行或被提示执行共享数据库迁移。
+开发者拉取新代码后,由 Codex 直接运行不带参数的 `deploy/local-init.sh`。脚本必须根据 Dockerfile、依赖锁文件、前端源码和静态资源计算镜像指纹,必要静态资源缺失或构建输入变化时自动重建;不得要求新人判断是否使用 `--build`。脚本和新版容器入口还必须自动补齐本机临时引导库的新增迁移,不能把 `Pending` 状态交给新人处理。迁移逻辑必须同时校验 `SYSTEM_DATABASE_MODE=bootstrap`、MySQL 驱动和固定容器主机名 `mysql`;处于共享系统数据库模式时必须直接跳过,普通开发者不得执行或被提示执行共享数据库迁移。
系统数据库连接测试从应用容器发起,但新人页面只显示密码输入框。主机、端口、库名、账号和 TLS 均来自使用系统数据库密码加密的团队固定连接资料;不得把这些高级连接项重新暴露给普通开发者。密码错误必须回到原页面显示中文提示,网络或数据库暂不可用也必须保留旧连接并显示中文原因,不得出现 500。
diff --git a/public/css/third_party/vue-html-editor_custom.css b/public/css/third_party/vue-html-editor_custom.css
index e8ad299c..995679d8 100644
--- a/public/css/third_party/vue-html-editor_custom.css
+++ b/public/css/third_party/vue-html-editor_custom.css
@@ -3,7 +3,7 @@
}
.ql-container {
- font-family: "Quicksand", sans-serif !important;
+ font-family: "JetBrains Mono", "Microsoft YaHei", "PingFang SC", monospace !important;
}
.ql-editor p {
@@ -285,4 +285,4 @@
.ql-snow .ql-hidden {
display: none !important;
-}
\ No newline at end of file
+}
diff --git a/resources/views/components/charts/apexcharts-loader.blade.php b/resources/views/components/charts/apexcharts-loader.blade.php
new file mode 100644
index 00000000..1c77cc52
--- /dev/null
+++ b/resources/views/components/charts/apexcharts-loader.blade.php
@@ -0,0 +1,5 @@
+@once
+ @push('chart_libraries')
+
+ @endpush
+@endonce
diff --git a/resources/views/components/layouts/admin/head.blade.php b/resources/views/components/layouts/admin/head.blade.php
index 4d41b944..7d2b3fb9 100644
--- a/resources/views/components/layouts/admin/head.blade.php
+++ b/resources/views/components/layouts/admin/head.blade.php
@@ -31,9 +31,6 @@
-
-
-
diff --git a/resources/views/components/layouts/admin/scripts.blade.php b/resources/views/components/layouts/admin/scripts.blade.php
index 564e27f8..9f0e317c 100644
--- a/resources/views/components/layouts/admin/scripts.blade.php
+++ b/resources/views/components/layouts/admin/scripts.blade.php
@@ -8,7 +8,7 @@
@stack('scripts_start')
-
+@stack('chart_libraries')
@stack('charts')
diff --git a/resources/views/components/layouts/auth/head.blade.php b/resources/views/components/layouts/auth/head.blade.php
index 8a690aaf..64cd7aa6 100644
--- a/resources/views/components/layouts/auth/head.blade.php
+++ b/resources/views/components/layouts/auth/head.blade.php
@@ -24,9 +24,6 @@
-
-
-
@@ -58,4 +55,3 @@
@stack('head_end')
-
diff --git a/resources/views/components/layouts/error/head.blade.php b/resources/views/components/layouts/error/head.blade.php
index 22378451..3454d1fb 100644
--- a/resources/views/components/layouts/error/head.blade.php
+++ b/resources/views/components/layouts/error/head.blade.php
@@ -22,9 +22,6 @@
-
-
-
diff --git a/resources/views/components/layouts/install/head.blade.php b/resources/views/components/layouts/install/head.blade.php
index 22378451..3454d1fb 100644
--- a/resources/views/components/layouts/install/head.blade.php
+++ b/resources/views/components/layouts/install/head.blade.php
@@ -22,9 +22,6 @@
-
-
-
diff --git a/resources/views/components/layouts/maintenance/head.blade.php b/resources/views/components/layouts/maintenance/head.blade.php
index 6f9c62e3..fe7d32ca 100644
--- a/resources/views/components/layouts/maintenance/head.blade.php
+++ b/resources/views/components/layouts/maintenance/head.blade.php
@@ -12,7 +12,6 @@
-
@stack('css')
diff --git a/resources/views/components/layouts/modules/head.blade.php b/resources/views/components/layouts/modules/head.blade.php
index 4d814158..06f88a74 100644
--- a/resources/views/components/layouts/modules/head.blade.php
+++ b/resources/views/components/layouts/modules/head.blade.php
@@ -24,9 +24,6 @@
-
-
-
diff --git a/resources/views/components/layouts/portal/head.blade.php b/resources/views/components/layouts/portal/head.blade.php
index db89e9ee..be03e139 100644
--- a/resources/views/components/layouts/portal/head.blade.php
+++ b/resources/views/components/layouts/portal/head.blade.php
@@ -24,9 +24,6 @@
-
-
-
diff --git a/resources/views/components/layouts/portal/scripts.blade.php b/resources/views/components/layouts/portal/scripts.blade.php
index 79c7ba88..44cbe7f8 100644
--- a/resources/views/components/layouts/portal/scripts.blade.php
+++ b/resources/views/components/layouts/portal/scripts.blade.php
@@ -8,7 +8,7 @@
@stack('scripts_start')
-
+@stack('chart_libraries')
@stack('charts')
diff --git a/resources/views/components/layouts/preview/head.blade.php b/resources/views/components/layouts/preview/head.blade.php
index db89e9ee..be03e139 100644
--- a/resources/views/components/layouts/preview/head.blade.php
+++ b/resources/views/components/layouts/preview/head.blade.php
@@ -24,9 +24,6 @@
-
-
-
diff --git a/resources/views/components/layouts/print/scripts.blade.php b/resources/views/components/layouts/print/scripts.blade.php
index 8f70ce84..b4558be4 100644
--- a/resources/views/components/layouts/print/scripts.blade.php
+++ b/resources/views/components/layouts/print/scripts.blade.php
@@ -8,7 +8,7 @@
@stack('scripts_start')
-
+@stack('chart_libraries')
@stack('charts')
diff --git a/resources/views/components/layouts/signed/head.blade.php b/resources/views/components/layouts/signed/head.blade.php
index db89e9ee..be03e139 100644
--- a/resources/views/components/layouts/signed/head.blade.php
+++ b/resources/views/components/layouts/signed/head.blade.php
@@ -24,9 +24,6 @@
-
-
-
diff --git a/resources/views/components/layouts/wizard/head.blade.php b/resources/views/components/layouts/wizard/head.blade.php
index e627c159..3b77dfa6 100644
--- a/resources/views/components/layouts/wizard/head.blade.php
+++ b/resources/views/components/layouts/wizard/head.blade.php
@@ -20,9 +20,6 @@
-
-
-
diff --git a/resources/views/components/reports/summary/chart.blade.php b/resources/views/components/reports/summary/chart.blade.php
index 824e38aa..c1cc5ef3 100644
--- a/resources/views/components/reports/summary/chart.blade.php
+++ b/resources/views/components/reports/summary/chart.blade.php
@@ -37,6 +37,8 @@
+@include('components.charts.apexcharts-loader')
+
@push('body_scripts')
{!! $charts['bar']->script() !!}
{!! $charts['donut']->script() !!}
diff --git a/resources/views/widgets/bar_chart.blade.php b/resources/views/widgets/bar_chart.blade.php
index c32c4aa0..f55e4af2 100644
--- a/resources/views/widgets/bar_chart.blade.php
+++ b/resources/views/widgets/bar_chart.blade.php
@@ -8,6 +8,8 @@
+@include('components.charts.apexcharts-loader')
+
@push('body_scripts')
{!! $chart->script() !!}
@endpush
diff --git a/resources/views/widgets/cash_flow.blade.php b/resources/views/widgets/cash_flow.blade.php
index 6fad8b80..45283b85 100644
--- a/resources/views/widgets/cash_flow.blade.php
+++ b/resources/views/widgets/cash_flow.blade.php
@@ -48,6 +48,8 @@
+@include('components.charts.apexcharts-loader')
+
@push('body_scripts')
{!! $chart->script() !!}
@endpush
diff --git a/resources/views/widgets/donut_chart.blade.php b/resources/views/widgets/donut_chart.blade.php
index 013d10f4..0a603e55 100644
--- a/resources/views/widgets/donut_chart.blade.php
+++ b/resources/views/widgets/donut_chart.blade.php
@@ -8,6 +8,8 @@
+@include('components.charts.apexcharts-loader')
+
@push('body_scripts')
{!! $chart->script() !!}
@endpush
diff --git a/resources/views/widgets/line_chart.blade.php b/resources/views/widgets/line_chart.blade.php
index 9ab1fbf1..ce1c50f9 100644
--- a/resources/views/widgets/line_chart.blade.php
+++ b/resources/views/widgets/line_chart.blade.php
@@ -8,6 +8,8 @@
+@include('components.charts.apexcharts-loader')
+
@push('body_scripts')
{!! $chart->script() !!}
@endpush
diff --git a/tests/Unit/Deployment/LocalStaticAssetsTest.php b/tests/Unit/Deployment/LocalStaticAssetsTest.php
new file mode 100644
index 00000000..b1613839
--- /dev/null
+++ b/tests/Unit/Deployment/LocalStaticAssetsTest.php
@@ -0,0 +1,187 @@
+assertIsString($dockerfile);
+ $this->assertStringContainsString(
+ 'com.caiwu.local-build-fingerprint="$LOCAL_BUILD_FINGERPRINT"',
+ $dockerfile
+ );
+ $this->assertStringContainsString(
+ 'node_modules/apexcharts/dist/apexcharts.min.js',
+ $dockerfile
+ );
+ $this->assertStringContainsString(
+ 'node_modules/js-cookie/dist/js.cookie.min.js',
+ $dockerfile
+ );
+
+ $this->assertIsString($healthcheck);
+ foreach ([
+ 'public/js/manifest.js',
+ 'public/js/vendor.js',
+ 'public/css/app.css',
+ 'public/mix-manifest.json',
+ 'public/vendor/apexcharts/apexcharts.min.js',
+ 'public/vendor/js-cookie/js.cookie.js',
+ ] as $requiredAsset) {
+ $this->assertStringContainsString($requiredAsset, $healthcheck);
+ }
+ $this->assertStringContainsString(
+ 'runuser -u www-data -- php artisan app:ready --quiet',
+ $healthcheck
+ );
+ $this->assertStringContainsString(
+ 'http://127.0.0.1:18088/auth/login',
+ $healthcheck
+ );
+ }
+
+ public function test_local_initializer_rebuilds_stale_or_incomplete_images(): void
+ {
+ $initializer = file_get_contents(base_path('deploy/local-init.sh'));
+
+ $this->assertIsString($initializer);
+ $this->assertStringContainsString(
+ 'calculate_local_image_fingerprint',
+ $initializer
+ );
+ $this->assertStringContainsString(
+ 'image_has_required_assets',
+ $initializer
+ );
+ $this->assertStringContainsString(
+ '现有镜像缺少必要静态资源',
+ $initializer
+ );
+ $this->assertStringContainsString(
+ '--build-arg "LOCAL_BUILD_FINGERPRINT=$local_image_fingerprint"',
+ $initializer
+ );
+ $this->assertStringContainsString(
+ '构建期间检测到源码变化,自动按最新源码再构建一次',
+ $initializer
+ );
+ $this->assertStringContainsString(
+ '本地镜像缺少必要静态资源,请查看上方构建日志',
+ $initializer
+ );
+ }
+
+ public function test_missing_static_files_are_not_routed_through_laravel(): void
+ {
+ $apache = file_get_contents(base_path('deploy/apache-vhost.conf'));
+
+ $this->assertIsString($apache);
+ $this->assertStringContainsString(
+ 'RewriteCond %{REQUEST_URI} ^/(public/|vendor/livewire/)',
+ $apache
+ );
+ $this->assertStringContainsString(
+ 'RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} !-f',
+ $apache
+ );
+ $this->assertStringContainsString(
+ 'RewriteRule ^ - [R=404,L]',
+ $apache
+ );
+ }
+
+ public function test_apexcharts_is_loaded_only_by_chart_views(): void
+ {
+ foreach ([
+ 'admin',
+ 'portal',
+ 'print',
+ ] as $layout) {
+ $scripts = file_get_contents(
+ resource_path("views/components/layouts/{$layout}/scripts.blade.php")
+ );
+
+ $this->assertIsString($scripts);
+ $this->assertStringContainsString("@stack('chart_libraries')", $scripts);
+ $this->assertStringNotContainsString(
+ "asset('public/vendor/apexcharts/apexcharts.min.js')",
+ $scripts
+ );
+ }
+
+ $loader = file_get_contents(
+ resource_path('views/components/charts/apexcharts-loader.blade.php')
+ );
+
+ $this->assertIsString($loader);
+ $this->assertStringContainsString(
+ "asset('public/vendor/apexcharts/apexcharts.min.js')",
+ $loader
+ );
+
+ $renderedLoader = Blade::render(<<<'BLADE'
+ @include('components.charts.apexcharts-loader')
+ @include('components.charts.apexcharts-loader')
+ @stack('chart_libraries')
+ BLADE);
+
+ $this->assertSame(
+ 1,
+ substr_count($renderedLoader, 'public/vendor/apexcharts/apexcharts.min.js')
+ );
+
+ foreach ([
+ 'views/widgets/line_chart.blade.php',
+ 'views/widgets/donut_chart.blade.php',
+ 'views/widgets/cash_flow.blade.php',
+ 'views/widgets/bar_chart.blade.php',
+ 'views/components/reports/summary/chart.blade.php',
+ ] as $chartView) {
+ $view = file_get_contents(resource_path($chartView));
+
+ $this->assertIsString($view);
+ $this->assertStringContainsString(
+ "@include('components.charts.apexcharts-loader')",
+ $view
+ );
+ }
+ }
+
+ public function test_views_and_component_styles_do_not_depend_on_quicksand(): void
+ {
+ $headFiles = glob(
+ resource_path('views/components/layouts/*/head.blade.php')
+ );
+
+ $this->assertIsArray($headFiles);
+ $this->assertNotEmpty($headFiles);
+
+ foreach ($headFiles as $headFile) {
+ $head = file_get_contents($headFile);
+
+ $this->assertIsString($head);
+ $this->assertStringNotContainsString(
+ 'public/vendor/quicksand/css/quicksand.css',
+ $head
+ );
+ }
+
+ foreach ([
+ base_path('config/apexcharts.php'),
+ public_path('css/third_party/vue-html-editor_custom.css'),
+ ] as $componentStyle) {
+ $contents = file_get_contents($componentStyle);
+
+ $this->assertIsString($contents);
+ $this->assertStringNotContainsString('Quicksand', $contents);
+ $this->assertStringContainsString('JetBrains Mono', $contents);
+ }
+ }
+}
diff --git a/tests/Unit/Deployment/WebProcessOwnershipTest.php b/tests/Unit/Deployment/WebProcessOwnershipTest.php
index 109aa5ba..be371630 100644
--- a/tests/Unit/Deployment/WebProcessOwnershipTest.php
+++ b/tests/Unit/Deployment/WebProcessOwnershipTest.php
@@ -30,7 +30,7 @@ class WebProcessOwnershipTest extends TestCase
$this->assertIsString($compose);
$this->assertStringContainsString(
- 'test: ["CMD", "runuser", "-u", "www-data", "--", "php", "artisan", "app:ready", "--quiet"]',
+ 'test: ["CMD", "jiangling-healthcheck"]',
$compose
);
$this->assertMatchesRegularExpression(