Commit 4e807b40 authored by xuchentao's avatar xuchentao

feat: initialize Jiayun official website

parents
# 上线前请替换为真实域名与高强度随机密钥
SITE_URL=https://www.jiayunche.com
CMS_PORT=8790
CMS_PASSWORD=change-this-password
CMS_SECRET=replace-with-at-least-32-random-characters
# CMS_API_KEY=optional-api-key
# CMS_DATA_DIR=/root/jiayun/shared/cms-data
# CMS_BUILD_LOCK=/root/jiayun/shared/site-build.lock
node_modules/
dist/
.astro/
.env
.DS_Store
.codex-tmp/
public/uploads/*
!public/uploads/.gitkeep
# 嘉运网约车官网 · GitLab CI/CD 安全部署
# 流程:幂等部署 → 配置 Nginx → 验证与清理
# 触发:push 到 main 分支
#
# ⚠️ GitLab 版本:11.7(较旧)
# - 不使用 workflow:rules
# - 不使用 rules
# - 不使用 needs/DAG
# - 不使用 resource_group
# - 不使用 cache:policy
# 仅使用 only、stages、script、when 等基础语法。
# Runner 注册时请使用标签:jiayun-prod
variables:
DEPLOY_ROOT: "/root/jiayun"
RELEASES_DIR: "/root/jiayun/releases"
SHARED_DIR: "/root/jiayun/shared"
SHARED_ENV: "/root/jiayun/shared/.env"
APP_NAME: "jiayun"
PORT: "8790"
SITE_URL: "http://127.0.0.1:8790/"
PM2_BIN: "/usr/bin/pm2"
PM2_HOME_DIR: "/root/.pm2"
PM2_USE_SUDO: "1"
PUBLIC_HOST: "101.126.10.129"
stages:
- deploy
- nginx
- verify
# CI_PIPELINE_ID 保证不同 pipeline 不会写同一个 release。同一 deploy job 被重试时,
# 会重新导出相同提交并执行 npm ci;若该 pipeline 已上线,则直接成功退出。
# ── 1. 幂等部署 ──────────────────────────────────────
# 创建 release、安装依赖和部署在同一个 job 中完成,不依赖跨 job 保留 node_modules。
deploy:
stage: deploy
tags:
- jiayun-prod
only:
- main
script:
- |
set -Eeuo pipefail
RELEASE_DIR="$RELEASES_DIR/${CI_COMMIT_SHA}-${CI_PIPELINE_ID}"
echo "[deploy] 提交:$CI_COMMIT_SHA"
echo "[deploy] 目标 Release:$RELEASE_DIR"
test -n "$CI_COMMIT_SHA" || (echo "CI_COMMIT_SHA 缺失" && exit 1)
test -n "$CI_PIPELINE_ID" || (echo "CI_PIPELINE_ID 缺失" && exit 1)
case "$CI_PIPELINE_ID" in
*[!0-9]*) echo "CI_PIPELINE_ID 必须是数字" >&2; exit 2 ;;
esac
test -f "$SHARED_ENV" || (echo "缺少生产配置:$SHARED_ENV" && exit 1)
test -x "$PM2_BIN" || (echo "PM2 不存在或不可执行:$PM2_BIN" && exit 1)
case "$RELEASE_DIR" in
"$RELEASES_DIR"/*) ;;
*) echo "release 路径越界" >&2; exit 2 ;;
esac
current_target=$(readlink -f "$DEPLOY_ROOT/current" 2>/dev/null || true)
if [ "$current_target" = "$RELEASE_DIR" ] && [ -f "$RELEASE_DIR/.deploy-success" ]; then
if [ -d "$RELEASE_DIR/node_modules" ] && curl --fail --silent --show-error --max-time 5 "$SITE_URL" >/dev/null; then
echo "[deploy] 该 pipeline 已部署且服务正常,无需重复执行"
exit 0
fi
echo "本次 release 已上线但内容不完整或服务异常,拒绝原地修改" >&2
exit 2
fi
if [ "$current_target" = "$RELEASE_DIR" ]; then
echo "current 已指向未标记成功的本次 release,拒绝原地修改" >&2
exit 2
fi
if [ -n "$current_target" ] && [ -f "$current_target/.pipeline-id" ]; then
current_pipeline_id=$(cat "$current_target/.pipeline-id")
case "$current_pipeline_id" in
*[!0-9]*|'') echo "当前版本的 pipeline 标识不合法" >&2; exit 2 ;;
esac
if [ "$current_pipeline_id" -gt "$CI_PIPELINE_ID" ]; then
echo "较新的 pipeline $current_pipeline_id 已部署,拒绝重试旧 pipeline $CI_PIPELINE_ID" >&2
exit 3
fi
fi
test ! -L "$RELEASE_DIR" || (echo "release 目录不能是软链接" && exit 2)
mkdir -p "$RELEASES_DIR" "$SHARED_DIR" "$RELEASE_DIR"
if [ -f "$RELEASE_DIR/.commit-sha" ] && [ "$(cat "$RELEASE_DIR/.commit-sha")" != "$CI_COMMIT_SHA" ]; then
echo "release 中的提交标识不匹配" >&2
exit 2
fi
if [ -f "$RELEASE_DIR/.pipeline-id" ] && [ "$(cat "$RELEASE_DIR/.pipeline-id")" != "$CI_PIPELINE_ID" ]; then
echo "release 中的 pipeline 标识不匹配" >&2
exit 2
fi
if [ -e "$RELEASE_DIR/.env" ] && [ ! -L "$RELEASE_DIR/.env" ]; then
echo "release 中的 .env 必须是软链接" >&2
exit 2
fi
git config --global --add safe.directory "$CI_PROJECT_DIR"
git archive "$CI_COMMIT_SHA" | tar -x -C "$RELEASE_DIR"
printf '%s\n' "$CI_COMMIT_SHA" > "$RELEASE_DIR/.commit-sha"
printf '%s\n' "$CI_PIPELINE_ID" > "$RELEASE_DIR/.pipeline-id"
ln -sfn "$SHARED_ENV" "$RELEASE_DIR/.env"
cd "$RELEASE_DIR"
echo "[deploy] 安装依赖"
npm ci
du -sh node_modules
echo "[deploy] 等待 CMS 共享构建锁"
node --env-file="$SHARED_ENV" scripts/run-with-site-lock.mjs -- bash scripts/deploy-release.sh "$DEPLOY_ROOT" "$RELEASE_DIR" "$APP_NAME" "$PM2_BIN" "$PM2_HOME_DIR" "$PM2_USE_SUDO" "$SITE_URL" "$CI_PIPELINE_ID"
echo "[deploy] 当前版本:$(readlink -f "$DEPLOY_ROOT/current")"
# ── 2. 安装并验证 Nginx 配置 ─────────────────────────
# 复制 current release 中的配置,检查语法后重新加载 Nginx。
configure_nginx:
stage: nginx
tags:
- jiayun-prod
only:
- main
script:
- echo "[nginx] 开始配置公网入口:$PUBLIC_HOST"
- test -f "$DEPLOY_ROOT/current/deploy/nginx/jiayun.conf" || (echo "缺少 Nginx 配置" && exit 2)
- sudo -n /usr/bin/install -o root -g root -m 0644 "$DEPLOY_ROOT/current/deploy/nginx/jiayun.conf" /etc/nginx/conf.d/jiayun.conf
- sudo -n /usr/sbin/nginx -t
- sudo -n /usr/bin/systemctl reload nginx
- 'curl --fail --silent --show-error --max-time 5 --header "Host: $PUBLIC_HOST" http://127.0.0.1/ >/dev/null'
- echo "[nginx] Nginx 配置与公网入口验证完成"
# ── 3. 验证并只保留当前 + 上一个成功版本 ─────────────
# when: always 用于输出最终状态;生产健康检查和自动回退已经在 deploy 的共享锁内完成。
verify:
stage: verify
tags:
- jiayun-prod
only:
- main
when: always
script:
- export RELEASE_DIR="$RELEASES_DIR/${CI_COMMIT_SHA}-${CI_PIPELINE_ID}"
- echo "[verify] 本次 Release:$RELEASE_DIR"
- echo "[verify] 当前指向:$(readlink -f "$DEPLOY_ROOT/current" 2>/dev/null || echo 未部署)"
- |
verify_status=0
current_target=$(readlink -f "$DEPLOY_ROOT/current" 2>/dev/null || true)
if [ "$current_target" = "$RELEASE_DIR" ]; then
if curl --fail --silent --show-error --max-time 5 "$SITE_URL" >/dev/null; then
echo "[verify] 服务运行正常:$SITE_URL"
else
echo "[verify] 服务无响应,输出本项目最近日志"
sudo -n env PM2_HOME="$PM2_HOME_DIR" "$PM2_BIN" logs "$APP_NAME" --lines 30 --nostream || true
verify_status=1
fi
else
echo "[verify] 本次版本未部署成功,线上 current 保持在其他版本"
fi
echo "[verify] PM2 当前运行项目:"
if ! sudo -n env PM2_HOME="$PM2_HOME_DIR" "$PM2_BIN" list; then
echo "[verify] PM2 项目列表读取失败"
verify_status=1
fi
bash "$CI_PROJECT_DIR/scripts/cleanup-releases.sh" "$DEPLOY_ROOT" "$RELEASE_DIR"
exit "$verify_status"
# 嘉运网约车官网与文章后台
本项目基于 Astro 5 + Node standalone 构建,公开官网与文章后台运行在同一个服务中。企业资料集中维护在 `src/data/site.ts`,文章由 Markdown 文件与内置 CMS 管理。
## 本地运行
需要 Node.js 20.20.x:
```bash
npm install
cp .env.example .env
npm run dev
```
- 官网:`http://localhost:4321/`
- 文章后台:`http://localhost:4321/admin/`
- 跑车指南:`http://localhost:4321/articles/`
生产模式:
```bash
npm run build
npm start
```
默认生产端口为 `8790`,可用 `CMS_PORT` 覆盖。
## 内容管理
- 工作稿:`src/content/articles/`
- 已发布版本:`src/content/published/`
- 分类:`src/content/categories.json`
- 上传图片:`public/uploads/`
生产环境建议设置 `CMS_DATA_DIR` 指向 release 目录之外的持久化路径。后台支持文章新建、保存草稿、预览、发布、下架、删除、图片上传和分类管理。发布操作会在共享锁内原子构建站点,失败时恢复内容快照。
## 必需环境变量
```dotenv
SITE_URL=https://你的正式域名
CMS_PORT=8790
CMS_PASSWORD=高强度后台密码
CMS_SECRET=至少32位随机字符串
CMS_DATA_DIR=/root/jiayun/shared/cms-data
CMS_BUILD_LOCK=/root/jiayun/shared/site-build.lock
```
生产环境必须修改示例密码和密钥。`CMS_API_KEY` 为可选的 Bearer API 访问密钥。
## SEO 与部署
项目包含 canonical、Open Graph、Organization/LocalBusiness/Service/FAQ/Article 结构化数据、sitemap、robots.txt、llms.txt 与 web manifest。修改正式域名时,同时更新 `SITE_URL``public/robots.txt` 中的 Sitemap。
`scripts/` 保留了参考项目的共享构建锁、原子替换、release 部署、健康检查与自动回退机制;`deploy/nginx/jiayun.conf` 是反向代理示例。GitLab CI 使用 release 目录部署,需按真实服务器调整 Runner 标签、域名/IP 与目录。
import { defineConfig } from "astro/config";
import sitemap from "@astrojs/sitemap";
import node from "@astrojs/node";
import path from "node:path";
const customOutDir = process.env.JIAYUN_BUILD_OUT_DIR;
export default defineConfig({
site: process.env.SITE_URL || "https://www.jiayunche.com",
integrations: [sitemap()],
adapter: node({ mode: "standalone" }),
...(customOutDir ? { outDir: path.resolve(customOutDir) } : {}),
build: { format: "directory", inlineStylesheets: "auto" },
trailingSlash: "ignore",
compressHTML: true,
});
# 嘉运网约车官网 Nginx 配置示例
server {
listen 80;
server_name _;
client_max_body_size 12m;
location / {
proxy_pass http://127.0.0.1:8790;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
}
}
This diff is collapsed.
{
"name": "jiayun-official-site",
"type": "module",
"version": "1.0.0",
"private": true,
"engines": {
"node": ">=20.20.0 <21"
},
"scripts": {
"dev": "node --env-file-if-exists=.env ./node_modules/astro/astro.js dev",
"build": "node scripts/build-site.mjs",
"build:inside-lock": "node scripts/build-site.mjs --inside-lock",
"build:astro": "astro check && astro build",
"preview": "astro preview",
"check": "astro check",
"start": "node --env-file-if-exists=.env server.mjs",
"admin": "npm start",
"serve": "npm run build && npm start"
},
"dependencies": {
"@astrojs/node": "9.5.5",
"@astrojs/sitemap": "^3.7.3",
"astro": "5.18.2",
"gray-matter": "^4.0.3",
"marked": "^14.1.4"
},
"devDependencies": {
"@astrojs/check": "^0.9.6",
"typescript": "^5.9.3"
}
}
This diff is collapsed.
This diff is collapsed.
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630">
<defs><linearGradient id="bg" x1="0" x2="1"><stop stop-color="#fff"/><stop offset="1" stop-color="#fff1e9"/></linearGradient></defs>
<rect width="1200" height="630" fill="url(#bg)"/>
<circle cx="1000" cy="90" r="210" fill="none" stroke="#ff7d41" stroke-opacity=".14" stroke-width="55"/>
<path d="M735 445c100-170 250-205 405-110" fill="none" stroke="#ff7d41" stroke-opacity=".35" stroke-width="4" stroke-dasharray="12 14"/>
<circle cx="755" cy="420" r="13" fill="#ff7d41" stroke="#fff" stroke-width="6"/><circle cx="1094" cy="319" r="13" fill="#00b700" stroke="#fff" stroke-width="6"/>
<text x="76" y="114" fill="#ff7d41" font-family="Arial, PingFang SC, Microsoft YaHei, sans-serif" font-size="22" font-weight="700" letter-spacing="5">JIAYUN MOBILITY</text>
<text x="76" y="245" fill="#231916" font-family="PingFang SC, Microsoft YaHei, sans-serif" font-size="70" font-weight="800">嘉运网约车</text>
<text x="76" y="335" fill="#231916" font-family="PingFang SC, Microsoft YaHei, sans-serif" font-size="48" font-weight="700">天津跑网约车,租车买车就找我们</text>
<text x="80" y="410" fill="#6b625f" font-family="PingFang SC, Microsoft YaHei, sans-serif" font-size="25">租售 · 办证 · 入驻 · 培训 · 维保,一站到位</text>
<rect x="78" y="486" width="210" height="64" rx="32" fill="#ff7d41"/><text x="183" y="527" text-anchor="middle" fill="#fff" font-family="PingFang SC, Microsoft YaHei, sans-serif" font-size="22" font-weight="700">值得托付</text>
</svg>
# 嘉运网约车 — 允许搜索引擎和 AI 搜索抓取公开内容
User-agent: *
Allow: /
Disallow: /admin/
Disallow: /api/
User-agent: GPTBot
Allow: /
Disallow: /admin/
Disallow: /api/
User-agent: Google-Extended
Allow: /
Disallow: /admin/
Disallow: /api/
Sitemap: https://www.jiayunche.com/sitemap-index.xml
{
"name": "嘉运网约车",
"short_name": "嘉运汽车",
"description": "天津一站式网约车综合服务商",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#ff7d41",
"lang": "zh-CN"
}
import { buildSiteAtomic, withSiteBuildLock } from "./site-build.mjs";
try {
const build = () => buildSiteAtomic({ onOutput: (text) => process.stdout.write(text) });
if (process.argv.includes("--inside-lock")) {
if (process.env.JIAYUN_SITE_LOCK_HELD !== "1") throw new Error("build:inside-lock 只能在共享构建锁中运行");
await build();
} else {
await withSiteBuildLock(build);
}
} catch (error) {
console.error(error instanceof Error ? error.message : error);
process.exitCode = 1;
}
#!/usr/bin/env bash
set -Eeuo pipefail
deploy_root=${1:?缺少部署根目录}
pipeline_release=${2:-}
case "$deploy_root" in
/*) ;;
*) echo "部署根目录必须是绝对路径" >&2; exit 2 ;;
esac
deploy_root=$(readlink -f "$deploy_root")
releases_dir="$deploy_root/releases"
current_link="$deploy_root/current"
previous_marker="$deploy_root/shared/previous-release"
if [ -n "$pipeline_release" ]; then
pipeline_release=$(readlink -f "$pipeline_release")
case "$pipeline_release" in
"$releases_dir"/*) ;;
*) echo "本 pipeline 的 release 路径越界" >&2; exit 2 ;;
esac
fi
if [ ! -d "$releases_dir" ]; then
echo "[cleanup] Releases 目录不存在,无需清理"
exit 0
fi
current_target=$(readlink -f "$current_link" 2>/dev/null || true)
previous_target=""
if [ -f "$previous_marker" ]; then
previous_target=$(sed -n '1p' "$previous_marker")
if [ -n "$previous_target" ] && [ -d "$previous_target" ]; then
previous_target=$(readlink -f "$previous_target")
fi
fi
echo "[cleanup] 保留当前版本:${current_target:-}"
echo "[cleanup] 保留上一成功版本:${previous_target:-}"
for candidate in "$releases_dir"/*; do
[ -e "$candidate" ] || continue
if [ -L "$candidate" ] || [ ! -d "$candidate" ]; then
echo "[cleanup] 跳过非普通目录:$candidate"
continue
fi
resolved=$(readlink -f "$candidate")
case "$resolved" in
"$releases_dir"/*) ;;
*) echo "[cleanup] 跳过越界路径:$candidate"; continue ;;
esac
if [ "$resolved" = "$current_target" ] || { [ -n "$previous_target" ] && [ "$resolved" = "$previous_target" ]; }; then
echo "[cleanup] 保留:$resolved"
continue
fi
if [ "$resolved" = "$pipeline_release" ] || [ -f "$resolved/.deploy-success" ]; then
echo "[cleanup] 删除旧 Release:$resolved"
rm -rf -- "$resolved"
else
echo "[cleanup] 跳过未完成 Release(可能属于其他 pipeline):$resolved"
fi
done
echo "[cleanup] 清理完成,当前 Releases:"
find "$releases_dir" -mindepth 1 -maxdepth 1 -type d -print | sort
#!/usr/bin/env bash
set -Eeuo pipefail
deploy_root=${1:?缺少部署根目录}
release_dir=${2:?缺少 release 目录}
app_name=${3:?缺少 PM2 应用名}
pm2_bin=${4:?缺少 PM2 可执行文件路径}
pm2_home=${5:?缺少 PM2_HOME}
pm2_use_sudo=${6:-1}
healthcheck_url=${7:-http://127.0.0.1:${CMS_PORT:-8790}/}
pipeline_id=${8:?缺少 CI pipeline ID}
case "$deploy_root" in
/*) ;;
*) echo "部署根目录必须是绝对路径" >&2; exit 2 ;;
esac
case "$release_dir" in
"$deploy_root"/releases/*) ;;
*) echo "release 目录必须位于 $deploy_root/releases/ 下" >&2; exit 2 ;;
esac
case "$app_name" in
*[!A-Za-z0-9_.-]*|'') echo "PM2 应用名包含不安全字符" >&2; exit 2 ;;
esac
case "$pm2_bin" in
/*) ;;
*) echo "PM2 可执行文件必须是绝对路径" >&2; exit 2 ;;
esac
case "$pm2_home" in
/*) ;;
*) echo "PM2_HOME 必须是绝对路径" >&2; exit 2 ;;
esac
case "$pm2_use_sudo" in
0|1) ;;
*) echo "PM2_USE_SUDO 只能是 0 或 1" >&2; exit 2 ;;
esac
case "$pipeline_id" in
*[!0-9]*|'') echo "CI pipeline ID 必须是数字" >&2; exit 2 ;;
esac
shared_dir="$deploy_root/shared"
shared_env="$shared_dir/.env"
current_link="$deploy_root/current"
if [ "${JIAYUN_SITE_LOCK_HELD:-}" != "1" ]; then
echo "部署必须通过 run-with-site-lock.mjs 执行" >&2
exit 2
fi
if [ ! -d "$release_dir" ] || [ ! -f "$release_dir/package-lock.json" ]; then
echo "release 目录不完整:$release_dir" >&2
exit 2
fi
if [ ! -f "$shared_env" ]; then
echo "缺少生产环境配置:$shared_env" >&2
exit 2
fi
case "${CMS_DATA_DIR:-}" in
"$shared_dir"/*) ;;
*) echo "CMS_DATA_DIR 必须位于 $shared_dir/ 下" >&2; exit 2 ;;
esac
case "${CMS_BUILD_LOCK:-}" in
"$shared_dir"/*) ;;
*) echo "CMS_BUILD_LOCK 必须位于 $shared_dir/ 下" >&2; exit 2 ;;
esac
if [ -e "$current_link" ] && [ ! -L "$current_link" ]; then
echo "$current_link 必须是软链接,拒绝覆盖真实目录" >&2
exit 2
fi
command -v curl >/dev/null
test -x "$pm2_bin"
if [ ! -f "$release_dir/.pipeline-id" ] || [ "$(cat "$release_dir/.pipeline-id")" != "$pipeline_id" ]; then
echo "release 的 pipeline 标识不匹配" >&2
exit 2
fi
pm2_command() {
if [ "$pm2_use_sudo" = "1" ]; then
sudo -n env PM2_HOME="$pm2_home" "$pm2_bin" "$@"
else
env PM2_HOME="$pm2_home" "$pm2_bin" "$@"
fi
}
# 只删除并重建 APP_NAME 对应的一个进程;不执行 pm2 kill、delete all、restart all 或 pm2 save。
start_current_app() {
if pm2_command describe "$app_name" >/dev/null 2>&1; then
pm2_command delete "$app_name"
fi
pm2_command start "$current_link/server.mjs" \
--name "$app_name" \
--cwd "$current_link" \
--node-args="--env-file=$shared_env"
}
previous_target=""
if [ -L "$current_link" ]; then
previous_target=$(readlink -f "$current_link")
fi
# GitLab 11.7 没有 resource_group。若较新的 pipeline 已上线,拒绝旧 pipeline 覆盖它。
if [ -n "$previous_target" ] && [ -f "$previous_target/.pipeline-id" ]; then
current_pipeline_id=$(cat "$previous_target/.pipeline-id")
case "$current_pipeline_id" in
*[!0-9]*|'') echo "当前版本的 pipeline 标识不合法" >&2; exit 2 ;;
esac
if [ "$current_pipeline_id" -gt "$pipeline_id" ]; then
echo "较新的 pipeline $current_pipeline_id 已部署,拒绝旧 pipeline $pipeline_id 覆盖" >&2
exit 3
fi
if [ "$current_pipeline_id" -eq "$pipeline_id" ] && [ "$previous_target" = "$release_dir" ]; then
echo "该 pipeline 已经部署,无需重复切换"
exit 0
fi
fi
switched=0
rollback() {
exit_code=$?
trap - ERR
if [ "$switched" = "1" ]; then
echo "新版本部署失败,正在恢复上一版本……" >&2
rollback_link="$deploy_root/.current-rollback-$$"
if [ -n "$previous_target" ] && [ -d "$previous_target" ]; then
ln -s "$previous_target" "$rollback_link"
mv -Tf "$rollback_link" "$current_link"
start_current_app || true
else
if pm2_command describe "$app_name" >/dev/null 2>&1; then
pm2_command delete "$app_name" || true
fi
echo "没有可恢复的上一版本,已停止本项目进程" >&2
fi
fi
exit "$exit_code"
}
trap rollback ERR
cd "$release_dir"
echo "[deploy] 使用共享用户内容执行最终构建"
npm run build:inside-lock
next_link="$deploy_root/.current-next-$$"
ln -s "$release_dir" "$next_link"
mv -Tf "$next_link" "$current_link"
switched=1
start_current_app
healthy=0
for attempt in $(seq 1 30); do
if curl --fail --silent --show-error --max-time 3 "$healthcheck_url" >/dev/null; then
healthy=1
break
fi
echo "等待服务启动... ($attempt/30)"
sleep 1
done
if [ "$healthy" != "1" ]; then
echo "健康检查失败:$healthcheck_url" >&2
pm2_command logs "$app_name" --lines 30 --nostream || true
false
fi
trap - ERR
previous_marker_tmp="$shared_dir/.previous-release-$$"
printf '%s\n' "$previous_target" > "$previous_marker_tmp"
mv -f "$previous_marker_tmp" "$shared_dir/previous-release"
printf '%s\n' "$pipeline_id" > "$release_dir/.deploy-success"
echo "部署成功:$release_dir"
import { spawn } from "node:child_process";
import { withSiteBuildLock } from "./site-build.mjs";
const separator = process.argv.indexOf("--");
const command = separator >= 0 ? process.argv.slice(separator + 1) : [];
if (!command.length) {
console.error("用法:node scripts/run-with-site-lock.mjs -- <部署命令> [参数]");
process.exit(2);
}
await withSiteBuildLock(() => new Promise((resolve, reject) => {
const child = spawn(command[0], command.slice(1), {
stdio: "inherit",
env: { ...process.env, JIAYUN_SITE_LOCK_HELD: "1" },
});
child.on("error", reject);
child.on("exit", (code, signal) => {
if (code === 0) resolve();
else reject(new Error(signal ? `部署命令被信号 ${signal} 终止` : `部署命令退出码:${code}`));
});
})).catch((error) => {
console.error(error instanceof Error ? error.message : error);
process.exitCode = 1;
});
export interface BuildOptions {
onOutput?: (text: string) => void;
}
export class SiteBuildLockBusyError extends Error {}
export function tryAcquireSiteBuildLock(): Promise<(() => Promise<void>) | null>;
export function withSiteBuildLock<T>(task: () => Promise<T>, options?: { timeoutMs?: number }): Promise<T>;
export function buildSiteAtomic(options?: BuildOptions): Promise<string>;
import crypto from "node:crypto";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { spawn } from "node:child_process";
const ROOT = process.cwd();
const RUNTIME_DIR = path.join(ROOT, ".runtime");
const DEFAULT_LOCK = process.env.CMS_DATA_DIR
? path.join(path.resolve(process.env.CMS_DATA_DIR), ".site-build.lock")
: path.join(RUNTIME_DIR, "site-build.lock");
const LOCK_FILE = path.resolve(process.env.CMS_BUILD_LOCK || DEFAULT_LOCK);
const RECOVERY_LOCK_FILE = `${LOCK_FILE}.recovery`;
const ASTRO_BIN = path.join(ROOT, "node_modules", "astro", "astro.js");
const DIST_DIR = path.join(ROOT, "dist");
const MAX_LOG_LENGTH = 2 * 1024 * 1024;
export class SiteBuildLockBusyError extends Error {
constructor() {
super("网站正在部署或生成静态页面,请稍后重试");
this.name = "SiteBuildLockBusyError";
}
}
const delay = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
async function fileExists(file) {
try { await fs.access(file); return true; } catch { return false; }
}
function processIsAlive(pid) {
if (!Number.isInteger(pid) || pid <= 0) return false;
try { process.kill(pid, 0); return true; } catch { return false; }
}
async function removeStaleLock() {
let recoveryHandle;
try {
recoveryHandle = await fs.open(RECOVERY_LOCK_FILE, "wx");
const raw = await fs.readFile(LOCK_FILE, "utf8");
const lock = JSON.parse(raw);
const sameHost = lock.hostname === os.hostname();
if (sameHost && !processIsAlive(Number(lock.pid))) {
await fs.unlink(LOCK_FILE);
return true;
}
} catch (error) {
if (error.code === "ENOENT") return true;
if (error.code === "EEXIST") return false;
} finally {
if (recoveryHandle) {
await recoveryHandle.close().catch(() => {});
await fs.unlink(RECOVERY_LOCK_FILE).catch(() => {});
}
}
return false;
}
async function acquireSiteBuildLock(timeoutMs = 15 * 60 * 1000) {
await fs.mkdir(path.dirname(LOCK_FILE), { recursive: true });
const started = Date.now();
const token = crypto.randomBytes(12).toString("hex");
while (true) {
try {
const handle = await fs.open(LOCK_FILE, "wx");
await handle.writeFile(JSON.stringify({ token, pid: process.pid, hostname: os.hostname(), startedAt: new Date().toISOString() }));
await handle.close();
return async () => {
try {
const current = JSON.parse(await fs.readFile(LOCK_FILE, "utf8"));
if (current.token === token) await fs.unlink(LOCK_FILE);
} catch (error) {
if (error.code !== "ENOENT") throw error;
}
};
} catch (error) {
if (error.code !== "EEXIST") throw error;
if (await removeStaleLock()) continue;
if (Date.now() - started >= timeoutMs) throw new SiteBuildLockBusyError();
await delay(300);
}
}
}
export async function tryAcquireSiteBuildLock() {
try {
return await acquireSiteBuildLock(0);
} catch (error) {
if (error instanceof SiteBuildLockBusyError) return null;
throw error;
}
}
export async function withSiteBuildLock(task, options = {}) {
const release = await acquireSiteBuildLock(options.timeoutMs);
try { return await task(); } finally { await release(); }
}
function runAstro(command, env, onOutput) {
return new Promise((resolve, reject) => {
const child = spawn(process.execPath, [ASTRO_BIN, command], { cwd: ROOT, env: { ...process.env, ...env } });
let log = "";
const append = (chunk) => {
const text = chunk.toString();
log = `${log}${text}`.slice(-MAX_LOG_LENGTH);
onOutput?.(text);
};
child.stdout.on("data", append);
child.stderr.on("data", append);
child.on("error", reject);
child.on("exit", (code) => code === 0 ? resolve(log) : reject(Object.assign(new Error(`astro ${command} 执行失败`), { log })));
});
}
export async function buildSiteAtomic(options = {}) {
await fs.mkdir(RUNTIME_DIR, { recursive: true });
const id = `${Date.now()}-${process.pid}-${crypto.randomBytes(4).toString("hex")}`;
const temporary = path.join(RUNTIME_DIR, `dist-${id}`);
const backup = path.join(RUNTIME_DIR, `dist-backup-${id}`);
let movedCurrent = false;
let log = "";
try {
log += await runAstro("check", {}, options.onOutput);
log += await runAstro("build", { JIAYUN_BUILD_OUT_DIR: temporary }, options.onOutput);
if (await fileExists(DIST_DIR)) {
await fs.rename(DIST_DIR, backup);
movedCurrent = true;
}
try {
await fs.rename(temporary, DIST_DIR);
} catch (error) {
if (movedCurrent) await fs.rename(backup, DIST_DIR);
throw error;
}
if (movedCurrent) {
await fs.rm(backup, { recursive: true, force: true }).catch((error) => {
options.onOutput?.(`\n旧构建目录清理失败,可稍后手动清理:${error.message}\n`);
});
}
return log;
} catch (error) {
await fs.rm(temporary, { recursive: true, force: true });
if (movedCurrent && !await fileExists(DIST_DIR) && await fileExists(backup)) await fs.rename(backup, DIST_DIR);
if (error.log) error.log = `${log}${error.log}`.slice(-MAX_LOG_LENGTH);
else error.log = log;
throw error;
}
}
process.env.PORT ||= process.env.CMS_PORT || "8790";
process.env.HOST ||= "127.0.0.1";
await import("./dist/server/entry.mjs");
---
import type { Article } from "../lib/articles";
import Icon from "./Icon.astro";
interface Props { items: Article[]; }
const { items } = Astro.props;
---
<div class="article-grid">
{items.map((article, index) => <article class:list={["article-card", index === 0 && "article-card--featured"]}>
<a href={`/articles/${article.data.slug}/`} class="article-card__intro" aria-label={article.data.title}><span>{article.data.category}</span><strong>嘉运跑车指南</strong><small>GUIDE {String(index + 1).padStart(2, "0")}</small></a>
<div class="article-card__body"><div class="article-meta"><time datetime={article.data.date.toISOString()}>{article.data.date.toISOString().slice(0, 10)}</time><span>{article.data.category}</span></div><h2><a href={`/articles/${article.data.slug}/`}>{article.data.title}</a></h2><p>{article.data.excerpt}</p><a class="text-link" href={`/articles/${article.data.slug}/`}>阅读全文 <Icon name="arrow" size={17} /></a></div>
</article>)}
</div>
---
import Base from "../layouts/Base.astro"; import Header from "./Header.astro"; import Footer from "./Footer.astro"; import ArticleListing from "./ArticleListing.astro"; import { site } from "../data/site"; import { getCategories, paginationWindow, type Article } from "../lib/articles";
interface Props { items: Article[]; allItems: Article[]; categoryNames: string[]; currentPage: number; lastPage: number; activeName?: string; activeSlug?: string; }
const { items, allItems, categoryNames, currentPage, lastPage, activeName, activeSlug } = Astro.props;
const categories = getCategories(allItems, categoryNames); const heading = activeName || "跑车指南"; const title = `${heading}${currentPage > 1 ? ` 第${currentPage}页` : ""}|${site.brand.name}`; const description = activeName ? `${activeName}相关的网约车入行、车辆、证照与运营实用信息。` : "嘉运网约车整理的司机入行、车辆租售、证照办理、平台注册与运营指南。";
const pageHref = (page: number) => activeSlug ? (page === 1 ? `/articles/topic/${activeSlug}/` : `/articles/topic/${activeSlug}/page/${page}/`) : (page === 1 ? "/articles/" : `/articles/page/${page}/`);
---
<Base title={title} description={description}><Header /><main id="main-content"><section class="page-hero journal-hero"><div class="container"><div class="breadcrumb"><a href="/">首页</a><span>/</span><a href="/articles/">跑车指南</a>{activeName && <><span>/</span>{activeName}</>}</div><p class="eyebrow"><span></span>JIAYUN JOURNAL</p><h1>{heading}</h1><p>把网约车入行与运营中的复杂信息,说得更清楚一点。</p></div></section><section class="section journal-section"><div class="container"><nav class="category-nav" aria-label="文章分类"><a href="/articles/" aria-current={!activeName ? "page" : undefined}>全部 <span>{allItems.length}</span></a>{categories.map((category) => <a href={`/articles/topic/${category.slug}/`} aria-current={activeName === category.name ? "page" : undefined}>{category.name} <span>{category.count}</span></a>)}</nav>{items.length ? <ArticleListing items={items} /> : <div class="empty-state"><h2>该分类还没有文章</h2><p>后续内容会通过文章后台持续更新。</p></div>}{lastPage > 1 && <nav class="pagination" aria-label="分页">{paginationWindow(currentPage, lastPage).map((page) => page === 0 ? <span>…</span> : <a href={pageHref(page)} aria-current={page === currentPage ? "page" : undefined}>{page}</a>)}</nav>}</div></section></main><Footer /></Base>
---
import { site } from "../data/site";
import Icon from "./Icon.astro";
interface Props { eyebrow?: string; title?: string; text?: string; }
const { eyebrow = "READY TO START", title = "准备好开始你的网约车计划了吗?", text = "到店把预算、时间安排和跑车目标说清楚,我们帮你匹配合适的车型、平台与入行路径。" } = Astro.props;
---
<section class="cta-section">
<div class="container cta-panel" data-reveal>
<div><p class="eyebrow eyebrow--light"><span></span>{eyebrow}</p><h2>{title}</h2><p>{text}</p></div>
<div class="cta-actions"><a class="button" href={`tel:${site.contact.tel}`}><Icon name="phone" size={19} /> 电话咨询</a><a class="button button--white" href={site.contact.mapUrl} target="_blank" rel="noopener">导航到店 <Icon name="arrow" size={18} /></a></div>
</div>
</section>
---
import logo from "../../黑底白字绿LOGO.svg";
import { site } from "../data/site";
import Icon from "./Icon.astro";
---
<footer class="site-footer">
<div class="container footer-grid">
<div class="footer-brand">
<a href="/" class="footer-logo" aria-label="嘉运网约车首页"><img src={logo.src} width="431" height="105" alt="嘉运网约车" /></a>
<p>{site.brand.tagline}</p>
<div class="footer-tags"><span>合规租售</span><span>透明收费</span><span>一站服务</span></div>
</div>
<div><h2>快速导航</h2><a href="/services/">服务方案</a><a href="/join/">司机招募</a><a href="/articles/">跑车指南</a><a href="/about/">关于嘉运</a></div>
<div><h2>核心服务</h2><a href="/services/#vehicle-rental">车辆租赁</a><a href="/services/#license-service">证照代办</a><a href="/services/#platform-access">平台入驻</a><a href="/services/#driver-training">司机培训</a></div>
<div class="footer-contact"><h2>联系嘉运</h2><a href={`tel:${site.contact.tel}`}><Icon name="phone" size={18} />{site.contact.phone}</a><a href={site.contact.mapUrl} target="_blank" rel="noopener"><Icon name="pin" size={18} />{site.contact.address}</a><p><Icon name="clock" size={18} />{site.contact.serviceHours}</p></div>
</div>
<div class="container footer-bottom"><p>© {new Date().getFullYear()} {site.brand.legalName}</p><p>平台规则、车型价格及优惠政策以门店与相关平台实时信息为准。</p></div>
</footer>
---
import logo from "../../白底黑字绿LOGO.svg";
import { site } from "../data/site";
import { categoryToSlug, getCategoryNames } from "../lib/articles";
const path = Astro.url.pathname;
const isActive = (href: string) => href === "/" ? path === "/" : path.startsWith(href);
const articleCategories = (await getCategoryNames()).map((label) => ({
href: `/articles/topic/${categoryToSlug(label)}/`,
label,
}));
const nav = site.nav.map((item) => item.href === "/articles/"
? { ...item, children: [{ href: "/articles/", label: "全部跑车指南" }, ...articleCategories] }
: item);
---
<div class="utility-bar">
<div class="container utility-bar__inner">
<span>曹操出行天津直营中心 · 滴滴出行合规租售服务商</span>
<a href={`tel:${site.contact.tel}`}>服务热线:{site.contact.phone}</a>
</div>
</div>
<header class="site-header" data-header>
<div class="container site-header__inner">
<a class="brand" href="/" aria-label="嘉运网约车首页"><img src={logo.src} width="431" height="105" alt="嘉运网约车" /></a>
<nav class="desktop-nav" aria-label="主导航">
<ul>
{nav.map((item) => (
<li class="nav-item">
<a class="nav-link" href={item.href} aria-current={isActive(item.href) ? "page" : undefined} aria-haspopup="true">
{item.label}
</a>
<div class:list={["nav-dropdown", item.children.length > 5 && "nav-dropdown--wide"]}>
<ul>
{item.children.map((child) => <li><a href={child.href}>{child.label}<span aria-hidden="true">↗</span></a></li>)}
</ul>
</div>
</li>
))}
</ul>
</nav>
<a class="button button--small header-cta" href={`tel:${site.contact.tel}`}>电话咨询</a>
<details class="mobile-nav">
<summary aria-label="打开导航菜单"><span></span><span></span><span></span></summary>
<nav data-mobile-nav aria-label="移动端导航">
{nav.map((item) => (
<div class="mobile-nav__group">
<a class="mobile-nav__primary" href={item.href} aria-current={isActive(item.href) ? "page" : undefined}>{item.label}</a>
<div class="mobile-subnav">{item.children.map((child) => <a href={child.href}>{child.label}</a>)}</div>
</div>
))}
<a class="button" href={`tel:${site.contact.tel}`}>拨打 {site.contact.phone}</a>
</nav>
</details>
</div>
</header>
---
interface Props { name: string; size?: number; }
const { name, size = 24 } = Astro.props;
const paths: Record<string, string> = {
car: "M3 13l2-5a3 3 0 012.8-2h8.4A3 3 0 0119 8l2 5v6h-2v-2H5v2H3v-6zm3.3-3l-1.2 3h13.8l-1.2-3a1 1 0 00-.9-.6H7.2a1 1 0 00-.9.6zM7 15.5h2m6 0h2",
key: "M14 7a5 5 0 11-3.4 8.7L3 23v-4l3-3 2 2 2-2m4-6h.01",
license: "M6 3h12a2 2 0 012 2v14a2 2 0 01-2 2H6a2 2 0 01-2-2V5a2 2 0 012-2zm3 5h6M8 13h8M8 17h5",
route: "M5 19a3 3 0 100-6 3 3 0 000 6zm14-8a3 3 0 100-6 3 3 0 000 6zM8 16h3a2 2 0 002-2V8a2 2 0 012-2h1",
coach: "M12 12a4 4 0 100-8 4 4 0 000 8zM4 22a8 8 0 0116 0M18 9l2 2 4-4",
service: "M14.7 6.3a4 4 0 01-5 5L4 17l3 3 5.7-5.7a4 4 0 005-5l-2.4 2.4-3-3 2.4-2.4z",
check: "M5 12l4 4L19 6",
phone: "M5 4h4l2 5-3 2a16 16 0 007 7l2-3 5 2v4c0 1-1 2-2 2C10 23 1 14 1 4c0-1 1-2 2-2h2",
pin: "M20 10c0 6-8 12-8 12S4 16 4 10a8 8 0 1116 0zm-8 2a2 2 0 100-4 2 2 0 000 4z",
clock: "M12 22a10 10 0 110-20 10 10 0 010 20zm0-15v5l3 2",
shield: "M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10zm-4-10l3 3 5-6",
arrow: "M5 12h14m-5-5l5 5-5 5",
};
---
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d={paths[name] || paths.arrow}></path></svg>
---
interface Props { eyebrow?: string; title: string; intro?: string; align?: "left" | "center"; }
const { eyebrow, title, intro, align = "left" } = Astro.props;
---
<div class:list={["section-heading", `section-heading--${align}`]}>
{eyebrow && <p class="eyebrow"><span></span>{eyebrow}</p>}
<h2>{title}</h2>
{intro && <p class="section-intro">{intro}</p>}
</div>
---
title: "办理网约车证照前,先准备哪些信息"
slug: "license-preparation"
date: 2026-07-22
updated: 2026-07-22
category: "合规办证"
author: "嘉运网约车"
excerpt: "网约车驾驶员证与车辆营运证的办理对象和要求不同。提前核对人员、车辆与平台信息,可以减少往返。"
status: "published"
---
网约车驾驶员证与车辆营运证的办理对象和要求不同。开始办理前,建议先核对人员、车辆与平台三方面信息。
## 人员情况
准备个人身份、驾驶证等基础信息,并确认自己是否满足当地最新申请条件。
## 车辆情况
车辆类型、登记信息和营运要求需要与当地政策相符。若尚未选车,建议在购车或签订长期租赁合同前先核对。
## 平台计划
明确准备接入的平台,有助于同步确认车型准入和注册环节,避免证照办理与平台计划脱节。
政策和审核要求可能调整,具体材料、周期与结果应以主管部门最新规定为准。
---
title: "租车还是买车?先看你的跑车计划"
slug: "rent-or-buy-guide"
date: 2026-07-23
updated: 2026-07-23
category: "车辆选择"
author: "嘉运网约车"
excerpt: "租车和买车没有统一答案。预算、运营周期、每日在线时长和风险承受能力,都会影响选择。"
status: "published"
---
租车和买车没有统一答案。更合适的方式,取决于你的预算、预计运营周期和风险承受能力。
## 短期试跑更看重灵活
如果刚入行、尚未确定长期安排,可以优先了解短租或月租方案,先验证自己是否适合网约车运营。
## 长期运营要核算总成本
准备长期全职跑车时,可以对比长租、以租代购和直接购车。不要只比较月度金额,还要把保险、维保、车辆残值和合同责任纳入考虑。
## 车型需要匹配平台
不同平台与运营场景对车型的要求可能不同。选车前应同步确认平台准入规则,避免出现车辆与计划不匹配的情况。
所有价格、金融与租赁条件,应以门店当期政策和正式合同为准。
---
title: "天津跑网约车,新人入行先理清这四件事"
slug: "tianjin-driver-getting-started"
date: 2026-07-24
updated: 2026-07-24
category: "入行指南"
author: "嘉运网约车"
excerpt: "准备跑网约车时,不要急着先选车。先把时间安排、合规证照、平台准入和车辆成本四件事梳理清楚。"
status: "published"
---
准备跑网约车时,不要急着先选车。先把下面四件事梳理清楚,后面的决定会更稳妥。
## 1. 你准备全职还是兼职
每天可投入的时间,会影响平台选择、租期和车辆成本。兼职更看重灵活,全职则需要更完整地核算长期成本与车辆稳定性。
## 2. 当前证照是否齐全
网约车运营涉及驾驶员证和车辆营运证。办理条件、材料和周期应以主管部门最新要求为准,可以先让专业人员帮助核对现有情况。
## 3. 平台准入规则
不同平台的准入、车型和运营规则会调整。注册前应核对当前政策,并结合自己的跑车时间和区域选择平台。
## 4. 把总成本算清楚
除了租金或车款,还应逐项了解押金、保险、维保和合同责任。不要只看单一月供或短期优惠。
嘉运可以把车型、租售、办证、平台与培训放在一起梳理,但最终方案仍应基于你的预算和实际安排。
[
"入行指南",
"车辆选择",
"合规办证",
"运营技巧"
]
---
title: "办理网约车证照前,先准备哪些信息"
slug: "license-preparation"
date: 2026-07-22
updated: 2026-07-22
category: "合规办证"
author: "嘉运网约车"
excerpt: "网约车驾驶员证与车辆营运证的办理对象和要求不同。提前核对人员、车辆与平台信息,可以减少往返。"
status: "published"
---
网约车驾驶员证与车辆营运证的办理对象和要求不同。开始办理前,建议先核对人员、车辆与平台三方面信息。
## 人员情况
准备个人身份、驾驶证等基础信息,并确认自己是否满足当地最新申请条件。
## 车辆情况
车辆类型、登记信息和营运要求需要与当地政策相符。若尚未选车,建议在购车或签订长期租赁合同前先核对。
## 平台计划
明确准备接入的平台,有助于同步确认车型准入和注册环节,避免证照办理与平台计划脱节。
政策和审核要求可能调整,具体材料、周期与结果应以主管部门最新规定为准。
---
title: "租车还是买车?先看你的跑车计划"
slug: "rent-or-buy-guide"
date: 2026-07-23
updated: 2026-07-23
category: "车辆选择"
author: "嘉运网约车"
excerpt: "租车和买车没有统一答案。预算、运营周期、每日在线时长和风险承受能力,都会影响选择。"
status: "published"
---
租车和买车没有统一答案。更合适的方式,取决于你的预算、预计运营周期和风险承受能力。
## 短期试跑更看重灵活
如果刚入行、尚未确定长期安排,可以优先了解短租或月租方案,先验证自己是否适合网约车运营。
## 长期运营要核算总成本
准备长期全职跑车时,可以对比长租、以租代购和直接购车。不要只比较月度金额,还要把保险、维保、车辆残值和合同责任纳入考虑。
## 车型需要匹配平台
不同平台与运营场景对车型的要求可能不同。选车前应同步确认平台准入规则,避免出现车辆与计划不匹配的情况。
所有价格、金融与租赁条件,应以门店当期政策和正式合同为准。
---
title: "天津跑网约车,新人入行先理清这四件事"
slug: "tianjin-driver-getting-started"
date: 2026-07-24
updated: 2026-07-24
category: "入行指南"
author: "嘉运网约车"
excerpt: "准备跑网约车时,不要急着先选车。先把时间安排、合规证照、平台准入和车辆成本四件事梳理清楚。"
status: "published"
---
准备跑网约车时,不要急着先选车。先把下面四件事梳理清楚,后面的决定会更稳妥。
## 1. 你准备全职还是兼职
每天可投入的时间,会影响平台选择、租期和车辆成本。兼职更看重灵活,全职则需要更完整地核算长期成本与车辆稳定性。
## 2. 当前证照是否齐全
网约车运营涉及驾驶员证和车辆营运证。办理条件、材料和周期应以主管部门最新要求为准,可以先让专业人员帮助核对现有情况。
## 3. 平台准入规则
不同平台的准入、车型和运营规则会调整。注册前应核对当前政策,并结合自己的跑车时间和区域选择平台。
## 4. 把总成本算清楚
除了租金或车款,还应逐项了解押金、保险、维保和合同责任。不要只看单一月供或短期优惠。
嘉运可以把车型、租售、办证、平台与培训放在一起梳理,但最终方案仍应基于你的预算和实际安排。
export const site = {
brand: {
name: "嘉运网约车",
shortName: "嘉运汽车",
englishName: "JIAYUN MOBILITY",
legalName: "嘉运晟通(天津)汽车销售服务有限公司",
tagline: "值得托付的一站式网约车综合服务商",
industry: "汽车 · 网约车租售 · 司机服务",
},
home: {
eyebrow: "天津本地一站式网约车服务",
headline: "天津跑网约车,租车买车就找我们",
subhead: "嘉运网约车拥有曹操出行天津直营中心与滴滴出行合规租售服务商双重官方资质。买车、租车、办证、入驻、培训、售后,一站到位。",
stats: [
{ value: "2项", label: "平台官方合作资质" },
{ value: "数千+", label: "天津司机服务经验" },
{ value: "7×16h", label: "日常客服在线" },
{ value: "一站式", label: "租售办证运营闭环" },
],
},
services: [
{ slug: "vehicle-sales", index: "01", icon: "car", name: "新能源车辆销售", intro: "结合网约车运营场景与预算,提供新能源营运车型选购建议和整车销售服务。", audience: "准备购车入行的司机与小型车队", value: "车型、预算与跑车平台一起规划", note: "具体车型、价格与金融方案以门店当期政策为准。" },
{ slug: "vehicle-rental", index: "02", icon: "key", name: "灵活车辆租赁", intro: "覆盖短租、月租、长租与以租代购,按跑车周期和资金计划匹配用车方案。", audience: "希望轻投入起步或灵活换车的司机", value: "多周期选择,降低入行决策压力", note: "租赁条件、押金与优惠以合同和门店当期政策为准。" },
{ slug: "license-service", index: "03", icon: "license", name: "合规证照代办", intro: "专业团队协助准备材料,办理网约车驾驶员证与车辆营运证,跟进关键节点。", audience: "首次入行或需要补齐合规手续的司机", value: "流程有人带,材料少走弯路", note: "办理周期与结果以主管部门审核为准。" },
{ slug: "platform-access", index: "04", icon: "route", name: "平台入驻对接", intro: "对接曹操出行、滴滴、高德等主流平台,协助注册并处理后续账号问题。", audience: "全职或兼职网约车司机", value: "结合实际跑车情况匹配平台与车型", note: "平台准入、派单与运营规则以各平台实时政策为准。" },
{ slug: "driver-training", index: "05", icon: "coach", name: "司机培训与运营", intro: "提供免费岗前培训,并由专属运营顾问一对一跟进跑车过程中的实际问题。", audience: "零经验新人及希望提升运营效率的司机", value: "从上岗准备到日常运营持续陪伴", note: "实际营收受在线时长、平台规则、市场供需等因素影响。" },
{ slug: "after-sales", index: "06", icon: "service", name: "售后维保与救援", intro: "承接日常维保咨询、车辆问题协调与道路救援,让司机跑车更安心。", audience: "嘉运服务车辆与合作司机", value: "线下门店与客服共同响应", note: "服务范围与费用以车辆方案及现场确认结果为准。" },
],
process: [
{ step: "01", title: "到店沟通", desc: "说清预算、跑车时间与经验情况。" },
{ step: "02", title: "方案匹配", desc: "同步规划车型、租售方式与适合的平台。" },
{ step: "03", title: "办证入驻", desc: "协助准备材料、办理证照与平台注册。" },
{ step: "04", title: "培训上岗", desc: "完成岗前培训,由运营顾问持续跟进。" },
],
about: {
heading: "扎根天津,把网约车入行的复杂事一次办清",
story: [
"嘉运网约车是天津本土一站式网约车综合服务商,获曹操出行天津直营中心及滴滴出行合规租售服务商双重官方认证。",
"我们提供新能源网约车销售、车辆租赁、证照代办、司机培训、平台入驻及售后维保全链条服务。坚持合规经营、透明收费,已服务数千名天津司机。",
],
advantages: [
{ title: "双平台官方资质", desc: "曹操出行天津直营中心与滴滴出行合规租售服务商,合作信息更直接。" },
{ title: "方案因人而定", desc: "根据预算、全职或兼职安排、跑车经验,组合适合的平台、车型与用车方式。" },
{ title: "线下实体门店", desc: "咨询、看车、签约、培训与售后均可面对面沟通,合同与费用当面确认。" },
{ title: "全程运营陪伴", desc: "从证照、注册到岗前培训和账号问题管理,专属顾问持续跟进。" },
],
},
faq: [
{ q: "没有网约车经验,怎么在嘉运入行?", a: "专业代办团队会协助办理人证车证,并根据你的预算、全职或兼职安排匹配平台、车型与用车方案;完成平台注册后,还会提供免费岗前培训和专属运营顾问跟进。" },
{ q: "租车方案有哪些?资金不够怎么办?", a: "可选择短租、月租、长租或低首付以租代购等方案。新人扶持和租金优惠会随当期政策变化,建议到店结合实际预算核算并以合同为准。" },
{ q: "嘉运的服务适合什么样的人?", a: "适合天津本地准备入行或已经从事网约车运营的全职、兼职司机,以及需要采购或租赁新能源营运车辆的个人和小型车队。" },
{ q: "可以协助办理哪些证件?", a: "可协助办理网约车驾驶员证和车辆营运证,并提示材料与流程节点。具体办理条件、周期和审核结果以主管部门要求为准。" },
{ q: "会协助注册哪些平台?", a: "可对接曹操出行、滴滴、高德等主流平台,并协助处理平台注册和后续账号问题。最终准入和运营规则以各平台实时政策为准。" },
{ q: "签约前如何确认费用?", a: "嘉运坚持明码标价。建议到店逐项确认租金或车款、押金、保险、维保、违约责任等内容,所有权利义务以双方签署的正式合同为准。" },
],
contact: {
phone: "022-87940507",
tel: "+862287940507",
address: "天津市南开区咸阳路45号小园新厦",
locality: "南开区",
region: "天津市",
serviceHours: "每日 7×16 小时客服在线",
heading: "到店聊聊你的跑车计划",
intro: "带上你的预算、时间安排和驾驶经验,我们帮你把车型、平台、证照与运营方案一次梳理清楚。",
mapUrl: "https://uri.amap.com/search?keyword=%E5%8D%97%E5%BC%80%E5%8C%BA%E5%92%B8%E9%98%B3%E8%B7%AF45%E5%8F%B7%E5%B0%8F%E5%9B%AD%E6%96%B0%E5%8E%A6&city=%E5%A4%A9%E6%B4%A5",
},
nav: [
{
href: "/",
label: "首页",
children: [
{ href: "/", label: "品牌首页" },
{ href: "/#services", label: "六项核心服务" },
{ href: "/#advantages", label: "官方资质与优势" },
{ href: "/#process", label: "四步服务流程" },
{ href: "/#faq", label: "首页常见问答" },
],
},
{
href: "/services/",
label: "服务方案",
children: [
{ href: "/services/#vehicle-sales", label: "新能源车辆销售" },
{ href: "/services/#vehicle-rental", label: "灵活车辆租赁" },
{ href: "/services/#license-service", label: "合规证照代办" },
{ href: "/services/#platform-access", label: "平台入驻对接" },
{ href: "/services/#driver-training", label: "司机培训与运营" },
{ href: "/services/#after-sales", label: "售后维保与救援" },
],
},
{
href: "/join/",
label: "司机招募",
children: [
{ href: "/join/", label: "招募与入行方案" },
{ href: "/join/#fit", label: "哪些人适合" },
{ href: "/join/#support", label: "新人全程支持" },
{ href: "/join/#income", label: "成本与收益提示" },
{ href: "/contact/?intent=join", label: "咨询司机招募" },
],
},
{
href: "/articles/",
label: "跑车指南",
children: [{ href: "/articles/", label: "全部跑车指南" }],
},
{
href: "/about/",
label: "关于嘉运",
children: [
{ href: "/about/", label: "公司与品牌介绍" },
{ href: "/about/#story", label: "嘉运品牌故事" },
{ href: "/about/#standards", label: "四项服务准则" },
{ href: "/about/#certification", label: "双平台官方资质" },
],
},
{
href: "/faq/",
label: "常见问题",
children: [
{ href: "/faq/", label: "全部常见问题" },
{ href: "/faq/#faq-1", label: "新人如何入行" },
{ href: "/faq/#faq-2", label: "租车与资金方案" },
{ href: "/faq/#faq-4", label: "合规证照办理" },
{ href: "/faq/#faq-6", label: "签约费用确认" },
],
},
{
href: "/contact/",
label: "联系我们",
children: [
{ href: "/contact/", label: "联系方式总览" },
{ href: "/contact/#contact-info", label: "电话与门店地址" },
{ href: "/contact/#visit", label: "到店前准备" },
{ href: "tel:+862287940507", label: "拨打 022-87940507" },
],
},
],
} as const;
---
import "../styles/global.css";
import logo from "../../白底黑字绿LOGO.svg";
import { site } from "../data/site";
interface Props {
title: string;
description: string;
image?: string;
ogType?: "website" | "article";
noindex?: boolean;
jsonLd?: Record<string, unknown> | Record<string, unknown>[];
}
const { title, description, image = "/og-cover.svg", ogType = "website", noindex = false, jsonLd = [] } = Astro.props;
const canonical = new URL(Astro.url.pathname, Astro.site).href;
const ogImage = new URL(image, Astro.site).href;
const organizationLd = {
"@context": "https://schema.org",
"@type": ["AutoDealer", "LocalBusiness", "Organization"],
"@id": new URL("/#organization", Astro.site).href,
name: site.brand.name,
alternateName: [site.brand.shortName, site.brand.englishName],
legalName: site.brand.legalName,
url: Astro.site?.href,
logo: new URL(logo.src, Astro.site).href,
slogan: site.brand.tagline,
description: site.home.subhead,
telephone: site.contact.phone,
address: {
"@type": "PostalAddress",
streetAddress: site.contact.address,
addressLocality: site.contact.locality,
addressRegion: site.contact.region,
addressCountry: "CN",
},
areaServed: { "@type": "City", name: "天津" },
};
const pageLd = Array.isArray(jsonLd) ? jsonLd : [jsonLd];
const allLd = [organizationLd, ...pageLd.filter((item) => Object.keys(item).length > 0)];
---
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#ff7d41" />
<meta name="color-scheme" content="light" />
<title>{title}</title>
<meta name="description" content={description} />
<meta name="robots" content={noindex ? "noindex,nofollow" : "index,follow,max-image-preview:large,max-snippet:-1,max-video-preview:-1"} />
<link rel="canonical" href={canonical} />
<link rel="icon" href={logo.src} type="image/svg+xml" />
<link rel="manifest" href="/site.webmanifest" />
<link rel="alternate" hreflang="zh-CN" href={canonical} />
<link rel="alternate" hreflang="x-default" href={canonical} />
<meta property="og:type" content={ogType} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:url" content={canonical} />
<meta property="og:image" content={ogImage} />
<meta property="og:site_name" content={site.brand.name} />
<meta property="og:locale" content="zh_CN" />
<meta name="twitter:card" content="summary_large_image" />
<script type="application/ld+json" set:html={JSON.stringify(allLd)} is:inline></script>
</head>
<body>
<a class="skip-link" href="#main-content">跳到主要内容</a>
<slot />
<script is:inline>
const header = document.querySelector('[data-header]');
const syncHeader = () => header?.classList.toggle('is-scrolled', window.scrollY > 12);
addEventListener('scroll', syncHeader, { passive: true });
syncHeader();
const observer = 'IntersectionObserver' in window
? new IntersectionObserver((entries) => entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('is-visible');
observer.unobserve(entry.target);
}
}), { threshold: 0.1, rootMargin: '0px 0px -36px' })
: null;
document.querySelectorAll('[data-reveal]').forEach((el) => observer ? observer.observe(el) : el.classList.add('is-visible'));
document.querySelectorAll('[data-mobile-nav] a').forEach((link) => {
link.addEventListener('click', () => link.closest('details')?.removeAttribute('open'));
});
</script>
</body>
</html>
This diff is collapsed.
import { getCategoryNames as readCategoryNames, getPublishedArticles as readPublishedArticles, type Article } from "./article-store";
export type { Article };
export const PAGE_SIZE = 9;
const CATEGORY_SLUGS: Record<string, string> = {
入行指南: "getting-started",
车辆选择: "vehicle-guide",
合规办证: "compliance",
运营技巧: "driver-operations",
};
function fallbackSlug(category: string): string {
const ascii = category.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
if (ascii) return ascii;
let hash = 0;
for (const char of category) hash = (hash * 31 + (char.codePointAt(0) ?? 0)) >>> 0;
return `c${hash.toString(36)}`;
}
export const categoryToSlug = (category: string) => CATEGORY_SLUGS[category] ?? fallbackSlug(category);
export function slugToCategory(slug: string, categories: string[]): string | undefined {
return categories.find((category) => categoryToSlug(category) === slug);
}
export async function getPublishedArticles(): Promise<Article[]> {
return readPublishedArticles();
}
export async function getCategoryNames(): Promise<string[]> {
return readCategoryNames();
}
export interface CategoryInfo {
name: string;
slug: string;
count: number;
}
export function getCategories(items: Article[], categoryNames: string[] = []): CategoryInfo[] {
const counts = new Map<string, number>(categoryNames.map((name) => [name, 0]));
for (const item of items) counts.set(item.data.category, (counts.get(item.data.category) ?? 0) + 1);
return [...counts.entries()]
.map(([name, count]) => ({ name, slug: categoryToSlug(name), count }))
.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name, "zh-CN"));
}
export const lastPageOf = (total: number, pageSize = PAGE_SIZE) => Math.max(1, Math.ceil(total / pageSize));
export const pageSlice = <T>(items: T[], page: number, pageSize = PAGE_SIZE) => items.slice((page - 1) * pageSize, page * pageSize);
export const fmtDate = (date: Date) => date.toISOString().slice(0, 10);
export function paginationWindow(current: number, last: number, span = 2): number[] {
const pages = new Set<number>([1, last]);
for (let page = current - span; page <= current + span; page++) if (page >= 1 && page <= last) pages.add(page);
const sorted = [...pages].sort((a, b) => a - b);
const result: number[] = [];
let previous = 0;
for (const page of sorted) {
if (previous && page - previous > 1) result.push(0);
result.push(page);
previous = page;
}
return result;
}
import crypto from "node:crypto";
import { marked } from "marked";
import {
StoreError,
addCategory,
createContentSnapshot,
deleteArticle,
discardDraft,
generateSlug,
getCategoryNames,
getCategoryStats,
getPublishStatus,
listWorkingArticles,
publishArticle,
readWorkingArticle,
removeCategory,
renameCategory,
safeSlug,
saveUpload,
unpublishArticle,
writeArticle,
} from "./article-store";
import { buildSiteAtomic, tryAcquireSiteBuildLock, withSiteBuildLock } from "../../scripts/site-build.mjs";
const PASSWORD = process.env.CMS_PASSWORD || "";
const SECRET = process.env.CMS_SECRET || crypto.randomBytes(32).toString("hex");
const API_KEY = process.env.CMS_API_KEY || "";
const loginAttempts = new Map<string, { count: number; until: number }>();
type BuildState = {
status: "idle" | "building" | "success" | "error";
ok: boolean | null;
log: string;
startedAt: string | null;
finishedAt: string | null;
};
let buildState: BuildState = { status: "idle", ok: null, log: "", startedAt: null, finishedAt: null };
const json = (data: unknown, status = 200, headers?: HeadersInit) => Response.json(data, { status, headers });
const safeEqual = (leftValue: unknown, rightValue: unknown): boolean => {
const left = Buffer.from(String(leftValue));
const right = Buffer.from(String(rightValue));
return left.length === right.length && crypto.timingSafeEqual(left, right);
};
const sessionToken = () => crypto.createHmac("sha256", SECRET).update("jiayun-cms-v1").digest("hex");
function cookies(request: Request): Record<string, string> {
return Object.fromEntries((request.headers.get("cookie") || "").split(";").filter(Boolean).map((part) => {
const index = part.indexOf("=");
return [part.slice(0, index).trim(), decodeURIComponent(part.slice(index + 1).trim())];
}));
}
function apiKeyValid(request: Request): boolean {
if (!API_KEY) return false;
const auth = request.headers.get("authorization")?.match(/^Bearer\s+(.+)$/i)?.[1];
const key = auth || request.headers.get("x-api-key");
return key ? safeEqual(key, API_KEY) : false;
}
const authed = (request: Request): boolean =>
Boolean(cookies(request).cms_session && safeEqual(cookies(request).cms_session, sessionToken())) || apiKeyValid(request);
async function bodyOf(request: Request): Promise<Record<string, unknown>> {
try { return await request.json(); } catch { return {}; }
}
function cookieHeader(request: Request, value: string, maxAge: number): string {
const forwarded = request.headers.get("x-forwarded-proto");
const secure = new URL(request.url).protocol === "https:" || forwarded === "https";
return `cms_session=${value}; HttpOnly; SameSite=Strict; Path=/; Max-Age=${maxAge}${secure ? "; Secure" : ""}`;
}
function loginAllowed(ip: string): boolean {
const state = loginAttempts.get(ip);
return !state || state.until < Date.now();
}
function loginFailed(ip: string): void {
const state = loginAttempts.get(ip) || { count: 0, until: 0 };
state.count += 1;
if (state.count >= 5) state.until = Date.now() + 10 * 60 * 1000;
loginAttempts.set(ip, state);
}
function requireSlug(value: string | undefined): string {
const slug = safeSlug(value);
if (!slug) throw new StoreError("网址标识不合法");
return slug;
}
async function startContentBuild(change: () => Promise<void>): Promise<boolean> {
if (buildState.status === "building") return false;
const releaseLock = await tryAcquireSiteBuildLock();
if (!releaseLock) return false;
buildState = { status: "building", ok: null, log: "", startedAt: new Date().toISOString(), finishedAt: null };
void (async () => {
const snapshot = await createContentSnapshot();
try {
await change();
const log = await buildSiteAtomic();
buildState = { status: "success", ok: true, log, startedAt: buildState.startedAt, finishedAt: new Date().toISOString() };
} catch (error) {
let rollbackMessage = "";
try { await snapshot.restore(); } catch (rollbackError) {
rollbackMessage = `\n内容回滚失败:${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`;
}
const message = error instanceof Error ? error.message : String(error);
const log = typeof error === "object" && error && "log" in error ? String(error.log || "") : "";
buildState = {
status: "error",
ok: false,
log: `${log}${log ? "\n" : ""}${message}${rollbackMessage}`,
startedAt: buildState.startedAt,
finishedAt: new Date().toISOString(),
};
}
})().finally(releaseLock).catch((error) => {
buildState = {
status: "error",
ok: false,
log: error instanceof Error ? error.message : String(error),
startedAt: buildState.startedAt,
finishedAt: new Date().toISOString(),
};
});
return true;
}
async function startOrConflict(change: () => Promise<void>): Promise<Response> {
if (!await startContentBuild(change)) {
return json({ error: "网站正在部署或生成静态页面,本次操作未受理,请稍后重试" }, 409, { "Retry-After": "5" });
}
return json({ ok: true, status: "building" });
}
export async function handleCmsApi(request: Request, routeValue: string, clientAddress = "unknown"): Promise<Response> {
const route = routeValue.replace(/^\/+|\/+$/g, "");
const parts = route.split("/").filter(Boolean);
const method = request.method.toUpperCase();
try {
if (route === "session") {
if (method === "GET") return json({ authed: authed(request) });
if (method === "POST") {
if (!PASSWORD) throw new StoreError("文章后台尚未配置管理密码", 503);
if (!loginAllowed(clientAddress)) throw new StoreError("登录尝试过于频繁,请稍后再试", 429);
const body = await bodyOf(request);
if (!safeEqual(body.password || "", PASSWORD)) {
loginFailed(clientAddress);
throw new StoreError("管理密码错误", 401);
}
loginAttempts.delete(clientAddress);
return json({ ok: true }, 200, { "Set-Cookie": cookieHeader(request, sessionToken(), 86400) });
}
if (method === "DELETE") return json({ ok: true }, 200, { "Set-Cookie": cookieHeader(request, "", 0) });
}
if (!authed(request)) throw new StoreError("请先登录文章后台", 401);
if (route === "categories") {
if (method === "GET") return json({ categories: await getCategoryNames(), stats: await getCategoryStats() });
const body = await bodyOf(request);
if (method === "POST") {
return startOrConflict(async () => { await addCategory(body.name); });
}
if (method === "PATCH") {
return startOrConflict(async () => { await renameCategory(body.current, body.name); });
}
if (method === "DELETE") {
return startOrConflict(async () => { await removeCategory(body.name, body.replacement); });
}
}
if (route === "build" && method === "GET") return json(buildState);
if (route === "articles") {
if (method === "GET") return json(await listWorkingArticles());
if (method === "POST") {
const body = await bodyOf(request);
if (!String(body.title || "").trim()) throw new StoreError("请填写文章标题");
if (!String(body.body || "").trim()) throw new StoreError("请填写文章正文");
const slug = await generateSlug(body.category);
const article = await withSiteBuildLock(() => writeArticle(slug, body, true));
return json({ ok: true, slug, publishStatus: await getPublishStatus(article) }, 201);
}
}
if (parts[0] === "articles" && parts[1]) {
const slug = requireSlug(parts[1]);
if (parts.length === 3 && parts[2] === "draft" && method === "DELETE") {
return json({ ok: true, ...await withSiteBuildLock(() => discardDraft(slug)) });
}
if (parts.length === 3 && parts[2] === "publish" && method === "POST") {
return startOrConflict(async () => { await publishArticle(slug); });
}
if (parts.length === 3 && parts[2] === "unpublish" && method === "POST") {
return startOrConflict(async () => { await unpublishArticle(slug); });
}
if (parts.length === 2 && method === "GET") {
const article = await readWorkingArticle(slug);
return json({ ...article, publishStatus: await getPublishStatus(article) });
}
if (parts.length === 2 && method === "PUT") {
const body = await bodyOf(request);
if (!String(body.title || "").trim()) throw new StoreError("请填写文章标题");
if (!String(body.body || "").trim()) throw new StoreError("请填写文章正文");
const article = await withSiteBuildLock(() => writeArticle(slug, body));
return json({ ok: true, slug, publishStatus: await getPublishStatus(article) });
}
if (parts.length === 2 && method === "DELETE") {
return startOrConflict(async () => { await deleteArticle(slug); });
}
}
if (route === "preview" && method === "POST") {
const body = await bodyOf(request);
return json({ html: await marked.parse(String(body.body || "")) });
}
if (route === "uploads" && method === "POST") {
const body = await bodyOf(request);
return json({ ok: true, url: await saveUpload(body.dataUrl) });
}
return json({ error: "接口不存在" }, 404);
} catch (error) {
if (error instanceof StoreError) return json({ error: error.message, ...error.details }, error.status);
if ((error as NodeJS.ErrnoException).code === "ENOENT") return json({ error: "文章不存在" }, 404);
console.error(error);
return json({ error: error instanceof Error ? error.message : "服务器处理失败" }, 500);
}
}
---
import Base from "../layouts/Base.astro"; import Header from "../components/Header.astro"; import Footer from "../components/Footer.astro";
---
<Base title="页面未找到|嘉运网约车" description="你访问的页面不存在。" noindex><Header /><main id="main-content" class="not-found"><div><span>404</span><h1>这条路线暂时走不通</h1><p>返回首页,继续了解嘉运网约车服务。</p><a class="button" href="/">返回首页</a></div></main><Footer /></Base>
---
import Base from "../../layouts/Base.astro"; import Header from "../../components/Header.astro"; import Footer from "../../components/Footer.astro"; import CTA from "../../components/CTA.astro"; import SectionHeading from "../../components/SectionHeading.astro"; import Icon from "../../components/Icon.astro"; import { site } from "../../data/site";
const title = `关于嘉运|${site.brand.name}`; const description = `了解${site.brand.legalName}的品牌定位、官方合作资质与一站式司机服务体系。`;
---
<Base title={title} description={description}><Header /><main id="main-content"><section class="page-hero page-hero--dark"><div class="container"><div class="breadcrumb"><a href="/">首页</a><span>/</span>关于嘉运</div><p class="eyebrow eyebrow--light"><span></span>ABOUT JIAYUN</p><h1>扎根天津,<br />让每一次选择都<em>更踏实</em></h1><p>{site.brand.tagline}</p></div></section>
<section class="section" id="story"><div class="container about-story"><div class="about-story__copy" data-reveal><SectionHeading eyebrow="WHO WE ARE" title={site.about.heading} />{site.about.story.map((paragraph) => <p>{paragraph}</p>)}<div class="about-signature"><strong>嘉运网约车</strong><span>{site.brand.legalName}</span></div></div><div class="about-story__facts" data-reveal><p>JIAYUN · TIANJIN</p><h3>扎根天津的网约车综合服务商</h3><dl><div><dt>服务经验</dt><dd>已服务数千名司机</dd></div><div><dt>线下门店</dt><dd>{site.contact.address}</dd></div><div><dt>服务内容</dt><dd>车辆租售、证照办理、平台入驻与司机运营支持</dd></div><div><dt>服务原则</dt><dd>合规、透明、有人负责</dd></div></dl></div></div></section>
<section class="section values-section" id="standards"><div class="container"><SectionHeading eyebrow="OUR STANDARDS" title="把承诺落实在每一个服务细节" intro="合规、透明、有人负责,是嘉运面对每位司机的基本准则。" align="center" /><div class="values-grid">{site.about.advantages.map((item, index) => <div data-reveal><span>0{index + 1}</span><Icon name={["shield","route","pin","coach"][index]} size={31} /><h3>{item.title}</h3><p>{item.desc}</p></div>)}</div></div></section>
<section class="section certification-section" id="certification"><div class="container certification-layout"><div><p class="eyebrow"><span></span>OFFICIAL PARTNER</p><h2>双平台官方合作资质</h2><p>嘉运获曹操出行天津直营中心及滴滴出行合规租售服务商双重官方认证,平台政策与服务信息能够更高效地触达司机。</p></div><div class="certification-cards"><div><small>CAOCAO MOBILITY</small><strong>曹操出行</strong><span>天津直营中心</span></div><div><small>DIDI CHUXING</small><strong>滴滴出行</strong><span>合规租售服务商</span></div></div></div></section><CTA title="面对面,把跑车方案聊明白" /></main><Footer /></Base>
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="robots" content="noindex,nofollow" />
<title>嘉运网约车 · 跑车指南后台</title>
<link rel="stylesheet" href="/admin/style.css" />
</head>
<body>
<section id="login" class="login hidden">
<form id="login-form" class="login-card">
<div class="brand-mark">嘉运网约车</div>
<h1>跑车指南后台</h1>
<p>输入管理密码,管理官网文章内容。</p>
<label>管理密码<input id="password" type="password" autocomplete="current-password" required /></label>
<button class="primary" type="submit">登录后台</button>
<div id="login-error" class="error"></div>
</form>
</section>
<div id="app" class="hidden">
<header class="topbar">
<div><strong>嘉运网约车</strong><span>跑车指南后台</span></div>
<div class="top-actions">
<span id="pending-badge" class="pending-badge hidden"></span>
<a class="button ghost" href="/articles/" target="_blank" rel="noopener">查看前台</a>
<button id="logout" class="ghost">退出</button>
</div>
</header>
<main id="list-view" class="view">
<div class="view-head"><div><p>CONTENT</p><h1>跑车指南</h1></div><div class="view-actions"><button id="manage-categories" class="ghost">管理分类</button><button id="new-article" class="primary">+ 新建文章</button></div></div>
<div id="article-list" class="article-list"></div>
</main>
<main id="edit-view" class="view hidden">
<div class="view-head editor-titlebar">
<div><button id="back" class="back-button" type="button">← 返回文章列表</button><h1 id="edit-heading">编辑文章</h1></div>
<span id="editor-status" class="status"></span>
</div>
<div id="version-note" class="version-note"></div>
<form id="edit-form">
<section class="field-panel">
<label>文章标题<input id="title" required /></label>
<div class="field-label">
<span>文章分类</span>
<div id="category-select" class="custom-select">
<input id="category" type="hidden" />
<button id="category-trigger" class="select-trigger" type="button" aria-haspopup="listbox" aria-expanded="false">
<span id="category-value"></span><span class="select-chevron" aria-hidden="true"></span>
</button>
<div id="category-menu" class="select-menu hidden">
<div id="category-options" class="select-options" role="listbox" aria-label="文章分类"></div>
<div class="category-create">
<span>添加新分类</span>
<div><input id="new-category" maxlength="20" placeholder="输入分类名称" /><button id="add-category" class="primary small" type="button">添加</button></div>
<p id="category-error" class="error"></p>
</div>
</div>
</div>
</div>
<p class="auto-meta">文章网址、摘要、作者和 SEO 信息将由系统自动生成,无需填写。</p>
</section>
<section class="editor-panel">
<div class="editor-head"><div><strong>文章正文</strong><small>右侧会自动显示预览</small></div><div><span id="upload-status"></span><button id="upload-button" class="ghost small" type="button">插入图片</button></div></div>
<input id="file" class="hidden" type="file" accept="image/png,image/jpeg,image/webp,image/gif" />
<div class="editor-grid"><textarea id="body" required spellcheck="false" placeholder="在这里撰写文章正文……"></textarea><div id="preview" class="preview" aria-live="polite"></div></div>
</section>
<div class="edit-actions">
<button class="ghost save-button" type="submit">保存草稿</button>
<button id="publish" class="primary" type="button">发布到官网</button>
<details id="more-actions" class="more-actions hidden">
<summary>更多操作</summary>
<div class="action-menu">
<button id="discard" class="menu-action hidden" type="button">放弃未发布修改</button>
<button id="unpublish" class="menu-action danger hidden" type="button">下架文章</button>
<button id="delete-article" class="menu-action danger hidden" type="button">删除文章</button>
</div>
</details>
<span id="message"></span>
</div>
</form>
</main>
</div>
<div id="category-modal" class="modal hidden">
<div class="modal-card category-manager">
<div class="modal-head"><div><strong>文章分类管理</strong><small>重命名会同步更新该分类下的文章</small></div><button id="close-category-modal" class="ghost small" type="button">关闭</button></div>
<div class="category-manager-body">
<div class="manager-create"><input id="manager-new-category" maxlength="20" placeholder="输入新分类名称" /><button id="manager-add-category" class="primary" type="button">添加分类</button></div>
<p id="manager-category-error" class="error"></p>
<div id="category-manager-list" class="category-manager-list"></div>
</div>
</div>
</div>
<div id="build-modal" class="modal hidden">
<div class="modal-card build-card">
<div><strong id="build-title">正在生成静态页面</strong><button id="close-modal" class="ghost small hidden" type="button">关闭</button></div>
<pre id="build-log">系统正在更新官网,请稍候。</pre>
</div>
</div>
<script is:inline src="/admin/app.js"></script>
</body>
</html>
import type { APIRoute } from "astro";
import { handleCmsApi } from "../../../lib/cms-api";
export const prerender = false;
const handler: APIRoute = ({ request, params, clientAddress }) =>
handleCmsApi(request, params.route || "", clientAddress);
export const GET = handler;
export const POST = handler;
export const PUT = handler;
export const PATCH = handler;
export const DELETE = handler;
---
import Base from "../../layouts/Base.astro"; import Header from "../../components/Header.astro"; import Footer from "../../components/Footer.astro"; import CTA from "../../components/CTA.astro"; import { getPublishedArticle, getPublishedArticles } from "../../lib/article-store"; import { categoryToSlug } from "../../lib/articles"; import { site } from "../../data/site";
export async function getStaticPaths() { const articles = await getPublishedArticles(); return articles.map((article) => ({ params: { slug: article.data.slug } })); }
const article = await getPublishedArticle(Astro.params.slug || ""); if (!article) return Astro.redirect("/404/");
const title = `${article.data.title}|跑车指南|${site.brand.name}`; const articleUrl = new URL(Astro.url.pathname, Astro.site).href;
const jsonLd = { "@context": "https://schema.org", "@type": "Article", headline: article.data.title, description: article.data.excerpt, datePublished: article.data.date.toISOString(), dateModified: article.data.updated.toISOString(), author: { "@type": "Organization", name: article.data.author }, publisher: { "@id": new URL("/#organization", Astro.site).href }, mainEntityOfPage: articleUrl };
---
<Base title={title} description={article.data.excerpt} ogType="article" jsonLd={jsonLd}><Header /><main id="main-content"><article class="article-page"><header><div class="container article-header"><div class="breadcrumb"><a href="/">首页</a><span>/</span><a href="/articles/">跑车指南</a><span>/</span><a href={`/articles/topic/${categoryToSlug(article.data.category)}/`}>{article.data.category}</a></div><span class="article-category">{article.data.category}</span><h1>{article.data.title}</h1><div class="article-byline"><span>{article.data.author}</span><time datetime={article.data.date.toISOString()}>发布于 {article.data.date.toISOString().slice(0, 10)}</time>{article.data.updated.valueOf() !== article.data.date.valueOf() && <time datetime={article.data.updated.toISOString()}>更新于 {article.data.updated.toISOString().slice(0, 10)}</time>}</div></div></header><div class="container article-layout"><div class="article-content" set:html={article.html}></div><aside><strong>温馨提示</strong><p>平台规则、办证要求与车型政策可能调整,请以主管部门、平台与门店实时信息为准。</p><a href="/articles/">← 返回跑车指南</a></aside></div></article><CTA title="文章没解决你的具体问题" text="每位司机的预算时间与当前证照情况都不同欢迎到店面对面梳理" /></main><Footer /></Base>
---
import ArticlesView from "../../components/ArticlesView.astro"; import { getCategoryNames, getPublishedArticles, lastPageOf, pageSlice } from "../../lib/articles";
const allItems = await getPublishedArticles(); const categoryNames = await getCategoryNames();
---
<ArticlesView items={pageSlice(allItems, 1)} allItems={allItems} categoryNames={categoryNames} currentPage={1} lastPage={lastPageOf(allItems.length)} />
---
import ArticlesView from "../../../components/ArticlesView.astro"; import { getCategoryNames, getPublishedArticles, lastPageOf, pageSlice } from "../../../lib/articles";
export async function getStaticPaths() { const all = await getPublishedArticles(); return Array.from({ length: lastPageOf(all.length) }, (_, i) => i + 1).filter((p) => p > 1).map((page) => ({ params: { page: String(page) } })); }
const allItems = await getPublishedArticles(); const categoryNames = await getCategoryNames(); const currentPage = Number(Astro.params.page || 1); const lastPage = lastPageOf(allItems.length); if (currentPage < 2 || currentPage > lastPage) return Astro.redirect("/articles/");
---
<ArticlesView items={pageSlice(allItems, currentPage)} allItems={allItems} categoryNames={categoryNames} currentPage={currentPage} lastPage={lastPage} />
---
import ArticlesView from "../../../../components/ArticlesView.astro"; import { categoryToSlug, getCategoryNames, getPublishedArticles, lastPageOf, pageSlice } from "../../../../lib/articles";
export async function getStaticPaths() { const categories = await getCategoryNames(); return categories.map((name) => ({ params: { slug: categoryToSlug(name) }, props: { activeName: name } })); }
const { activeName } = Astro.props; const activeSlug = Astro.params.slug!; const allItems = await getPublishedArticles(); const categoryNames = await getCategoryNames(); const filtered = allItems.filter((item) => item.data.category === activeName);
---
<ArticlesView items={pageSlice(filtered, 1)} allItems={allItems} categoryNames={categoryNames} currentPage={1} lastPage={lastPageOf(filtered.length)} activeName={activeName} activeSlug={activeSlug} />
---
import ArticlesView from "../../../../../components/ArticlesView.astro"; import { categoryToSlug, getCategoryNames, getPublishedArticles, lastPageOf, pageSlice } from "../../../../../lib/articles";
export async function getStaticPaths() { const all = await getPublishedArticles(); const categories = await getCategoryNames(); return categories.flatMap((name) => { const filtered = all.filter((item) => item.data.category === name); return Array.from({ length: lastPageOf(filtered.length) }, (_, i) => i + 1).filter((p) => p > 1).map((page) => ({ params: { slug: categoryToSlug(name), page: String(page) }, props: { activeName: name } })); }); }
const { activeName } = Astro.props; const activeSlug = Astro.params.slug!; const allItems = await getPublishedArticles(); const categoryNames = await getCategoryNames(); const filtered = allItems.filter((item) => item.data.category === activeName); const currentPage = Number(Astro.params.page || 1); const lastPage = lastPageOf(filtered.length);
---
<ArticlesView items={pageSlice(filtered, currentPage)} allItems={allItems} categoryNames={categoryNames} currentPage={currentPage} lastPage={lastPage} activeName={activeName} activeSlug={activeSlug} />
---
import Base from "../../layouts/Base.astro"; import Header from "../../components/Header.astro"; import Footer from "../../components/Footer.astro"; import Icon from "../../components/Icon.astro"; import { site } from "../../data/site";
const title = `联系我们|${site.brand.name}`; const description = `联系嘉运网约车:${site.contact.phone},地址:${site.contact.address}。`;
const intent = Astro.url.searchParams.get("intent"); const lead = intent === "join" ? "想了解司机招募与入行方案?" : intent === "rent" ? "想了解车辆租赁方案?" : site.contact.heading;
---
<Base title={title} description={description}><Header /><main id="main-content"><section class="contact-hero"><div class="container contact-hero__inner"><div><div class="breadcrumb"><a href="/">首页</a><span>/</span>联系我们</div><p class="eyebrow"><span></span>CONTACT US</p><h1>{lead}</h1><p>{site.contact.intro}</p></div><div class="contact-number"><small>SERVICE HOTLINE</small><a href={`tel:${site.contact.tel}`}>{site.contact.phone}</a><span>{site.contact.serviceHours}</span></div></div></section><section class="section contact-section" id="contact-info"><div class="container contact-grid"><div class="contact-cards"><a href={`tel:${site.contact.tel}`}><Icon name="phone" size={27} /><span><small>联系电话</small><strong>{site.contact.phone}</strong></span><Icon name="arrow" size={19} /></a><a href={site.contact.mapUrl} target="_blank" rel="noopener"><Icon name="pin" size={27} /><span><small>线下门店</small><strong>{site.contact.address}</strong></span><Icon name="arrow" size={19} /></a><div><Icon name="clock" size={27} /><span><small>服务时间</small><strong>{site.contact.serviceHours}</strong></span></div></div><div class="visit-panel"><p>JIAYUN · TIANJIN</p><h2>欢迎到店沟通</h2><dl><div><dt>门店地址</dt><dd>{site.contact.address}</dd></div><div><dt>营业时间</dt><dd>{site.contact.serviceHours}</dd></div><div><dt>到店建议</dt><dd>可提前电话联系,便于顾问根据你的需求准备对应方案。</dd></div></dl><a class="button button--secondary" href={site.contact.mapUrl} target="_blank" rel="noopener">打开地图导航 <Icon name="arrow" size={17} /></a></div></div></section><section class="section visit-tips" id="visit"><div class="container"><h2>到店前,建议准备这些信息</h2><div><span>01</span><p><b>计划投入</b>大致预算与希望的租售方式</p></div><div><span>02</span><p><b>时间安排</b>全职或兼职、每日可跑时长</p></div><div><span>03</span><p><b>当前情况</b>驾驶经验、已有证件与平台注册状态</p></div></div></section></main><Footer /></Base>
---
import Base from "../../layouts/Base.astro"; import Header from "../../components/Header.astro"; import Footer from "../../components/Footer.astro"; import CTA from "../../components/CTA.astro"; import { site } from "../../data/site";
const title = `常见问题|${site.brand.name}`; const description = "关于天津网约车入行、租车方案、证照办理、平台注册与费用确认的常见问题。";
const faqLd = { "@context": "https://schema.org", "@type": "FAQPage", mainEntity: site.faq.map((item) => ({ "@type": "Question", name: item.q, acceptedAnswer: { "@type": "Answer", text: item.a } })) };
---
<Base title={title} description={description} jsonLd={faqLd}><Header /><main id="main-content"><section class="page-hero"><div class="container"><div class="breadcrumb"><a href="/">首页</a><span>/</span>常见问题</div><p class="eyebrow"><span></span>FAQ</p><h1>重要的事,<br />签约之前<em>问清楚</em></h1><p>这里整理了司机最常问的问题。你的实际方案仍建议到店逐项确认。</p></div></section><section class="section"><div class="container faq-page-layout"><aside><span>QUICK CONTACT</span><h2>还有其他问题?</h2><p>客服每日 7×16 小时在线,也欢迎直接到店面对面沟通。</p><a class="button" href={`tel:${site.contact.tel}`}>{site.contact.phone}</a></aside><div class="faq-page-list">{site.faq.map((item, index) => <details id={`faq-${index + 1}`} open={index === 0}><summary><span>{String(index + 1).padStart(2, "0")}</span><strong>{item.q}</strong><i></i></summary><p>{item.a}</p></details>)}</div></div></section><CTA /></main><Footer /></Base>
---
import Base from "../layouts/Base.astro";
import Header from "../components/Header.astro";
import Footer from "../components/Footer.astro";
import SectionHeading from "../components/SectionHeading.astro";
import CTA from "../components/CTA.astro";
import Icon from "../components/Icon.astro";
import { site } from "../data/site";
const title = `${site.brand.name}|天津网约车租售、办证与司机服务`;
const description = site.home.subhead;
const serviceLd = site.services.map((service) => ({ "@type": "Service", name: service.name, description: service.intro, provider: { "@id": new URL("/#organization", Astro.site).href }, areaServed: "天津市" }));
---
<Base title={title} description={description} jsonLd={{ "@context": "https://schema.org", "@type": "ItemList", itemListElement: serviceLd.map((item, index) => ({ "@type": "ListItem", position: index + 1, item })) }}>
<Header />
<main id="main-content">
<section class="hero">
<div class="container hero__inner">
<div class="hero-copy" data-reveal>
<p class="eyebrow"><span></span>{site.home.eyebrow}</p>
<h1>天津<em>跑网约车,租车买车</em>就找我们</h1>
<p class="hero-lead">{site.home.subhead}</p>
<div class="hero-actions"><a class="button" href={`tel:${site.contact.tel}`}><Icon name="phone" size={19} />立即咨询</a><a class="button button--secondary" href="/services/">查看服务方案<Icon name="arrow" size={18} /></a></div>
<div class="hero-trust"><span><Icon name="shield" size={18} />双平台官方资质</span><span><Icon name="check" size={18} />明码标价</span><span><Icon name="check" size={18} />线下实体门店</span></div>
</div>
</div>
</section>
<section class="stats-strip" aria-label="嘉运服务数据"><div class="container stats-grid">{site.home.stats.map((item) => <div><strong>{item.value}</strong><span>{item.label}</span></div>)}</div></section>
<section class="section services-preview" id="services">
<div class="container">
<SectionHeading eyebrow="ONE-STOP SERVICE" title="从选车到上岗,一站解决跑车准备" intro="把原本需要多处奔波的租售、办证、平台注册与培训,放进同一套服务流程。" />
<div class="service-grid">{site.services.map((service) => <a class="service-card" href={`/services/#${service.slug}`} data-reveal><div class="service-card__icon"><Icon name={service.icon} size={28} /></div><span class="service-card__index">{service.index}</span><h3>{service.name}</h3><p>{service.intro}</p><span class="text-link">了解详情 <Icon name="arrow" size={17} /></span></a>)}</div>
</div>
</section>
<section class="section trust-section" id="advantages">
<div class="container trust-layout">
<div class="trust-proof" data-reveal><p>OFFICIAL PARTNERSHIP</p><h3>双重官方认证</h3><span>正规合作 · 合规服务</span><dl><div><dt>曹操出行</dt><dd>天津直营中心</dd></div><div><dt>滴滴出行</dt><dd>合规租售服务商</dd></div></dl></div>
<div class="trust-copy" data-reveal><p class="eyebrow"><span></span>WHY JIAYUN</p><h2>官方资质是起点,<br />把服务做实才是答案</h2><p>网约车入行涉及车辆、证照、平台和日常运营。嘉运用线下实体门店承接每一个环节,合同与费用面对面确认,遇到问题能找到人。</p><div class="advantage-list">{site.about.advantages.map((item, index) => <div><span>{String(index + 1).padStart(2, "0")}</span><div><h3>{item.title}</h3><p>{item.desc}</p></div></div>)}</div><a class="button button--secondary" href="/about/">了解嘉运汽车 <Icon name="arrow" size={18} /></a></div>
</div>
</section>
<section class="section process-section" id="process"><div class="container"><SectionHeading eyebrow="HOW IT WORKS" title="四步开启跑车计划" intro="先了解真实情况,再匹配方案;不让你在不清楚规则时仓促做决定。" align="center" /><div class="process-grid">{site.process.map((item) => <div class="process-card" data-reveal><span>{item.step}</span><div class="process-icon"><Icon name={site.services[Number(item.step) - 1]?.icon || "arrow"} size={25} /></div><h3>{item.title}</h3><p>{item.desc}</p></div>)}</div></div></section>
<section class="section faq-preview" id="faq"><div class="container faq-layout"><SectionHeading eyebrow="FAQ" title="入行之前,先把关键问题问清楚" intro="车型、租期、证照、平台和费用都应该基于你的情况逐项确认。" /><div class="faq-list">{site.faq.slice(0, 4).map((item, index) => <details open={index === 0}><summary><span>{String(index + 1).padStart(2, "0")}</span>{item.q}<i></i></summary><p>{item.a}</p></details>)}</div></div></section>
<CTA />
</main>
<Footer />
</Base>
---
import Base from "../../layouts/Base.astro"; import Header from "../../components/Header.astro"; import Footer from "../../components/Footer.astro"; import CTA from "../../components/CTA.astro"; import SectionHeading from "../../components/SectionHeading.astro"; import Icon from "../../components/Icon.astro"; import { site } from "../../data/site";
const title = `天津网约车司机招募|${site.brand.name}`; const description = "嘉运面向天津全职、兼职网约车司机提供车辆租售、合规办证、平台注册、免费岗前培训与专属运营顾问服务。";
---
<Base title={title} description={description}><Header /><main id="main-content"><section class="join-hero"><div class="container join-hero__inner"><div><p class="eyebrow"><span></span>DRIVER RECRUITMENT</p><h1>想在天津跑网约车?<br /><em>从这里稳稳起步</em></h1><p>全职、兼职都可以聊。车型怎么选、平台怎么配、证件怎么办,嘉运按你的实际情况逐项梳理。</p><div class="hero-actions"><a class="button" href={`tel:${site.contact.tel}`}><Icon name="phone" size={19} />咨询入行方案</a><a class="button button--secondary" href="#fit">看看是否适合</a></div></div><div class="join-card"><span>新人入行支持</span><ul><li><Icon name="check" size={18} />多种租售方案</li><li><Icon name="check" size={18} />人证车证协助</li><li><Icon name="check" size={18} />主流平台对接</li><li><Icon name="check" size={18} />免费岗前培训</li><li><Icon name="check" size={18} />专属顾问跟进</li></ul><small>优惠与扶持政策以门店当期方案为准</small></div></div></section>
<section class="section" id="fit"><div class="container"><SectionHeading eyebrow="IS IT FOR YOU" title="这几类人,都可以来嘉运聊聊" intro="先了解实际情况,再判断适合购车、租车,还是先从短周期方案起步。" /><div class="fit-grid"><div><strong>01</strong><h3>零经验新人</h3><p>对平台、车型和证照流程不熟,希望有人全程带着入行。</p></div><div><strong>02</strong><h3>兼职司机</h3><p>需要根据可投入时间,评估更灵活的车辆与平台组合。</p></div><div><strong>03</strong><h3>全职司机</h3><p>关注长期成本、车辆稳定性和后续运营支持。</p></div><div><strong>04</strong><h3>个人与小车队</h3><p>需要采购或租赁新能源营运车辆,并统一衔接平台服务。</p></div></div></div></section>
<section class="section join-support" id="support"><div class="container join-support__layout"><div><p class="eyebrow"><span></span>FULL SUPPORT</p><h2>上岗不是终点,<br />后续有人跟进才安心</h2><p>嘉运提供从岗前准备到日常运营的持续支持,遇到平台注册、账号或车辆问题,可以联系专属顾问与线下门店。</p><a class="button button--secondary" href="/services/">查看全部服务 <Icon name="arrow" size={18} /></a></div><div class="support-stack">{site.process.map((item) => <div><span>{item.step}</span><div><h3>{item.title}</h3><p>{item.desc}</p></div></div>)}</div></div></section>
<section class="section income-note" id="income"><div class="container"><Icon name="shield" size={28} /><div><h2>先算清成本,再谈跑车计划</h2><p>网约车收入会受到在线时长、接单效率、平台规则、市场供需和车辆成本等多种因素影响。嘉运不作绝对收益承诺,建议到店结合个人情况核算。</p></div></div></section><CTA title="告诉我们你的时间与预算,匹配一套入行方案" /></main><Footer /></Base>
import type { APIRoute } from "astro";
import { site } from "../data/site";
export const prerender = true;
export const GET: APIRoute = ({ site: siteUrl }) => {
const base = (siteUrl?.href || "https://www.jiayunche.com/").replace(/\/$/, "");
const body = `# ${site.brand.name}\n\n> ${site.brand.tagline}\n\n${site.home.subhead}\n\n## 核心服务\n${site.services.map((item) => `- ${item.name}${item.intro}`).join("\n")}\n\n## 联系方式\n- 电话:${site.contact.phone}\n- 地址:${site.contact.address}\n- 服务时间:${site.contact.serviceHours}\n\n## 重要页面\n- 首页:${base}/\n- 服务方案:${base}/services/\n- 司机招募:${base}/join/\n- 跑车指南:${base}/articles/\n- 关于嘉运:${base}/about/\n- 常见问题:${base}/faq/\n- 联系我们:${base}/contact/\n\n## 信息边界\n- 平台规则、准入条件、车型价格与优惠政策以实时信息为准。\n- 嘉运不对网约车营收作绝对承诺。\n`;
return new Response(body, { headers: { "Content-Type": "text/plain; charset=utf-8" } });
};
---
import Base from "../../layouts/Base.astro"; import Header from "../../components/Header.astro"; import Footer from "../../components/Footer.astro"; import CTA from "../../components/CTA.astro"; import SectionHeading from "../../components/SectionHeading.astro"; import Icon from "../../components/Icon.astro"; import { site } from "../../data/site";
const title = `服务方案|${site.brand.name}`; const description = "嘉运网约车提供新能源车辆销售与租赁、合规证照代办、平台入驻、司机培训、运营指导、售后维保与道路救援。";
---
<Base title={title} description={description}>
<Header /><main id="main-content"><section class="page-hero"><div class="container"><div class="breadcrumb"><a href="/">首页</a><span>/</span>服务方案</div><p class="eyebrow"><span></span>OUR SERVICES</p><h1>跑网约车需要的,<br /><em>一站配齐</em></h1><p>不只给一台车,更从合规、平台与运营角度,帮你规划完整的跑车方案。</p></div></section>
<section class="section"><div class="container service-detail-list">{site.services.map((service, index) => <article class:list={["service-detail", index % 2 && "service-detail--reverse"]} id={service.slug} data-reveal><div class="service-detail__summary"><span>SERVICE {service.index}</span><h3>{service.name}</h3><p>{service.value}</p><dl><div><dt>服务对象</dt><dd>{service.audience}</dd></div><div><dt>服务方式</dt><dd>到店沟通后按实际情况匹配方案</dd></div></dl></div><div class="service-detail__copy"><p class="eyebrow"><span></span>SERVICE {service.index}</p><h2>{service.name}</h2><p class="large-copy">{service.intro}</p><dl><div><dt>适合谁</dt><dd>{service.audience}</dd></div><div><dt>核心价值</dt><dd>{service.value}</dd></div></dl><p class="service-note"><Icon name="check" size={17} />{service.note}</p><a class="button button--secondary" href={`tel:${site.contact.tel}`}>咨询这项服务 <Icon name="arrow" size={18} /></a></div></article>)}</div></section>
<section class="section process-section"><div class="container"><SectionHeading eyebrow="SERVICE FLOW" title="你的情况不同,方案也应该不同" intro="从沟通到上岗,每一步都有明确的下一步。" align="center" /><div class="process-grid">{site.process.map((item) => <div class="process-card"><span>{item.step}</span><h3>{item.title}</h3><p>{item.desc}</p></div>)}</div></div></section><CTA /></main><Footer />
</Base>
import type { APIRoute } from "astro";
import path from "node:path";
import fs from "node:fs/promises";
import { UPLOADS_DIR } from "../../lib/article-store";
export const prerender = false;
const MIME_TYPES: Record<string, string> = {
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".webp": "image/webp",
".gif": "image/gif",
};
export const GET: APIRoute = async ({ params }) => {
const filename = params.file || "";
if (!/^[a-zA-Z0-9._-]+$/.test(filename)) return new Response("Not Found", { status: 404 });
try {
const content = await fs.readFile(path.join(UPLOADS_DIR, filename));
return new Response(content, {
headers: {
"Content-Type": MIME_TYPES[path.extname(filename).toLowerCase()] || "application/octet-stream",
"Cache-Control": "public, max-age=31536000, immutable",
},
});
} catch {
return new Response("Not Found", { status: 404 });
}
};
This diff is collapsed.
{
"extends": "astro/tsconfigs/strict"
}
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment