commit 72648b491450ae95e942b191640f06cb5d2c942e Author: douba Date: Thu Oct 29 06:20:35 2026 +0000 初始化兜爸心选 App 官网项目(构建与发布) - src/:从线上 shop.douba.top/public/app 拉取的页面源码(v0.3.8) - 版本号、更新日期、安装包大小改为模板变量,统一由 site.config.json 管理, 安装包大小在构建时从 APK 实际字节数计算 - scripts/build.mjs:构建到 dist/,校验 APK、模板变量与资源引用 - scripts/deploy.mjs:与线上比对差异 → 上传 → 备份 → 原子切换 → 线上验证 - scripts/rollback.mjs、scripts/preview.mjs:回滚与本地预览 - AGENTS.md:项目说明、发版流程与服务器/nginx 约定 由 AI 助手协助完成。 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f17f4dd --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +dist/ +releases/*.apk +node_modules/ +.DS_Store diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..47a8a32 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,133 @@ +# AGENTS.md · 兜爸心选 App 官网(介绍与下载页) + +「兜爸心选」移动应用的官方介绍与下载页。 + +- **对外访问地址:**(不带斜杠,应用商店登记的就是这个地址,对外宣传一律用它) +- `https://shop.douba.top/app/` 同样可以访问;页面里的 `canonical`、`og:url` 和 JSON-LD 的 `url` 写的是带斜杠的版本 +- 开发者主体:北京臻惠网络科技有限公司 · 京ICP备17065521号 +- 应用包名:`com.yizhezhe.douba`(基于 DCloud uni-app 构建) +- 代码仓库:`git@gitea.nexjs.com:jone/douba-app-web.git`(分支 `main`;APK 和 dist 不进仓库) + +站点是纯静态的:手写 HTML + CSS + 原生 JS,**运行时没有任何依赖**。本项目只负责一件事:在本地编辑 `src/`,构建出 `dist/`,再发布到服务器。构建和发布脚本只用 Node 内置模块,**不需要 `npm install`**。 + +## 常用命令 + +```bash +npm run build # src/ → dist/,渲染版本变量并校验资源引用 +npm run preview # 构建并在 http://localhost:8080/app 本地预览 +npm run deploy:check # 构建并列出与线上的差异,不改服务器(发布前先跑这个) +npm run deploy # 正式发布到 8.130.191.152 +npm run rollback # 线上目录换回最近一次发布前的备份 +npm run rollback:list # 列出服务器上的备份 +``` + +发布需要本机能免密 SSH 到 `root@8.130.191.152`(公钥登录),需要 Node 18 及以上版本。 + +## 项目结构 + +``` +douba-app/ +├── site.config.json 版本号、发布日期、部署目标 —— 发版时改这里 +├── src/ 站点源码(HTML 中可以使用 {{变量}}) +│ ├── index.html 官网首页(单页:首屏 / 三条承诺 / 品牌理念 / 应用功能 / 应用截图 / 下载 / FAQ / 页脚) +│ ├── styles.css 首页样式(含响应式断点) +│ ├── main.js 首页轻交互(无依赖 IIFE) +│ ├── terms.html 《用户服务协议》 +│ ├── permissions.html 《权限说明》(Android 权限清单与第三方 SDK) +│ ├── legal.css 两个合规文本页共用的样式 +│ └── assets/ +│ ├── brand.webp 品牌 Logo +│ ├── favicon.png +│ ├── og.jpg 分享卡片图(og:image) +│ ├── qr.svg 指向 shop.douba.top/app 的二维码 +│ ├── shot-mama-1~3.webp 孕育模块真实截图(600×1301) +│ ├── shot-mall-1~3.webp 商城模块真实截图(600×1360) +│ └── p1~p5.webp 商品图,只用于「应用截图」区的 HTML 仿真手机界面 +├── releases/ APK 安装包,文件名必须是 douba-v<版本>.apk(不进 git) +├── scripts/ +│ ├── build.mjs 构建 + 校验 +│ ├── deploy.mjs 发布 +│ ├── rollback.mjs 回滚 +│ └── preview.mjs 本地预览服务器 +└── dist/ 构建产物(不进 git,不要手改) +``` + +《隐私政策》不属于本项目:页面链接的是站点根目录的 `/protocol.html`,由 CRMEB 商城维护,改它之前先确认。 + +## 模板变量与 site.config.json + +构建时只渲染 `.html` 里的 `{{变量}}`。CSS 和 JS 原样复制。使用未定义的变量会导致构建失败。 + +| 变量 | 来源 | 用在哪里 | +|---|---|---| +| `{{version}}` | `version` | JSON-LD `softwareVersion`、首屏、下载区、FAQ | +| `{{channel}}` | `channel`(如「内测」) | 「v0.3.8(内测)」「内测版」 | +| `{{releaseDate}}` | `releaseDate`(YYYY-MM-DD,北京时间) | 下载区「更新时间」 | +| `{{screenshotVersion}}` | `screenshotVersion` | 截图区「截图取自 Android 版 v…」 | +| `{{apkFile}}` | 由 `version` 推出:`douba-v.apk` | 下载按钮链接 | +| `{{apkSize}}` | **自动读取** `releases/` 里 APK 的实际大小(十进制 MB) | 首屏、下载按钮、下载区 | +| `{{permissions.version}}` / `{{permissions.updatedCn}}` | `permissions.version` / `permissions.updated` | 权限说明页的适用版本、更新日期 | + +截图版本和权限说明有单独的变量,因为它们只在截图或权限确实变了的时候才更新,不一定跟着每个版本走。 +`terms.html` 里的协议更新日期、生效日期属于法律文本,是写死的,不做成变量。 + +构建会做三项校验,任何一项不通过都会中止,不会产出 dist: +- `releases/` 里存在与 `version` 对应的 APK; +- 没有未定义的模板变量; +- HTML 的 `src`/`href`/`content`(包括 `og:image` 这类写成 `https://shop.douba.top/app/...` 的完整地址)和 CSS 的 `url()`,凡是指向 `/app` 内的文件都必须存在。指向站点根目录的路径(如 `/protocol.html`)跳过不查。 + +## 发布新版 APK + +1. 把新安装包放进 `releases/douba-v<新版本>.apk`。 +2. 修改 `site.config.json`:`version`、`releaseDate`(按北京时间);渠道变了就改 `channel`;截图换了就改 `screenshotVersion`;权限或 SDK 有变化就改 `permissions` 并同步修改 `src/permissions.html` 的清单。 +3. 截图有变化的话,替换 `src/assets/shot-*.webp`(保持 600px 宽)。 +4. `npm run preview` 本地看一遍,然后 `npm run deploy:check` 确认差异,没问题再 `npm run deploy`。 + +iOS 上架后:在 `src/index.html` 里把 `.dl-actions` 中 `is-disabled` 的「iOS 版即将上架」换成 App Store 链接,同时更新首屏 `.hero-meta`、FAQ「iPhone 可以用吗?」和 JSON-LD 的 `operatingSystem`。 + +## 发布机制(deploy.mjs) + +1. 构建,然后用 sha256 对比 dist 和线上文件,列出新增(+)、修改(~)、删除(-)的文件。没有差异就直接退出。 +2. 把站点文件打包上传。APK 只有在线上没有同一文件(sha256 不同)时才上传。 +3. 在服务器的 `/www/wwwroot/shop.douba.top/.deploy/`(位于 `public/` 之外,公网访问不到)组装新目录: + - 线上 `download/` 里已有的 APK 全部以**硬链接**保留,旧版下载链接不会失效,也不占额外空间; + - 校验 APK 的 sha256,统一设为 `root:root`,目录权限 755、文件权限 644。 +4. `mv public/app → .deploy/backups/app-<北京时间>`,再 `mv 新目录 → public/app`。两步都在同一分区内重命名,切换是瞬间完成的。备份保留最近 `keepBackups` 份(默认 5 份)。 +5. 线上验证:请求 `/app`,确认页面版本号正确;请求 APK 地址,确认大小和本地一致。验证失败会报错,这时用 `npm run rollback` 回滚。 + +不要直接在服务器上改 `public/app/` 下的文件:下次发布会被 `src/` 覆盖。确实需要临时在线上修改的话,事后要同步回 `src/`。 + +## 页面要点 + +- `index.html` 各区块用 `` 注释分隔,锚点:`#about` `#features` `#screens` `#download` `#faq`。 +- **首屏手机**(`#heroSlides`):6 张真实截图自动轮播,由 `main.js` 控制(4.2 秒一张;悬停、切到后台时暂停;系统开启「减少动效」时不轮播)。第一张不加 `loading="lazy"`,其余都加。 +- **应用截图区**(`.shots`):前 3 张是真实截图(`shot-mama-*`);后 3 张(商城首页 / 分类 / 商品详情)是**用 HTML + CSS 画出来的仿真界面**,商品名和价格是写死的示例数据。换成真实截图时,按前 3 个 `
` 的写法改,同时更新 `figcaption`。 +- **入场动画**:给元素加 `data-reveal`,`main.js` 用 IntersectionObserver 在元素进入视口时加 `.is-in`;`html.js` 类表示 JS 已运行(没有 JS 时内容照常显示)。 +- **FAQ**:用原生 `
`,`main.js` 保证同时只展开一个。 + +## 编码约定 + +- **首页资源一律用绝对路径 `/app/...`。** 因为 nginx 让 `/app`(不带斜杠)直接返回 `index.html`,这时相对路径会解析到站点根目录,样式和图片会全部失效。 +- 合规页(`terms.html`、`permissions.html`)用的是相对路径(`legal.css`、`assets/...`)。它们只会通过 `/app/xxx.html` 访问,所以没问题;返回首页统一链接 `/app/`。 +- 视觉方向是「品牌手账」:纸感底色、印章红、宋体标题、楷体手写批注。颜色、字体只用 `:root` 里的 CSS 变量(`--paper` `--ink` `--red` `--serif` `--hand` 等),不要新写死色值。`legal.css` 里复制了一份同名变量,改主色时**两处都要改**。 +- 不引入任何框架、CDN、字体文件或统计脚本;`main.js` 保持 ES5 写法、无依赖。 +- 图片用 webp,`` 写上 `width`/`height`,防止加载时布局跳动;首屏以外的图片加 `loading="lazy" decoding="async"`。 +- 无障碍:保留 skip link、`aria-label`、装饰元素上的 `aria-hidden="true"`,以及 `prefers-reduced-motion` 下关闭动效的处理。 +- 文案为简体中文,中文与英文、数字之间留空格(如「Android 5.0 及以上」「61.5 MB」)。 +- 响应式断点:1080 / 900 / 760 / 560px(见 `styles.css` 末尾)。 +- 公司名、ICP 备案号、包名、客服时间(工作日 9:00–18:00)在多个页面里写死,修改时全局搜索替换。 +- 合规文本(用户协议、权限说明、隐私政策)的内容改动要经过运营或法务确认,不要只为了排版顺手改措辞。 + +## 服务器与 nginx + +- 站点:`shop.douba.top` 是 CRMEB PRO(ThinkPHP)商城,Web 根目录是 `/www/wwwroot/shop.douba.top/public/`。本项目只管 `public/app/`,**不要动 `public/` 下的其他文件**。 +- 站点配置(宝塔面板):`/www/server/panel/vhost/nginx/shop.douba.top.conf` +- 本页专用规则:`/www/server/panel/vhost/nginx/extension/shop.douba.top/app.conf` + ```nginx + location = /app { root /www/wwwroot/shop.douba.top/public; try_files /app/index.html =404; } + ``` + 应用商店登记的地址不带斜杠,所以 `/app` 不能 301 跳转,**不要删这条规则**。 + 同一个文件里还有一条 `location ~ ^/app/download/[^/]+\.apk$` 规则:让 APK 以标准类型 `application/vnd.android.package-archive` 返回,不存在的 APK 返回 404(不会被商城接走)。改这个文件前,先把原文件备份到 `/www/wwwroot/shop.douba.top/.deploy/nginx-backup/`。 +- 不存在的路径会被商城的 rewrite 规则接走,返回 CRMEB 页面和 **200 状态码**,所以不能用状态码判断文件是否存在,要看返回的内容。 +- 静态文件的缓存头是 `max-age=43200` + `no-cache`(每次请求都会向服务器验证),发布后一般刷新就能看到;微信内置浏览器缓存可能更久。 +- 改 nginx 配置后先 `nginx -t` 检查,再 `nginx -s reload`。 diff --git a/package.json b/package.json new file mode 100644 index 0000000..56b37de --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "name": "douba-app", + "private": true, + "description": "兜爸心选 App 官网(介绍与下载页),发布到 https://shop.douba.top/app", + "type": "module", + "scripts": { + "build": "node scripts/build.mjs", + "preview": "node scripts/build.mjs && node scripts/preview.mjs", + "deploy:check": "node scripts/deploy.mjs --dry-run", + "deploy": "node scripts/deploy.mjs", + "rollback": "node scripts/rollback.mjs", + "rollback:list": "node scripts/rollback.mjs --list" + }, + "engines": { + "node": ">=18" + } +} diff --git a/scripts/build.mjs b/scripts/build.mjs new file mode 100644 index 0000000..f89e75d --- /dev/null +++ b/scripts/build.mjs @@ -0,0 +1,117 @@ +// 构建:src/ → dist/,渲染 {{变量}},拷入 APK,并校验页面引用的本地资源都存在 +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const SRC = path.join(ROOT, 'src'); +const DIST = path.join(ROOT, 'dist'); +const RELEASES = path.join(ROOT, 'releases'); + +export function loadConfig() { + return JSON.parse(fs.readFileSync(path.join(ROOT, 'site.config.json'), 'utf8')); +} + +export function apkFileName(cfg) { + return `douba-v${cfg.version}.apk`; +} + +// 2026-10-29 → 2026 年 10 月 29 日 +function cnDate(iso) { + const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso || ''); + if (!m) throw new Error(`日期格式应为 YYYY-MM-DD:${iso}`); + return `${m[1]} 年 ${Number(m[2])} 月 ${Number(m[3])} 日`; +} + +// 字节 → "61.5 MB"(十进制,和手机系统显示一致) +function mb(bytes) { + return `${(bytes / 1e6).toFixed(1)} MB`; +} + +function walk(dir) { + return fs.readdirSync(dir, { withFileTypes: true }).flatMap((e) => { + const p = path.join(dir, e.name); + return e.isDirectory() ? walk(p) : [p]; + }); +} + +function render(text, vars, file) { + return text.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (_, key) => { + if (!(key in vars)) throw new Error(`${file}: 未定义的模板变量 {{${key}}}`); + return vars[key]; + }); +} + +// 找出 HTML 的 src/href 和 CSS 的 url(),确认指向 /app 内的文件都存在 +function checkRefs(files) { + const missing = []; + for (const file of files) { + const ext = path.extname(file); + if (ext !== '.html' && ext !== '.css') continue; + const text = fs.readFileSync(file, 'utf8'); + const re = ext === '.html' ? /\s(?:src|href|content)="([^"]+)"/g : /url\(["']?([^"')]+)["']?\)/g; + for (let [, raw] of text.matchAll(re)) { + raw = raw.replace(/^https:\/\/shop\.douba\.top(?=\/app)/, ''); // og:image 等写的是本站完整地址 + if (/\s/.test(raw) || (!raw.includes('/') && !raw.includes('.'))) continue; // content 里的普通文本 + const ref = raw.split(/[?#]/)[0]; + if (!ref || /^(https?:|data:|mailto:|tel:|\/\/)/.test(raw) || /^(#|%23)/.test(raw)) continue; // %23 = 内联 SVG 里的 url(#id) + let target; + if (ref.startsWith('/app/')) target = path.join(DIST, ref.slice(5)); + else if (ref === '/app' || ref === '/app/') target = path.join(DIST, 'index.html'); + else if (ref.startsWith('/')) continue; // 站点根下的商城页面(如 /protocol.html),不归本项目管 + else target = path.join(path.dirname(file), ref); + if (fs.existsSync(target) && fs.statSync(target).isDirectory()) target = path.join(target, 'index.html'); + if (!fs.existsSync(target)) missing.push(`${path.relative(DIST, file)} → ${raw}`); + } + } + if (missing.length) throw new Error(`以下引用的文件不存在:\n ${missing.join('\n ')}`); +} + +export function build() { + const cfg = loadConfig(); + const apkFile = apkFileName(cfg); + const apkSrc = path.join(RELEASES, apkFile); + if (!fs.existsSync(apkSrc)) { + throw new Error(`找不到安装包 releases/${apkFile}(site.config.json 的 version 是 ${cfg.version})`); + } + const apkBytes = fs.statSync(apkSrc).size; + + const vars = { + version: cfg.version, + channel: cfg.channel, + releaseDate: cfg.releaseDate, + screenshotVersion: cfg.screenshotVersion, + 'permissions.version': cfg.permissions.version, + 'permissions.updatedCn': cnDate(cfg.permissions.updated), + apkFile, + apkSize: mb(apkBytes), + }; + cnDate(cfg.releaseDate); // 只做格式校验 + + fs.rmSync(DIST, { recursive: true, force: true }); + for (const file of walk(SRC)) { + const out = path.join(DIST, path.relative(SRC, file)); + fs.mkdirSync(path.dirname(out), { recursive: true }); + if (path.extname(file) === '.html') { + fs.writeFileSync(out, render(fs.readFileSync(file, 'utf8'), vars, path.relative(ROOT, file))); + } else { + fs.copyFileSync(file, out); + } + } + fs.mkdirSync(path.join(DIST, 'download'), { recursive: true }); + fs.copyFileSync(apkSrc, path.join(DIST, 'download', apkFile)); + + checkRefs(walk(DIST)); + return { cfg, apkFile, apkBytes, vars }; +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + try { + const { cfg, apkFile, vars } = build(); + console.log(`构建完成 → dist/`); + console.log(` 版本 v${cfg.version}(${cfg.channel}) · ${apkFile} · ${vars.apkSize} · 更新时间 ${cfg.releaseDate}`); + } catch (e) { + console.error(`构建失败:${e.message}`); + process.exit(1); + } +} diff --git a/scripts/deploy.mjs b/scripts/deploy.mjs new file mode 100644 index 0000000..bba795a --- /dev/null +++ b/scripts/deploy.mjs @@ -0,0 +1,136 @@ +// 发布:构建 → 对比线上差异 → 上传 → 备份线上目录 → 切换 → 线上验证 +// node scripts/deploy.mjs 正式发布 +// node scripts/deploy.mjs --dry-run 只构建并列出差异,不改服务器 +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import crypto from 'node:crypto'; +import { spawnSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import { build } from './build.mjs'; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const DIST = path.join(ROOT, 'dist'); +const dryRun = process.argv.includes('--dry-run'); + +function run(cmd, args, opts = {}) { + const r = spawnSync(cmd, args, { encoding: 'utf8', maxBuffer: 64 << 20, ...opts }); + if (r.status !== 0) { + throw new Error(`${cmd} ${args.slice(0, 2).join(' ')} 失败(退出码 ${r.status})\n${r.stderr || ''}`); + } + return r.stdout; +} +const ssh = (host, script) => run('ssh', ['-o', 'BatchMode=yes', host, 'bash -s'], { input: `set -euo pipefail\n${script}` }); + +const sha256 = (file) => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex'); + +function walk(dir) { + return fs.readdirSync(dir, { withFileTypes: true }).flatMap((e) => { + const p = path.join(dir, e.name); + return e.isDirectory() ? walk(p) : [p]; + }); +} + +// 北京时间时间戳,用作备份目录名 +function stamp() { + const d = new Date(Date.now() + 8 * 3600e3).toISOString(); + return d.slice(0, 19).replace(/[-:]/g, '').replace('T', '-'); +} + +async function main() { + const { cfg, apkFile, apkBytes, vars } = build(); + const { host, siteDir, publicUrl, keepBackups } = cfg.deploy; + const live = `${siteDir}/public/app`; + const deployDir = `${siteDir}/.deploy`; // 在 public/ 之外,不会被公网访问 + const ts = stamp(); + console.log(`构建完成:v${cfg.version}(${cfg.channel}) · ${apkFile} · ${vars.apkSize}`); + + // ---- 对比线上(APK 单独比较,download/ 里的其他旧包不参与) ---- + const local = new Map( + walk(DIST) + .filter((f) => !f.startsWith(path.join(DIST, 'download') + path.sep)) + .map((f) => [path.relative(DIST, f), sha256(f)]), + ); + const remoteOut = ssh(host, ` + cd ${live} + find . -type f ! -path './download/*' -exec sha256sum {} + | sed 's| \\./| |' + echo "__APK__ $(sha256sum download/${apkFile} 2>/dev/null | cut -d' ' -f1)" + `); + const remote = new Map(); + let remoteApkSha = ''; + for (const line of remoteOut.trim().split('\n')) { + if (line.startsWith('__APK__')) remoteApkSha = line.slice(8).trim(); + else if (line) { + const [sum, file] = line.split(/\s{2}/); + remote.set(file, sum); + } + } + const added = [...local.keys()].filter((f) => !remote.has(f)); + const changed = [...local.keys()].filter((f) => remote.has(f) && remote.get(f) !== local.get(f)); + const removed = [...remote.keys()].filter((f) => !local.has(f)); + const localApkSha = sha256(path.join(DIST, 'download', apkFile)); + const uploadApk = remoteApkSha !== localApkSha; + + console.log('\n与线上的差异:'); + for (const f of added) console.log(` + ${f}`); + for (const f of changed) console.log(` ~ ${f}`); + for (const f of removed) console.log(` - ${f}`); + console.log(` APK ${apkFile}:${uploadApk ? (remoteApkSha ? '线上不同,需上传' : '线上没有,需上传') : '线上已是同一文件,跳过上传'}`); + if (!added.length && !changed.length && !removed.length && !uploadApk) { + console.log('\n线上已经是最新,无需发布。'); + return; + } + if (dryRun) { + console.log('\n--dry-run:未改动服务器。'); + return; + } + + // ---- 上传 ---- + const tgz = path.join(os.tmpdir(), `douba-app-${ts}.tgz`); + run('tar', ['czf', tgz, '-C', DIST, '--exclude=./download', '.']); + const remoteTgz = `/tmp/douba-app-${ts}.tgz`; + const remoteApk = `/tmp/douba-app-${ts}.apk`; + console.log('\n上传站点文件…'); + run('scp', ['-q', '-o', 'BatchMode=yes', tgz, `${host}:${remoteTgz}`]); + fs.rmSync(tgz); + if (uploadApk) { + console.log(`上传 ${apkFile}(${vars.apkSize},需要一会儿)…`); + run('scp', ['-q', '-o', 'BatchMode=yes', path.join(DIST, 'download', apkFile), `${host}:${remoteApk}`], { stdio: ['ignore', 'inherit', 'pipe'] }); + } + + // ---- 服务器端:组装新目录 → 备份旧目录 → 切换 ---- + // 新目录与线上同在一个分区,两次 mv 都是瞬间完成的重命名 + console.log('切换线上目录…'); + ssh(host, ` + stage=${deployDir}/stage-${ts} + mkdir -p "$stage/download" ${deployDir}/backups + tar xzf ${remoteTgz} -C "$stage" + # 保留线上已有的 APK(硬链接,不占额外空间),旧版下载链接不会失效 + for f in ${live}/download/*.apk; do [ -e "$f" ] && ln "$f" "$stage/download/"; done + ${uploadApk ? `mv -f ${remoteApk} "$stage/download/${apkFile}"` : ':'} + [ "$(sha256sum "$stage/download/${apkFile}" | cut -d' ' -f1)" = "${localApkSha}" ] || { echo "APK 校验失败" >&2; exit 1; } + chown -R root:root "$stage" + find "$stage" -type d -exec chmod 755 {} + + find "$stage" -type f -exec chmod 644 {} + + mv ${live} ${deployDir}/backups/app-${ts} + mv "$stage" ${live} + rm -f ${remoteTgz} + ls -1d ${deployDir}/backups/app-* | sort -r | tail -n +${keepBackups + 1} | xargs -r rm -rf + `); + + // ---- 线上验证 ---- + console.log('验证线上…'); + const page = run('curl', ['-fsS', '-H', 'Cache-Control: no-cache', publicUrl]); + if (!page.includes(`"softwareVersion": "${cfg.version}"`)) throw new Error(`${publicUrl} 返回的页面不是 v${cfg.version}`); + const head = run('curl', ['-fsSI', `${publicUrl}/download/${apkFile}`]); + const len = Number((/content-length:\s*(\d+)/i.exec(head) || [])[1]); + if (len !== apkBytes) throw new Error(`APK 下载地址返回的大小 ${len} 与本地 ${apkBytes} 不一致`); + + console.log(`\n发布成功:${publicUrl} (v${cfg.version},备份 ${deployDir}/backups/app-${ts})`); + console.log('如需回滚:npm run rollback'); +} + +main().catch((e) => { + console.error(`\n发布失败:${e.message}`); + process.exit(1); +}); diff --git a/scripts/preview.mjs b/scripts/preview.mjs new file mode 100644 index 0000000..cef2456 --- /dev/null +++ b/scripts/preview.mjs @@ -0,0 +1,28 @@ +// 本地预览 dist/:按线上路径挂在 /app 下(/app 和 /app/ 都返回首页) +import http from 'node:http'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const DIST = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'dist'); +const PORT = Number(process.env.PORT) || 8080; +const TYPES = { + '.html': 'text/html; charset=utf-8', '.css': 'text/css; charset=utf-8', '.js': 'text/javascript; charset=utf-8', + '.svg': 'image/svg+xml', '.webp': 'image/webp', '.png': 'image/png', '.jpg': 'image/jpeg', + '.apk': 'application/vnd.android.package-archive', +}; + +http.createServer((req, res) => { + const url = decodeURIComponent(req.url.split('?')[0]); + if (url === '/') { res.writeHead(302, { Location: '/app' }); return res.end(); } + if (url !== '/app' && !url.startsWith('/app/')) { + res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' }); + return res.end(`本地预览只包含 /app,${url} 属于线上商城`); + } + let file = path.join(DIST, url.slice(4)); + if (!file.startsWith(DIST)) { res.writeHead(403); return res.end(); } + if (fs.existsSync(file) && fs.statSync(file).isDirectory()) file = path.join(file, 'index.html'); + if (!fs.existsSync(file)) { res.writeHead(404); return res.end('Not Found'); } + res.writeHead(200, { 'Content-Type': TYPES[path.extname(file)] || 'application/octet-stream' }); + fs.createReadStream(file).pipe(res); +}).listen(PORT, () => console.log(`本地预览:http://localhost:${PORT}/app`)); diff --git a/scripts/rollback.mjs b/scripts/rollback.mjs new file mode 100644 index 0000000..dd86885 --- /dev/null +++ b/scripts/rollback.mjs @@ -0,0 +1,27 @@ +// 回滚:把线上目录换回最近一次发布前的备份 +// node scripts/rollback.mjs 回滚到最近的备份 +// node scripts/rollback.mjs --list 列出可用备份 +import { spawnSync } from 'node:child_process'; +import { loadConfig } from './build.mjs'; + +const { host, siteDir, publicUrl } = loadConfig().deploy; +const live = `${siteDir}/public/app`; +const backups = `${siteDir}/.deploy/backups`; +const list = process.argv.includes('--list'); + +const script = list + ? `ls -1d ${backups}/app-* 2>/dev/null | sort -r || echo "(没有备份)"` + : ` + latest=$(ls -1d ${backups}/app-* 2>/dev/null | sort -r | head -1) + [ -n "$latest" ] || { echo "没有可回滚的备份" >&2; exit 1; } + mv ${live} ${siteDir}/.deploy/rolled-back-$(date +%Y%m%d-%H%M%S) + mv "$latest" ${live} + echo "已回滚到 $latest" + `; + +const r = spawnSync('ssh', ['-o', 'BatchMode=yes', host, 'bash -s'], { + input: `set -euo pipefail\n${script}`, + stdio: ['pipe', 'inherit', 'inherit'], +}); +if (r.status !== 0) process.exit(r.status ?? 1); +if (!list) console.log(`请打开 ${publicUrl} 确认。`); diff --git a/site.config.json b/site.config.json new file mode 100644 index 0000000..648ace8 --- /dev/null +++ b/site.config.json @@ -0,0 +1,16 @@ +{ + "version": "0.3.8", + "channel": "内测", + "releaseDate": "2026-10-29", + "screenshotVersion": "0.3.8", + "permissions": { + "version": "0.3.8", + "updated": "2026-10-29" + }, + "deploy": { + "host": "root@8.130.191.152", + "siteDir": "/www/wwwroot/shop.douba.top", + "publicUrl": "https://shop.douba.top/app", + "keepBackups": 5 + } +} diff --git a/src/assets/brand.webp b/src/assets/brand.webp new file mode 100644 index 0000000..b65fd9f Binary files /dev/null and b/src/assets/brand.webp differ diff --git a/src/assets/favicon.png b/src/assets/favicon.png new file mode 100644 index 0000000..d28d107 Binary files /dev/null and b/src/assets/favicon.png differ diff --git a/src/assets/og.jpg b/src/assets/og.jpg new file mode 100644 index 0000000..38f6bbb Binary files /dev/null and b/src/assets/og.jpg differ diff --git a/src/assets/p1.webp b/src/assets/p1.webp new file mode 100644 index 0000000..2255bf9 Binary files /dev/null and b/src/assets/p1.webp differ diff --git a/src/assets/p2.webp b/src/assets/p2.webp new file mode 100644 index 0000000..608bb3e Binary files /dev/null and b/src/assets/p2.webp differ diff --git a/src/assets/p3.webp b/src/assets/p3.webp new file mode 100644 index 0000000..75661bd Binary files /dev/null and b/src/assets/p3.webp differ diff --git a/src/assets/p4.webp b/src/assets/p4.webp new file mode 100644 index 0000000..e3debb9 Binary files /dev/null and b/src/assets/p4.webp differ diff --git a/src/assets/p5.webp b/src/assets/p5.webp new file mode 100644 index 0000000..f8f97fa Binary files /dev/null and b/src/assets/p5.webp differ diff --git a/src/assets/qr.svg b/src/assets/qr.svg new file mode 100644 index 0000000..3389535 --- /dev/null +++ b/src/assets/qr.svg @@ -0,0 +1 @@ + diff --git a/src/assets/shot-mall-1.webp b/src/assets/shot-mall-1.webp new file mode 100644 index 0000000..471d1a3 Binary files /dev/null and b/src/assets/shot-mall-1.webp differ diff --git a/src/assets/shot-mall-2.webp b/src/assets/shot-mall-2.webp new file mode 100644 index 0000000..99e3b28 Binary files /dev/null and b/src/assets/shot-mall-2.webp differ diff --git a/src/assets/shot-mall-3.webp b/src/assets/shot-mall-3.webp new file mode 100644 index 0000000..7b06b12 Binary files /dev/null and b/src/assets/shot-mall-3.webp differ diff --git a/src/assets/shot-mama-1.webp b/src/assets/shot-mama-1.webp new file mode 100644 index 0000000..cbe1034 Binary files /dev/null and b/src/assets/shot-mama-1.webp differ diff --git a/src/assets/shot-mama-2.webp b/src/assets/shot-mama-2.webp new file mode 100644 index 0000000..dca9975 Binary files /dev/null and b/src/assets/shot-mama-2.webp differ diff --git a/src/assets/shot-mama-3.webp b/src/assets/shot-mama-3.webp new file mode 100644 index 0000000..794a3ee Binary files /dev/null and b/src/assets/shot-mama-3.webp differ diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..704cb96 --- /dev/null +++ b/src/index.html @@ -0,0 +1,466 @@ + + + + + +兜爸心选 · 孕婴童好物一站购齐|官方应用官网 + + + + + + + + + + + + + + + + + + +
+ + +
+ + +
+
+

官方应用官网 · 孕婴童全品类

+

兜爸心选

+

孕婴童好物,
一站购齐。

+

用父爱的严谨与担当,为宝妈和宝宝甄选全球优质好物。
用心守护爱的每一步。

+ + + +
    +
  • 版本 v{{version}}({{channel}})
  • +
  • 安装包 {{apkSize}}
  • +
  • Android 5.0 及以上
  • +
  • iOS 版即将上架
  • +
+
+ +
+ + +
+ 孕育首页:孕周倒计时与数胎动、体重记录 + 今日孕期小知识 + 今日脚印与兜豆兑换 + 商城首页:今日推荐与分类入口 + 商品分类 + 商品详情 +
+ +

每一件,都先给
自家孩子用。

+ +
+
+
+ + +
+
+
+ 01 +

全球甄选

+

正规渠道、品牌直供,从源头开始把第一道关。

+
+
+ 02 +

严苛比对

+

成分表、检测报告、真实口碑,逐项比对过才上架。

+
+
+ 03 +

爸爸把关

+

先给自家孩子用;不好用的东西,我们不卖。

+
+
+
+ + +
+
+
+

01 / 品牌理念

+

一个爸爸的
购物清单

+

挑宝宝的东西,标准只有一条:
敢不敢给自己的孩子用。

+
    +
  • 一看成分与配方,说不清楚的不上
  • +
  • 二看检测报告与资质,缺一项不上
  • +
  • 三看真实评价与售后,口碑不好不上
  • +
+
+ +
+ + +

选品笔记 · NO.001

+
+

兜爸心选是一款专注孕婴童的购物 App。从孕期营养、纸尿裤、洗护用品,到辅食、玩具、绘本与出行装备——一家人从孕期到学龄前的日常所需,都能在一个 App 里买齐。

+

我们把选品当成给自家孩子挑东西:先看原料与配方,再看检测报告与真实评价,最后才谈价格。渠道不清楚的不上,成分说不明白的不上,自己用着不顺手的不上。

+

我们不追求把货架堆满,只把值得放进宝宝生活里的东西留下来。

+
+

—— 兜爸

+ +
+
+
+ + +
+
+
+

02 / 应用功能

+

打开它,
孕婴童的日常就够了

+

买东西、查物流、找工具,都在同一个地方。

+
+ +
+
+ 01 + + + +

好物一站购齐

+

奶粉、纸尿裤、洗护、辅食、玩具、绘本……按孕周和月龄挑,不用在几个 App 之间来回比价。

+
+ +
+ 02 + + + +

孕育工具随手用

+

孕周计算、产检提醒、数胎动等孕育小工具,在 App 里一键直达,不用再到处收藏链接。

+
+ +
+ 03 + + + +

订单物流看得见

+

下单、发货、派送、签收,每一步都能查到;退换货进度,也在同一页里。

+
+ +
+ 04 + + + +

专属客服找得到

+

关于商品、尺码、喂养、退换的任何问题,都有人耐心回答,不是机器人绕圈。

+
+
+
+
+ + +
+
+
+
+

03 / 应用截图

+

界面一览

+
+

截图取自 Android 版 v{{screenshotVersion}}
实际界面以应用内为准

+
+ +

左右滑动,查看更多截图 →

+
+
+
+
兜爸心选 App 孕育首页:孕周倒计时与孕育工具
+
+
孕育首页 孕 29 周倒计时、数胎动与体重,一屏看全
+
+ +
+
+
兜爸心选 App 今日孕期小知识
+
+
孕期知识 本周话题与今日小知识,按孕周推送
+
+ +
+
+
兜爸心选 App 今日脚印与兜豆兑换
+
+
今日脚印 一天的记录收成一条时间轴,兜豆还能换实物
+
+ +
+
+
+
9:41
+
+ + +
+
+

孕婴童好物节

+

从孕期到学龄前
一个 App 买齐

+ 0–3 岁 · 全品类 +
+
奶粉纸尿裤洗护玩具
+
兜爸今日推荐换一批
+
+
+
+

大王光之羽 环贴式纸尿裤

+

¥220¥338

+
+
+
+

海龟爸爸 婴童物理防晒乳

+

¥99¥129

+
+
+
+

纽强 婴幼儿保湿面霜

+

¥198

+
+
+
+

贝肽斯 宝宝防晒遮阳帽

+

¥35.9

+
+
+
+ 首页 + 分类 + 购物车 + 我的 +
+
+
+
商城首页 今日推荐与分类入口,想买的都在第一屏
+
+ +
+
+
+
9:41
+
全部分类
+
+
    +
  • 奶粉
  • 纸尿裤
  • 洗护
  • 辅食
  • 玩具
  • 绘本
  • 出行
  • 妈妈
  • +
+
+

大王光之羽 环贴式纸尿裤

品牌直供

¥220
+

纽强 婴幼儿保湿面霜

敏感肌可用

¥198
+

贝肽斯 宝宝防晒遮阳帽

夏季上新

¥35.9
+

海龟爸爸 婴童物理防晒乳

纯物理配方

¥99
+

沃隆 每日坚果妈妈款

孕期零食

¥118
+

大王 短裤式成长裤

学步期

¥228
+

纽强 婴儿身体乳

秋冬保湿

¥168
+

海龟爸爸 防晒喷雾

户外补涂

¥89
+
+
+
+ 首页 + 分类 + 购物车 + 我的 +
+
+
+
分类 按品类、孕周与月龄,筛到合适的那款
+
+ +
+
+
+
9:41
+
商品详情
+
+
+

¥220¥338已售 2352

+

大王光之羽系列 干爽超薄柔透双护 环贴式纸尿裤

+
品牌直供正品保障7 天无理由
+
尺码S / M / L / XL
+
发货48 小时内 · 顺丰
+
兜爸说夏天用很省心:薄、透气,夜里也不容易闷。
+
+
+ 客服 + 收藏 + 加入购物车 +
+
+
+
商品详情 成分、尺码、发货与爸爸的试用笔记
+
+
+
+
+ + +
+
+
+ + +
+

04 / 下载安装

+

把严选,
装进口袋

+

支持 Android 手机。下载后按提示允许「安装未知来源应用」即可完成安装,安装包不含任何捆绑。

+ + + +

微信内打开时,请点右上角「···」→「在浏览器打开」再下载。

+ +
+
当前版本
v{{version}}({{channel}})
+
更新时间
{{releaseDate}}
+
系统要求
Android 5.0+
+
安装包大小
{{apkSize}}
+
应用类别
购物
+
应用包名
com.yizhezhe.douba
+
语言
简体中文
+
开发者
北京臻惠网络科技有限公司
+
+ + +
+ +
+
+ 扫码打开兜爸心选官网 +

扫码在手机上打开

+

shop.douba.top/app

+
+

电脑访问时,扫码可把下载页发到手机

+
+
+
+
+ + +
+
+
+

05 / 常见问题

+

下载之前,
你可能想知道

+

还有别的疑问?
请在 App 内「我的 → 联系客服」找我们
工作日 9:00–18:00

+
+ +
+
+ 怎么下载安装兜爸心选? +

当前提供的是 Android {{channel}}版(v{{version}})。点击页面上的「下载 Android 版」按钮,下载完成后在通知栏或文件管理里点击安装包;若手机提示「禁止安装未知来源应用」,在系统设置里允许浏览器或文件管理器安装应用即可。正式版上架后会在本页同步更新。

+
+
+ iPhone 可以用吗? +

iOS 版本正在准备上架 App Store,上架后会在本页更新下载入口。目前 iPhone 用户可以先用微信小程序「兜爸心选」购物,体验与 App 一致。

+
+
+ App 里卖的东西是正品吗? +

我们只与品牌方或品牌授权渠道合作,商品页会标注货源与保障说明。收到货后如有疑问,可在 App 内联系客服,支持 7 天无理由退换(生鲜及特殊商品除外)。

+
+
+ App 会收集哪些个人信息? +

为完成下单与配送,我们会收集必要的账号、收货与支付信息;具体范围、用途与第三方 SDK 清单,请见《隐私政策》与《权限说明》。未经同意,我们不会向他人提供你的个人信息。

+
+
+
+
+
+ + + + + + diff --git a/src/legal.css b/src/legal.css new file mode 100644 index 0000000..19a708a --- /dev/null +++ b/src/legal.css @@ -0,0 +1,50 @@ +/* 兜爸心选官网 · 合规文本页 */ +:root{ + --paper:#FBF5EC;--card:#FFFCF7;--ink:#231C18;--ink-2:#4B3F37;--muted:#7E7064; + --red:#CF152C;--line:#E6D7C3; + --serif:"Songti SC","STSong","Noto Serif CJK SC","Noto Serif SC","Source Han Serif SC","SimSun",serif; + --sans:"PingFang SC","Hiragino Sans GB","Microsoft YaHei","Noto Sans CJK SC",sans-serif; +} +*,*::before,*::after{box-sizing:border-box} +body{ + margin:0;background:var(--paper);color:var(--ink);font-family:var(--sans); + font-size:16px;line-height:1.9;-webkit-font-smoothing:antialiased; + background-image:radial-gradient(rgba(35,28,24,.07) 1px,transparent 1px); + background-size:26px 26px; +} +a{color:var(--red);text-decoration:none;border-bottom:1px solid rgba(207,21,44,.28)} +a:hover{border-bottom-color:var(--red)} +.wrap{width:min(820px,calc(100% - 40px));margin-inline:auto} +.top{display:flex;align-items:center;justify-content:space-between;gap:20px;padding:22px 0} +.top img{height:44px;width:auto} +.top a.back{font-size:13.5px;color:var(--ink-2);border:0;border-bottom:1px solid transparent;padding-bottom:2px} +.top a.back:hover{color:var(--red);border-bottom-color:var(--red)} +.paper{ + background:var(--card);border:1px solid var(--line);border-radius:6px; + padding:64px 64px 56px;margin:10px 0 60px; + box-shadow:0 40px 70px -55px rgba(35,28,24,.5); + position:relative; +} +.paper::before{content:"";position:absolute;left:0;right:0;top:0;height:4px;background:var(--red);border-radius:6px 6px 0 0;opacity:.85} +h1{font-family:var(--serif);font-size:40px;line-height:1.3;margin:0 0 10px;letter-spacing:.02em} +.meta{font-size:13px;color:var(--muted);letter-spacing:.02em;margin-bottom:38px} +h2{font-family:var(--serif);font-size:21px;margin:38px 0 10px;letter-spacing:.03em} +h3{font-size:16px;margin:22px 0 6px;font-weight:600} +p{margin:10px 0} +ul{margin:8px 0 8px;padding-left:1.3em} +li{margin:6px 0} +.note{background:rgba(207,21,44,.06);border-left:3px solid rgba(207,21,44,.5);padding:14px 18px;border-radius:0 8px 8px 0;font-size:14.5px;color:var(--ink-2)} +.table{border-top:1px solid var(--line);margin-top:14px} +.row{display:grid;grid-template-columns:170px 1fr;gap:18px;padding:14px 0;border-bottom:1px solid var(--line)} +.row dt{font-weight:600;color:var(--ink)} +.row dd{margin:0;color:var(--ink-2);font-size:15px} +.foot{font-size:12.5px;color:var(--muted);padding:0 0 50px;line-height:2} +.foot a{color:var(--muted);border-bottom:1px solid rgba(126,112,100,.3)} +@media (max-width:640px){ + .paper{padding:38px 22px 34px;border-radius:4px;margin-bottom:36px} + h1{font-size:30px} + h2{font-size:19px;margin-top:30px} + body{font-size:15px} + .row{grid-template-columns:1fr;gap:4px} + .top img{height:38px} +} diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..b405ac5 --- /dev/null +++ b/src/main.js @@ -0,0 +1,71 @@ +/* 兜爸心选官网 · 轻交互(无依赖) */ +(function () { + document.documentElement.classList.add('js'); + + var nav = document.getElementById('nav'); + var onScroll = function () { + if (!nav) return; + nav.classList.toggle('is-stuck', window.scrollY > 24); + }; + onScroll(); + window.addEventListener('scroll', onScroll, { passive: true }); + + var items = document.querySelectorAll('[data-reveal]'); + if ('IntersectionObserver' in window) { + var io = new IntersectionObserver(function (entries) { + entries.forEach(function (e) { + if (e.isIntersecting) { + e.target.classList.add('is-in'); + io.unobserve(e.target); + } + }); + }, { rootMargin: '0px 0px -12% 0px', threshold: 0.12 }); + items.forEach(function (el) { io.observe(el); }); + } else { + items.forEach(function (el) { el.classList.add('is-in'); }); + } + + // 同一时间只展开一个 FAQ + var qas = document.querySelectorAll('.qa'); + qas.forEach(function (d) { + d.addEventListener('toggle', function () { + if (!d.open) return; + qas.forEach(function (o) { if (o !== d) o.open = false; }); + }); + }); + + // 首屏手机屏:6 张界面截图自动轮换(悬停暂停 / 切后台暂停 / 尊重"减少动效") + (function () { + var box = document.getElementById('heroSlides'); + if (!box) return; + var slides = Array.prototype.slice.call(box.querySelectorAll('.hs-slide')); + if (slides.length < 2) return; + var reduce = window.matchMedia('(prefers-reduced-motion: reduce)'); + var idx = 0, timer = null, hover = false; + + function show(n) { + slides[idx].classList.remove('is-on'); + idx = (n + slides.length) % slides.length; + slides[idx].classList.add('is-on'); + } + function stop() { if (timer) { clearInterval(timer); timer = null; } } + function start() { + stop(); + if (reduce.matches) return; + timer = setInterval(function () { + if (hover || document.hidden) return; + show(idx + 1); + }, 4200); + } + box.addEventListener('mouseenter', function () { hover = true; }); + box.addEventListener('mouseleave', function () { hover = false; }); + document.addEventListener('visibilitychange', function () { if (!document.hidden) start(); }); + if (reduce.addEventListener) reduce.addEventListener('change', start); + start(); + })(); + + // 外链统一安全属性 + document.querySelectorAll('a[href^="http"]').forEach(function (a) { + a.rel = 'noopener'; + }); +})(); diff --git a/src/permissions.html b/src/permissions.html new file mode 100644 index 0000000..e790718 --- /dev/null +++ b/src/permissions.html @@ -0,0 +1,82 @@ + + + + + +权限说明 · 兜爸心选 + + + + + + +
+ + +
+

兜爸心选 · 权限说明

+

适用版本:Android 版 v{{permissions.version}}({{channel}}) 更新日期:{{permissions.updatedCn}}

+ +

我们只会在实现具体功能所必需的场景下申请权限,并在您使用对应功能时向您说明。您可以在手机「设置 → 应用 → 兜爸心选 → 权限」中随时开启或关闭;关闭后相关功能可能无法使用,但不影响浏览商品等其他功能。

+ +

一、权限清单与用途

+
+
+
相机
+
用于扫一扫(扫码进入活动/商品)、拍照上传评价晒图与头像。仅在您主动点击拍照或扫码时调用。
+
+
+
相册与存储
+
用于选择图片上传(头像、评价)、保存商品图与活动海报、下载并安装应用更新包。
+
+
+
麦克风
+
用于客服会话中的语音消息、语音搜索等需要录音的功能。不会在后台悄悄录音。
+
+
+
位置信息
+
用于定位所在城市、展示附近门店与配送范围、计算运费与预计送达时间。您也可以手动选择城市而不授权定位。
+
+
+
电话与设备信息
+
用于设备识别与账号安全风控,以及在客服页一键拨打客服电话。我们不会读取您的通话内容与通讯录。
+
+
+
通知与桌面角标
+
用于发送订单状态、物流进度、活动提醒;桌面角标用于提示未读消息。可在系统设置中关闭通知。
+
+
+
安装应用
+
用于 App 内检测到新版本后下载并安装更新包。安装前系统会再次向您确认。
+
+
+
网络与基础运行
+
用于联网、判断网络状态、保持消息接收、振动提醒与蓝牙设备连接(如使用相关外设)。
+
+
+ +

二、关于第三方 SDK

+

应用基于 DCloud uni-app 构建;微信登录、支付与分享使用微信开放平台 SDK,支付宝支付使用支付宝 SDK,定位能力使用高德地图 SDK。上述 SDK 可能在其功能运行时申请相应权限或收集必要的设备信息,具体处理规则请见《兜爸心选隐私保护政策》第十节「第三方服务提供商清单」。

+ +

三、如何关闭权限

+
    +
  • Android:设置 → 应用(或「应用管理」)→ 兜爸心选 → 权限,逐项关闭。
  • +
  • 关闭相机/相册后,扫码、晒图与头像上传将不可用;关闭定位后,需手动选择城市;关闭通知后,将收不到订单与物流提醒。
  • +
  • 如您希望撤回全部授权或注销账号,可通过 App 内「我的 → 联系客服」提出,我们会在 15 个工作日内处理。
  • +
+ +

四、联系我们

+

客服入口:App 内「我的 → 联系客服」。
+ 隐私与个人信息相关事项,请见《兜爸心选隐私保护政策》第九节联系方式。

+
+ +

+ 北京臻惠网络科技有限公司 · 京ICP备17065521号
+ © 2026 兜爸心选 · 返回官网 · 用户协议 · 隐私政策 +

+
+ + diff --git a/src/styles.css b/src/styles.css new file mode 100644 index 0000000..da59201 --- /dev/null +++ b/src/styles.css @@ -0,0 +1,656 @@ +/* ============================================================ + 兜爸心选 · 官方应用官网 + 视觉方向:品牌手账 —— 纸感底、印章红、宋体标题、手写批注 + ============================================================ */ + +:root{ + --paper:#FBF5EC; + --paper-2:#F4E9D8; + --card:#FFFCF7; + --ink:#231C18; + --ink-2:#4B3F37; + --muted:#7E7064; + --red:#CF152C; + --red-deep:#A60F21; + --red-soft:rgba(207,21,44,.08); + --line:#E6D7C3; + --cream:#FFF7EC; + + --serif:"Songti SC","STSong","Noto Serif CJK SC","Noto Serif SC","Source Han Serif SC","SimSun",serif; + --sans:"PingFang SC","Hiragino Sans GB","Microsoft YaHei","Noto Sans CJK SC","Noto Sans SC",sans-serif; + --hand:"Kaiti SC","STKaiti","KaiTi","Kaiti TC","Yuanti SC",cursive; + + --wrap:1240px; + --ease:cubic-bezier(.16,1,.3,1); +} + +*,*::before,*::after{box-sizing:border-box} +html{-webkit-text-size-adjust:100%;scroll-behavior:smooth} +body{ + margin:0; + background:var(--paper); + color:var(--ink); + font-family:var(--sans); + font-size:16px; + line-height:1.75; + -webkit-font-smoothing:antialiased; + overflow-x:hidden; +} +img{max-width:100%;display:block} +a{color:inherit;text-decoration:none} +ul,ol,dl,dd{margin:0;padding:0;list-style:none} +p{margin:0} +h1,h2,h3,h4{margin:0;font-weight:700;line-height:1.2} +svg{display:block} +::selection{background:rgba(207,21,44,.16)} + +/* 纸纹 */ +body::after{ + content:"";position:fixed;inset:0;z-index:9;pointer-events:none;opacity:.045; + background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)'/%3E%3C/svg%3E"); + mix-blend-mode:multiply; +} + +.wrap{width:min(var(--wrap),calc(100% - 56px));margin-inline:auto} +section[id]{scroll-margin-top:96px} +.skip{position:absolute;left:-9999px;top:0;background:var(--ink);color:#fff;padding:10px 16px;z-index:99} +.skip:focus{left:12px;top:12px} + +/* ---------- 通用文字 ---------- */ +.kicker{ + font-size:11px;letter-spacing:.22em;text-transform:uppercase; + color:var(--red);font-weight:600;margin-bottom:22px; +} +.kicker--light{color:rgba(255,247,236,.7)} +.h2{ + font-family:var(--serif); + font-size:clamp(34px,4.2vw,56px); + line-height:1.22;letter-spacing:.01em;font-weight:700; +} +.sec-sub{margin-top:18px;color:var(--muted);font-size:16px} + +/* ---------- 按钮 ---------- */ +.btn{ + --btn-bg:var(--ink);--btn-fg:#fff; + display:inline-flex;align-items:center;justify-content:center;gap:9px; + background:var(--btn-bg);color:var(--btn-fg); + border:1px solid transparent;border-radius:999px; + font-size:15px;font-weight:600;line-height:1; + padding:14px 24px;cursor:pointer;white-space:nowrap; + transition:transform .35s var(--ease),box-shadow .35s var(--ease),background .25s,color .25s,border-color .25s; +} +.btn .ico{width:18px;height:18px;flex:none} +.btn--red{--btn-bg:var(--red);--btn-fg:#fff;box-shadow:0 12px 26px -14px rgba(207,21,44,.9)} +.btn--red:hover{--btn-bg:var(--red-deep);transform:translateY(-2px);box-shadow:0 18px 30px -14px rgba(207,21,44,.85)} +.btn--ghost{background:transparent;color:var(--ink);border-color:rgba(35,28,24,.22)} +.btn--ghost:hover{border-color:var(--ink);transform:translateY(-2px)} +.btn--cream{background:var(--cream);color:var(--red);box-shadow:0 16px 32px -20px rgba(0,0,0,.6)} +.btn--cream:hover{transform:translateY(-2px);background:#fff} +.btn--outline{background:transparent;color:rgba(255,247,236,.92);border-color:rgba(255,247,236,.38)} +.btn--outline.is-disabled{cursor:default;opacity:.8} +.btn--sm{padding:11px 20px;font-size:13.5px} +.btn--lg{padding:18px 30px;font-size:16px} +.btn-note{font-size:12px;font-weight:400;opacity:.62;letter-spacing:.02em} + +/* ---------- 导航 ---------- */ +.nav{ + position:fixed;inset:0 0 auto;z-index:50; + padding:14px 0;transition:padding .35s var(--ease),background .35s,border-color .35s,box-shadow .35s; + border-bottom:1px solid transparent; +} +.nav-inner{ + width:min(var(--wrap),calc(100% - 56px));margin-inline:auto; + display:flex;align-items:center;gap:40px; +} +.nav.is-stuck{ + padding:8px 0;background:rgba(251,245,236,.86); + backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px); + border-bottom-color:rgba(230,215,195,.9); +} +/* logo:白色圆角卡片 + 柔和阴影 + 缓动(hover 上浮 / 按下回弹 / 首屏淡入) */ +.brand{ + flex:none;display:flex;align-items:center; + padding:8px 12px;border:0;border-radius:16px;background:#fff; + box-shadow:0 1px 2px rgba(35,28,24,.05),0 10px 24px -14px rgba(35,28,24,.34); + transition:padding .45s var(--ease),border-radius .45s var(--ease), + box-shadow .45s var(--ease),transform .45s var(--ease); + animation:brandIn .9s var(--ease) both; +} +.brand img{height:56px;width:auto;transition:height .45s var(--ease)} +.nav.is-stuck .brand{padding:6px 10px;border-radius:13px} +.nav.is-stuck .brand img{height:44px} +.brand:hover{ + transform:translateY(-3px); + box-shadow:0 2px 4px rgba(35,28,24,.06),0 18px 34px -16px rgba(35,28,24,.45); +} +.brand:active{transform:translateY(-1px) scale(.985);transition-duration:.18s} +.brand:focus-visible{outline:2px solid var(--red);outline-offset:3px} +@keyframes brandIn{from{opacity:0;transform:translateY(-10px) scale(.94)}to{opacity:1;transform:none}} +@media (prefers-reduced-motion:reduce){ + .brand{animation:none;transition:none} +} +.nav-links{display:flex;gap:34px;margin-left:auto;font-size:14.5px;color:var(--ink-2)} +.nav-links a{position:relative;padding:4px 0;transition:color .25s} +.nav-links a::after{ + content:"";position:absolute;left:0;right:0;bottom:0;height:1px; + background:var(--red);transform:scaleX(0);transform-origin:left;transition:transform .4s var(--ease); +} +.nav-links a:hover{color:var(--red)} +.nav-links a:hover::after{transform:scaleX(1)} + +/* ---------- HERO ---------- */ +.hero{position:relative;padding:168px 0 78px;overflow:hidden} +.hero-bg{position:absolute;inset:0;pointer-events:none} +.hero-bg .blob{ + position:absolute;right:-8%;top:-16%;width:760px;height:760px;border-radius:50%; + background:radial-gradient(circle at 50% 50%,rgba(207,21,44,.16),rgba(207,21,44,.05) 42%,transparent 68%); + filter:blur(6px); +} +.hero-bg .grid{ + position:absolute;inset:0; + background-image:radial-gradient(rgba(35,28,24,.16) 1px,transparent 1px); + background-size:24px 24px; + -webkit-mask-image:radial-gradient(120% 80% at 78% 8%,#000 0%,transparent 62%); + mask-image:radial-gradient(120% 80% at 78% 8%,#000 0%,transparent 62%); + opacity:.7; +} +.hero-inner{position:relative;display:grid;grid-template-columns:1.02fr .98fr;align-items:center;gap:40px} + +.eyebrow{ + display:inline-flex;align-items:center;gap:10px; + font-size:12.5px;letter-spacing:.16em;color:var(--ink-2); + border:1px solid var(--line);border-radius:999px;padding:8px 18px;background:rgba(255,252,247,.6); +} +.eyebrow-dot{width:6px;height:6px;border-radius:50%;background:var(--red);box-shadow:0 0 0 4px rgba(207,21,44,.12)} + +.hero-title{ + font-family:var(--serif); + font-size:clamp(64px,8.6vw,124px); + line-height:1.02;letter-spacing:.02em; + margin:26px 0 8px;font-weight:700; +} +.hero-lede{ + font-family:var(--serif); + font-size:clamp(28px,3.2vw,42px); + line-height:1.5;color:var(--ink);margin-top:6px;font-weight:700; +} +.hero-lede .hl{position:relative;display:inline-block;color:var(--red)} +.hero-lede .hl::after{ + content:"";position:absolute;left:-2%;right:-2%;bottom:2px;height:12px; + background:linear-gradient(90deg,rgba(207,21,44,.26),rgba(207,21,44,.14)); + border-radius:60% 40% 55% 45% / 100% 100% 0 0; + transform:rotate(-.6deg);z-index:-1; +} +.hero-desc{margin-top:24px;font-size:16.5px;line-height:1.95;color:var(--muted);max-width:32em;text-wrap:balance} +.about-sub,.about-sub,.feature p,.promise p,.dl-desc,.sec-sub{text-wrap:pretty} +.cta-row{display:flex;flex-wrap:wrap;gap:14px;margin-top:36px} +.hero-meta{ + display:flex;flex-wrap:wrap;gap:0 18px;margin-top:30px; + font-size:12.5px;letter-spacing:.04em;color:var(--muted); +} +.hero-meta li{position:relative;padding-right:18px} +.hero-meta li:not(:last-child)::after{ + content:"";position:absolute;right:7px;top:50%;width:3px;height:3px;border-radius:50%;background:var(--line); +} + +/* 手机模型 */ +.hero-art{position:relative;display:flex;justify-content:center;align-items:center;min-height:640px} +.phone{ + position:relative;width:328px;padding:var(--bezel,8px);border-radius:48px; + background:linear-gradient(160deg,#3A302A,#17120F 46%,#241D19); + box-shadow: + 0 2px 0 rgba(255,255,255,.14) inset, + 0 50px 90px -50px rgba(35,28,24,.75), + 0 20px 40px -30px rgba(35,28,24,.5); + flex:none; +} +.phone::after{ + content:"";position:absolute;inset:0;border-radius:48px;pointer-events:none; + background:linear-gradient(120deg,rgba(255,255,255,.16),transparent 32%,transparent 70%,rgba(255,255,255,.05)); + -webkit-mask:linear-gradient(#000,#000) content-box,linear-gradient(#000,#000); + -webkit-mask-composite:xor;mask-composite:exclude;padding:1px; +} +.phone-screen{ + position:relative;border-radius:39px;overflow:hidden;background:#FCFAF6; + height:632px;display:flex;flex-direction:column; +} +.phone-screen > *{flex:none} +.phone--hero{transform:rotate(-3.4deg)} +.phone--hero .phone-screen{height:680px} + +/* 首屏手机屏:6 张界面截图自动轮换(mama-box 3 张 + 商城 3 张) */ +.hero-slides{position:relative;overflow:hidden;background:#fff} +.hs-slide{ + position:absolute;inset:0;display:block;width:100%;height:100%; + object-fit:cover;object-position:top center; + opacity:0;transition:opacity .9s var(--ease);will-change:opacity; +} +.hs-slide.is-on{opacity:1;animation:hsKen 8.4s linear both} +@keyframes hsKen{from{transform:scale(1)}to{transform:scale(1.055)}} +@media (prefers-reduced-motion:reduce){ + .hs-slide{transition:none} + .hs-slide.is-on{animation:none} +} + +.statusbar{display:flex;justify-content:space-between;align-items:center;padding:8px 18px 2px;font-size:10px;font-weight:700;color:#2B231E;letter-spacing:.02em} +.sb-right{display:flex;align-items:center;gap:4px} +.sb-right i{display:block;background:#2B231E;border-radius:1px} +.sb-right .sig{width:12px;height:8px;clip-path:polygon(0 100%,100% 0,100% 100%)} +.sb-right .wifi{width:10px;height:8px;clip-path:polygon(50% 100%,0 25%,100% 25%)} +.sb-right .batt{width:16px;height:8px;border-radius:2px;opacity:.9} + +.app-top{display:flex;align-items:center;gap:8px;padding:4px 14px 8px} +.mini-logo{height:26px;width:auto;flex:none} +.search{ + flex:1;display:flex;align-items:center;gap:5px; + height:30px;padding:0 10px;border-radius:999px; + background:#F2E9DC;color:#9A8B7C;font-size:10px; +} +.search svg{width:12px;height:12px;fill:none;stroke:#9A8B7C;stroke-width:2;stroke-linecap:round;flex:none} + +.promo{ + position:relative;margin:0 14px;border-radius:15px;padding:10px 14px 11px; + background: + radial-gradient(120% 120% at 90% 0%,rgba(255,255,255,.18),transparent 55%), + linear-gradient(150deg,#D9192F,#A60F21); + color:#FFEEE6;overflow:hidden; + box-shadow:0 12px 22px -16px rgba(166,15,33,.9); +} +.promo-k{font-size:9.5px;letter-spacing:.16em;opacity:.82} +.promo-t{font-family:var(--serif);font-size:16px;font-weight:700;line-height:1.38;margin-top:4px} +.promo-tag{display:inline-block;margin-top:8px;padding:2px 8px;border-radius:999px;background:rgba(255,255,255,.16);font-size:8.5px;letter-spacing:.04em} +.promo-doodle{position:absolute;right:8px;top:10px;width:44px;height:44px;color:#fff;opacity:.32} + +.chips{display:flex;gap:6px;padding:10px 14px 2px;overflow:hidden} +.chip{flex:none;font-size:9.5px;padding:5px 10px;border-radius:999px;background:#F2E9DC;color:#6E6156} +.chip--on{background:var(--red);color:#fff} + +.sect-head{display:flex;justify-content:space-between;align-items:baseline;padding:7px 15px 4px;font-size:11px;color:#2B231E} +.sect-head b{font-size:12px;letter-spacing:.02em} +.sect-head span{font-size:9px;color:#A6988A} + +.pgrid{display:grid;grid-template-columns:1fr 1fr;gap:6px;padding:0 14px} +.pcard{background:#fff;border-radius:11px;padding:5px;box-shadow:0 6px 14px -12px rgba(35,28,24,.5)} +.pimg{height:118px;border-radius:8px;overflow:hidden;background:#F6F0E7} +.pimg img{width:100%;height:100%;object-fit:cover} +.pname{ + margin:5px 3px 2px;font-size:9px;line-height:1.4;color:#4B3F37; + display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;height:2.9em; +} +.pprice{margin:0 3px 3px;font-size:10px;color:var(--red);display:flex;align-items:baseline;gap:4px} +.pprice em{font-style:normal;font-weight:700;font-size:11px} +.pprice s{color:#A6988A;font-size:9px} + +.tabbar{ + margin-top:auto;display:flex;border-top:1px solid #F0E6D9;background:#FFFDFA; + padding:6px 4px 9px; +} +.tab{flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;font-size:8.5px;color:#A6988A} +.tab svg{width:17px;height:17px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round} +.tab--on{color:var(--red)} + +/* 印章 */ +.stamp{ + position:absolute;left:-6px;top:12px;z-index:3; + width:126px;height:126px;border-radius:50%; + border:2px solid rgba(207,21,44,.72); + display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px; + color:rgba(207,21,44,.86);transform:rotate(-11deg); + background:rgba(255,252,247,.36); + mix-blend-mode:multiply; + box-shadow:0 0 0 6px rgba(207,21,44,.06) inset; +} +.stamp-top{font-family:var(--serif);font-size:20px;font-weight:700;letter-spacing:.14em;text-indent:.14em} +.stamp-line{width:52px;height:1px;background:rgba(207,21,44,.5)} +.stamp-bottom{font-family:var(--hand);font-size:10.5px;letter-spacing:.14em;text-indent:.14em} + +.hand-note{ + position:absolute;left:-4px;bottom:44px; + font-family:var(--hand);font-size:19px;line-height:1.6;color:var(--red); + transform:rotate(-4deg); +} +.hand-arrow{ + position:absolute;left:118px;bottom:16px;width:96px;color:rgba(207,21,44,.5); + transform:rotate(-22deg); +} + +/* ---------- 三条承诺 ---------- */ +.promises{border-top:1px solid var(--line);border-bottom:1px solid var(--line);background:rgba(255,252,247,.5)} +.promises-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:48px;padding:46px 0} +.promise{position:relative;padding-left:0} +.p-num{font-family:var(--serif);font-size:12px;letter-spacing:.2em;color:var(--red)} +.promise h3{font-family:var(--serif);font-size:23px;margin:8px 0 6px;letter-spacing:.04em} +.promise p{font-size:14.5px;line-height:1.8;color:var(--muted)} + +/* ---------- 品牌理念 ---------- */ +.about{padding:118px 0 110px} +.about-inner{display:grid;grid-template-columns:.86fr 1.14fr;gap:80px;align-items:start} +.about-head{position:sticky;top:120px} +.about-sub{margin-top:26px;font-size:16px;line-height:1.95;color:var(--muted)} + +.note{ + position:relative;background:var(--card); + border:1px solid var(--line);border-radius:4px; + padding:56px 54px 44px; + box-shadow:0 40px 70px -50px rgba(35,28,24,.55),0 2px 0 rgba(255,255,255,.8) inset; + transform:rotate(-.55deg); +} +.note::before{ + content:"";position:absolute;inset:0;border-radius:4px;pointer-events:none; + background-image:repeating-linear-gradient(to bottom,transparent 0 33px,rgba(35,28,24,.045) 33px 34px); + opacity:.9; +} +.tape{ + position:absolute;width:112px;height:30px;background:rgba(226,196,148,.55); + box-shadow:0 2px 6px rgba(35,28,24,.08);backdrop-filter:blur(1px); +} +.tape--l{left:-22px;top:26px;transform:rotate(-38deg)} +.tape--r{right:-18px;bottom:40px;transform:rotate(-32deg)} +.note-label{ + position:relative;font-size:10.5px;letter-spacing:.24em;color:var(--muted); + margin-bottom:26px;text-transform:uppercase; +} +.note-body{position:relative} +.note-body p{font-size:17px;line-height:2.02;color:var(--ink-2);letter-spacing:.01em} +.note-body p + p{margin-top:18px} +.note-body p:first-child::first-letter{ + float:left;font-family:var(--serif);font-size:56px;line-height:.9;color:var(--red); + margin:6px 12px 0 0; +} +.sign{position:relative;margin-top:26px;text-align:right;font-family:var(--hand);font-size:21px;color:var(--red);letter-spacing:.06em} +.checks{margin-top:34px;border-top:1px solid var(--line)} +.checks li{ + display:flex;align-items:baseline;gap:14px;padding:15px 0; + border-bottom:1px dashed var(--line);font-size:14px;line-height:1.7;color:var(--muted); +} +.checks li span{flex:none;font-family:var(--serif);font-size:12px;letter-spacing:.18em;color:var(--red)} + +.note-star{position:absolute;right:30px;bottom:24px;width:34px;color:rgba(207,21,44,.35)} + +/* ---------- 应用功能 ---------- */ +.features{padding:104px 0 110px;background:var(--paper-2);border-top:1px solid rgba(230,215,195,.7)} +.sec-head{max-width:720px} +.feature-grid{display:grid;grid-template-columns:1fr 1fr;gap:22px;margin-top:56px} +.feature{ + position:relative;background:var(--card);border:1px solid rgba(230,215,195,.9); + border-radius:22px;padding:36px 38px 38px; + transition:transform .5s var(--ease),box-shadow .5s var(--ease),border-color .5s; +} +.feature:hover{transform:translateY(-5px);border-color:rgba(207,21,44,.35);box-shadow:0 30px 50px -40px rgba(35,28,24,.55)} +.feature .f-num{ + position:absolute;right:36px;top:30px; + font-family:var(--serif);font-size:13px;letter-spacing:.2em;color:rgba(35,28,24,.2); +} +.feature::after{ + content:"";position:absolute;left:38px;right:38px;top:0;height:2px;border-radius:2px; + background:linear-gradient(90deg,var(--red),rgba(207,21,44,0)); + transform:scaleX(0);transform-origin:left;transition:transform .6s var(--ease); +} +.feature:hover::after{transform:scaleX(1)} +.f-ico{ + width:56px;height:56px;border-radius:50%;border:1px dashed rgba(207,21,44,.4); + display:grid;place-items:center;color:var(--red);background:var(--red-soft); +} +.f-ico svg{width:28px;height:28px} +.feature h3{font-family:var(--serif);font-size:23px;margin:22px 0 10px;letter-spacing:.03em} +.feature p{font-size:15px;line-height:1.85;color:var(--muted)} + +/* ---------- 应用截图 ---------- */ +.screens{padding:104px 0 118px;background:var(--paper);border-top:1px solid var(--line)} +.sec-head--row{display:flex;justify-content:space-between;align-items:flex-end;max-width:none;gap:40px} +.sec-note{font-size:12.5px;line-height:1.9;color:var(--muted);text-align:right;letter-spacing:.02em} +.shots-hint{display:none;font-size:12px;letter-spacing:.06em;color:var(--muted);margin-top:10px} +.shots{display:grid;grid-template-columns:repeat(3,1fr);gap:38px;row-gap:78px;margin-top:64px;justify-items:center} +.shot{width:100%;display:flex;flex-direction:column;align-items:center} +.phone--shot{--bezel:6px;width:300px;padding:var(--bezel);border-radius:44px} +.phone--shot .phone-screen{height:680px;border-radius:36px} +/* 真机截图(mama-box 首页):铺满屏幕区,顶部对齐 */ +.phone--shot .phone-screen .shot-img{width:100%;height:100%;object-fit:cover;object-position:top center;display:block} +.phone--shot .statusbar{padding-top:8px} +.phone--shot .promo-t{font-size:16px} +.phone--shot .pgrid{gap:7px} +.shot:nth-child(1) .phone--shot{transform:rotate(-2.2deg)} +.shot:nth-child(2) .phone--shot{transform:translateY(-18px)} +.shot:nth-child(3) .phone--shot{transform:rotate(2.2deg)} +.shot:nth-child(4) .phone--shot{transform:rotate(-2.2deg)} +.shot:nth-child(5) .phone--shot{transform:translateY(-18px)} +.shot:nth-child(6) .phone--shot{transform:rotate(2.2deg)} +.shot .phone--shot{transition:transform .6s var(--ease),box-shadow .6s} +.shot:hover .phone--shot{transform:rotate(0) translateY(-6px)} +.shot figcaption{margin-top:26px;font-size:13.5px;line-height:1.7;color:var(--muted);text-align:center;max-width:300px} +.shot figcaption b{color:var(--ink);font-weight:600} + +/* 分类页 / 详情页 mock */ +.subpage-head{display:flex;align-items:center;justify-content:space-between;padding:6px 14px 12px;font-size:12px;color:#2B231E} +.subpage-head b{font-weight:600} +.subpage-head .back{width:18px;height:18px} +.subpage-head .share{color:#A6988A;letter-spacing:2px} +.cat{display:flex;flex:1;overflow:hidden;border-top:1px solid #F0E6D9} +.cat-rail{width:78px;background:#F6F0E7;padding-top:4px;overflow:hidden} +.cat-rail li{font-size:10px;color:#7E7064;text-align:center;padding:9px 0;position:relative} +.cat-rail li.on{background:#FCFAF6;color:var(--red);font-weight:600} +.cat-rail li.on::before{content:"";position:absolute;left:0;top:50%;transform:translateY(-50%);width:3px;height:16px;background:var(--red);border-radius:0 3px 3px 0} +.cat-list{flex:1;padding:10px 10px 0;overflow:hidden} +.row{display:flex;gap:8px;margin-bottom:11px} +.rimg{width:58px;height:58px;border-radius:9px;overflow:hidden;flex:none;background:#F6F0E7} +.rimg img{width:100%;height:100%;object-fit:cover} +.rt{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px} +.rt p{margin:0;font-size:9.5px;line-height:1.4;color:#4B3F37;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden} +.rt .rtag{display:inline-block;font-size:8px;color:var(--red);border:1px solid rgba(207,21,44,.3);border-radius:3px;padding:0 4px;width:max-content} +.rt em{font-style:normal;font-size:11px;font-weight:700;color:var(--red);margin-top:auto} + +.dimg{width:100%;aspect-ratio:1/1;background:#F6F0E7} +.dimg img{width:100%;height:100%;object-fit:cover} +.dbox{padding:12px 14px 0;flex:1;overflow:hidden} +.dprice{display:flex;align-items:baseline;gap:6px;color:var(--red)} +.dprice em{font-style:normal;font-family:var(--sans);font-size:19px;font-weight:700} +.dprice s{color:#A6988A;font-size:9px} +.dprice span{margin-left:auto;color:#A6988A;font-size:9px} +.dname{margin-top:6px;font-size:11px;line-height:1.5;color:#2B231E} +.dtags{display:flex;gap:5px;margin-top:8px} +.dtags span{font-size:8px;padding:2px 5px;border-radius:3px;background:rgba(207,21,44,.07);color:var(--red)} +.dline{display:flex;gap:12px;font-size:9px;padding:8px 0;border-top:1px dashed #EDE2D3;color:#A6988A} +.dline b{font-weight:500;color:#4B3F37} +.dnote{margin-top:6px;background:#FBF1E7;border-radius:8px;padding:8px 10px;font-size:9px;line-height:1.6;color:#6E6156} +.dnote-k{display:inline-block;font-size:8px;background:var(--red);color:#fff;border-radius:3px;padding:1px 5px;margin-right:4px} +.dbuy{margin-top:auto;display:flex;align-items:center;gap:10px;border-top:1px solid #F0E6D9;padding:8px 12px 12px;background:#FFFDFA} +.dico{display:flex;flex-direction:column;align-items:center;gap:1px;font-size:7.5px;color:#7E7064} +.dico svg{width:15px;height:15px;fill:none;stroke:currentColor;stroke-width:1.6} +.dbtn{margin-left:auto;background:var(--red);color:#fff;font-size:10.5px;font-weight:600;border-radius:999px;padding:9px 22px} + +/* ---------- 下载 ---------- */ +.download{padding:0 0 104px} +.dl-card{ + position:relative;overflow:hidden;color:var(--cream); + border-radius:32px;padding:66px 64px; + background: + radial-gradient(90% 120% at 88% 8%,rgba(255,255,255,.16),transparent 55%), + radial-gradient(120% 100% at 0% 100%,rgba(0,0,0,.22),transparent 60%), + linear-gradient(140deg,#D2152C,#A30E20 70%); + display:grid;grid-template-columns:1.16fr .84fr;gap:56px;align-items:center; + box-shadow:0 50px 80px -60px rgba(166,15,33,.9); +} +.dl-card::before{ + content:"";position:absolute;inset:0;pointer-events:none;opacity:.55; + background-image:radial-gradient(rgba(255,255,255,.14) 1px,transparent 1px); + background-size:22px 22px; + -webkit-mask-image:radial-gradient(80% 100% at 100% 0%,#000,transparent 70%); + mask-image:radial-gradient(80% 100% at 100% 0%,#000,transparent 70%); +} +.dl-star{position:absolute;color:rgba(255,247,236,.35);font-size:22px} +.dl-star--1{left:46%;top:34px} +.dl-star--2{left:52%;bottom:48px;font-size:15px} +.dl-left{position:relative} +.dl-title{font-family:var(--serif);font-size:clamp(38px,4.4vw,62px);line-height:1.24;letter-spacing:.01em} +.dl-desc{margin-top:20px;font-size:15px;line-height:1.9;color:rgba(255,247,236,.8);max-width:34em} +.dl-actions{display:flex;flex-wrap:wrap;gap:12px;margin-top:32px;align-items:center} +.dl-meta{display:grid;grid-template-columns:1fr 1fr;gap:16px 24px;margin-top:38px;max-width:440px} +.dl-meta dt{font-size:10.5px;letter-spacing:.18em;color:rgba(255,247,236,.55);margin-bottom:3px} +.dl-meta dd{font-size:14.5px;color:var(--cream)} +.tbd{color:inherit;border-bottom:1px dashed currentColor;opacity:.55;cursor:help} +.dl-tip{display:flex;align-items:center;gap:8px;margin-top:22px;font-size:12.5px;line-height:1.7;color:rgba(255,247,236,.72)} +.dl-tip svg{width:15px;height:15px;flex:none;opacity:.8} +.dl-legal{margin-top:22px;font-size:12.5px;line-height:1.8;color:rgba(255,247,236,.76);max-width:34em} +.dl-legal a{color:rgba(255,247,236,.9);border-bottom:1px solid rgba(255,247,236,.35)} + +.dl-right{position:relative;display:flex;flex-direction:column;align-items:center;gap:14px} +.qr-card{background:var(--card);color:var(--ink);border-radius:20px;padding:20px 20px 18px;text-align:center;box-shadow:0 30px 60px -40px rgba(0,0,0,.6);transform:rotate(1.6deg)} +.qr{width:176px;height:176px} +.qr-t{margin-top:12px;font-size:13px;color:var(--ink-2)} +.qr-url{margin-top:2px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11.5px;letter-spacing:.02em;color:var(--muted)} +.qr-hint{font-size:11.5px;color:rgba(255,247,236,.55);text-align:center} + +/* ---------- FAQ ---------- */ +.faq{padding:20px 0 130px} +.faq-inner{display:grid;grid-template-columns:.82fr 1.18fr;gap:80px;align-items:start} +.faq-head{position:sticky;top:120px} +.faq-contact{margin-top:30px;font-size:14px;line-height:2;color:var(--muted)} +.faq-contact a{color:var(--red);border-bottom:1px solid rgba(207,21,44,.35);padding-bottom:1px} +.faq-list{border-top:1px solid var(--line)} +.qa{border-bottom:1px solid var(--line)} +.qa summary{ + list-style:none;cursor:pointer;display:flex;justify-content:space-between;align-items:center;gap:24px; + padding:26px 2px;font-family:var(--serif);font-size:20px;letter-spacing:.02em;color:var(--ink); + transition:color .3s; +} +.qa summary::-webkit-details-marker{display:none} +.qa summary:hover{color:var(--red)} +.qa summary i{position:relative;width:16px;height:16px;flex:none;transition:transform .4s var(--ease)} +.qa summary i::before,.qa summary i::after{content:"";position:absolute;background:currentColor;border-radius:2px} +.qa summary i::before{left:0;right:0;top:7px;height:1.5px} +.qa summary i::after{top:0;bottom:0;left:7px;width:1.5px;transition:opacity .3s} +.qa[open] summary i{transform:rotate(180deg)} +.qa[open] summary i::after{opacity:0} +.qa-body{padding:0 44px 28px 2px;color:var(--muted);font-size:15px;line-height:1.95} +.qa-body a{color:var(--red);border-bottom:1px solid rgba(207,21,44,.3)} + +/* ---------- 页脚 ---------- */ +.footer{background:var(--ink);color:#EADFD3;padding:74px 0 34px} +.footer-top{display:grid;grid-template-columns:1fr 1.25fr;gap:64px} +.foot-logo{font-family:var(--serif);font-size:32px;letter-spacing:.06em;color:var(--cream)} +.foot-slogan{margin-top:14px;font-size:14px;line-height:1.95;color:rgba(234,223,211,.6)} +.foot-cols{display:grid;grid-template-columns:repeat(3,1fr);gap:28px} +.foot-cols h4{font-size:11px;letter-spacing:.2em;color:rgba(234,223,211,.45);margin-bottom:16px;font-weight:600} +.foot-cols a,.foot-plain{display:block;font-size:13.5px;color:rgba(234,223,211,.82);padding:5px 0;transition:color .25s} +.foot-cols a:hover{color:#fff} +.foot-legal{margin-top:54px;padding-top:22px;border-top:1px solid rgba(234,223,211,.14);font-size:12px;line-height:2;color:rgba(234,223,211,.45)} + +/* ---------- 动效 ---------- */ +.js [data-reveal]{opacity:0;transform:translateY(26px);transition:opacity .9s var(--ease),transform .9s var(--ease)} +.js [data-reveal].is-in{opacity:1;transform:none} +.hero-copy > *{opacity:0;transform:translateY(22px);animation:rise .95s var(--ease) forwards} +.hero-copy > *:nth-child(1){animation-delay:.05s} +.hero-copy > *:nth-child(2){animation-delay:.14s} +.hero-copy > *:nth-child(3){animation-delay:.23s} +.hero-copy > *:nth-child(4){animation-delay:.32s} +.hero-copy > *:nth-child(5){animation-delay:.41s} +.hero-copy > *:nth-child(6){animation-delay:.5s} +@keyframes rise{to{opacity:1;transform:none}} +.hero-art{opacity:0;animation:rise 1.1s .3s var(--ease) forwards} +.phone--hero{animation:float 7s ease-in-out infinite} +@keyframes float{ + 0%,100%{transform:rotate(-3.4deg) translateY(0)} + 50%{transform:rotate(-3deg) translateY(-10px)} +} + +/* ---------- 响应式 ---------- */ +@media (max-width:1080px){ + .hero-inner{gap:20px} + .hero-art{min-height:auto;transform:scale(.94)} + .about-inner,.faq-inner{grid-template-columns:1fr;gap:52px} + .about-head,.faq-head{position:static} + .dl-card{padding:56px 46px;gap:40px} + .dl-meta{grid-template-columns:1fr 1fr} +} +@media (max-width:900px){ + .wrap,.nav-inner{width:calc(100% - 40px)} + .hero{padding:132px 0 72px} + .hero-inner{grid-template-columns:1fr;gap:54px} + .hero-art{order:2;justify-content:center;transform:none;min-height:0;margin-top:8px} + .stamp{display:none} + .hand-note{right:4%;bottom:0;font-size:17px} + .hand-arrow{display:none} + .stamp{left:0;top:-6px;width:104px;height:104px} + .promises-grid{grid-template-columns:1fr;gap:26px;padding:38px 0} + .promise{display:grid;grid-template-columns:44px 1fr;gap:0 16px;align-items:baseline} + .promise h3{margin:0} + .promise p{grid-column:2} + .hero-meta{display:grid;grid-template-columns:1fr 1fr;gap:6px 12px} + .feature{padding:28px 26px 30px;border-radius:18px} + .f-ico{width:48px;height:48px} + .f-ico svg{width:24px;height:24px} + .feature h3{font-size:20px;margin:16px 0 8px} + .feature .f-num{right:24px;top:24px} + .about{padding:84px 0 78px} + .note{padding:44px 34px 38px} + .note-body p{font-size:16px} + .features{padding:78px 0 84px} + .feature-grid{grid-template-columns:1fr;gap:16px;margin-top:40px} + .screens{padding:78px 0 92px} + .sec-head--row{flex-direction:column;align-items:flex-start;gap:18px} + .sec-note{text-align:left} + .shots-hint{display:block} + .shots{ + grid-template-columns:none;display:flex;overflow-x:auto;gap:22px; + margin:44px -20px 0;padding:0 20px 18px;scroll-snap-type:x mandatory; + -webkit-overflow-scrolling:touch;scrollbar-width:none; + } + .shots::-webkit-scrollbar{display:none} + .shot{flex:none;width:300px;scroll-snap-align:center} + .shot:nth-child(n) .phone--shot{transform:none} + .download{padding-bottom:84px} + .dl-card{grid-template-columns:1fr;padding:44px 30px;gap:36px;border-radius:26px} + .dl-actions{flex-direction:column;align-items:stretch} + .dl-actions .btn{width:100%} + .dl-right{align-items:center;text-align:center} + .dl-right .qr-hint{text-align:center} + .qr-card{transform:none} + .faq{padding-bottom:92px} + .footer-top{grid-template-columns:1fr;gap:40px} + .foot-cols{grid-template-columns:1fr 1fr;gap:24px 20px} +} +@media (max-width:760px){ + .nav-links{display:none} + .brand{padding:6px 9px;border-radius:13px} + .brand img{height:44px} + .nav.is-stuck .brand{padding:5px 8px;border-radius:11px} + .nav.is-stuck .brand img{height:36px} +} +@media (max-width:560px){ + .wrap,.nav-inner{width:calc(100% - 32px)} + .hero{padding-top:118px} + .hero-title{font-size:clamp(52px,15vw,78px)} + .hero-lede{font-size:27px} + .hero-desc{font-size:15px} + .cta-row{flex-direction:column;align-items:stretch;gap:10px} + .cta-row .btn{width:100%} + .hero-meta{gap:6px 0;flex-direction:column;font-size:12px} + .hero-meta li{padding-right:0} + .hero-meta li:not(:last-child)::after{display:none} + .hand-note{display:none} + .stamp{width:92px;height:92px} + .stamp-top{font-size:16px} + .stamp-bottom{font-size:9px} + .promises-grid{padding:32px 0} + .about-sub{font-size:15px} + .note{padding:38px 22px 30px} + .note-body p{font-size:15.3px;line-height:1.95} + .note-body p:first-child::first-letter{font-size:44px} + .tape{display:none} + .dl-card{padding:36px 22px} + .dl-meta{grid-template-columns:1fr 1fr;gap:14px} + .dl-meta dd{font-size:13px} + .qa summary{font-size:17.5px;padding:22px 0;gap:16px} + .qa-body{padding-right:8px;font-size:14.5px} + .foot-cols{grid-template-columns:1fr} + .foot-legal{margin-top:38px;font-size:11.5px} +} +@media (prefers-reduced-motion:reduce){ + *{animation:none!important;transition-duration:.001ms!important;scroll-behavior:auto!important} + .js [data-reveal]{opacity:1;transform:none} + .hero-copy > *,.hero-art{opacity:1;transform:none} +} + +/* 页脚备案链接 */ +.foot-icp{color:rgba(234,223,211,.82);border-bottom:1px solid rgba(234,223,211,.28)} +.foot-icp:hover{color:#fff} diff --git a/src/terms.html b/src/terms.html new file mode 100644 index 0000000..8955b03 --- /dev/null +++ b/src/terms.html @@ -0,0 +1,95 @@ + + + + + +用户协议 · 兜爸心选 + + + + + + +
+ + +
+

兜爸心选用户服务协议

+

更新日期:2026 年 10 月 27 日 生效日期:2026 年 10 月 27 日

+ +

本协议是您与北京臻惠网络科技有限公司(以下简称「我们」)之间,就您下载、安装、注册和使用「兜爸心选」移动应用及相关服务所订立的协议。请您在注册或使用前完整阅读;您勾选同意或实际使用服务,即视为已阅读并接受本协议。

+ +

一、协议范围

+

本协议适用于「兜爸心选」移动应用(Android / iOS,应用包名 com.yizhezhe.douba)及通过该应用提供的商品浏览、下单、支付、配送查询与售后等服务。

+

《兜爸心选隐私保护政策》是本协议不可分割的组成部分,与本协议具有同等效力。如本协议与隐私政策存在不一致,就个人信息处理事项以隐私政策为准。

+ +

二、账号注册与使用

+
    +
  • 您可以使用手机号验证码或微信授权方式注册、登录。您应保证所提供的手机号、收货信息真实、准确、有效。
  • +
  • 账号仅限您本人使用,不得出借、转让、售卖或以其他方式提供给他人使用。
  • +
  • 您应妥善保管账号及验证码。因您保管不善导致的损失,由您自行承担;发现账号异常时,请及时通过 App 内客服联系我们。
  • +
  • 未满 14 周岁的未成年人应在监护人陪同下阅读本协议并使用本服务;监护人应对其使用行为进行指导与监督。
  • +
+ +

三、商品信息与下单

+
    +
  • 商品名称、规格、价格、库存、优惠等信息以您下单时应用内展示为准。因系统延迟、价格标注错误或库存变动导致订单无法履行的,我们会及时通知您并办理退款。
  • +
  • 您提交订单并完成支付后,订单成立。预售、跨境等特殊商品的发货时间以商品页说明为准。
  • +
  • 跨境商品可能需要您提供真实姓名与身份证信息用于海关清关,具体见隐私政策。
  • +
+ +

四、支付、配送与发票

+
    +
  • 我们支持微信支付、支付宝等第三方支付渠道。支付过程由相应支付机构完成,我们不保存您的银行卡号、支付密码等信息。
  • +
  • 配送范围、时效与运费以下单页展示为准。因地址填写错误、无人签收等原因导致的退回或二次配送费用,由您承担。
  • +
  • 您可在订单完成后按应用内指引申请电子发票,具体以实际支持情况为准。
  • +
+ +

五、退换货与售后

+
    +
  • 除生鲜、定制、拆封后影响二次销售等依法不适用无理由退货的商品外,您可在收货后 7 日内申请无理由退货。
  • +
  • 商品存在质量问题、错发、漏发的,请在签收后及时联系客服并提供必要凭证,我们将按实际情况为您办理退换、补发或退款。
  • +
  • 退换货商品的完好标准、运费承担及退款到账时间,以应用内售后规则与客服说明为准。
  • +
+ +

六、用户行为规范

+

您承诺不利用本服务从事下列行为:

+
    +
  • 发布违法、侵权、虚假、骚扰性或违背公序良俗的内容;
  • +
  • 恶意刷单、套取优惠、利用技术手段干扰系统正常运行;
  • +
  • 侵犯他人知识产权、商业秘密或个人隐私;
  • +
  • 其他违反法律法规或损害平台、商家、其他用户合法权益的行为。
  • +
+

如您违反上述约定,我们有权视情节采取限制功能、取消订单、冻结或注销账号等措施,并保留追究法律责任的权利。

+ +

七、知识产权

+

本应用内的软件、界面设计、文字、图片、商标及「兜爸心选」品牌标识,其知识产权归我们或相关权利人所有。未经书面许可,任何人不得复制、修改、传播或用于商业用途。

+ +

八、服务变更、中断与责任限制

+
    +
  • 我们可能因系统维护、升级或不可抗力暂停部分服务,并尽可能提前公告。
  • +
  • 因网络故障、设备故障、第三方支付或物流服务异常等非我们可控原因造成的损失,我们将协助处理,但在法律允许范围内不承担赔偿责任。
  • +
  • 本协议中任何条款不免除我们因故意或重大过失造成您人身、财产损害依法应承担的责任。
  • +
+ +

九、协议变更与终止

+

我们可能根据法律法规或业务调整更新本协议,更新后会在应用内或本页面公示。重大变更将再次征得您的同意。您可随时停止使用服务并申请注销账号,注销后的信息处理方式见隐私政策。

+ +

十、法律适用与争议解决

+

本协议适用中华人民共和国大陆地区法律。因本协议产生的争议,双方应友好协商;协商不成的,任一方可向被告住所地有管辖权的人民法院提起诉讼。

+ +

十一、联系我们

+

客服入口:App 内「我的 → 联系客服」。
+ 隐私与个人信息相关事项,请见《兜爸心选隐私保护政策》第九节联系方式。

+
+ +

+ 北京臻惠网络科技有限公司 · 京ICP备17065521号
+ © 2026 兜爸心选 · 返回官网 · 权限说明 · 隐私政策 +

+
+ +