feat: 新增AppStack布局组件并试点收敛页面根
This commit is contained in:
parent
bf80cf15b6
commit
6cfb3b2f93
93
client/src/components/base/AppStack.vue
Normal file
93
client/src/components/base/AppStack.vue
Normal 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-5(20px),横向 space-4(16px)
|
||||
*/
|
||||
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>
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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%;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user