This commit is contained in:
Zyronon
2025-12-17 10:29:07 +08:00
parent 6eb15e180a
commit f23cc79a98
12 changed files with 173 additions and 156 deletions

3
components.d.ts vendored
View File

@@ -10,6 +10,7 @@ declare module 'vue' {
export interface GlobalComponents {
About: typeof import('./src/components/About.vue')['default']
ArticleList: typeof import('./src/components/list/ArticleList.vue')['default']
ArticleSettting: typeof import('./src/components/setting/ArticleSettting.vue')['default']
Audio: typeof import('./src/components/base/Audio.vue')['default']
BackIcon: typeof import('./src/components/BackIcon.vue')['default']
BaseButton: typeof import('./src/components/BaseButton.vue')['default']
@@ -23,6 +24,7 @@ declare module 'vue' {
ChannelIcons: typeof import('./src/components/ChannelIcons/ChannelIcons.vue')['default']
Checkbox: typeof import('./src/components/base/checkbox/Checkbox.vue')['default']
Close: typeof import('./src/components/icon/Close.vue')['default']
CommonSetting: typeof import('./src/components/setting/CommonSetting.vue')['default']
ConflictNotice: typeof import('./src/components/ConflictNotice.vue')['default']
DeleteIcon: typeof import('./src/components/icon/DeleteIcon.vue')['default']
Dialog: typeof import('./src/components/dialog/Dialog.vue')['default']
@@ -159,5 +161,6 @@ declare module 'vue' {
WeChat: typeof import('./src/components/ChannelIcons/WeChat.vue')['default']
WordItem: typeof import('./src/components/WordItem.vue')['default']
WordList: typeof import('./src/components/list/WordList.vue')['default']
WordSetting: typeof import('./src/components/setting/WordSetting.vue')['default']
}
}

View File

@@ -28,6 +28,7 @@
--toolbar-width: 50rem;
--panel-width: 24rem;
--modal-padding: 1.3rem;
--space: 0.9rem;
--stat-gap: 1rem;
--shadow: rgba(0, 0, 0, 0.08) 0px 4px 12px;
@@ -245,7 +246,7 @@ body {
display: flex;
flex-direction: column;
&>.page-content {
& > .page-content {
padding: 10rem;
box-sizing: border-box;
overflow: auto;
@@ -532,6 +533,6 @@ a {
.btn-no-margin {
.base-button + .base-button {
margin-left: 0!important;
margin-left: 0 !important;
}
}

View File

@@ -1,6 +1,6 @@
<script setup lang="tsx">
import { nextTick, onMounted, useSlots } from "vue";
import { nextTick, useSlots } from "vue";
import { Sort } from "@/types/types.ts";
import MiniDialog from "@/components/dialog/MiniDialog.vue";
import BaseIcon from "@/components/BaseIcon.vue";
@@ -16,6 +16,7 @@ import Dialog from "@/components/dialog/Dialog.vue";
import BaseInput from "@/components/base/BaseInput.vue";
import { Host } from "@/config/env.ts";
let list = defineModel('list')
const props = withDefaults(defineProps<{
loading?: boolean
@@ -26,7 +27,6 @@ const props = withDefaults(defineProps<{
del?: Function
batchDel?: Function
add?: Function
request?: Function
total: number
}>(), {
loading: true,
@@ -36,19 +36,16 @@ const props = withDefaults(defineProps<{
importLoading: false,
del: () => void 0,
add: () => void 0,
request: () => void 0,
batchDel: () => void 0
})
const emit = defineEmits<{
add: []
click: [val: {
item: any,
index: number
}],
importData: [e: Event]
exportData: []
sort: [type: Sort,pageNo: number,pageSize: number]
}>()
let listRef: any = $ref()
@@ -71,15 +68,22 @@ function scrollToItem(index: number) {
})
}
let pageNo = $ref(1)
let pageSize = $ref(50)
let currentList = $computed(() => {
if (searchKey) {
return list.value.filter(v => v.word.includes(searchKey))
}
if (!props.showPagination) return list.value
return list.value.slice((pageNo - 1) * pageSize, (pageNo - 1) * pageSize + pageSize)
})
let selectIds = $ref([])
let selectAll = $computed(() => {
return !!selectIds.length
})
function toggleSelect(item) {
let rIndex = selectIds.findIndex(v => v === item.id)
if (rIndex > -1) {
@@ -93,10 +97,11 @@ function toggleSelectAll() {
if (selectAll) {
selectIds = []
} else {
selectIds = list2.map(v => v.id)
selectIds = currentList.map(v => v.id)
}
}
let searchKey = $ref('')
let showSortDialog = $ref(false)
let showSearchInput = $ref(false)
let showImportDialog = $ref(false)
@@ -104,10 +109,13 @@ let showImportDialog = $ref(false)
const closeImportDialog = () => showImportDialog = false
function sort(type: Sort) {
if ([Sort.reverse, Sort.random].includes(type)) {
emit('sort', type,params.pageNo,params.pageSize)
}else{
emit('sort', type,1,params.total)
if (type === Sort.reverse) {
Toast.success('已翻转排序')
list.value = reverse(cloneDeep(list.value))
}
if (type === Sort.random) {
Toast.success('已随机排序')
list.value = shuffle(cloneDeep(list.value))
}
showSortDialog = false
}
@@ -118,8 +126,7 @@ function handleBatchDel() {
}
function handlePageNo(e) {
params.pageNo = e
getData()
pageNo = e
scrollToTop()
}
@@ -128,52 +135,8 @@ const s = useSlots()
defineExpose({
scrollToBottom,
scrollToItem,
closeImportDialog,
getData
closeImportDialog
})
let list2 = $ref([])
let loading2 = $ref(false)
let params = $ref({
pageNo: 1,
pageSize: 50,
total: 0,
sortType: null,
searchKey: ''
})
function search(key: string) {
console.log('key',key)
if(!params.searchKey) {
params.pageNo = 1
}
params.searchKey = key
getData()
}
function cancelSearch() {
params.searchKey = ''
showSearchInput = false
getData()
}
async function getData() {
loading2 = true
console.log('params',params);
let {list, total} = await props.request(params)
console.log('list',list)
list2 = list
params.total = total
loading2 = false
}
onMounted(async () => {
getData()
})
defineRender(
() => {
const d = (item) => <Checkbox
@@ -190,8 +153,8 @@ defineRender(
<div class="flex gap-4">
<BaseInput
clearable
modelValue={params.searchKey}
onUpdate:modelValue={debounce(e => search(e), 500)}
modelValue={searchKey}
onUpdate:modelValue={debounce(e => searchKey = e)}
class="flex-1"
autofocus>
{{
@@ -200,17 +163,17 @@ defineRender(
/>
}}
</BaseInput>
<BaseButton onClick={cancelSearch}>取消</BaseButton>
<BaseButton onClick={() => (showSearchInput = false, searchKey = '')}>取消</BaseButton>
</div>
) : (
<div class="flex justify-between">
<div class="flex gap-2 items-center">
<Checkbox
disabled={!list2.length}
disabled={!currentList.length}
onChange={() => toggleSelectAll()}
modelValue={selectAll}
size="large"/>
<span>{selectIds.length} / {params.total}</span>
<span>{selectIds.length} / {list.value.length}</span>
</div>
<div class="flex gap-2 relative">
@@ -238,19 +201,19 @@ defineRender(
{props.exportLoading ? <IconEosIconsLoading/> : <IconPhExportLight/>}
</BaseIcon>
<BaseIcon
onClick={() => emit('add')}
onClick={props.add}
title="添加单词">
<IconFluentAdd20Regular/>
</BaseIcon>
<BaseIcon
disabled={!list2.length}
disabled={!currentList.length}
title="改变顺序"
onClick={() => showSortDialog = !showSortDialog}
>
<IconFluentArrowSort20Regular/>
</BaseIcon>
<BaseIcon
disabled={!list2.length}
disabled={!currentList.length}
onClick={() => showSearchInput = !showSearchInput}
title="搜索">
<IconFluentSearch20Regular/>
@@ -263,12 +226,10 @@ defineRender(
<div class="mini-row-title">
列表顺序设置
</div>
<div class="flex flex-col gap2 btn-no-margin">
<BaseButton onClick={() => sort(Sort.reverse)}>翻转当前页</BaseButton>
<BaseButton onClick={() => sort(Sort.reverseAll)}>翻转所有</BaseButton>
<div class="line"></div>
<BaseButton onClick={() => sort(Sort.random)}>随机当前页</BaseButton>
<BaseButton onClick={() => sort(Sort.randomAll)}>随机所有</BaseButton>
<div class="mini-row">
<BaseButton size="small" onClick={() => sort(Sort.reverse)}>翻转
</BaseButton>
<BaseButton size="small" onClick={() => sort(Sort.random)}>随机</BaseButton>
</div>
</MiniDialog>
</div>
@@ -278,15 +239,15 @@ defineRender(
</div>
}
{
loading2 ?
props.loading ?
<div class="h-full w-full center text-4xl">
<IconEosIconsLoading color="gray"/>
</div>
: list2.length ? (
: currentList.length ? (
<>
<div class="flex-1 overflow-auto"
ref={e => listRef = e}>
{list2.map((item, index) => {
{currentList.map((item, index) => {
return (
<div class="list-item-wrapper"
key={item.word}
@@ -299,13 +260,13 @@ defineRender(
{
props.showPagination && <div class="flex justify-end">
<Pagination
currentPage={params.pageNo}
currentPage={pageNo}
onUpdate:current-page={handlePageNo}
pageSize={params.pageSize}
onUpdate:page-size={(e) => params.pageSize = e}
pageSize={pageSize}
onUpdate:page-size={(e) => pageSize = e}
pageSizes={[20, 50, 100, 200]}
layout="prev, pager, next, total"
total={params.total}/>
layout="prev, pager, next"
total={props.total}/>
</div>
}
</>
@@ -347,4 +308,6 @@ defineRender(
}
)
</script>
<style scoped lang="scss"></style>
<style scoped lang="scss">
</style>

View File

@@ -1,6 +1,6 @@
<script setup lang="tsx">
import { nextTick, useSlots } from "vue";
import { nextTick, onMounted, useSlots } from "vue";
import { Sort } from "@/types/types.ts";
import MiniDialog from "@/components/dialog/MiniDialog.vue";
import BaseIcon from "@/components/BaseIcon.vue";
@@ -16,7 +16,6 @@ import Dialog from "@/components/dialog/Dialog.vue";
import BaseInput from "@/components/base/BaseInput.vue";
import { Host } from "@/config/env.ts";
let list = defineModel('list')
const props = withDefaults(defineProps<{
loading?: boolean
@@ -27,6 +26,7 @@ const props = withDefaults(defineProps<{
del?: Function
batchDel?: Function
add?: Function
request?: Function
total: number
}>(), {
loading: true,
@@ -36,16 +36,19 @@ const props = withDefaults(defineProps<{
importLoading: false,
del: () => void 0,
add: () => void 0,
request: () => void 0,
batchDel: () => void 0
})
const emit = defineEmits<{
add: []
click: [val: {
item: any,
index: number
}],
importData: [e: Event]
exportData: []
sort: [type: Sort,pageNo: number,pageSize: number]
}>()
let listRef: any = $ref()
@@ -68,22 +71,15 @@ function scrollToItem(index: number) {
})
}
let pageNo = $ref(1)
let pageSize = $ref(50)
let currentList = $computed(() => {
if (searchKey) {
return list.value.filter(v => v.word.includes(searchKey))
}
if (!props.showPagination) return list.value
return list.value.slice((pageNo - 1) * pageSize, (pageNo - 1) * pageSize + pageSize)
})
let selectIds = $ref([])
let selectAll = $computed(() => {
return !!selectIds.length
})
function toggleSelect(item) {
let rIndex = selectIds.findIndex(v => v === item.id)
if (rIndex > -1) {
@@ -97,11 +93,10 @@ function toggleSelectAll() {
if (selectAll) {
selectIds = []
} else {
selectIds = currentList.map(v => v.id)
selectIds = list2.map(v => v.id)
}
}
let searchKey = $ref('')
let showSortDialog = $ref(false)
let showSearchInput = $ref(false)
let showImportDialog = $ref(false)
@@ -109,13 +104,10 @@ let showImportDialog = $ref(false)
const closeImportDialog = () => showImportDialog = false
function sort(type: Sort) {
if (type === Sort.reverse) {
Toast.success('已翻转排序')
list.value = reverse(cloneDeep(list.value))
}
if (type === Sort.random) {
Toast.success('已随机排序')
list.value = shuffle(cloneDeep(list.value))
if ([Sort.reverse, Sort.random].includes(type)) {
emit('sort', type,params.pageNo,params.pageSize)
}else{
emit('sort', type,1,params.total)
}
showSortDialog = false
}
@@ -126,7 +118,8 @@ function handleBatchDel() {
}
function handlePageNo(e) {
pageNo = e
params.pageNo = e
getData()
scrollToTop()
}
@@ -135,8 +128,52 @@ const s = useSlots()
defineExpose({
scrollToBottom,
scrollToItem,
closeImportDialog
closeImportDialog,
getData
})
let list2 = $ref([])
let loading2 = $ref(false)
let params = $ref({
pageNo: 1,
pageSize: 50,
total: 0,
sortType: null,
searchKey: ''
})
function search(key: string) {
console.log('key',key)
if(!params.searchKey) {
params.pageNo = 1
}
params.searchKey = key
getData()
}
function cancelSearch() {
params.searchKey = ''
showSearchInput = false
getData()
}
async function getData() {
loading2 = true
console.log('params',params);
let {list, total} = await props.request(params)
console.log('list',list)
list2 = list
params.total = total
loading2 = false
}
onMounted(async () => {
getData()
})
defineRender(
() => {
const d = (item) => <Checkbox
@@ -153,8 +190,8 @@ defineRender(
<div class="flex gap-4">
<BaseInput
clearable
modelValue={searchKey}
onUpdate:modelValue={debounce(e => searchKey = e)}
modelValue={params.searchKey}
onUpdate:modelValue={debounce(e => search(e), 500)}
class="flex-1"
autofocus>
{{
@@ -163,17 +200,17 @@ defineRender(
/>
}}
</BaseInput>
<BaseButton onClick={() => (showSearchInput = false, searchKey = '')}>取消</BaseButton>
<BaseButton onClick={cancelSearch}>取消</BaseButton>
</div>
) : (
<div class="flex justify-between">
<div class="flex gap-2 items-center">
<Checkbox
disabled={!currentList.length}
disabled={!list2.length}
onChange={() => toggleSelectAll()}
modelValue={selectAll}
size="large"/>
<span>{selectIds.length} / {list.value.length}</span>
<span>{selectIds.length} / {params.total}</span>
</div>
<div class="flex gap-2 relative">
@@ -201,19 +238,19 @@ defineRender(
{props.exportLoading ? <IconEosIconsLoading/> : <IconPhExportLight/>}
</BaseIcon>
<BaseIcon
onClick={props.add}
onClick={() => emit('add')}
title="添加单词">
<IconFluentAdd20Regular/>
</BaseIcon>
<BaseIcon
disabled={!currentList.length}
disabled={!list2.length}
title="改变顺序"
onClick={() => showSortDialog = !showSortDialog}
>
<IconFluentArrowSort20Regular/>
</BaseIcon>
<BaseIcon
disabled={!currentList.length}
disabled={!list2.length}
onClick={() => showSearchInput = !showSearchInput}
title="搜索">
<IconFluentSearch20Regular/>
@@ -226,10 +263,12 @@ defineRender(
<div class="mini-row-title">
列表顺序设置
</div>
<div class="mini-row">
<BaseButton size="small" onClick={() => sort(Sort.reverse)}>翻转
</BaseButton>
<BaseButton size="small" onClick={() => sort(Sort.random)}>随机</BaseButton>
<div class="flex flex-col gap2 btn-no-margin">
<BaseButton onClick={() => sort(Sort.reverse)}>翻转当前页</BaseButton>
<BaseButton onClick={() => sort(Sort.reverseAll)}>翻转所有</BaseButton>
<div class="line"></div>
<BaseButton onClick={() => sort(Sort.random)}>随机当前页</BaseButton>
<BaseButton onClick={() => sort(Sort.randomAll)}>随机所有</BaseButton>
</div>
</MiniDialog>
</div>
@@ -239,15 +278,15 @@ defineRender(
</div>
}
{
props.loading ?
loading2 ?
<div class="h-full w-full center text-4xl">
<IconEosIconsLoading color="gray"/>
</div>
: currentList.length ? (
: list2.length ? (
<>
<div class="flex-1 overflow-auto"
ref={e => listRef = e}>
{currentList.map((item, index) => {
{list2.map((item, index) => {
return (
<div class="list-item-wrapper"
key={item.word}
@@ -260,13 +299,13 @@ defineRender(
{
props.showPagination && <div class="flex justify-end">
<Pagination
currentPage={pageNo}
currentPage={params.pageNo}
onUpdate:current-page={handlePageNo}
pageSize={pageSize}
onUpdate:page-size={(e) => pageSize = e}
pageSize={params.pageSize}
onUpdate:page-size={(e) => params.pageSize = e}
pageSizes={[20, 50, 100, 200]}
layout="prev, pager, next"
total={props.total}/>
layout="prev, pager, next, total"
total={params.total}/>
</div>
}
</>
@@ -308,6 +347,4 @@ defineRender(
}
)
</script>
<style scoped lang="scss">
</style>
<style scoped lang="scss"></style>

View File

@@ -89,7 +89,7 @@ watch(() => props.modelValue, n => {
// console.log('n', n)
if (n) {
id = Date.now()
runtimeStore.modalList.push({id, close})
runtimeStore.modalList.push({ id, close })
zIndex = 999 + runtimeStore.modalList.length
visible = true
} else {
@@ -101,7 +101,7 @@ onMounted(() => {
if (props.modelValue === undefined) {
visible = true
id = Date.now()
runtimeStore.modalList.push({id, close})
runtimeStore.modalList.push({ id, close })
zIndex = 999 + runtimeStore.modalList.length
}
})
@@ -175,7 +175,7 @@ async function cancel() {
<div class="right">
<BaseButton type="info" @click="cancel">{{ cancelButtonText }}</BaseButton>
<BaseButton
id="dialog-ok"
id="dialog-ok"
:loading="confirmButtonLoading"
@click="ok">{{ confirmButtonText }}
</BaseButton>
@@ -292,7 +292,8 @@ $header-height: 4rem;
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.3rem 1.3rem 1rem;
padding: var(--modal-padding);
padding-bottom: 0;
border-radius: $radius $radius 0 0;
.title {
@@ -315,7 +316,7 @@ $header-height: 4rem;
display: flex;
&.padding {
padding: .2rem 1.6rem 1.6rem;
padding: .2rem var(--modal-padding);
}
.content {
@@ -327,7 +328,7 @@ $header-height: 4rem;
.modal-footer {
display: flex;
justify-content: space-between;
padding: var(--space);
padding: var(--modal-padding);
}
}
}

View File

@@ -36,7 +36,7 @@ import ArticleAudio from "@/pages/article/components/ArticleAudio.vue";
import { AppEnv, DICT_LIST, LIB_JS_URL, PracticeSaveArticleKey, TourConfig } from "@/config/env.ts";
import { addStat, setUserDictProp } from "@/apis";
import { useRuntimeStore } from "@/stores/runtime.ts";
import SettingDialog from "@/components/SettingDialog.vue";
import SettingDialog from "@/components/setting/SettingDialog.vue";
const store = useBaseStore()
const runtimeStore = useRuntimeStore()

View File

@@ -43,28 +43,21 @@ import { useExport } from "@/hooks/export.ts";
import MigrateDialog from "@/components/MigrateDialog.vue";
import Log from "@/pages/setting/Log.vue";
import About from "@/components/About.vue";
import CommonSetting from "@/components/setting/CommonSetting.vue";
import ArticleSettting from "@/components/setting/ArticleSettting.vue";
import WordSetting from "@/components/setting/WordSetting.vue";
const emit = defineEmits<{
toggleDisabledDialogEscKey: [val: boolean]
}>()
const tabIndex = $ref(4)
const tabIndex = $ref(0)
const settingStore = useSettingStore()
const runtimeStore = useRuntimeStore()
const store = useBaseStore()
//@ts-ignore
const gitLastCommitHash = ref(LATEST_COMMIT_HASH);
const simpleWords = $computed({
get: () => store.simpleWords.join(','),
set: v => {
try {
store.simpleWords = v.split(',');
} catch (e) {
}
}
})
let editShortcutKey = $ref('')
@@ -311,6 +304,18 @@ function transferOk() {
<div class="flex flex-1 overflow-hidden gap-4">
<div class="left">
<div class="tabs">
<div class="tab" :class="tabIndex === 0 && 'active'" @click="tabIndex = 0">
<IconFluentSettings20Regular width="20"/>
<span>通用</span>
</div>
<div class="tab" :class="tabIndex === 1 && 'active'" @click="tabIndex = 1">
<IconFluentTextUnderlineDouble20Regular width="20"/>
<span>单词</span>
</div>
<div class="tab" :class="tabIndex === 2 && 'active'" @click="tabIndex = 2">
<IconFluentBookLetter20Regular width="20"/>
<span>文章</span>
</div>
<div class="tab" :class="tabIndex === 4 && 'active'" @click="tabIndex = 4">
<IconFluentDatabasePerson20Regular width="20"/>
<span>数据管理</span>
@@ -339,6 +344,11 @@ function transferOk() {
<div class="col-line"></div>
<div class="flex-1 overflow-y-auto overflow-x-hidden pr-4 content">
<CommonSetting v-if="tabIndex === 0"/>
<WordSetting v-if="tabIndex === 1"/>
<ArticleSettting v-if="tabIndex === 2"/>
<div class="body" v-if="tabIndex === 3">
<div class="row">
<label class="main-title">功能</label>

View File

@@ -2,8 +2,7 @@
import BaseTable from "@/components/BaseTable.vue";
import WordItem from "@/components/WordItem.vue";
import {useBaseStore} from "@/stores/base.ts";
import {defineAsyncComponent} from "vue";
import { defineAsyncComponent } from "vue";
import { useRuntimeStore } from "@/stores/runtime.ts";
const Dialog = defineAsyncComponent(() => import('@/components/dialog/Dialog.vue'))
@@ -19,8 +18,10 @@ defineEmits<{
<template>
<!-- todo 这里显示的时候可以选中并高亮当前index-->
<!-- todo 这个组件的分布器需要直接可跳转指定页面并显示一页有多少个-->
<Dialog v-model="model" title="修改学习进度">
<div class="px-4 pb-4 h-80vh w-30rem">
<Dialog v-model="model"
padding
title="修改学习进度">
<div class="py-4 h-80vh w-30rem">
<BaseTable
class="h-full"
:list='runtimeStore.editDict.words'

View File

@@ -7,7 +7,7 @@ import { PracticeData, ShortcutKey } from "@/types/types.ts";
import BaseIcon from "@/components/BaseIcon.vue";
import Tooltip from "@/components/base/Tooltip.vue";
import Progress from '@/components/base/Progress.vue'
import SettingDialog from "@/components/SettingDialog.vue";
import SettingDialog from "@/components/setting/SettingDialog.vue";
const statStore = usePracticeStore()
const settingStore = useSettingStore()

View File

@@ -63,7 +63,9 @@ watch(() => model.value, (n) => {
<template>
<Dialog
v-model="model"
title="学习设置" :footer="true"
title="学习设置"
padding
:footer="true"
@ok="changePerDayStudyNumber">
<div class="target-modal color-main" id="mode">
<div class="center">
@@ -122,7 +124,7 @@ watch(() => model.value, (n) => {
class="mt-1"
:max="200" v-model="tempPerDayStudyNumber"/>
</div>
<div class="mb-6 flex gap-space">
<div class="flex gap-space">
<span class="shrink-0 w-20">学习进度</span>
<div class="flex-1">
<Slider :min="0"
@@ -156,7 +158,6 @@ watch(() => model.value, (n) => {
.target-modal {
width: 35rem;
padding: 0 var(--space);
:deep(.inner) {
font-size: 1.8rem;

View File

@@ -18,8 +18,8 @@ let showTranslate = $ref(false)
</script>
<template>
<Dialog v-model="model" title="任务">
<div class="px-4 pb-4 h-80vh flex gap-4">
<Dialog v-model="model" padding title="任务">
<div class="pb-4 h-80vh flex gap-4">
<div class="h-full flex flex-col gap-2">
<div class="flex justify-between items-center">
<span class="title">新词 {{data.new.length}}</span>

View File

@@ -29,6 +29,7 @@ watch(() => model.value, (n) => {
<template>
<Dialog v-model="model" title="随机复习设置"
:footer="true"
:padding="true"
@ok="emit('ok',num)">
<div class="target-modal color-main">
<div class="flex gap-4 items-end mb-2">
@@ -52,7 +53,6 @@ watch(() => model.value, (n) => {
.target-modal {
width: 30rem;
padding: 0 var(--space);
.lh {
color: rgb(176, 116, 211)