This commit is contained in:
Zyronon
2026-01-04 19:41:29 +08:00
committed by GitHub
parent d70d529426
commit 4fd3e51961
11 changed files with 4448 additions and 303 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -18,7 +18,7 @@
--article-width: 50vw;
--article-toolbar-width: 50vw;
--article-panel-width: 20rem;
--article-panel-margin-left: calc(50% + var(--article-width) / 2 + 1rem);
--article-panel-margin-left: calc(50vw + var(--article-width) / 2 + var(--aside-width) / 2 + 1rem);
--toolbar-width: 50rem;
--panel-width: 24rem;
@@ -26,7 +26,7 @@
--modal-padding: 1.3rem;
--space: 0.9rem;
--stat-gap: 1rem;
--word-panel-margin-left: calc(50% + var(--toolbar-width) / 2 + 1rem);
--word-panel-margin-left: calc(50vw + var(--aside-width) / 2 + var(--toolbar-width) / 2 + 1rem);
--anim-time: 0.5s;
--color-input-color: black;
@@ -374,13 +374,8 @@ a {
@apply flex-col;
}
.card {
@apply rounded-xl p-4 mb-8 shadow-lg box-border relative;
background: var(--color-second);
}
.card-white {
@extend .card;
@apply card;
background: var(--color-card-bg);
}

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import {useSettingStore} from "@/stores/setting.ts";
import { useSettingStore } from '@/stores/setting.ts'
const settingStore = useSettingStore()
defineProps<{
@@ -8,12 +8,16 @@ defineProps<{
</script>
<template>
<div class="flex justify-center relative h-screen"
:class="!settingStore.showToolbar && 'footer-hide'">
<div class="flex justify-center relative" :class="!settingStore.showToolbar && 'footer-hide'">
<div class="wrap">
<slot name="practice"></slot>
</div>
<div class="panel-wrap" :style="{left:panelLeft}" :class="{'has-panel': settingStore.showPanel}" @click.self="settingStore.showPanel = false">
<div
class="panel-wrap"
:style="{ left: panelLeft }"
:class="{ 'has-panel': settingStore.showPanel }"
@click.self="settingStore.showPanel = false"
>
<slot name="panel"></slot>
</div>
<div class="footer-wrap">
@@ -23,17 +27,11 @@ defineProps<{
</template>
<style scoped lang="scss">
.wrap {
transition: all var(--anim-time);
height: calc(100vh - 8rem);
}
.footer-hide {
.wrap {
height: calc(100vh - 3rem) !important;
}
.footer-wrap {
bottom: -6rem;
}
@@ -41,14 +39,14 @@ defineProps<{
.footer-wrap {
position: fixed;
bottom: calc(0.8rem + env(safe-area-inset-bottom, 0px));
bottom: calc(env(safe-area-inset-bottom, 0px));
transition: all var(--anim-time);
z-index: 999;
}
.panel-wrap {
position: absolute;
top: .8rem;
position: fixed;
top: 0.8rem;
z-index: 1;
height: calc(100vh - 1.8rem);
}
@@ -61,24 +59,24 @@ defineProps<{
padding: 0 1rem;
box-sizing: border-box;
}
.footer-hide {
.wrap {
height: calc(100vh - 2rem) !important;
}
.footer-wrap {
bottom: calc(-10rem + env(safe-area-inset-bottom, 0px));
}
}
.footer-wrap {
bottom: calc(0.5rem + env(safe-area-inset-bottom, 0px));
left: 0.5rem;
right: 0.5rem;
width: auto;
}
.panel-wrap {
position: fixed;
top: 0;
@@ -92,10 +90,10 @@ defineProps<{
justify-content: center;
padding: 1rem;
box-sizing: border-box;
// 当面板未显示时,禁用指针事件
pointer-events: none;
// 只有当面板显示时才添加背景蒙版并启用指针事件
&.has-panel {
background: rgba(0, 0, 0, 0.5);
@@ -110,19 +108,19 @@ defineProps<{
height: calc(100vh - 5rem);
padding: 0 0.5rem;
}
.footer-hide {
.wrap {
height: calc(100vh - 1.5rem) !important;
}
}
.footer-wrap {
bottom: calc(0.3rem + env(safe-area-inset-bottom, 0px));
left: 0.3rem;
right: 0.3rem;
}
.panel-wrap {
padding: 0.5rem;
left: 0 !important;

View File

@@ -4,7 +4,7 @@ import BackIcon from '@/components/BackIcon.vue'
import Empty from '@/components/Empty.vue'
import ArticleList from '@/components/list/ArticleList.vue'
import { useBaseStore } from '@/stores/base.ts'
import { Article, Dict, DictId, DictType } from '@/types/types.ts'
import { Article, Dict, DictId, DictType, ShortcutKey } from '@/types/types.ts'
import { useRuntimeStore } from '@/stores/runtime.ts'
import BaseButton from '@/components/BaseButton.vue'
import { useRoute, useRouter } from 'vue-router'
@@ -19,6 +19,7 @@ import { useSettingStore } from '@/stores/setting.ts'
import { useFetch } from '@vueuse/core'
import { AppEnv, DICT_LIST } from '@/config/env.ts'
import { detail } from '@/apis'
import BaseIcon from '@/components/BaseIcon.vue'
const runtimeStore = useRuntimeStore()
const settingStore = useSettingStore()
@@ -101,6 +102,7 @@ async function init() {
}
}
}
selectArticle = runtimeStore.editDict.articles[0]
loading = false
}
}
@@ -174,11 +176,17 @@ const list = $computed(() => {
}),
].concat(runtimeStore.editDict.articles)
})
let showAudio = $ref(false)
let showTranslate = $ref(true)
</script>
<template>
<div class="bg-second h-screen overflow-hidden">
<div class="mb-0 flex h-full p-space box-border flex-col" v-if="showBookDetail">
<div class="center h-screen overflow-hidden">
<div
class="mb-0 flex p-space box-border flex-col bg-second w-full 3xl:w-7/10 2xl:w-8/10 xl:w-full 2xl:card 2xl:h-[97vh] h-full"
v-if="showBookDetail"
>
<div class="dict-header flex justify-between items-center relative">
<BackIcon class="dict-back z-2" />
<div class="dict-title absolute text-2xl text-align-center w-full">{{ runtimeStore.editDict.name }}</div>
@@ -191,9 +199,8 @@ const list = $computed(() => {
<BaseButton :loading="studyLoading || loading" @click="addMyStudyList">学习</BaseButton>
</div>
</div>
<div class="flex flex-1 overflow-hidden mt-3">
<div class="w-70 overflow-auto">
<div class="3xl:w-80 2xl:w-60 xl:w-55 lg:w-50 overflow-auto">
<ArticleList
:show-desc="true"
v-if="runtimeStore.editDict.length"
@@ -217,11 +224,49 @@ const list = $computed(() => {
<div class="text-lg">介绍{{ runtimeStore.editDict.description }}</div>
</div>
<div class="text-base" v-if="totalSpend">总学习时长{{ totalSpend }}</div>
<div class="line my-3"></div>
</template>
<template v-else>
<div class="">
<div class="text-3xl flex justify-between items-center relative">
<span>
<span class="font-bold">{{ selectArticle.title }}</span>
<span class="ml-6 text-2xl" v-if="showTranslate">{{ selectArticle.titleTranslate }}</span>
</span>
<div>
<BaseIcon title="显示音频" @click="showAudio = !showAudio">
<IconBxVolumeFull />
</BaseIcon>
<BaseIcon :title="`开关释义显示`" @click="showTranslate = !showTranslate">
<IconFluentTranslate16Regular v-if="showTranslate" />
<IconFluentTranslateOff16Regular v-else />
</BaseIcon>
</div>
</div>
<ArticleAudio
v-if="showAudio"
class="mt-4"
:article="selectArticle"
:autoplay="settingStore.articleAutoPlayNext"
@ended="next"
/>
<div class="mb-4 mt-4 text-2xl" v-if="selectArticle?.question?.text">
Question: {{ selectArticle?.question?.text }}
</div>
<div class="text-2xl line-height-normal en-article-family" v-if="selectArticle.text">
<div class="my-6" v-for="t in selectArticle.text.split('\n\n')">{{ t }}</div>
<div class="text-right italic mb-5">{{ selectArticle?.quote?.text }}</div>
</div>
</div>
<div class="line my-10"></div>
<div class="mt-6" v-if="showTranslate">
<div class="text-xl line-height-normal" v-if="selectArticle.textTranslate">
<div class="my-5" v-for="t in selectArticle.textTranslate.split('\n\n')">{{ t }}</div>
<div class="text-right italic mb-5">{{ selectArticle?.quote?.translate }}</div>
</div>
<Empty v-else />
</div>
<div class="font-family text-base mb-4 pr-2" v-if="currentPractice.length">
<div class="line my-10"></div>
<div class="text-2xl font-bold">学习记录</div>
<div class="mt-1 mb-3">总学习时长{{ msToHourMinute(total(currentPractice, 'spend')) }}</div>
<div
@@ -232,35 +277,6 @@ const list = $computed(() => {
<span>{{ msToHourMinute(i.spend) }}</span>
</div>
</div>
<div class="en-article-family">
<div class="text-3xl">
<span class="">{{ selectArticle.title }}</span>
<span class="ml-6 text-2xl">{{ selectArticle.titleTranslate }}</span>
</div>
<div class="mb-10 mt-6 text-2xl" v-if="selectArticle?.question?.text">
<div class="flex justify-between items-center">
<span class="">First Listen and then answer the following question.</span>
<ArticleAudio
class="w-100!"
:article="selectArticle"
:autoplay="settingStore.articleAutoPlayNext"
@ended="next"
/>
</div>
<div class="decoration-dashed underline">{{ selectArticle?.question?.text }}</div>
</div>
<div class="text-2xl line-height-normal" v-if="selectArticle.text">
<div class="my-5" v-for="t in selectArticle.text.split('\n\n')">{{ t }}</div>
<div class="text-right italic mb-5">{{ selectArticle?.quote?.text }}</div>
</div>
</div>
<div class="mt-13">
<div class="text-xl line-height-normal" v-if="selectArticle.textTranslate">
<div class="my-5" v-for="t in selectArticle.textTranslate.split('\n\n')">{{ t }}</div>
<div class="text-right italic mb-5">{{ selectArticle?.quote?.translate }}</div>
</div>
<Empty v-else />
</div>
</template>
</div>
<Empty v-else />

View File

@@ -11,11 +11,7 @@ import SettingDialog from '@/components/setting/SettingDialog.vue'
import { AppEnv, DICT_LIST, LIB_JS_URL, TourConfig } from '@/config/env.ts'
import { genArticleSectionData, usePlaySentenceAudio } from '@/hooks/article.ts'
import { useArticleOptions } from '@/hooks/dict.ts'
import {
useDisableEventListener,
useOnKeyboardEventListener,
useStartKeyboardEventListener,
} from '@/hooks/event.ts'
import { useDisableEventListener, useOnKeyboardEventListener, useStartKeyboardEventListener } from '@/hooks/event.ts'
import useTheme from '@/hooks/theme.ts'
import ArticleAudio from '@/pages/article/components/ArticleAudio.vue'
import EditSingleArticleModal from '@/pages/article/components/EditSingleArticleModal.vue'
@@ -36,16 +32,7 @@ import {
Statistics,
Word,
} from '@/types/types.ts'
import {
_getDictDataByUrl,
_nextTick,
cloneDeep,
isMobile,
loadJsLib,
msToMinute,
resourceWrap,
total,
} from '@/utils'
import { _getDictDataByUrl, _nextTick, cloneDeep, isMobile, loadJsLib, msToMinute, resourceWrap, total } from '@/utils'
import { getPracticeArticleCache, setPracticeArticleCache } from '@/utils/cache.ts'
import { emitter, EventKey, useEvents } from '@/utils/eventBus.ts'
import { computed, onMounted, onUnmounted, provide, watch } from 'vue'
@@ -382,10 +369,7 @@ function wrong(word: Word) {
}
function nextWord(word: ArticleWord) {
if (
!store.allIgnoreWords.includes(word.word.toLowerCase()) &&
word.type === PracticeArticleWordType.Word
) {
if (!store.allIgnoreWords.includes(word.word.toLowerCase()) && word.type === PracticeArticleWordType.Word) {
statStore.inputWordNumber++
}
}
@@ -506,10 +490,7 @@ provide('currentPractice', currentPractice)
<template v-slot:panel>
<Panel :style="{ width: 'var(--article-panel-width)' }">
<template v-slot:title>
<span
>{{ store.sbook.name }} ({{ store.sbook.lastLearnIndex + 1 }} /
{{ articleData.list.length }})</span
>
<span>{{ store.sbook.name }} ({{ store.sbook.lastLearnIndex + 1 }} / {{ articleData.list.length }})</span>
</template>
<div class="panel-page-item pl-4">
<ArticleList
@@ -525,28 +506,28 @@ provide('currentPractice', currentPractice)
</Panel>
</template>
<template v-slot:footer>
<div class="footer">
<Tooltip :title="settingStore.showToolbar ? '收起' : '展开'">
<IconFluentChevronLeft20Filled
@click="settingStore.showToolbar = !settingStore.showToolbar"
class="arrow"
:class="!settingStore.showToolbar && 'down'"
color="#999"
/>
</Tooltip>
<div class="bottom">
<div class="footer pb-3">
<div class="center h-10">
<Tooltip :title="settingStore.showToolbar ? '收起' : '展开'">
<IconFluentChevronLeft20Filled
@click="settingStore.showToolbar = !settingStore.showToolbar"
:class="!settingStore.showToolbar && 'down'"
color="#999"
class="arrow"
/>
</Tooltip>
</div>
<div class="bottom ">
<div class="flex justify-between items-center gap-2">
<div class="stat">
<div class="row">
<div class="num">
{{ currentPractice.length }}/{{ msToMinute(total(currentPractice, 'spend')) }}
</div>
<div class="num">{{ currentPractice.length }}/{{ msToMinute(total(currentPractice, 'spend')) }}</div>
<div class="line"></div>
<div class="name">记录</div>
</div>
<div class="row">
<!-- <div class="num">{{statStore.spend }}分钟</div>-->
<div class="num">{{ Math.floor(statStore.spend / 1000 / 60) }}分钟</div>
<!-- <div class="num">{{statStore.spend }}分钟</div>-->
<div class="num">{{ Math.floor(statStore.spend / 1000 / 60) }}分钟</div>
<div class="line"></div>
<div class="name">时间</div>
</div>
@@ -557,9 +538,7 @@ provide('currentPractice', currentPractice)
<IconFluentQuestionCircle20Regular width="18" />
<template #reference>
<div>
统计词数{{
settingStore.ignoreSimpleWord ? '不包含' : '包含'
}}简单词不包含已掌握
统计词数{{ settingStore.ignoreSimpleWord ? '不包含' : '包含' }}简单词不包含已掌握
<div>简单词可在设置 -> 练习设置 -> 简单词过滤中修改</div>
</div>
</template>
@@ -581,10 +560,7 @@ provide('currentPractice', currentPractice)
<div class="flex gap-2 center">
<SettingDialog type="article" />
<BaseIcon
:title="`下一句(${settingStore.shortcutKeyMap[ShortcutKey.Next]})`"
@click="skip"
>
<BaseIcon :title="`下一句(${settingStore.shortcutKeyMap[ShortcutKey.Next]})`" @click="skip">
<IconFluentArrowBounce20Regular class="transform-rotate-180" />
</BaseIcon>
<BaseIcon
@@ -636,6 +612,7 @@ provide('currentPractice', currentPractice)
<style scoped lang="scss">
.footer {
width: var(--article-toolbar-width);
@apply bg-primary;
.bottom {
@apply relative w-full box-border rounded-lg bg-second shadow-lg z-2;
@@ -667,9 +644,6 @@ provide('currentPractice', currentPractice)
}
.arrow {
position: absolute;
top: -40%;
left: 50%;
cursor: pointer;
transition: all 0.5s;
transform: rotate(-90deg);
@@ -677,7 +651,6 @@ provide('currentPractice', currentPractice)
font-size: 1.2rem;
&.down {
top: -70%;
transform: rotate(90deg);
}
}

View File

@@ -846,11 +846,10 @@ $translate-lh: 3.2;
$article-lh: 2.4;
.typing-article {
height: 100%;
overflow: auto;
color: var(--color-article);
width: var(--article-width);
font-size: 1.6rem;
margin-bottom: 10rem;
header {
word-wrap: break-word;

View File

@@ -7,6 +7,7 @@ import useTheme from '@/hooks/theme.ts'
import BaseIcon from '@/components/BaseIcon.vue'
import { useRuntimeStore } from '@/stores/runtime.ts'
import { jump2Feedback } from '@/utils'
import { watch } from 'vue'
const settingStore = useSettingStore()
const runtimeStore = useRuntimeStore()
@@ -17,13 +18,19 @@ const { toggleTheme, getTheme } = useTheme()
function goHome() {
window.location.href = '/'
}
watch(
() => settingStore.sideExpand,
n => {
document.documentElement.style.setProperty('--aside-width', n ? '12rem' : '4.5rem')
},{immediate: true}
)
</script>
<template>
<div class="layout anim">
<!-- 第一个aside 占位用-->
<div class="aside space" :class="{ expand: settingStore.sideExpand }"></div>
<div class="aside anim fixed" :class="{ expand: settingStore.sideExpand }">
<div class="aside space"></div>
<div class="aside anim fixed">
<div class="top">
<Logo v-if="settingStore.sideExpand" />
<div class="row" @click="goHome">
@@ -42,11 +49,7 @@ function goHome() {
<div class="row" @click="router.push('/setting')">
<IconFluentSettings20Regular />
<span v-if="settingStore.sideExpand">设置</span>
<div
class="red-point"
:class="!settingStore.sideExpand && 'top-1 right-0'"
v-if="runtimeStore.isNew"
></div>
<div class="red-point" :class="!settingStore.sideExpand && 'top-1 right-0'" v-if="runtimeStore.isNew"></div>
</div>
<div class="row" @click="router.push('/feedback')">
<IconFluentCommentEdit20Regular />
@@ -88,36 +91,21 @@ function goHome() {
<IconFluentHome20Regular />
<span>主页</span>
</div>
<div
class="nav-item"
@click="router.push('/words')"
:class="{ active: $route.path.includes('/words') }"
>
<div class="nav-item" @click="router.push('/words')" :class="{ active: $route.path.includes('/words') }">
<IconFluentTextUnderlineDouble20Regular />
<span>单词</span>
</div>
<div
class="nav-item"
@click="router.push('/articles')"
:class="{ active: $route.path.includes('/articles') }"
>
<div class="nav-item" @click="router.push('/articles')" :class="{ active: $route.path.includes('/articles') }">
<IconFluentBookLetter20Regular />
<span>文章</span>
</div>
<div
class="nav-item"
@click="router.push('/setting')"
:class="{ active: $route.path === '/setting' }"
>
<div class="nav-item" @click="router.push('/setting')" :class="{ active: $route.path === '/setting' }">
<IconFluentSettings20Regular />
<span>设置</span>
<div class="red-point" v-if="runtimeStore.isNew"></div>
</div>
</div>
<div
class="nav-toggle"
@click="settingStore.mobileNavCollapsed = !settingStore.mobileNavCollapsed"
>
<div class="nav-toggle" @click="settingStore.mobileNavCollapsed = !settingStore.mobileNavCollapsed">
<IconFluentChevronDown20Filled v-if="!settingStore.mobileNavCollapsed" />
<IconFluentChevronUp20Filled v-else />
</div>
@@ -146,7 +134,7 @@ function goHome() {
flex-direction: column;
justify-content: space-between;
box-shadow: rgb(0 0 0 / 3%) 0px 0px 12px 0px;
width: 4.5rem;
width: var(--aside-width);
z-index: 2;
.row {
@@ -167,10 +155,6 @@ function goHome() {
font-size: 1.3rem !important;
}
}
&.expand {
width: var(--aside-width);
}
}
// 移动端顶部菜单栏

View File

@@ -466,13 +466,13 @@ async function next(isTyping: boolean = true) {
}
} else if (settingStore.wordPracticeMode === WordPracticeMode.ListenOnly) {
if (statStore.stage === WordPracticeStage.ListenNewWord) {
nextStage(taskWords.review, '开始听写昨日',true)
nextStage(taskWords.review, '开始听写昨日', true)
} else if (statStore.stage === WordPracticeStage.ListenReview) {
nextStage(taskWords.write, '开始听写之前')
} else if (statStore.stage === WordPracticeStage.ListenReviewAll) complete()
} else if (settingStore.wordPracticeMode === WordPracticeMode.DictationOnly) {
if (statStore.stage === WordPracticeStage.DictationNewWord) {
nextStage(taskWords.review, '开始默写昨日',true)
nextStage(taskWords.review, '开始默写昨日', true)
} else if (statStore.stage === WordPracticeStage.DictationReview) {
nextStage(taskWords.write, '开始默写之前')
} else if (statStore.stage === WordPracticeStage.DictationReviewAll) complete()
@@ -486,13 +486,13 @@ async function next(isTyping: boolean = true) {
if (statStore.stage === WordPracticeStage.Shuffle) complete()
} else if (settingStore.wordPracticeMode === WordPracticeMode.Review) {
if (statStore.stage === WordPracticeStage.IdentifyReview) {
nextStage(shuffle(taskWords.review), '开始听写昨日',true)
nextStage(shuffle(taskWords.review), '开始听写昨日', true)
} else if (statStore.stage === WordPracticeStage.ListenReview) {
nextStage(shuffle(taskWords.review), '开始默写昨日')
} else if (statStore.stage === WordPracticeStage.DictationReview) {
nextStage(taskWords.write, '开始自测之前')
} else if (statStore.stage === WordPracticeStage.IdentifyReviewAll) {
nextStage(shuffle(taskWords.write), '开始听写之前',true)
nextStage(shuffle(taskWords.write), '开始听写之前', true)
} else if (statStore.stage === WordPracticeStage.ListenReviewAll) {
nextStage(shuffle(taskWords.write), '开始默写之前')
} else if (statStore.stage === WordPracticeStage.DictationReviewAll) complete()
@@ -732,8 +732,12 @@ useEvents([
<template>
<PracticeLayout v-loading="loading" panelLeft="var(--word-panel-margin-left)">
<template v-slot:practice>
<div class="practice-word">
<div class="absolute z-1 top-4 w-full" v-if="settingStore.showNearWord">
<div class="practice-word mb-50">
<div
class="fixed z-1 top-4 w-full"
style="left: calc(50vw + var(--aside-width) / 2 - var(--toolbar-width) / 2);width:var(--toolbar-width)"
v-if="settingStore.showNearWord"
>
<div class="center gap-2 cursor-pointer float-left" @click="prev" v-if="prevWord">
<IconFluentArrowLeft16Regular class="arrow" width="22" />
<Tooltip :title="`上一个(${settingStore.shortcutKeyMap[ShortcutKey.Previous]})`">
@@ -752,7 +756,6 @@ useEvents([
<TypeWord ref="typingRef" :word="word" @wrong="onTypeWrong" @complete="next" @know="onWordKnow" />
</div>
</template>
{{ Math.ceil(store.sdict.length / store.sdict.perDayStudyNumber) }}
<template v-slot:panel>
<Panel>
<template v-slot:title>

View File

@@ -343,7 +343,6 @@ const stages = $computed(() => {
.bottom {
@apply relative w-full box-border rounded-xl bg-second shadow-lg z-10;
padding: 0.2rem var(--space) calc(0.4rem + env(safe-area-inset-bottom, 0px)) var(--space);
border: 1px solid var(--color-item-border);
.stat {
@apply flex justify-around gap-[var(--stat-gap)] mt-2;

View File

@@ -729,11 +729,10 @@ useEvents([
.typing-word {
width: 100%;
flex: 1;
overflow: auto;
//overflow: auto;
word-break: break-word;
position: relative;
color: var(--color-font-2);
padding-bottom: 8rem;
.phonetic,
.translate {

View File

@@ -1,5 +1,5 @@
// uno.config.ts
import {defineConfig, presetWind3} from 'unocss'
import { defineConfig, presetWind3 } from 'unocss'
export default defineConfig({
shortcuts: {
@@ -20,21 +20,20 @@ export default defineConfig({
'py-space': 'py-[var(--space)]',
'border-item': 'border-[var(--color-item-border)]',
'border-item-solid': 'border-1 border-solid border-[var(--color-item-border)]',
card: 'rounded-xl p-4 mb-8 shadow-lg box-border relative bg-second',
},
presets: [
presetWind3(),
],
presets: [presetWind3()],
// 自定义断点
theme: {
breakpoints: {
'xs': '480px', // 自定义小断点
'sm': '640px',
'md': '768px',
'lg': '1024px',
'xl': '1280px',
xs: '480px', // 自定义小断点
sm: '640px',
md: '768px',
lg: '1024px',
xl: '1280px',
'2xl': '1536px',
'3xl': '1920px',
'4k': '2560px',
}
},
},
})