feat(frontend): 新增首页新闻区块显示开关功能
- 首页新增 showNewsSection 字段,支持通过全局配置隐藏或显示新闻区块 - 主页组件根据 showNewsSection 判断是否渲染新闻区块组件 - 数据库添加 show_news_section 字段,默认值为 true - 新增对应数据库迁移脚本以支持字段的添加和删除 - 更新 Payload 配置,新增“显示首页新闻区块”复选框,带默认选中说明 - 添加部署脚本 rsync-deploy.sh,支持项目文件的增量同步与便捷发布 - 优化 Dockerfile,调整用户组参数,新增 public 目录确保部署环境完整
This commit is contained in:
+3
-4
@@ -1,5 +1,3 @@
|
|||||||
# syntax=docker/dockerfile:1
|
|
||||||
|
|
||||||
FROM uhub.service.ucloud.cn/mysql-images/node:22-bookworm-slim AS deps
|
FROM uhub.service.ucloud.cn/mysql-images/node:22-bookworm-slim AS deps
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -13,6 +11,7 @@ WORKDIR /app
|
|||||||
|
|
||||||
COPY --from=deps /app/node_modules ./node_modules
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
COPY . .
|
COPY . .
|
||||||
|
RUN mkdir -p public
|
||||||
|
|
||||||
ARG NEXT_PUBLIC_SITE_URL
|
ARG NEXT_PUBLIC_SITE_URL
|
||||||
ARG PAYLOAD_SECRET
|
ARG PAYLOAD_SECRET
|
||||||
@@ -35,7 +34,7 @@ ENV HOSTNAME=0.0.0.0
|
|||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
RUN addgroup --system --gid 1001 nodejs \
|
RUN addgroup --system --gid 1001 nodejs \
|
||||||
&& adduser --system --uid 1001 --gid nodejs nextjs
|
&& adduser --system --uid 1001 --ingroup nodejs nextjs
|
||||||
|
|
||||||
COPY --from=builder /app/public ./public
|
COPY --from=builder /app/public ./public
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||||
@@ -48,4 +47,4 @@ USER nextjs
|
|||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
CMD ["node", "server.js"]
|
CMD ["node", "server.js"]
|
||||||
+1
-1
@@ -66,4 +66,4 @@ networks:
|
|||||||
driver: bridge
|
driver: bridge
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
bootstrap_node_modules:
|
bootstrap_node_modules:
|
||||||
Executable
+305
@@ -0,0 +1,305 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Deploy source code to the production server via rsync.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# # Full deploy (sync entire project)
|
||||||
|
# ./scripts/rsync-deploy.sh
|
||||||
|
# ./scripts/rsync-deploy.sh --dry-run
|
||||||
|
#
|
||||||
|
# # Deploy a specific directory
|
||||||
|
# ./scripts/rsync-deploy.sh src/
|
||||||
|
# ./scripts/rsync-deploy.sh src/payload/
|
||||||
|
#
|
||||||
|
# # Deploy one or more files
|
||||||
|
# ./scripts/rsync-deploy.sh src/payload/globals/Homepage.ts
|
||||||
|
# ./scripts/rsync-deploy.sh src/payload/globals/Homepage.ts src/app/sitemap.ts
|
||||||
|
#
|
||||||
|
# # Mix directories and files
|
||||||
|
# ./scripts/rsync-deploy.sh src/migrations/ src/payload/globals/Homepage.ts
|
||||||
|
#
|
||||||
|
# Options:
|
||||||
|
# --dry-run, -n Show what would be transferred without making changes
|
||||||
|
# --target HOST SSH target (default: luxsin-website or $DEPLOY_TARGET)
|
||||||
|
# --port PORT SSH port (default: 22 or $SSH_PORT)
|
||||||
|
# --delete Enable --delete for specific-path syncs (full deploy always uses --delete)
|
||||||
|
# --help, -h Show this help message
|
||||||
|
#
|
||||||
|
# Environment variables:
|
||||||
|
# DEPLOY_TARGET SSH target (default: luxsin-website)
|
||||||
|
# REMOTE_DIR Remote project root (default: /data/project/eversolo-app)
|
||||||
|
# SSH_PORT Custom SSH port (default: 22)
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
|
||||||
|
DEPLOY_TARGET="${DEPLOY_TARGET:-luxsin-website}"
|
||||||
|
REMOTE_DIR="${REMOTE_DIR:-/data/project/eversolo-app}"
|
||||||
|
SSH_PORT="${SSH_PORT:-22}"
|
||||||
|
DRY_RUN=0
|
||||||
|
FORCE_DELETE=0
|
||||||
|
PATHS=()
|
||||||
|
|
||||||
|
# ── Parse arguments ──────────────────────────────────────────────
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--dry-run|-n)
|
||||||
|
DRY_RUN=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--target|-t)
|
||||||
|
DEPLOY_TARGET="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--port|-p)
|
||||||
|
SSH_PORT="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--delete)
|
||||||
|
FORCE_DELETE=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--help|-h)
|
||||||
|
sed -n '3,/^$/p' "$0" | sed 's/^# \{0,1\}//'
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
--*)
|
||||||
|
echo "ERROR: Unknown option: $1" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
PATHS+=("$1")
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── Verify rsync is available ────────────────────────────────────
|
||||||
|
if ! command -v rsync >/dev/null 2>&1; then
|
||||||
|
echo "ERROR: rsync is not installed." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
SSH_CMD="ssh -p ${SSH_PORT}"
|
||||||
|
|
||||||
|
# ── Full-deploy excludes ─────────────────────────────────────────
|
||||||
|
# Only upload files needed to build and run the Docker deployment.
|
||||||
|
# Server-managed files (.env, data/, media/, etc.) are excluded and
|
||||||
|
# will NOT be deleted by --delete on the remote.
|
||||||
|
FULL_EXCLUDES=(
|
||||||
|
# Build artifacts
|
||||||
|
--exclude='.next/'
|
||||||
|
--exclude='.next-dev/'
|
||||||
|
--exclude='.turbo/'
|
||||||
|
--exclude='tsconfig.tsbuildinfo'
|
||||||
|
--exclude='out/'
|
||||||
|
|
||||||
|
# Dependencies (installed on server via npm ci / Docker)
|
||||||
|
--exclude='node_modules/'
|
||||||
|
|
||||||
|
# Environment files (managed on server)
|
||||||
|
--exclude='.env'
|
||||||
|
--exclude='.env.*'
|
||||||
|
|
||||||
|
# Server-managed data and uploads
|
||||||
|
--exclude='data/'
|
||||||
|
--exclude='media/'
|
||||||
|
--exclude='files/'
|
||||||
|
--exclude='videos/'
|
||||||
|
--exclude='public/'
|
||||||
|
|
||||||
|
# Local build / packaging artifacts
|
||||||
|
--exclude='deploy-packages/'
|
||||||
|
--exclude='output/'
|
||||||
|
--exclude='tmp-*'
|
||||||
|
--exclude='*.log'
|
||||||
|
--exclude='*.tar.gz'
|
||||||
|
--exclude='*.tgz'
|
||||||
|
--exclude='*.dump'
|
||||||
|
|
||||||
|
# Documentation (not needed for runtime)
|
||||||
|
--exclude='docs/'
|
||||||
|
--exclude='*.md'
|
||||||
|
|
||||||
|
# Cloudflare deployment (not used in Docker deployment)
|
||||||
|
--exclude='cloudflare/'
|
||||||
|
--exclude='Dockerfile.cloudflare'
|
||||||
|
--exclude='wrangler.jsonc'
|
||||||
|
|
||||||
|
# Version control
|
||||||
|
--exclude='.git/'
|
||||||
|
--exclude='.github/'
|
||||||
|
|
||||||
|
# Dev tools and editor configs
|
||||||
|
--exclude='.claude/'
|
||||||
|
--exclude='.playwright-cli/'
|
||||||
|
--exclude='.tmp/'
|
||||||
|
--exclude='.wrangler/'
|
||||||
|
--exclude='.vercel/'
|
||||||
|
--exclude='.vscode/'
|
||||||
|
--exclude='.idea/'
|
||||||
|
--exclude='.githooks/'
|
||||||
|
|
||||||
|
# OS artifacts
|
||||||
|
--exclude='.DS_Store'
|
||||||
|
--exclude='._*'
|
||||||
|
--exclude='__MACOSX/'
|
||||||
|
|
||||||
|
# Raw seed data (not needed in production)
|
||||||
|
--exclude='src/seed/raw/'
|
||||||
|
|
||||||
|
# TLS certificates (managed on server)
|
||||||
|
--exclude='nginx/certs/*.pem'
|
||||||
|
--exclude='nginx/certs/*.key'
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── Minimal excludes for specific-path syncs ─────────────────────
|
||||||
|
# When uploading a specific directory or file, only filter out
|
||||||
|
# OS junk and dangerous directories. Project-level excludes like
|
||||||
|
# *.md or docs/ do not apply.
|
||||||
|
MINIMAL_EXCLUDES=(
|
||||||
|
--exclude='.DS_Store'
|
||||||
|
--exclude='._*'
|
||||||
|
--exclude='__MACOSX/'
|
||||||
|
--exclude='*.log'
|
||||||
|
--exclude='node_modules/'
|
||||||
|
--exclude='.next/'
|
||||||
|
--exclude='.next-dev/'
|
||||||
|
--exclude='nginx/certs/*.pem'
|
||||||
|
--exclude='nginx/certs/*.key'
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── Build rsync flags ────────────────────────────────────────────
|
||||||
|
BASE_FLAGS=(-a -v -z)
|
||||||
|
if [[ $DRY_RUN -eq 1 ]]; then
|
||||||
|
BASE_FLAGS+=(--dry-run)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Summary header ───────────────────────────────────────────────
|
||||||
|
print_header() {
|
||||||
|
local scope="$1"
|
||||||
|
echo "=============================================="
|
||||||
|
echo " Eversolo Web — rsync Deploy"
|
||||||
|
echo "=============================================="
|
||||||
|
echo " Target: ${DEPLOY_TARGET}:${REMOTE_DIR}"
|
||||||
|
echo " SSH Port: ${SSH_PORT}"
|
||||||
|
echo " Scope: ${scope}"
|
||||||
|
echo " Mode: $([[ $DRY_RUN -eq 1 ]] && echo 'DRY RUN' || echo 'LIVE')"
|
||||||
|
echo "=============================================="
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Full deploy ──────────────────────────────────────────────────
|
||||||
|
full_deploy() {
|
||||||
|
print_header "Full project"
|
||||||
|
rsync "${BASE_FLAGS[@]}" --delete \
|
||||||
|
-e "$SSH_CMD" \
|
||||||
|
"${FULL_EXCLUDES[@]}" \
|
||||||
|
./ "${DEPLOY_TARGET}:${REMOTE_DIR}/"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Sync a single directory ──────────────────────────────────────
|
||||||
|
sync_directory() {
|
||||||
|
local dir="$1"
|
||||||
|
# Ensure no leading ./
|
||||||
|
dir="${dir#./}"
|
||||||
|
# Ensure trailing slash for rsync "contents" semantics
|
||||||
|
dir="${dir%/}/"
|
||||||
|
|
||||||
|
if [[ ! -d "$dir" ]]; then
|
||||||
|
echo "ERROR: Directory not found: $dir" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " → ${dir} → ${REMOTE_DIR}/${dir}"
|
||||||
|
local flags=("${BASE_FLAGS[@]}")
|
||||||
|
if [[ $FORCE_DELETE -eq 1 ]]; then
|
||||||
|
flags+=(--delete)
|
||||||
|
fi
|
||||||
|
|
||||||
|
rsync "${flags[@]}" \
|
||||||
|
-e "$SSH_CMD" \
|
||||||
|
"${MINIMAL_EXCLUDES[@]}" \
|
||||||
|
"./${dir}" "${DEPLOY_TARGET}:${REMOTE_DIR}/${dir}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Sync one or more files ───────────────────────────────────────
|
||||||
|
sync_files() {
|
||||||
|
# Use --relative so rsync preserves the full directory structure
|
||||||
|
# e.g. ./src/payload/globals/Homepage.ts → /remote/src/payload/globals/Homepage.ts
|
||||||
|
local file_args=()
|
||||||
|
for f in "$@"; do
|
||||||
|
f="${f#./}"
|
||||||
|
if [[ ! -f "$f" ]]; then
|
||||||
|
echo "ERROR: File not found: $f" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
echo " → ${f}"
|
||||||
|
file_args+=("./${f}")
|
||||||
|
done
|
||||||
|
|
||||||
|
rsync "${BASE_FLAGS[@]}" --relative \
|
||||||
|
-e "$SSH_CMD" \
|
||||||
|
"${MINIMAL_EXCLUDES[@]}" \
|
||||||
|
"${file_args[@]}" \
|
||||||
|
"${DEPLOY_TARGET}:${REMOTE_DIR}/"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Execute ─────────────────────────────────────────────────────
|
||||||
|
if [[ ${#PATHS[@]} -eq 0 ]]; then
|
||||||
|
# ── Full deploy ────────────────────────────────────────────────
|
||||||
|
full_deploy
|
||||||
|
else
|
||||||
|
# ── Specific paths: separate directories from files ───────────
|
||||||
|
DIRS=()
|
||||||
|
FILES=()
|
||||||
|
|
||||||
|
for p in "${PATHS[@]}"; do
|
||||||
|
# Strip leading ./
|
||||||
|
p="${p#./}"
|
||||||
|
if [[ -d "$p" ]]; then
|
||||||
|
DIRS+=("$p")
|
||||||
|
elif [[ -f "$p" ]]; then
|
||||||
|
FILES+=("$p")
|
||||||
|
else
|
||||||
|
echo "ERROR: Path does not exist: $p" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
print_header "Specific paths (${#PATHS[@]} item(s))"
|
||||||
|
|
||||||
|
# Sync directories
|
||||||
|
if [[ ${#DIRS[@]} -gt 0 ]]; then
|
||||||
|
for d in "${DIRS[@]}"; do
|
||||||
|
sync_directory "$d" || exit 1
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Sync files (batch with --relative)
|
||||||
|
if [[ ${#FILES[@]} -gt 0 ]]; then
|
||||||
|
sync_files "${FILES[@]}" || exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Post-deploy instructions ────────────────────────────────────
|
||||||
|
echo ""
|
||||||
|
if [[ $DRY_RUN -eq 1 ]]; then
|
||||||
|
echo "✓ Dry run complete — no files were transferred."
|
||||||
|
echo " Run without --dry-run to apply changes."
|
||||||
|
else
|
||||||
|
echo "✓ Upload complete."
|
||||||
|
if [[ ${#PATHS[@]} -eq 0 ]]; then
|
||||||
|
echo ""
|
||||||
|
echo "Next steps (run on the production server):"
|
||||||
|
echo ""
|
||||||
|
echo " 1. Run database migrations:"
|
||||||
|
echo " cd ${REMOTE_DIR}"
|
||||||
|
echo " docker compose run --rm --no-deps bootstrap bash -lc 'npm ci --include=dev && npm run payload:migrate && npm run payload:migrate:status'"
|
||||||
|
echo ""
|
||||||
|
echo " 2. Rebuild and restart the app:"
|
||||||
|
echo " docker compose build app"
|
||||||
|
echo " docker compose up -d app"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
@@ -428,9 +428,11 @@ export default async function HomePage({
|
|||||||
(review) => !!review && typeof review === 'object',
|
(review) => !!review && typeof review === 'object',
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Suspense fallback={null}>
|
{homepage.showNewsSection !== false ? (
|
||||||
<HomeNewsSection homepage={homepage} locale={locale} />
|
<Suspense fallback={null}>
|
||||||
</Suspense>
|
<HomeNewsSection homepage={homepage} locale={locale} />
|
||||||
|
</Suspense>
|
||||||
|
) : null}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { MigrateDownArgs, MigrateUpArgs, sql } from '@payloadcms/db-postgres'
|
||||||
|
|
||||||
|
export async function up({ db }: MigrateUpArgs): Promise<void> {
|
||||||
|
await db.execute(sql`
|
||||||
|
ALTER TABLE "homepage"
|
||||||
|
ADD COLUMN IF NOT EXISTS "show_news_section" boolean DEFAULT true;
|
||||||
|
|
||||||
|
UPDATE "homepage"
|
||||||
|
SET "show_news_section" = true
|
||||||
|
WHERE "show_news_section" IS NULL;
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down({ db }: MigrateDownArgs): Promise<void> {
|
||||||
|
await db.execute(sql`
|
||||||
|
ALTER TABLE "homepage"
|
||||||
|
DROP COLUMN IF EXISTS "show_news_section";
|
||||||
|
`)
|
||||||
|
}
|
||||||
@@ -79,6 +79,7 @@ import * as migration_20260603_010000_update_submission_workflow_fields from './
|
|||||||
import * as migration_20260603_020000_add_support_photos_collection from './20260603_020000_add_support_photos_collection';
|
import * as migration_20260603_020000_add_support_photos_collection from './20260603_020000_add_support_photos_collection';
|
||||||
import * as migration_20260603_030000_add_after_service_upload_token from './20260603_030000_add_after_service_upload_token';
|
import * as migration_20260603_030000_add_after_service_upload_token from './20260603_030000_add_after_service_upload_token';
|
||||||
import * as migration_20260604_010000_localize_homepage_hero_media_controls from './20260604_010000_localize_homepage_hero_media_controls';
|
import * as migration_20260604_010000_localize_homepage_hero_media_controls from './20260604_010000_localize_homepage_hero_media_controls';
|
||||||
|
import * as migration_20260724_120000_add_homepage_show_news_section from './20260724_120000_add_homepage_show_news_section';
|
||||||
|
|
||||||
export const migrations = [
|
export const migrations = [
|
||||||
{
|
{
|
||||||
@@ -486,4 +487,9 @@ export const migrations = [
|
|||||||
down: migration_20260604_010000_localize_homepage_hero_media_controls.down,
|
down: migration_20260604_010000_localize_homepage_hero_media_controls.down,
|
||||||
name: '20260604_010000_localize_homepage_hero_media_controls'
|
name: '20260604_010000_localize_homepage_hero_media_controls'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
up: migration_20260724_120000_add_homepage_show_news_section.up,
|
||||||
|
down: migration_20260724_120000_add_homepage_show_news_section.down,
|
||||||
|
name: '20260724_120000_add_homepage_show_news_section'
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -2149,6 +2149,10 @@ export interface Homepage {
|
|||||||
}[]
|
}[]
|
||||||
| null;
|
| null;
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* Uncheck to hide the news grid from the homepage. Does not affect the /news page or navigation entries.
|
||||||
|
*/
|
||||||
|
showNewsSection?: boolean | null;
|
||||||
/**
|
/**
|
||||||
* Choose whether the homepage News section reads from the external WordPress blog or from local CMS News Highlights.
|
* Choose whether the homepage News section reads from the external WordPress blog or from local CMS News Highlights.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -427,6 +427,16 @@ export const Homepage: GlobalConfig = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'showNewsSection',
|
||||||
|
label: { en: 'Show News Section', zh: '显示首页新闻区块' },
|
||||||
|
type: 'checkbox',
|
||||||
|
defaultValue: true,
|
||||||
|
admin: {
|
||||||
|
description:
|
||||||
|
'Uncheck to hide the news grid from the homepage. Does not affect the /news page or navigation entries.',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'newsSource',
|
name: 'newsSource',
|
||||||
label: { en: 'News Source', zh: '新闻来源' },
|
label: { en: 'News Source', zh: '新闻来源' },
|
||||||
|
|||||||
Reference in New Issue
Block a user