feat(h5): 小程序原生顶栏 + 本地联调与一键部署

nh=1 隐藏 H5 顶栏并由 cover 承载 logo;H5 仅 4px 顶距避免重复留白。新增 dev:mp、deploy:h5 与联调文档。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-08-25 01:38:32 +08:00
co-authored by Cursor
parent f4bd57ce1b
commit 57687de360
12 changed files with 188 additions and 10 deletions
+1
View File
@@ -3,6 +3,7 @@ __pycache__/
.venv/
*.log
.env
deploy.env
.DS_Store
# ESS — machine-local root pointer(仓库内用 scripts 包装器)
+10 -3
View File
@@ -1,7 +1,7 @@
<template>
<div class="app-shell" :class="{ 'mp-embed': mpEmbed }">
<div class="app-shell" :class="shellClass">
<div class="app-body">
<AppHeader />
<AppHeader v-if="showHeader" />
<router-view />
</div>
<IcpFooter class="app-footer" />
@@ -10,10 +10,17 @@
</template>
<script setup lang="ts">
import { computed } from 'vue'
import AppHeader from './components/AppHeader.vue'
import IcpFooter from './components/IcpFooter.vue'
import TabBar from './components/TabBar.vue'
import { isMpEmbed } from './lib/mpEmbed'
import { isMpEmbed, isMpNativeHeader } from './lib/mpEmbed'
const mpEmbed = isMpEmbed()
const mpNativeHeader = isMpNativeHeader()
const showHeader = !mpNativeHeader
const shellClass = computed(() => ({
'mp-embed': mpEmbed,
'mp-native-header': mpNativeHeader,
}))
</script>
+10 -1
View File
@@ -1,5 +1,5 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { applyMpEmbedDom, initMpEmbed, isMpEmbed } from './mpEmbed'
import { applyMpEmbedDom, initMpEmbed, isMpEmbed, isMpNativeHeader } from './mpEmbed'
describe('mpEmbed', () => {
beforeEach(() => {
@@ -28,6 +28,15 @@ describe('mpEmbed', () => {
expect(document.documentElement.style.getPropertyValue('--yxg-safe-top')).toBe('47px')
})
it('uses native mini-program header mode with ?nh=1', () => {
window.history.replaceState({}, '', '/psy/?mp=1&nh=1')
initMpEmbed()
expect(isMpNativeHeader()).toBe(true)
expect(document.documentElement.classList.contains('mp-native-header')).toBe(true)
expect(document.documentElement.style.getPropertyValue('--yxg-safe-top')).toBe('0px')
expect(document.documentElement.style.getPropertyValue('--yxg-page-top')).toBe('4px')
})
it('returns false without mp query', () => {
initMpEmbed()
expect(isMpEmbed()).toBe(false)
+32 -4
View File
@@ -1,8 +1,19 @@
const MP_EMBED_KEY = 'yxg_mp_embed'
const MP_NATIVE_HEADER_KEY = 'yxg_mp_native_header'
const MP_QUERY = 'mp'
const NH_QUERY = 'nh'
const SBH_QUERY = 'sbh'
const SBH_KEY = 'yxg_sbh'
const HEADER_H_PX = 50
const MP_CONTENT_TOP_PX = 4
/** Native cover-view handles logo; H5 only needs a tiny gap below web-view top. */
function applyNativeHeaderMode(): void {
if (typeof document === 'undefined') return
const root = document.documentElement
root.style.setProperty('--yxg-safe-top', '0px')
root.style.setProperty('--yxg-page-top', `${MP_CONTENT_TOP_PX}px`)
}
function applySafeTopInset(px: number): void {
if (typeof document === 'undefined' || px <= 0) return
@@ -19,11 +30,22 @@ function readStoredSbh(): number {
return parseInt(raw, 10)
}
/** True when mini-program renders the brand header above web-view. */
export function isMpNativeHeader(): boolean {
if (typeof window === 'undefined') return false
return sessionStorage.getItem(MP_NATIVE_HEADER_KEY) === '1'
}
/** Apply document-level embed chrome (safe-area wash, hide HTML title). */
export function applyMpEmbedDom(): void {
if (typeof document === 'undefined' || !isMpEmbed()) return
document.title = '\u200b'
document.documentElement.classList.add('mp-embed')
if (isMpNativeHeader()) {
document.documentElement.classList.add('mp-native-header')
applyNativeHeaderMode()
return
}
const stored = readStoredSbh()
if (stored > 0) applySafeTopInset(stored)
}
@@ -35,10 +57,16 @@ export function initMpEmbed(): void {
if (params.get(MP_QUERY) === '1') {
sessionStorage.setItem(MP_EMBED_KEY, '1')
}
const sbh = params.get(SBH_QUERY)
if (sbh && /^\d+$/.test(sbh)) {
sessionStorage.setItem(SBH_KEY, sbh)
applySafeTopInset(parseInt(sbh, 10))
if (params.get(NH_QUERY) === '1') {
sessionStorage.setItem(MP_NATIVE_HEADER_KEY, '1')
applyNativeHeaderMode()
}
if (!isMpNativeHeader()) {
const sbh = params.get(SBH_QUERY)
if (sbh && /^\d+$/.test(sbh)) {
sessionStorage.setItem(SBH_KEY, sbh)
applySafeTopInset(parseInt(sbh, 10))
}
}
applyMpEmbedDom()
}
+5 -1
View File
@@ -57,8 +57,12 @@ router.beforeEach((to) => {
q.mp = '1'
changed = true
}
if (sessionStorage.getItem('yxg_mp_native_header') === '1' && q.nh !== '1') {
q.nh = '1'
changed = true
}
const storedSbh = sessionStorage.getItem('yxg_sbh')
if (storedSbh && q.sbh !== storedSbh) {
if (storedSbh && q.sbh !== storedSbh && q.nh !== '1') {
q.sbh = storedSbh
changed = true
}
+1 -1
View File
@@ -61,7 +61,7 @@ html.mp-embed body{
.app-shell.mp-embed .home,
.app-shell.mp-embed .ps{
margin-top:0;
padding-top:8px;
padding-top:var(--yxg-page-top);
min-height:100%;
}
.app-footer{
+6
View File
@@ -0,0 +1,6 @@
# 复制为 deploy.env(勿提交 deploy.env
# 用法: ./scripts/deploy-h5.sh 或 DEPLOY_ENV=./deploy.env ./scripts/deploy-h5.sh
H5_DEPLOY_USER=ubuntu
H5_DEPLOY_HOST=your-server.example.com
H5_DEPLOY_PATH=/var/www/h5.yuxingu.com.cn/psy
+2
View File
@@ -2,6 +2,8 @@
适用于:**微信小程序**`yuxingu-Miniprogram-dev`)嵌入 **愈心魔方 H5**`digital-psychology/apps/user-h5`)的展示期上线。
**本地联调(免每次改服务器)** → 见 [LOCAL-DEV-MINI-PROGRAM-H5.md](LOCAL-DEV-MINI-PROGRAM-H5.md) · `npm run dev:mp`
---
## 1. 架构
+56
View File
@@ -0,0 +1,56 @@
# 愈心魔方 H5 · 本地联调(免每次改服务器)
改 H5 时**不必**先部署线上;用本机 Vite + 微信开发者工具即可预览。
## 日常流程
```bash
# 终端 1:H5(会打印局域网 IP)
cd digital-psychology
npm run dev:mp
# 终端 2(需要登录/接口时)
npm run dev:api
# 或仅数据库: npm run deps:up && npm run dev:api
```
```text
微信开发者工具
├── 打开 yuxingu-Miniprogram-dev
├── 详情 → 本地设置 → 勾选「不校验合法域名、web-view…」
├── 编译运行(开发模式,非发行)
└── 打开 Tab「愈心魔方」→ 自动加载本机 http://<你的IP>:5173/psy/
```
小程序侧规则(已配置):
- **开发编译**`import.meta.env.DEV`)→ `util/yxgConfig.local.js` 里的局域网地址
- **发行 / 上传体验版** → 自动使用 `https://h5.yuxingu.com.cn/psy/`
只需改一次 `yuxingu-Miniprogram-dev/util/yxgConfig.local.js`
```js
export const LOCAL_H5_HOST = '192.168.x.x:5173' // 与 dev:mp 打印一致
```
## 浏览器自测 embed
```text
http://localhost:5173/psy/?mp=1&sbh=88
```
## 确认要上线时(一条命令)
```bash
cp deploy.env.example deploy.env # 首次填写服务器
chmod +x scripts/deploy-h5.sh
./scripts/deploy-h5.sh
```
## 局限
| 项 | 说明 |
|----|------|
| 真机预览 | 手机与电脑同一 WiFi;开发者工具可勾选「真机调试」 |
| 业务域名 | 本地为 `http`,仅开发模式 + 关闭域名校验时可用 |
| 与线上一致 | 发行前仍建议在体验版走 HTTPS 域名验收一次 |
+2
View File
@@ -9,7 +9,9 @@
],
"scripts": {
"dev:h5": "npm run dev -w @yuxingu/user-h5",
"dev:mp": "bash scripts/dev-mp-h5.sh",
"build:h5": "npm run build -w @yuxingu/user-h5",
"deploy:h5": "bash scripts/deploy-h5.sh",
"dev:admin": "npm run dev -w @yuxingu/admin-h5",
"build:admin": "npm run build -w @yuxingu/admin-h5",
"dev:api": "cd apps/api && go run ./cmd/server",
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
# 构建并 rsync 上传 user-h5 dist(配置见 deploy.env.example
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
ENV_FILE="${DEPLOY_ENV:-$ROOT/deploy.env}"
if [ ! -f "$ENV_FILE" ]; then
echo "缺少 $ENV_FILE — 请复制 deploy.env.example 为 deploy.env 并填写服务器信息" >&2
exit 1
fi
# shellcheck disable=SC1090
source "$ENV_FILE"
: "${H5_DEPLOY_USER:?在 deploy.env 设置 H5_DEPLOY_USER}"
: "${H5_DEPLOY_HOST:?在 deploy.env 设置 H5_DEPLOY_HOST}"
: "${H5_DEPLOY_PATH:?在 deploy.env 设置 H5_DEPLOY_PATH}"
echo ">>> build h5"
npm run build:h5
DEST="${H5_DEPLOY_USER}@${H5_DEPLOY_HOST}:${H5_DEPLOY_PATH}/"
echo ">>> rsync → $DEST"
rsync -avz --delete "${ROOT}/apps/user-h5/dist/" "$DEST"
echo ">>> done"
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
# 本地小程序 + H5 联调:启动 Vite,并打印微信开发者工具所需地址
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
pick_ip() {
local addr=""
if command -v ipconfig >/dev/null 2>&1; then
addr="$(ipconfig getifaddr en0 2>/dev/null || ipconfig getifaddr en1 2>/dev/null || true)"
fi
if [ -z "$addr" ]; then
addr="$(hostname -I 2>/dev/null | awk '{print $1}' || true)"
fi
echo "$addr"
}
IP="$(pick_ip)"
IP="${IP:-127.0.0.1}"
echo ""
echo "══════════════════════════════════════════════════════════"
echo " 愈心魔方 · 本地 H5 联调"
echo "══════════════════════════════════════════════════════════"
echo " 1. 确认小程序 util/yxgConfig.local.js 中 LOCAL_H5_HOST 为:"
echo " ${IP}:5173"
echo " 2. digital-psychology 本窗口保持运行(Vite + API 代理 /psy/api → :8080"
echo " 需要接口时另开终端: npm run dev:api 或 npm run deps:up"
echo " 3. 微信开发者工具 → 详情 → 本地设置 → 勾选:"
echo " 「不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书」"
echo " 4. 编译运行小程序(开发模式),打开「愈心魔方」Tab"
echo ""
echo " 浏览器自测: http://${IP}:5173/psy/?mp=1&sbh=88"
echo "══════════════════════════════════════════════════════════"
echo ""
npm run dev:h5