696 lines
21 KiB
Vue
696 lines
21 KiB
Vue
<script setup lang="ts">
|
||
import { computed, onMounted, ref } from 'vue'
|
||
import { plansApi, profileApi } from '../../api'
|
||
import { useRequest } from '../../composables/useRequest'
|
||
import { useResponsive } from '../../composables/useResponsive'
|
||
import { useAppStore } from '../../stores/app'
|
||
import { formatMinutes, formatDateShort, weekdayName, daysSince } from '../../utils/format'
|
||
import AppCard from '../../components/base/AppCard.vue'
|
||
import AppButton from '../../components/base/AppButton.vue'
|
||
import AppBadge from '../../components/base/AppBadge.vue'
|
||
import AppIcon from '../../components/base/AppIcon.vue'
|
||
import AppModal from '../../components/base/AppModal.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'
|
||
|
||
const { isMobile } = useResponsive()
|
||
const app = useAppStore()
|
||
|
||
const today = useRequest(() => plansApi.today())
|
||
const week = useRequest(() => plansApi.week())
|
||
const review = useRequest(() => plansApi.review())
|
||
|
||
// 新建任务表单
|
||
const showCreate = ref(false)
|
||
const saving = ref(false)
|
||
const form = ref({ title: '', type: '刷题', module: '', target: '', note: '' })
|
||
|
||
const MODULE_OPTIONS = ['言语理解', '数量关系', '判断推理', '资料分析', '常识判断'] as const
|
||
|
||
const examProfile = useRequest(() => profileApi.get(), false)
|
||
|
||
onMounted(() => {
|
||
if (!examProfile.data.value) examProfile.refresh()
|
||
})
|
||
|
||
const todayData = computed(() => today.data.value)
|
||
const weekDays = computed(() => week.data.value?.days ?? [])
|
||
const reviewData = computed(() => review.data.value)
|
||
|
||
// 顶部 4 指标(桌面)
|
||
const weekMetric = computed(() => {
|
||
const w = weekDays.value
|
||
const total = w.reduce((sum, d) => sum + d.total, 0)
|
||
const done = w.reduce((sum, d) => sum + d.done, 0)
|
||
return { total, done }
|
||
})
|
||
const weekCompletion = computed(() => {
|
||
const m = weekMetric.value
|
||
return m.total === 0 ? 0 : Math.round((m.done / m.total) * 100)
|
||
})
|
||
const studyMinutes = computed(() => reviewData.value?.studyMinutes ?? 0)
|
||
const streakDays = computed(() => reviewData.value?.streakDays ?? 0)
|
||
const weekNumber = computed(() => reviewData.value?.weekNumber ?? 1)
|
||
|
||
// 明日预告
|
||
const tomorrowTasks = computed(() => {
|
||
const target = new Date()
|
||
target.setDate(target.getDate() + 1)
|
||
const iso = `${target.getFullYear()}-${String(target.getMonth() + 1).padStart(2, '0')}-${String(target.getDate()).padStart(2, '0')}`
|
||
return weekDays.value.find((d) => d.date === iso)?.tasks ?? []
|
||
})
|
||
|
||
const planMessage = computed(() => {
|
||
const d = todayData.value
|
||
if (!d) return ''
|
||
return `${formatDateShort(d.date)} ${weekdayName(d.date)} · 完成 ${d.done}/${d.total} 项`
|
||
})
|
||
|
||
const todayProgress = computed(() => {
|
||
const d = todayData.value
|
||
if (!d || d.total === 0) return 0
|
||
return Math.round((d.done / d.total) * 100)
|
||
})
|
||
|
||
// 本周计划进度(移动)
|
||
const weekProgress = computed(() => weekCompletion.value)
|
||
const weekAnswered = computed(() => reviewData.value?.answered ?? 0)
|
||
const weekTargetAnswered = computed(() => Math.max(weekAnswered.value, Math.max(weekMetric.value.total * 10, 10)))
|
||
const weekAnsweredPercent = computed(() => {
|
||
const t = weekTargetAnswered.value
|
||
return t === 0 ? 0 : Math.min(100, Math.round((weekAnswered.value / t) * 100))
|
||
})
|
||
|
||
const taskTypeTone = (type: string) =>
|
||
(type ?? '').includes('复习') || (type ?? '').includes('复盘') ? 'warning' : 'soft'
|
||
const taskTypeLabel = (type: string) => (type && type.length > 6 ? type.slice(0, 6) : type || '任务')
|
||
|
||
async function toggle(task: { id: string; status: string }) {
|
||
try {
|
||
const result = await plansApi.toggleTask(task.id)
|
||
app.toast(result.status === 'done' ? '任务已完成' : '已恢复为待完成', 'success')
|
||
today.refresh()
|
||
week.refresh()
|
||
review.refresh()
|
||
} catch (err) {
|
||
app.toast(err instanceof Error ? err.message : '操作失败', 'error')
|
||
}
|
||
}
|
||
|
||
async function createTask() {
|
||
const f = form.value
|
||
if (!f.title.trim() || !f.target.trim()) {
|
||
app.toast('请填写任务标题与目标', 'error')
|
||
return
|
||
}
|
||
saving.value = true
|
||
try {
|
||
const date = new Date()
|
||
const iso = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
|
||
await plansApi.createTask({
|
||
date: iso,
|
||
title: f.title.trim(),
|
||
type: f.type,
|
||
...(f.module ? { module: f.module as '言语理解' | '数量关系' | '判断推理' | '资料分析' | '常识判断' } : {}),
|
||
target: f.target.trim(),
|
||
...(f.note ? { note: f.note } : {})
|
||
})
|
||
app.toast('任务已创建', 'success')
|
||
showCreate.value = false
|
||
form.value = { title: '', type: '刷题', module: '', target: '', note: '' }
|
||
today.refresh()
|
||
week.refresh()
|
||
review.refresh()
|
||
} catch (err) {
|
||
app.toast(err instanceof Error ? err.message : '创建失败', 'error')
|
||
} finally {
|
||
saving.value = false
|
||
}
|
||
}
|
||
|
||
const profileDays = computed(() => (examProfile.data.value ? daysSince(examProfile.data.value.startedAt) : 0))
|
||
</script>
|
||
|
||
<template>
|
||
<div class="plan">
|
||
<AppLoading v-if="today.loading.value" fullscreen />
|
||
<AppError
|
||
v-else-if="today.error.value"
|
||
fullscreen
|
||
title="计划加载失败"
|
||
:message="today.error.value"
|
||
retry
|
||
@retry="today.refresh"
|
||
/>
|
||
|
||
<!-- ============ 移动端今日计划 ============ -->
|
||
<div v-else-if="isMobile && todayData" class="plan-mobile">
|
||
<header class="plan-mobile__head">
|
||
<h1>今日计划</h1>
|
||
<p>{{ planMessage }}</p>
|
||
</header>
|
||
|
||
<template v-if="todayData.total > 0">
|
||
<div class="plan-list">
|
||
<AppCard
|
||
v-for="task in todayData.tasks"
|
||
:key="task.id"
|
||
class="plan-task"
|
||
:class="{ 'is-done': task.status === 'done' }"
|
||
>
|
||
<AppButton
|
||
class="plan-task__check"
|
||
:class="{ 'is-done': task.status === 'done' }"
|
||
variant="ghost"
|
||
size="md"
|
||
:aria-label="task.status === 'done' ? '标记为待完成' : '标记为完成'"
|
||
@click="toggle(task)"
|
||
>
|
||
<AppIcon v-if="task.status === 'done'" name="check" :size="16" />
|
||
</AppButton>
|
||
<div class="plan-task__body">
|
||
<p class="plan-task__title">{{ task.title }}</p>
|
||
<p class="plan-task__meta">
|
||
{{ task.type }}<template v-if="task.module"> · {{ task.module }}</template> · {{ task.target }}
|
||
</p>
|
||
</div>
|
||
<AppBadge class="plan-task__badge" :variant="taskTypeTone(task.type)">{{
|
||
taskTypeLabel(task.type)
|
||
}}</AppBadge>
|
||
</AppCard>
|
||
</div>
|
||
|
||
<div class="plan-add">
|
||
<AppButton variant="secondary" block @click="showCreate = true">
|
||
<AppIcon name="check" :size="16" />新建任务
|
||
</AppButton>
|
||
</div>
|
||
|
||
<!-- 本周计划 -->
|
||
<AppCard class="plan-week">
|
||
<div class="plan-week__head">
|
||
<h3>本周计划</h3>
|
||
<span class="plan-week__tag">第 {{ reviewData?.weekNumber ?? 1 }} 周</span>
|
||
</div>
|
||
<div class="plan-week__meta">
|
||
<span>完成 {{ weekMetric.done }} 项 / 目标 {{ weekMetric.total }} 项</span>
|
||
</div>
|
||
<div class="progress">
|
||
<div class="progress__bar" :style="{ width: `${weekProgress}%` }" />
|
||
</div>
|
||
<span class="plan-week__percent">{{ weekProgress }}%</span>
|
||
</AppCard>
|
||
|
||
<!-- 周报复盘入口 -->
|
||
<RouterLink class="plan-review-link" to="/plan/review">
|
||
<span>本周复盘 · 查看学习表现与薄弱考点</span>
|
||
<AppIcon name="chevron-right" :size="16" />
|
||
</RouterLink>
|
||
|
||
<!-- 明日预告 -->
|
||
<AppCard v-if="tomorrowTasks.length > 0" title="明日预告">
|
||
<div class="plan-tomorrow__row" v-for="task in tomorrowTasks" :key="task.id">
|
||
<span class="plan-tomorrow__dot" />
|
||
<div class="plan-tomorrow__body">
|
||
<p class="plan-tomorrow__title">{{ task.title }}</p>
|
||
<p class="plan-tomorrow__meta">{{ task.type }}<template v-if="task.module"> · {{ task.module }}</template> · {{ task.target }}</p>
|
||
</div>
|
||
</div>
|
||
</AppCard>
|
||
</template>
|
||
|
||
<!-- 今日空状态 -->
|
||
<AppEmpty
|
||
v-else
|
||
icon="calendar"
|
||
title="今天还没有任务"
|
||
description="按早/午/晚时段安排,碎片时间不浪费。"
|
||
>
|
||
<AppButton variant="primary" @click="showCreate = true">创建今日任务</AppButton>
|
||
</AppEmpty>
|
||
</div>
|
||
|
||
<!-- ============ 桌面端备考计划 ============ -->
|
||
<div v-else-if="todayData" class="plan-desktop">
|
||
<AppPageHeader title="备考计划" :subtitle="`安排阶段任务,稳步推进备考进度 · 备考第 ${profileDays} 天`" sticky-mobile>
|
||
<RouterLink class="btn-link" to="/plan/review">
|
||
<AppIcon name="chart" :size="16" />周报复盘
|
||
</RouterLink>
|
||
<AppButton variant="primary" @click="showCreate = true">
|
||
<AppIcon name="check" :size="16" />新建任务
|
||
</AppButton>
|
||
</AppPageHeader>
|
||
|
||
<!-- 4 指标卡 -->
|
||
<div class="plan-metrics">
|
||
<div class="plan-metric">
|
||
<span class="plan-metric__label">本周任务</span>
|
||
<strong class="plan-metric__value">{{ weekMetric.total }} 项</strong>
|
||
<span class="plan-metric__sub">已完成 {{ weekMetric.done }} 项</span>
|
||
</div>
|
||
<div class="plan-metric">
|
||
<span class="plan-metric__label">本周学习时长</span>
|
||
<strong class="plan-metric__value">{{ formatMinutes(studyMinutes) }}</strong>
|
||
<span class="plan-metric__sub">每日目标循序渐进</span>
|
||
</div>
|
||
<div class="plan-metric">
|
||
<span class="plan-metric__label">本周完成率</span>
|
||
<strong class="plan-metric__value">{{ weekCompletion }}%</strong>
|
||
<span class="plan-metric__sub">较上周持续提升</span>
|
||
</div>
|
||
<div class="plan-metric">
|
||
<span class="plan-metric__label">连续打卡</span>
|
||
<strong class="plan-metric__value">{{ streakDays }} 天</strong>
|
||
<span class="plan-metric__sub">本周第 {{ weekNumber }} 周</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="plan-grid">
|
||
<!-- 今日任务 -->
|
||
<AppCard class="plan-today" title="今日任务" icon="calendar">
|
||
<AppEmpty
|
||
v-if="todayData.total === 0"
|
||
icon="calendar"
|
||
title="今天还没有任务"
|
||
description="点击右上角新建今日任务,安排阶段节奏。"
|
||
/>
|
||
<ul v-else class="plan-today__list">
|
||
<li
|
||
v-for="task in todayData.tasks"
|
||
:key="task.id"
|
||
class="plan-today__item"
|
||
:class="{ 'is-done': task.status === 'done' }"
|
||
>
|
||
<AppButton
|
||
class="plan-today__check"
|
||
:class="{ 'is-done': task.status === 'done' }"
|
||
variant="ghost"
|
||
size="md"
|
||
:aria-label="task.status === 'done' ? '标记为待完成' : '标记为完成'"
|
||
@click="toggle(task)"
|
||
>
|
||
<AppIcon v-if="task.status === 'done'" name="check" :size="14" />
|
||
</AppButton>
|
||
<span class="plan-today__title">{{ task.title }} · {{ task.target }}</span>
|
||
<AppBadge v-if="task.status === 'done'" variant="success">已完成</AppBadge>
|
||
<AppBadge v-else class="plan-today__pending">待完成</AppBadge>
|
||
</li>
|
||
</ul>
|
||
</AppCard>
|
||
|
||
<!-- 本周安排 -->
|
||
<AppCard class="plan-side" title="本周安排" icon="book">
|
||
<AppEmpty v-if="weekDays.length === 0" icon="calendar" title="暂无本周任务" />
|
||
<div v-else class="plan-side__list">
|
||
<div v-for="day in weekDays" :key="day.date" class="plan-side__day">
|
||
<p class="plan-side__date">{{ formatDateShort(day.date) }} · {{ weekdayName(day.date) }}</p>
|
||
<ul v-if="day.tasks.length > 0" class="plan-side__tasks">
|
||
<li v-for="task in day.tasks" :key="task.id" :class="{ 'is-done': task.status === 'done' }">
|
||
{{ task.title }}
|
||
</li>
|
||
</ul>
|
||
<p v-else class="plan-side__blank">暂无安排</p>
|
||
</div>
|
||
</div>
|
||
</AppCard>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 新建任务弹层 -->
|
||
<AppModal v-if="showCreate" title="新建计划任务" :max-width="480" @close="showCreate = false">
|
||
<div class="form-stack">
|
||
<label class="field">
|
||
<span class="field__label">任务标题</span>
|
||
<input v-model="form.title" class="field__input" placeholder="如:言语理解专项" />
|
||
</label>
|
||
<label class="field">
|
||
<span class="field__label">任务类型</span>
|
||
<select v-model="form.type" class="field__input">
|
||
<option value="刷题">刷题</option>
|
||
<option value="阅读">阅读</option>
|
||
<option value="复习">复习</option>
|
||
<option value="模考">模考</option>
|
||
</select>
|
||
</label>
|
||
<div class="field-grid">
|
||
<label class="field">
|
||
<span class="field__label">模块(可选)</span>
|
||
<select v-model="form.module" class="field__input">
|
||
<option value="">不指定</option>
|
||
<option v-for="m in MODULE_OPTIONS" :key="m" :value="m">{{ m }}</option>
|
||
</select>
|
||
</label>
|
||
<label class="field">
|
||
<span class="field__label">目标</span>
|
||
<input v-model="form.target" class="field__input" placeholder="如:完成 20 题" />
|
||
</label>
|
||
</div>
|
||
<label class="field">
|
||
<span class="field__label">备注(可选)</span>
|
||
<textarea v-model="form.note" class="field__input field__textarea" placeholder="补充说明" />
|
||
</label>
|
||
</div>
|
||
<template #footer>
|
||
<AppButton variant="ghost" @click="showCreate = false">取消</AppButton>
|
||
<AppButton variant="primary" :disabled="saving" @click="createTask">
|
||
{{ saving ? '保存中…' : '保存任务' }}
|
||
</AppButton>
|
||
</template>
|
||
</AppModal>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.plan {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--space-5);
|
||
}
|
||
/* 指标卡 */
|
||
.plan-metrics {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: var(--card-gap-desktop);
|
||
}
|
||
.plan-metric {
|
||
background: var(--bg-card);
|
||
border: 1px solid var(--border-subtle);
|
||
border-radius: var(--radius-lg);
|
||
padding: var(--card-padding-desktop);
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--space-2);
|
||
}
|
||
.plan-metric__label {
|
||
font-size: var(--fs-label);
|
||
color: var(--text-secondary);
|
||
}
|
||
.plan-metric__value {
|
||
font-family: var(--font-num);
|
||
font-size: var(--fs-data-num);
|
||
font-weight: var(--fw-data-num);
|
||
color: var(--text-primary);
|
||
}
|
||
.plan-metric__sub {
|
||
font-size: var(--fs-label);
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.plan-grid {
|
||
display: grid;
|
||
grid-template-columns: 2fr 1fr;
|
||
gap: var(--card-gap-desktop);
|
||
align-items: start;
|
||
}
|
||
|
||
/* 今日任务列表 */
|
||
.plan-today__list {
|
||
list-style: none;
|
||
margin: 0;
|
||
padding: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--space-2);
|
||
}
|
||
.plan-today__item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--space-2);
|
||
padding: var(--space-3);
|
||
border: 1px solid var(--border-subtle);
|
||
border-radius: var(--radius-md);
|
||
}
|
||
.plan-today__item.is-done {
|
||
background: var(--success-soft);
|
||
border-color: transparent;
|
||
}
|
||
.plan-today__check {
|
||
display: grid !important;
|
||
place-items: center;
|
||
width: 20px !important;
|
||
height: 20px !important;
|
||
flex: none;
|
||
padding: 0 !important;
|
||
border-radius: 50% !important;
|
||
border: 1.5px solid var(--text-muted) !important;
|
||
background: transparent !important;
|
||
color: var(--text-muted) !important;
|
||
gap: 0 !important;
|
||
cursor: pointer;
|
||
}
|
||
.plan-today__check.is-done {
|
||
background: var(--success) !important;
|
||
border-color: var(--success) !important;
|
||
color: #fff !important;
|
||
}
|
||
.plan-today__title {
|
||
flex: 1;
|
||
font-size: var(--fs-subtitle);
|
||
color: var(--text-primary);
|
||
}
|
||
.plan-today__pending {
|
||
color: var(--text-muted);
|
||
background: var(--bg-soft);
|
||
display: inline-flex;
|
||
padding: 2px var(--space-2);
|
||
border-radius: var(--radius-sm);
|
||
font-size: var(--fs-label);
|
||
}
|
||
|
||
/* 本周安排 */
|
||
.plan-side__list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--space-4);
|
||
}
|
||
.plan-side__date {
|
||
margin: 0 0 var(--space-1);
|
||
font-size: var(--fs-label);
|
||
font-weight: var(--fw-label-bold);
|
||
color: var(--text-secondary);
|
||
}
|
||
.plan-side__tasks {
|
||
list-style: none;
|
||
margin: 0;
|
||
padding: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
.plan-side__tasks li {
|
||
font-size: var(--fs-subtitle);
|
||
color: var(--text-primary);
|
||
}
|
||
.plan-side__tasks li.is-done {
|
||
color: var(--text-muted);
|
||
text-decoration: line-through;
|
||
}
|
||
.plan-side__blank {
|
||
margin: 0;
|
||
font-size: var(--fs-label);
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
/* 移动端 */
|
||
.plan-mobile {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--space-3);
|
||
}
|
||
.plan-mobile__head h1 {
|
||
margin: 0;
|
||
font-size: 22px;
|
||
font-weight: var(--fw-h1);
|
||
color: var(--text-primary);
|
||
}
|
||
.plan-mobile__head p {
|
||
margin: var(--space-1) 0 0;
|
||
font-size: var(--fs-subtitle);
|
||
color: var(--text-secondary);
|
||
}
|
||
.plan-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--space-2);
|
||
}
|
||
.plan-task {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--space-3);
|
||
padding: var(--card-padding-mobile);
|
||
}
|
||
.plan-task.is-done {
|
||
background: var(--success-soft);
|
||
border-color: transparent;
|
||
}
|
||
.plan-task__check {
|
||
display: grid !important;
|
||
place-items: center;
|
||
width: 26px !important;
|
||
height: 26px !important;
|
||
flex: none;
|
||
padding: 0 !important;
|
||
border-radius: 50% !important;
|
||
border: 1.5px solid var(--text-muted) !important;
|
||
background: transparent !important;
|
||
color: var(--text-muted) !important;
|
||
gap: 0 !important;
|
||
cursor: pointer;
|
||
}
|
||
.plan-task__check.is-done {
|
||
background: var(--success) !important;
|
||
border-color: var(--success) !important;
|
||
color: #fff !important;
|
||
}
|
||
.plan-task__body {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
.plan-task__title {
|
||
margin: 0;
|
||
font-size: var(--fs-subtitle);
|
||
font-weight: var(--fw-card-title);
|
||
color: var(--text-primary);
|
||
}
|
||
.plan-task__meta {
|
||
margin: 2px 0 0;
|
||
font-size: var(--fs-label);
|
||
color: var(--text-secondary);
|
||
}
|
||
.plan-task__badge {
|
||
flex: none;
|
||
}
|
||
.plan-add {
|
||
margin-top: var(--space-1);
|
||
}
|
||
.plan-week {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--space-3);
|
||
}
|
||
.plan-week__head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
.plan-week__head h3 {
|
||
margin: 0;
|
||
font-size: var(--fs-card-title);
|
||
font-weight: var(--fw-card-title);
|
||
color: var(--text-primary);
|
||
}
|
||
.plan-week__tag {
|
||
font-size: var(--fs-label);
|
||
color: var(--text-muted);
|
||
}
|
||
.plan-week__meta {
|
||
font-size: var(--fs-subtitle);
|
||
color: var(--text-secondary);
|
||
}
|
||
.progress {
|
||
height: 8px;
|
||
border-radius: var(--radius-pill);
|
||
background: var(--bg-soft);
|
||
overflow: hidden;
|
||
}
|
||
.progress__bar {
|
||
height: 100%;
|
||
border-radius: var(--radius-pill);
|
||
background: var(--primary);
|
||
transition: width var(--motion-switch) var(--ease-default);
|
||
}
|
||
.plan-week__percent {
|
||
align-self: flex-end;
|
||
font-size: var(--fs-label);
|
||
color: var(--primary);
|
||
font-weight: var(--fw-label-bold);
|
||
}
|
||
.plan-review-link {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: var(--space-3) var(--space-4);
|
||
background: var(--bg-soft);
|
||
border-radius: var(--radius-lg);
|
||
color: var(--primary);
|
||
font-size: var(--fs-subtitle);
|
||
text-decoration: none;
|
||
}
|
||
.plan-tomorrow__row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--space-3);
|
||
padding: var(--space-2) 0;
|
||
}
|
||
.plan-tomorrow__dot {
|
||
width: 10px;
|
||
height: 10px;
|
||
flex: none;
|
||
border-radius: 50%;
|
||
background: var(--bg-soft);
|
||
}
|
||
.plan-tomorrow__title {
|
||
margin: 0;
|
||
font-size: var(--fs-subtitle);
|
||
font-weight: var(--fw-card-title);
|
||
color: var(--text-primary);
|
||
}
|
||
.plan-tomorrow__meta {
|
||
margin: 2px 0 0;
|
||
font-size: var(--fs-label);
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
@media (max-width: 1023px) {
|
||
.plan-metrics {
|
||
grid-template-columns: repeat(2, 1fr);
|
||
}
|
||
.plan-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
|
||
/* 弹层表单字段堆叠 */
|
||
.form-stack {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--space-4);
|
||
}
|
||
.field {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--space-2);
|
||
}
|
||
.field__label {
|
||
font-size: var(--fs-label);
|
||
font-weight: var(--fw-label-bold);
|
||
color: var(--text-secondary);
|
||
}
|
||
.field__input {
|
||
height: 40px;
|
||
padding: 0 var(--space-3);
|
||
border: 1px solid var(--border-subtle);
|
||
border-radius: var(--radius-btn);
|
||
background: var(--bg-card);
|
||
color: var(--text-primary);
|
||
font-size: var(--fs-subtitle);
|
||
}
|
||
.field__input:focus {
|
||
outline: none;
|
||
border-color: var(--primary);
|
||
}
|
||
.field__textarea {
|
||
height: auto;
|
||
min-height: 72px;
|
||
padding: var(--space-3);
|
||
resize: vertical;
|
||
}
|
||
.field-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: var(--space-3);
|
||
}
|
||
</style>
|