feat: 新增AppStack布局组件并试点收敛页面根

This commit is contained in:
liyy 2026-09-02 16:43:55 +08:00
parent bf80cf15b6
commit 6cfb3b2f93
5 changed files with 103 additions and 21 deletions

View File

@ -0,0 +1,93 @@
<script setup lang="ts">
import { computed } from 'vue'
const props = withDefaults(
defineProps<{
/** 主轴方向;默认纵向 */
direction?: 'row' | 'column'
/**
* 子项间距兼容三种写法
* - token space-3 / space-4 / space-5
* - 纯数字字符串'12' 12px
* - 其它 CSS '12px 16px''var(--space-3)' 等原样透传
* 缺省纵向 space-520px横向 space-416px
*/
gap?: string
/** 交叉轴对齐 */
align?: 'start' | 'center' | 'end' | 'stretch' | 'baseline'
/** 主轴对齐 */
justify?: 'start' | 'center' | 'end' | 'between' | 'around' | 'evenly'
/** 是否允许换行(横向时使用) */
wrap?: boolean
}>(),
{ direction: 'column', gap: '', align: 'stretch', justify: 'start', wrap: false }
)
/** token 名 / 纯数字 → 兼容的 CSS gap其它原样透传 */
const gapStyle = computed(() => {
const raw = props.gap.trim()
if (!raw) return props.direction === 'row' ? 'var(--space-4)' : 'var(--space-5)'
if (/^space-[1-8]$/.test(raw)) return `var(--${raw})`
if (/^\d+$/.test(raw)) return `${raw}px`
return raw
})
</script>
<template>
<div
class="app-stack"
:class="{
'app-stack--row': direction === 'row',
'app-stack--wrap': wrap,
[`app-stack--align-${align}`]: align !== 'stretch',
[`app-stack--justify-${justify}`]: justify !== 'start'
}"
:style="{ '--stack-gap': gapStyle }"
>
<slot />
</div>
</template>
<style scoped>
.app-stack {
display: flex;
flex-direction: column;
align-items: stretch;
justify-content: flex-start;
gap: var(--stack-gap, var(--space-5));
min-width: 0;
}
.app-stack--row {
flex-direction: row;
}
.app-stack--wrap {
flex-wrap: wrap;
}
.app-stack--align-center {
align-items: center;
}
.app-stack--align-start {
align-items: flex-start;
}
.app-stack--align-end {
align-items: flex-end;
}
.app-stack--align-baseline {
align-items: baseline;
}
.app-stack--justify-center {
justify-content: center;
}
.app-stack--justify-end {
justify-content: flex-end;
}
.app-stack--justify-between {
justify-content: space-between;
}
.app-stack--justify-around {
justify-content: space-around;
}
.app-stack--justify-evenly {
justify-content: space-evenly;
}
</style>

View File

@ -29,7 +29,7 @@ const route = useRoute()
min-height: 100vh;
}
.content {
padding: 0 var(--padding-mobile) var(--tabbar-height-mobile);
padding: var(--padding-mobile) var(--padding-mobile) var(--tabbar-height-mobile);
}
.tabbar {
position: fixed;

View File

@ -17,6 +17,7 @@ import AppLoading from '../../components/feedback/AppLoading.vue'
import AppError from '../../components/feedback/AppError.vue'
import AppEmpty from '../../components/feedback/AppEmpty.vue'
import StatCard from '../../components/dashboard/StatCard.vue'
import AppStack from '../../components/base/AppStack.vue'
const router = useRouter()
const { isMobile } = useResponsive()
@ -86,7 +87,7 @@ async function confirmRemove() {
</script>
<template>
<div class="mock-page">
<AppStack class="mock-page">
<AppLoading v-if="analysis.loading.value && !hasData" fullscreen />
<AppError
v-else-if="analysis.error.value"
@ -213,15 +214,10 @@ async function confirmRemove() {
</div>
</template>
</AppModal>
</div>
</AppStack>
</template>
<style scoped>
.mock-page {
display: flex;
flex-direction: column;
gap: var(--space-5);
}
.mock-page__stats {
display: grid;
grid-template-columns: repeat(3, 1fr);

View File

@ -10,6 +10,7 @@ import AppButton from '../../components/base/AppButton.vue'
import AppIcon from '../../components/base/AppIcon.vue'
import AppModal from '../../components/base/AppModal.vue'
import AppPageHeader from '../../components/base/AppPageHeader.vue'
import AppStack from '../../components/base/AppStack.vue'
import AppLoading from '../../components/feedback/AppLoading.vue'
import AppError from '../../components/feedback/AppError.vue'
import AppEmpty from '../../components/feedback/AppEmpty.vue'
@ -83,7 +84,7 @@ async function doImport() {
</script>
<template>
<div class="news">
<AppStack class="news" gap="space-4">
<AppLoading v-if="list.loading.value" fullscreen />
<AppError
v-else-if="list.error.value"
@ -197,16 +198,10 @@ async function doImport() {
<AppButton variant="primary" @click="doImport">开始导入</AppButton>
</template>
</AppModal>
</div>
</AppStack>
</template>
<style scoped>
.news {
display: flex;
flex-direction: column;
gap: var(--space-4);
}
/* 分类 tab */
.news-tabs {
display: flex;

View File

@ -6,6 +6,7 @@ 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 AppStack from '../../components/base/AppStack.vue'
import AppLoading from '../../components/feedback/AppLoading.vue'
import AppError from '../../components/feedback/AppError.vue'
import { formatDateShort, formatDateWeekday } from '../../utils/format'
@ -110,7 +111,7 @@ function goAiExplain() {
</script>
<template>
<div class="wrong-detail">
<AppStack class="wrong-detail">
<AppLoading v-if="loading" fullscreen text="正在加载错题详情…" />
<AppError v-else-if="error" fullscreen title="加载失败" :message="error" retry @retry="load" />
@ -232,14 +233,11 @@ function goAiExplain() {
</div>
</section>
</template>
</div>
</AppStack>
</template>
<style scoped>
.wrong-detail {
display: flex;
flex-direction: column;
gap: var(--space-5);
max-width: 680px;
margin: 0 auto;
width: 100%;