refactor: 移动端去除layout顶栏,页面头统一收敛到AppPageHeader
This commit is contained in:
parent
fbf134ccc6
commit
bf80cf15b6
@ -1,21 +1,59 @@
|
||||
<script setup lang="ts">
|
||||
withDefaults(
|
||||
import { useRouter } from 'vue-router'
|
||||
import AppIcon from './AppIcon.vue'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
title: string
|
||||
title?: string
|
||||
subtitle?: string
|
||||
/** page:一级页头(h1+副标题+右段动作);detail:详情/子页返回头(紧凑标题,默认带返回) */
|
||||
size?: 'page' | 'detail'
|
||||
/** 是否渲染左段返回按钮 */
|
||||
back?: boolean
|
||||
/** 返回目标;缺省时使用 history.back() */
|
||||
backTo?: string
|
||||
/** 移动端是否纵向堆叠(部分页面移动版按钮在标题下方) */
|
||||
stackMobile?: boolean
|
||||
/** 移动端吸顶(替代原 layout topbar 的吸顶行为;仅移动端生效) */
|
||||
stickyMobile?: boolean
|
||||
}>(),
|
||||
{ subtitle: '', stackMobile: false }
|
||||
{ title: '', subtitle: '', size: 'page', back: false, backTo: '', stackMobile: false, stickyMobile: false }
|
||||
)
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
function goBack() {
|
||||
if (props.backTo) router.push(props.backTo)
|
||||
else router.back()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="page-head" :class="{ 'page-head--stack': stackMobile }">
|
||||
<header
|
||||
class="page-head"
|
||||
:class="{
|
||||
'page-head--stack': stackMobile,
|
||||
'page-head--detail': size === 'detail',
|
||||
'page-head--sticky-mobile': stickyMobile
|
||||
}"
|
||||
>
|
||||
<button
|
||||
v-if="back"
|
||||
type="button"
|
||||
class="page-head__back"
|
||||
aria-label="返回"
|
||||
@click="goBack"
|
||||
>
|
||||
<AppIcon name="chevron-left" :size="20" />
|
||||
</button>
|
||||
|
||||
<div class="page-head__title">
|
||||
<h1>{{ title }}</h1>
|
||||
<p v-if="subtitle">{{ subtitle }}</p>
|
||||
<slot name="title">
|
||||
<h1 v-if="title">{{ title }}</h1>
|
||||
<p v-if="subtitle">{{ subtitle }}</p>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<div v-if="$slots.default" class="page-head__actions"><slot /></div>
|
||||
</header>
|
||||
</template>
|
||||
@ -49,9 +87,53 @@ withDefaults(
|
||||
gap: var(--space-3);
|
||||
flex: none;
|
||||
}
|
||||
|
||||
/* 返回按钮(左段) */
|
||||
.page-head__back {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
flex: none;
|
||||
border-radius: 50%;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-subtle);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
transition: background var(--motion-hover-fast) var(--ease-default);
|
||||
}
|
||||
.page-head__back:hover {
|
||||
background: var(--bg-soft);
|
||||
}
|
||||
|
||||
/* 详情/子页返回头:紧凑行内布局 */
|
||||
.page-head--detail {
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
.page-head--detail .page-head__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
.page-head--detail h1 {
|
||||
font-size: var(--fs-h3);
|
||||
font-weight: var(--fw-h1);
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.page-head--stack {
|
||||
flex-direction: column;
|
||||
}
|
||||
/* 吸顶(替代原 layout topbar;负边距让背景铺满屏宽) */
|
||||
.page-head--sticky-mobile {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 30;
|
||||
margin-left: calc(-1 * var(--padding-mobile));
|
||||
margin-right: calc(-1 * var(--padding-mobile));
|
||||
padding: var(--space-2) var(--padding-mobile);
|
||||
background: var(--bg-canvas);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,22 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import AppIcon from '../components/base/AppIcon.vue'
|
||||
import { mobileNav } from '../router/nav'
|
||||
|
||||
const route = useRoute()
|
||||
const title = computed(() => String(route.meta.title ?? '备考通'))
|
||||
// 首页以自己的问候语作为页头,不再重复显示顶部栏标题
|
||||
const isHome = computed(() => route.path === '/')
|
||||
// 刷题相关页面自带页头(AppPageHeader),隐藏顶栏标题避免重复
|
||||
const hideTopbar = computed(() => isHome.value || route.path.startsWith('/practice'))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="layout-mobile">
|
||||
<header v-if="!hideTopbar" class="topbar">
|
||||
<h1>{{ title }}</h1>
|
||||
</header>
|
||||
<main class="content"><slot /></main>
|
||||
<nav class="tabbar">
|
||||
<RouterLink
|
||||
@ -37,22 +28,6 @@ const hideTopbar = computed(() => isHome.value || route.path.startsWith('/practi
|
||||
.layout-mobile {
|
||||
min-height: 100vh;
|
||||
}
|
||||
.topbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 56px;
|
||||
padding: 0 var(--padding-mobile);
|
||||
background: var(--bg-canvas);
|
||||
}
|
||||
.topbar h1 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: var(--fw-h1);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.content {
|
||||
padding: 0 var(--padding-mobile) var(--tabbar-height-mobile);
|
||||
}
|
||||
|
||||
@ -130,7 +130,7 @@ watch(
|
||||
|
||||
<template>
|
||||
<div class="mock-form">
|
||||
<AppPageHeader :title="title" subtitle="仅支持行测五个模块分数,总分自动汇总">
|
||||
<AppPageHeader :title="title" subtitle="仅支持行测五个模块分数,总分自动汇总" sticky-mobile>
|
||||
<AppButton variant="ghost" @click="back">
|
||||
<AppIcon name="chevron-left" :size="16" />返回模考分析
|
||||
</AppButton>
|
||||
|
||||
@ -98,7 +98,7 @@ async function confirmRemove() {
|
||||
/>
|
||||
|
||||
<template v-else>
|
||||
<AppPageHeader title="模考分析" subtitle="行测成绩趋势 · 目标分差 · 模块对比" :stack-mobile="isMobile">
|
||||
<AppPageHeader title="模考分析" subtitle="行测成绩趋势 · 目标分差 · 模块对比" :stack-mobile="isMobile" sticky-mobile>
|
||||
<AppButton variant="primary" @click="goEntry">
|
||||
<AppIcon name="plus" :size="16" />录入成绩
|
||||
</AppButton>
|
||||
|
||||
@ -8,8 +8,7 @@ import { formatDateShort } from '../../utils/format'
|
||||
import AppLoading from '../../components/feedback/AppLoading.vue'
|
||||
import AppError from '../../components/feedback/AppError.vue'
|
||||
import AppEmpty from '../../components/feedback/AppEmpty.vue'
|
||||
import AppIcon from '../../components/base/AppIcon.vue'
|
||||
import AppButton from '../../components/base/AppButton.vue'
|
||||
import AppPageHeader from '../../components/base/AppPageHeader.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const id = computed(() => String(route.params.id ?? ''))
|
||||
@ -32,12 +31,9 @@ const html = computed(() => renderMarkdown(detail.data.value?.content ?? ''))
|
||||
/>
|
||||
|
||||
<template v-else-if="detail.data.value">
|
||||
<header class="head">
|
||||
<AppButton class="head__back" variant="ghost" size="md" aria-label="返回" @click="$router.back()">
|
||||
<AppIcon name="chevron-left" :size="20" />
|
||||
</AppButton>
|
||||
<span class="head__label">要闻详情</span>
|
||||
</header>
|
||||
<AppPageHeader back class="news-detail__head" sticky-mobile>
|
||||
<template #title><span class="head__label">要闻详情</span></template>
|
||||
</AppPageHeader>
|
||||
|
||||
<article class="article">
|
||||
<section class="article__hero">
|
||||
@ -61,25 +57,9 @@ const html = computed(() => renderMarkdown(detail.data.value?.content ?? ''))
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
.news-detail__head {
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
.head__back {
|
||||
display: grid !important;
|
||||
place-items: center;
|
||||
width: 36px !important;
|
||||
height: 36px !important;
|
||||
padding: 0 !important;
|
||||
border: none !important;
|
||||
border-radius: 50% !important;
|
||||
background: var(--bg-card) !important;
|
||||
color: var(--text-primary) !important;
|
||||
gap: 0 !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
.head__label {
|
||||
font-size: var(--fs-subtitle);
|
||||
color: var(--text-secondary);
|
||||
|
||||
@ -96,7 +96,7 @@ async function doImport() {
|
||||
|
||||
<template v-else>
|
||||
<!-- 页头 -->
|
||||
<AppPageHeader :title="isMobile ? '要闻公告' : '要闻'" subtitle="最新招考资讯与政策动态">
|
||||
<AppPageHeader :title="isMobile ? '要闻公告' : '要闻'" subtitle="最新招考资讯与政策动态" sticky-mobile>
|
||||
<AppButton variant="ghost" @click="openImport">订阅推送</AppButton>
|
||||
<AppButton variant="primary" @click="openImport">
|
||||
<AppIcon name="inbox" :size="16" />发布公告
|
||||
|
||||
@ -4,8 +4,7 @@ import { plansApi } from '../../api'
|
||||
import { useRequest } from '../../composables/useRequest'
|
||||
import { formatDateShort, formatMinutes, weekdayName } from '../../utils/format'
|
||||
import AppCard from '../../components/base/AppCard.vue'
|
||||
import AppIcon from '../../components/base/AppIcon.vue'
|
||||
import AppButton from '../../components/base/AppButton.vue'
|
||||
import AppPageHeader from '../../components/base/AppPageHeader.vue'
|
||||
import AppLoading from '../../components/feedback/AppLoading.vue'
|
||||
import AppError from '../../components/feedback/AppError.vue'
|
||||
import AppEmpty from '../../components/feedback/AppEmpty.vue'
|
||||
@ -45,13 +44,10 @@ const weakTone = ['danger', 'warning', 'primary']
|
||||
|
||||
<template v-else-if="data">
|
||||
<!-- 页头 -->
|
||||
<header class="review__head">
|
||||
<AppButton class="review__back" variant="ghost" size="md" aria-label="返回" @click="$router.back()">
|
||||
<AppIcon name="chevron-left" :size="20" />
|
||||
</AppButton>
|
||||
<h1>周报复盘</h1>
|
||||
<AppPageHeader back sticky-mobile>
|
||||
<template #title><h1 class="review__title">周报复盘</h1></template>
|
||||
<span class="review__export">本周小结</span>
|
||||
</header>
|
||||
</AppPageHeader>
|
||||
|
||||
<!-- 周区间 + 汇总 -->
|
||||
<section class="review-hero">
|
||||
@ -120,32 +116,13 @@ const weakTone = ['danger', 'warning', 'primary']
|
||||
flex-direction: column;
|
||||
gap: var(--space-5);
|
||||
}
|
||||
.review__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
.review__back {
|
||||
display: grid !important;
|
||||
place-items: center;
|
||||
width: 36px !important;
|
||||
height: 36px !important;
|
||||
padding: 0 !important;
|
||||
border: none !important;
|
||||
border-radius: 50% !important;
|
||||
background: var(--bg-card) !important;
|
||||
color: var(--text-primary) !important;
|
||||
gap: 0 !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
.review__head h1 {
|
||||
.review__title {
|
||||
margin: 0;
|
||||
font-size: var(--fs-h1);
|
||||
font-weight: var(--fw-h1);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.review__export {
|
||||
margin-left: auto;
|
||||
font-size: var(--fs-label);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
@ -234,7 +234,7 @@ const profileDays = computed(() => (examProfile.data.value ? daysSince(examProfi
|
||||
|
||||
<!-- ============ 桌面端备考计划 ============ -->
|
||||
<div v-else-if="todayData" class="plan-desktop">
|
||||
<AppPageHeader title="备考计划" :subtitle="`安排阶段任务,稳步推进备考进度 · 备考第 ${profileDays} 天`">
|
||||
<AppPageHeader title="备考计划" :subtitle="`安排阶段任务,稳步推进备考进度 · 备考第 ${profileDays} 天`" sticky-mobile>
|
||||
<RouterLink class="btn-link" to="/plan/review">
|
||||
<AppIcon name="chart" :size="16" />周报复盘
|
||||
</RouterLink>
|
||||
|
||||
@ -7,6 +7,7 @@ import AppCard from '../../components/base/AppCard.vue'
|
||||
import AppButton from '../../components/base/AppButton.vue'
|
||||
import AppIcon from '../../components/base/AppIcon.vue'
|
||||
import AppBadge from '../../components/base/AppBadge.vue'
|
||||
import AppPageHeader from '../../components/base/AppPageHeader.vue'
|
||||
import AppLoading from '../../components/feedback/AppLoading.vue'
|
||||
import AppError from '../../components/feedback/AppError.vue'
|
||||
|
||||
@ -68,15 +69,11 @@ const mistakeText = computed(() => {
|
||||
|
||||
<template v-else-if="explain">
|
||||
<!-- 顶栏 -->
|
||||
<header class="ai-explain__top">
|
||||
<button type="button" class="ai-explain__back" @click="back">
|
||||
<AppIcon name="chevron-left" :size="20" />
|
||||
</button>
|
||||
<h1 class="ai-explain__title">{{ title }}</h1>
|
||||
<AppPageHeader :title="title" size="detail" back>
|
||||
<AppBadge :variant="isAi ? 'primary' : 'soft'">
|
||||
{{ isAi ? 'AI 生成' : '题库解析回退' }}
|
||||
</AppBadge>
|
||||
</header>
|
||||
</AppPageHeader>
|
||||
|
||||
<!-- 结论 -->
|
||||
<AppCard title="讲解结论" icon="sparkles">
|
||||
@ -131,30 +128,6 @@ const mistakeText = computed(() => {
|
||||
flex-direction: column;
|
||||
gap: var(--space-5);
|
||||
}
|
||||
.ai-explain__top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
.ai-explain__back {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
flex: none;
|
||||
border-radius: 50%;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-subtle);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
}
|
||||
.ai-explain__title {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
font-size: var(--fs-h2);
|
||||
font-weight: var(--fw-h2);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.ai-explain__summary {
|
||||
margin: 0;
|
||||
font-size: var(--fs-body);
|
||||
|
||||
@ -5,6 +5,7 @@ import { reviewApi, type ReviewDetail } from '../../api'
|
||||
import AppButton from '../../components/base/AppButton.vue'
|
||||
import AppBadge from '../../components/base/AppBadge.vue'
|
||||
import AppIcon from '../../components/base/AppIcon.vue'
|
||||
import AppPageHeader from '../../components/base/AppPageHeader.vue'
|
||||
import AppLoading from '../../components/feedback/AppLoading.vue'
|
||||
import AppError from '../../components/feedback/AppError.vue'
|
||||
import { formatDateShort, formatDateWeekday } from '../../utils/format'
|
||||
@ -75,10 +76,6 @@ async function markMastered() {
|
||||
}
|
||||
}
|
||||
|
||||
function back() {
|
||||
router.back()
|
||||
}
|
||||
|
||||
const statusLabel = computed(() =>
|
||||
assessResult.value
|
||||
? assessResult.value.status === 'mastered'
|
||||
@ -119,15 +116,9 @@ function goAiExplain() {
|
||||
|
||||
<template v-else-if="detail">
|
||||
<!-- 顶栏:返回 + 标题 + 状态 -->
|
||||
<header class="wd__top">
|
||||
<button type="button" class="wd__back" @click="back">
|
||||
<AppIcon name="chevron-left" :size="20" />
|
||||
</button>
|
||||
<h1 class="wd__title">错题详情</h1>
|
||||
<span class="wd__status-badge">
|
||||
<AppBadge :variant="statusMeta.variant">{{ statusLabel }}</AppBadge>
|
||||
</span>
|
||||
</header>
|
||||
<AppPageHeader title="错题详情" size="detail" back>
|
||||
<AppBadge :variant="statusMeta.variant">{{ statusLabel }}</AppBadge>
|
||||
</AppPageHeader>
|
||||
|
||||
<!-- 题目卡片 -->
|
||||
<section class="wd__question">
|
||||
@ -255,34 +246,6 @@ function goAiExplain() {
|
||||
}
|
||||
|
||||
/* 顶栏 */
|
||||
.wd__top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
.wd__back {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
flex: none;
|
||||
border-radius: 50%;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-subtle);
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
}
|
||||
.wd__title {
|
||||
margin: 0;
|
||||
font-size: var(--fs-h3);
|
||||
font-weight: var(--fw-h1);
|
||||
color: var(--text-primary);
|
||||
flex: 1;
|
||||
}
|
||||
.wd__status-badge {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
/* 题目 */
|
||||
.wd__question {
|
||||
background: var(--bg-card);
|
||||
|
||||
@ -168,7 +168,7 @@ function editProfile() {
|
||||
|
||||
<!-- ============ 桌面端我的 ============ -->
|
||||
<div v-else-if="d" class="profile-desktop">
|
||||
<AppPageHeader title="我的" subtitle="管理个人资料与备考偏好">
|
||||
<AppPageHeader title="我的" subtitle="管理个人资料与备考偏好" sticky-mobile>
|
||||
<AppButton variant="secondary" @click="editProfile">
|
||||
<AppIcon name="user" :size="16" />编辑资料
|
||||
</AppButton>
|
||||
|
||||
@ -326,7 +326,7 @@ function markAnswer(key: OptionKey) {
|
||||
<div class="questions">
|
||||
<!-- ======================= 手动录入模式 ======================= -->
|
||||
<template v-if="mode === 'entry'">
|
||||
<AppPageHeader title="题库管理" subtitle="手动录入题目 · 支持逐题与批量 JSON 导入">
|
||||
<AppPageHeader title="题库管理" subtitle="手动录入题目 · 支持逐题与批量 JSON 导入" sticky-mobile>
|
||||
<AppButton variant="ghost" @click="mode = 'list'">
|
||||
<AppIcon name="chevron-left" :size="16" />返回列表
|
||||
</AppButton>
|
||||
@ -454,7 +454,7 @@ function markAnswer(key: OptionKey) {
|
||||
/>
|
||||
|
||||
<template v-else>
|
||||
<AppPageHeader title="题库管理" subtitle="手动录入题目 · 支持逐题与批量 JSON 导入">
|
||||
<AppPageHeader title="题库管理" subtitle="手动录入题目 · 支持逐题与批量 JSON 导入" sticky-mobile>
|
||||
<AppButton variant="secondary" @click="openImport">
|
||||
<AppIcon name="inbox" :size="16" />导入真题
|
||||
</AppButton>
|
||||
|
||||
@ -96,7 +96,7 @@ async function save() {
|
||||
|
||||
<template v-else-if="settings.data.value">
|
||||
<!-- 页头 -->
|
||||
<AppPageHeader title="设置" subtitle="调整偏好、通知与数据策略" stack-mobile>
|
||||
<AppPageHeader title="设置" subtitle="调整偏好、通知与数据策略" stack-mobile sticky-mobile>
|
||||
<AppButton variant="primary" :disabled="saving" @click="save">
|
||||
<AppIcon name="check" :size="16" />{{ saving ? '保存中…' : '保存设置' }}
|
||||
</AppButton>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user