feat(h5): 小程序 embed 模式与全站 ICP 备案页脚
H5 支持 ?mp=1 嵌入微信 WebView;user-h5/admin-h5 底部展示蜀ICP备2025140386号-2 并链至工信部;补充小程序+H5 部署说明。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<footer class="icp-footer">
|
||||
<a class="icp-link" :href="ICP_MIIT_URL" target="_blank" rel="noopener noreferrer">{{ ICP_RECORD }}</a>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ICP_MIIT_URL, ICP_RECORD } from '@yuxingu/utils'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.icp-footer {
|
||||
text-align: center;
|
||||
padding: 1rem 0 0.5rem;
|
||||
}
|
||||
.icp-link {
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
color: var(--muted, #8a7f7b);
|
||||
text-decoration: none;
|
||||
}
|
||||
.icp-link:hover {
|
||||
color: var(--accent, #e54d42);
|
||||
}
|
||||
</style>
|
||||
@@ -2,6 +2,7 @@
|
||||
import { onMounted } from 'vue'
|
||||
import { RouterLink, RouterView, useRouter } from 'vue-router'
|
||||
import BrandLogo from '@/components/BrandLogo.vue'
|
||||
import IcpFooter from '@/components/IcpFooter.vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const auth = useAuthStore()
|
||||
@@ -55,7 +56,10 @@ async function onLogout() {
|
||||
</div>
|
||||
</aside>
|
||||
<main class="main">
|
||||
<RouterView />
|
||||
<div class="main-body">
|
||||
<RouterView />
|
||||
</div>
|
||||
<IcpFooter />
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
@@ -91,7 +95,13 @@ nav a.router-link-active {
|
||||
}
|
||||
.foot { margin-top: auto; display: flex; flex-direction: column; gap: 0.5rem; }
|
||||
.role { font-size: 0.75rem; color: var(--muted); }
|
||||
.main { padding: 1.5rem 1.75rem; }
|
||||
.main {
|
||||
padding: 1.5rem 1.75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.main-body { flex: 1; min-width: 0; }
|
||||
@media (max-width: 800px) {
|
||||
.shell { grid-template-columns: 1fr; }
|
||||
.nav { border-right: 0; border-bottom: 1px solid var(--line); }
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import BrandLogo from '@/components/BrandLogo.vue'
|
||||
import IcpFooter from '@/components/IcpFooter.vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const auth = useAuthStore()
|
||||
@@ -46,11 +47,24 @@ async function onSubmit() {
|
||||
{{ loading ? '登录中…' : '登录' }}
|
||||
</button>
|
||||
</form>
|
||||
<IcpFooter />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.wrap { min-height: 100vh; display: grid; place-items: center; padding: 1.5rem; }
|
||||
.wrap {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
.wrap :deep(.icp-footer) {
|
||||
margin-top: auto;
|
||||
width: 100%;
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
.login { width: min(380px, 100%); text-align: center; }
|
||||
.login :deep(.field) { text-align: left; }
|
||||
h1 { margin: 0 0 0.25rem; font-size: 1.45rem; font-family: "Noto Serif SC", "Songti SC", serif; }
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 357 KiB |
@@ -1,12 +1,22 @@
|
||||
<template>
|
||||
<div class="app-shell">
|
||||
<AppHeader />
|
||||
<div class="app-shell" :class="{ 'mp-embed': mpEmbed }">
|
||||
<AppHeader v-if="!mpEmbed || showEmbedHeader" />
|
||||
<router-view />
|
||||
<TabBar />
|
||||
<IcpFooter />
|
||||
<TabBar v-if="!mpEmbed" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import AppHeader from './components/AppHeader.vue'
|
||||
import IcpFooter from './components/IcpFooter.vue'
|
||||
import TabBar from './components/TabBar.vue'
|
||||
import { isMpEmbed } from './lib/mpEmbed'
|
||||
|
||||
const route = useRoute()
|
||||
const mpEmbed = isMpEmbed()
|
||||
/** Sub-pages in web-view: show back only; home hides duplicate chrome. */
|
||||
const showEmbedHeader = computed(() => mpEmbed && route.path !== '/')
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<footer class="icp-footer" :class="{ 'mp-embed': mpEmbed }">
|
||||
<a class="icp-link" :href="ICP_MIIT_URL" target="_blank" rel="noopener noreferrer">{{ ICP_RECORD }}</a>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ICP_MIIT_URL, ICP_RECORD } from '@yuxingu/utils'
|
||||
import { isMpEmbed } from '../lib/mpEmbed'
|
||||
|
||||
const mpEmbed = isMpEmbed()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.icp-footer {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 100%;
|
||||
max-width: var(--yxg-max-w);
|
||||
bottom: calc(var(--yxg-nav-h) + env(safe-area-inset-bottom, 0px));
|
||||
z-index: 90;
|
||||
text-align: center;
|
||||
padding: 4px 12px;
|
||||
background: rgba(255, 252, 250, 0.92);
|
||||
border-top: 1px solid rgba(240, 230, 224, 0.6);
|
||||
}
|
||||
.icp-footer.mp-embed {
|
||||
bottom: env(safe-area-inset-bottom, 0px);
|
||||
}
|
||||
.icp-link {
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
color: #9a8f8b;
|
||||
text-decoration: none;
|
||||
}
|
||||
.icp-link:active {
|
||||
opacity: 0.75;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,24 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
import { initMpEmbed, isMpEmbed } from './mpEmbed'
|
||||
|
||||
describe('mpEmbed', () => {
|
||||
beforeEach(() => {
|
||||
sessionStorage.clear()
|
||||
window.history.replaceState({}, '', '/psy/')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sessionStorage.clear()
|
||||
})
|
||||
|
||||
it('persists embed flag from ?mp=1', () => {
|
||||
window.history.replaceState({}, '', '/psy/?mp=1')
|
||||
initMpEmbed()
|
||||
expect(isMpEmbed()).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false without mp query', () => {
|
||||
initMpEmbed()
|
||||
expect(isMpEmbed()).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,17 @@
|
||||
const MP_EMBED_KEY = 'yxg_mp_embed'
|
||||
const MP_QUERY = 'mp'
|
||||
|
||||
/** Persist ?mp=1 from mini-program web-view entry. Call once before router mount. */
|
||||
export function initMpEmbed(): void {
|
||||
if (typeof window === 'undefined') return
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
if (params.get(MP_QUERY) === '1') {
|
||||
sessionStorage.setItem(MP_EMBED_KEY, '1')
|
||||
}
|
||||
}
|
||||
|
||||
/** True when H5 runs inside WeChat mini-program web-view (no H5 tab bar). */
|
||||
export function isMpEmbed(): boolean {
|
||||
if (typeof window === 'undefined') return false
|
||||
return sessionStorage.getItem(MP_EMBED_KEY) === '1'
|
||||
}
|
||||
@@ -3,6 +3,9 @@ import { createPinia } from 'pinia'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import { initAnalytics, trackPageView } from './lib/analytics'
|
||||
import { initMpEmbed } from './lib/mpEmbed'
|
||||
|
||||
initMpEmbed()
|
||||
import '@yuxingu/ui/tokens.css'
|
||||
import './styles/app.css'
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { isMpEmbed } from '../lib/mpEmbed'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory('/psy/'),
|
||||
@@ -48,4 +49,9 @@ const router = createRouter({
|
||||
},
|
||||
})
|
||||
|
||||
router.beforeEach((to) => {
|
||||
if (!isMpEmbed() || to.query.mp === '1') return true
|
||||
return { path: to.path, query: { ...to.query, mp: '1' }, hash: to.hash, replace: true }
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -14,9 +14,12 @@ body{
|
||||
}
|
||||
.app-shell{
|
||||
max-width:var(--yxg-max-w);margin:0 auto;min-height:100vh;
|
||||
padding-bottom:calc(var(--yxg-nav-h) + env(safe-area-inset-bottom,0px) + 12px);
|
||||
padding-bottom:calc(var(--yxg-nav-h) + env(safe-area-inset-bottom,0px) + 36px);
|
||||
position:relative;
|
||||
}
|
||||
.app-shell.mp-embed{
|
||||
padding-bottom:calc(env(safe-area-inset-bottom,0px) + 36px);
|
||||
}
|
||||
a{color:inherit;text-decoration:none}
|
||||
|
||||
/* ── Page shell (测测式:暖色洗底 + 白 sheet) ── */
|
||||
|
||||
Reference in New Issue
Block a user