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:
@@ -3,6 +3,7 @@ __pycache__/
|
||||
.venv/
|
||||
*.log
|
||||
.env
|
||||
deploy.env
|
||||
.DS_Store
|
||||
|
||||
# ESS — machine-local root pointer(仓库内用 scripts 包装器)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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,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. 架构
|
||||
|
||||
@@ -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 域名验收一次 |
|
||||
@@ -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",
|
||||
|
||||
Executable
+26
@@ -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"
|
||||
Executable
+37
@@ -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
|
||||
Reference in New Issue
Block a user