refactor: AppStack极简化为direction+gap,响应式方向由使用页传入

This commit is contained in:
liyy 2026-09-03 14:44:40 +08:00
parent 6cfb3b2f93
commit bf5e826bbe
2 changed files with 23 additions and 70 deletions

View File

@ -3,27 +3,17 @@ import { computed } from 'vue'
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
/** 主轴方向;默认纵向 */ /** 单行横向 / 单列纵向;默认纵向。响应式由使用处自行决定传 row 还是 column */
direction?: 'row' | 'column' direction?: 'row' | 'column'
/** /**
* 子项间距兼容三种写法 * 子项间距token space-3或纯数字字符串'12' 12px
* - token space-3 / space-4 / space-5 * 缺省纵向 space-520px横向 space-416px
* - 纯数字字符串'12' 12px
* - 其它 CSS '12px 16px''var(--space-3)' 等原样透传
* 缺省纵向 space-520px横向 space-416px
*/ */
gap?: string 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 } { direction: 'column', gap: '' }
) )
/** token 名 / 纯数字 → 兼容的 CSS gap其它原样透传 */
const gapStyle = computed(() => { const gapStyle = computed(() => {
const raw = props.gap.trim() const raw = props.gap.trim()
if (!raw) return props.direction === 'row' ? 'var(--space-4)' : 'var(--space-5)' if (!raw) return props.direction === 'row' ? 'var(--space-4)' : 'var(--space-5)'
@ -36,12 +26,7 @@ const gapStyle = computed(() => {
<template> <template>
<div <div
class="app-stack" class="app-stack"
:class="{ :class="{ 'app-stack--row': direction === 'row' }"
'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 }" :style="{ '--stack-gap': gapStyle }"
> >
<slot /> <slot />
@ -49,6 +34,7 @@ const gapStyle = computed(() => {
</template> </template>
<style scoped> <style scoped>
/* 默认样式纵向、stretch、start特殊布局由使用页绑 class 覆盖 */
.app-stack { .app-stack {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -60,34 +46,4 @@ const gapStyle = computed(() => {
.app-stack--row { .app-stack--row {
flex-direction: 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> </style>

View File

@ -120,7 +120,12 @@ async function confirmRemove() {
<template v-else> <template v-else>
<!-- 最近成绩 / 目标分差 / 较上次变化 --> <!-- 最近成绩 / 目标分差 / 较上次变化 -->
<section class="mock-page__stats"> <AppStack
class="mock-page__stats"
:class="{ 'is-col': isMobile }"
:direction="isMobile ? 'column' : 'row'"
gap="space-4"
>
<StatCard <StatCard
label="最近成绩" label="最近成绩"
:value="`${latest?.total ?? 0} 分`" :value="`${latest?.total ?? 0} 分`"
@ -143,7 +148,7 @@ async function confirmRemove() {
:tone="deltaTone" :tone="deltaTone"
:sub-tone="deltaTone" :sub-tone="deltaTone"
/> />
</section> </AppStack>
<!-- 成绩趋势 --> <!-- 成绩趋势 -->
<AppCard title="行测成绩趋势" icon="chart"> <AppCard title="行测成绩趋势" icon="chart">
@ -158,10 +163,10 @@ async function confirmRemove() {
<AppCard title="模块均分对比" icon="chart"> <AppCard title="模块均分对比" icon="chart">
<ul class="mock-page__modules"> <ul class="mock-page__modules">
<li v-for="m in moduleAverages" :key="m.module" class="mock-page__module"> <li v-for="m in moduleAverages" :key="m.module" class="mock-page__module">
<div class="mock-page__module-head"> <AppStack direction="row" class="mock-page__module-head">
<span class="mock-page__module-name">{{ m.module }}</span> <span class="mock-page__module-name">{{ m.module }}</span>
<strong class="mock-page__module-value">{{ m.average }}<small> </small></strong> <strong class="mock-page__module-value">{{ m.average }}<small> </small></strong>
</div> </AppStack>
<div class="mock-page__module-track"> <div class="mock-page__module-track">
<span class="mock-page__module-fill" :style="{ width: `${Math.min(100, m.average)}%` }" /> <span class="mock-page__module-fill" :style="{ width: `${Math.min(100, m.average)}%` }" />
</div> </div>
@ -186,14 +191,14 @@ async function confirmRemove() {
</div> </div>
<p v-if="r.note" class="mock-page__record-note">{{ r.note }}</p> <p v-if="r.note" class="mock-page__record-note">{{ r.note }}</p>
</div> </div>
<div class="mock-page__record-actions"> <AppStack direction="row" gap="space-1" class="mock-page__record-actions">
<AppButton variant="ghost" size="md" @click="goEdit(r)"> <AppButton variant="ghost" size="md" @click="goEdit(r)">
<AppIcon name="pen" :size="15" />编辑 <AppIcon name="pen" :size="15" />编辑
</AppButton> </AppButton>
<AppButton variant="ghost" size="md" class="is-danger" @click="removing = r"> <AppButton variant="ghost" size="md" class="is-danger" @click="removing = r">
<AppIcon name="close" :size="15" />删除 <AppIcon name="close" :size="15" />删除
</AppButton> </AppButton>
</div> </AppStack>
</li> </li>
</ul> </ul>
</AppCard> </AppCard>
@ -206,22 +211,22 @@ async function confirmRemove() {
确定要删除{{ removing.name }}{{ formatDateShort(removing.date) }}{{ removing.total }} 删除后趋势与模块分析将同步更新且不可恢复 确定要删除{{ removing.name }}{{ formatDateShort(removing.date) }}{{ removing.total }} 删除后趋势与模块分析将同步更新且不可恢复
</p> </p>
<template #footer> <template #footer>
<div class="mock-page__confirm-actions"> <AppStack direction="row" gap="space-3" class="mock-page__confirm-actions">
<AppButton variant="ghost" @click="removing = null">取消</AppButton> <AppButton variant="ghost" @click="removing = null">取消</AppButton>
<AppButton variant="danger" :loading="deleting" @click="confirmRemove"> <AppButton variant="danger" :loading="deleting" @click="confirmRemove">
{{ deleting ? '删除中…' : '确认删除' }} {{ deleting ? '删除中…' : '确认删除' }}
</AppButton> </AppButton>
</div> </AppStack>
</template> </template>
</AppModal> </AppModal>
</AppStack> </AppStack>
</template> </template>
<style scoped> <style scoped>
.mock-page__stats { /* PC 横向等宽(移动端 is-col 时自然撑满不限制高度) */
display: grid; .mock-page__stats:not(.is-col) > :deep(*) {
grid-template-columns: repeat(3, 1fr); flex: 1 1 0;
gap: var(--space-4); min-width: 0;
} }
.mock-page__sub { .mock-page__sub {
margin: 0 0 var(--space-3); margin: 0 0 var(--space-3);
@ -245,7 +250,6 @@ async function confirmRemove() {
gap: var(--space-4); gap: var(--space-4);
} }
.mock-page__module-head { .mock-page__module-head {
display: flex;
align-items: baseline; align-items: baseline;
justify-content: space-between; justify-content: space-between;
margin-bottom: var(--space-2); margin-bottom: var(--space-2);
@ -335,8 +339,6 @@ async function confirmRemove() {
color: var(--text-muted); color: var(--text-muted);
} }
.mock-page__record-actions { .mock-page__record-actions {
display: flex;
gap: var(--space-1);
flex: none; flex: none;
} }
.mock-page__record-actions .is-danger { .mock-page__record-actions .is-danger {
@ -349,15 +351,10 @@ async function confirmRemove() {
color: var(--text-primary); color: var(--text-primary);
} }
.mock-page__confirm-actions { .mock-page__confirm-actions {
display: flex;
justify-content: flex-end; justify-content: flex-end;
gap: var(--space-3);
} }
@media (max-width: 767px) { @media (max-width: 767px) {
.mock-page__stats {
grid-template-columns: 1fr;
}
.mock-page__record { .mock-page__record {
flex-direction: column; flex-direction: column;
} }