This commit is contained in:
Zyronon
2025-12-13 16:54:25 +08:00
parent 9913421cd7
commit b7d6e4e09c
6 changed files with 486 additions and 153 deletions

View File

@@ -1,11 +1,11 @@
<script setup lang="tsx">
import {nextTick, useSlots} from "vue";
import {Sort} from "@/types/types.ts";
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";
import BaseButton from "@/components/BaseButton.vue";
import {cloneDeep, debounce, reverse, shuffle} from "@/utils";
import { cloneDeep, debounce, reverse, shuffle } from "@/utils";
import PopConfirm from "@/components/PopConfirm.vue";
import Empty from "@/components/Empty.vue";
import Pagination from '@/components/base/Pagination.vue'
@@ -14,7 +14,7 @@ import Checkbox from "@/components/base/checkbox/Checkbox.vue";
import DeleteIcon from "@/components/icon/DeleteIcon.vue";
import Dialog from "@/components/dialog/Dialog.vue";
import BaseInput from "@/components/base/BaseInput.vue";
import {Host} from "@/config/env.ts";
import { Host } from "@/config/env.ts";
let list = defineModel('list')
@@ -27,6 +27,8 @@ const props = withDefaults(defineProps<{
del?: Function
batchDel?: Function
add?: Function
request?: Function
total: number
}>(), {
loading: true,
showToolbar: true,
@@ -35,6 +37,7 @@ const props = withDefaults(defineProps<{
importLoading: false,
del: () => void 0,
add: () => void 0,
request: () => void 0,
batchDel: () => void 0
})
@@ -63,11 +66,10 @@ function scrollToTop() {
function scrollToItem(index: number) {
nextTick(() => {
listRef?.children[index]?.scrollIntoView({block: 'center', behavior: 'smooth'})
listRef?.children[index]?.scrollIntoView({ block: 'center', behavior: 'smooth' })
})
}
let pageNo = $ref(1)
let pageSize = $ref(50)
let currentList = $computed(() => {
@@ -83,6 +85,12 @@ let selectAll = $computed(() => {
return !!selectIds.length
})
let list2 = $ref([])
onMounted(async () => {
list2 = await props.request({ pageNo, pageSize })
console.log('list2',list2)
})
function toggleSelect(item) {
let rIndex = selectIds.findIndex(v => v === item.id)
if (rIndex > -1) {
@@ -136,6 +144,7 @@ defineExpose({
scrollToItem,
closeImportDialog
})
defineRender(
() => {
const d = (item) => <Checkbox
@@ -175,136 +184,136 @@ defineRender(
<span>{selectIds.length} / {list.value.length}</span>
</div>
<div class="flex gap-2 relative">
{
selectIds.length ?
<PopConfirm title="确认删除所有选中数据?"
onConfirm={handleBatchDel}
>
<BaseIcon
class="del"
title="删除">
<DeleteIcon/>
</BaseIcon>
</PopConfirm>
: null
}
<BaseIcon
onClick={() => showImportDialog = true}
title="导入">
<IconSystemUiconsImport/>
</BaseIcon>
<BaseIcon
onClick={() => emit('exportData')}
title="导出">
{props.exportLoading ? <IconEosIconsLoading/> : <IconPhExportLight/>}
</BaseIcon>
<BaseIcon
onClick={props.add}
title="添加单词">
<IconFluentAdd20Regular/>
</BaseIcon>
<BaseIcon
disabled={!currentList.length}
title="改变顺序"
onClick={() => showSortDialog = !showSortDialog}
>
<IconFluentArrowSort20Regular/>
</BaseIcon>
<BaseIcon
disabled={!currentList.length}
onClick={() => showSearchInput = !showSearchInput}
title="搜索">
<IconFluentSearch20Regular/>
</BaseIcon>
<MiniDialog
modelValue={showSortDialog}
onUpdate:modelValue={e => showSortDialog = e}
style="width: 8rem;"
>
<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>
</MiniDialog>
</div>
</div>
)
}
</div>
}
{
props.loading ?
<div class="h-full w-full center text-4xl">
<IconEosIconsLoading color="gray"/>
</div>
: currentList.length ? (
<>
<div class="flex-1 overflow-auto"
ref={e => listRef = e}>
{currentList.map((item, index) => {
return (
<div class="list-item-wrapper"
key={item.word}
>
{s.default({checkbox: d, item, index: (pageSize * (pageNo - 1)) + index + 1})}
</div>
<div class="flex gap-2 relative">
{
selectIds.length ?
<PopConfirm title="确认删除所有选中数据?"
onConfirm={handleBatchDel}
>
<BaseIcon
class="del"
title="删除">
<DeleteIcon/>
</BaseIcon>
</PopConfirm>
: null
}
<BaseIcon
onClick={() => showImportDialog = true}
title="导入">
<IconSystemUiconsImport/>
</BaseIcon>
<BaseIcon
onClick={() => emit('exportData')}
title="导出">
{props.exportLoading ? <IconEosIconsLoading/> : <IconPhExportLight/>}
</BaseIcon>
<BaseIcon
onClick={props.add}
title="添加单词">
<IconFluentAdd20Regular/>
</BaseIcon>
<BaseIcon
disabled={!currentList.length}
title="改变顺序"
onClick={() => showSortDialog = !showSortDialog}
>
<IconFluentArrowSort20Regular/>
</BaseIcon>
<BaseIcon
disabled={!currentList.length}
onClick={() => showSearchInput = !showSearchInput}
title="搜索">
<IconFluentSearch20Regular/>
</BaseIcon>
<MiniDialog
modelValue={showSortDialog}
onUpdate:modelValue={e => showSortDialog = e}
style="width: 8rem;"
>
<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>
</MiniDialog>
</div>
</div>
)
})}
}
</div>
{
props.showPagination && <div class="flex justify-end">
<Pagination
currentPage={pageNo}
onUpdate:current-page={handlePageNo}
pageSize={pageSize}
onUpdate:page-size={(e) => pageSize = e}
pageSizes={[20, 50, 100, 200]}
layout="prev, pager, next"
total={list.value.length}/>
}
{
props.loading ?
<div class="h-full w-full center text-4xl">
<IconEosIconsLoading color="gray"/>
</div>
}
</>
) : <Empty/>
}
: currentList.length ? (
<>
<div class="flex-1 overflow-auto"
ref={e => listRef = e}>
{currentList.map((item, index) => {
return (
<div class="list-item-wrapper"
key={item.word}
>
{s.default({ checkbox: d, item, index: (pageSize * (pageNo - 1)) + index + 1 })}
</div>
)
})}
</div>
{
props.showPagination && <div class="flex justify-end">
<Pagination
currentPage={pageNo}
onUpdate:current-page={handlePageNo}
pageSize={pageSize}
onUpdate:page-size={(e) => pageSize = e}
pageSizes={[20, 50, 100, 200]}
layout="prev, pager, next"
total={props.total}/>
</div>
}
</>
) : <Empty/>
}
<Dialog modelValue={showImportDialog}
onUpdate:modelValue={closeImportDialog}
title="导入教程"
>
<div className="w-100 p-4 pt-0">
<div>请按照模板的格式来填写数据</div>
<div class="color-red">单词项为必填其他项可不填</div>
<div>翻译一行一个翻译前面词性后面内容如n.取消多个翻译请换行</div>
<div>例句一行原文一行译文多个请换<span class="color-red"></span></div>
<div>短语一行原文一行译文多个请换<span class="color-red"></span></div>
<div>同义词同根词词源请前往官方字典然后编辑其中某个单词参考其格式</div>
<div class="mt-6">
模板下载地址<a href={`https://${Host}/libs/单词导入模板.xlsx`}>单词导入模板</a>
</div>
<div class="mt-4">
<BaseButton
onClick={() => {
let d: HTMLDivElement = document.querySelector('#upload-trigger')
d.click()
}}
loading={props.importLoading}>导入</BaseButton>
<input
id="upload-trigger"
type="file"
accept=".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
onChange={e => emit('importData', e)}
class="w-0 h-0 opacity-0"/>
</div>
<Dialog modelValue={showImportDialog}
onUpdate:modelValue={closeImportDialog}
title="导入教程"
>
<div className="w-100 p-4 pt-0">
<div>请按照模板的格式来填写数据</div>
<div class="color-red">单词项为必填其他项可不填</div>
<div>翻译一行一个翻译前面词性后面内容如n.取消多个翻译请换行</div>
<div>例句一行原文一行译文多个请换<span class="color-red"></span></div>
<div>短语一行原文一行译文多个请换<span class="color-red"></span></div>
<div>同义词同根词词源请前往官方字典然后编辑其中某个单词参考其格式</div>
<div class="mt-6">
模板下载地址<a href={`https://${Host}/libs/单词导入模板.xlsx`}>单词导入模板</a>
</div>
<div class="mt-4">
<BaseButton
onClick={() => {
let d: HTMLDivElement = document.querySelector('#upload-trigger')
d.click()
}}
loading={props.importLoading}>导入</BaseButton>
<input
id="upload-trigger"
type="file"
accept=".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
onChange={e => emit('importData', e)}
class="w-0 h-0 opacity-0"/>
</div>
</div>
</Dialog>
</div>
</Dialog>
</div>
)
}
)
}
)
</script>
<style scoped lang="scss">

View File

@@ -0,0 +1,313 @@
<script setup lang="tsx">
import { nextTick, useSlots } from "vue";
import { Sort } from "@/types/types.ts";
import MiniDialog from "@/components/dialog/MiniDialog.vue";
import BaseIcon from "@/components/BaseIcon.vue";
import BaseButton from "@/components/BaseButton.vue";
import { cloneDeep, debounce, reverse, shuffle } from "@/utils";
import PopConfirm from "@/components/PopConfirm.vue";
import Empty from "@/components/Empty.vue";
import Pagination from '@/components/base/Pagination.vue'
import Toast from '@/components/base/toast/Toast.ts'
import Checkbox from "@/components/base/checkbox/Checkbox.vue";
import DeleteIcon from "@/components/icon/DeleteIcon.vue";
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
showToolbar?: boolean
showPagination?: boolean
exportLoading?: boolean
importLoading?: boolean
del?: Function
batchDel?: Function
add?: Function
total: number
}>(), {
loading: true,
showToolbar: true,
showPagination: true,
exportLoading: false,
importLoading: false,
del: () => void 0,
add: () => void 0,
batchDel: () => void 0
})
const emit = defineEmits<{
click: [val: {
item: any,
index: number
}],
importData: [e: Event]
exportData: []
}>()
let listRef: any = $ref()
function scrollToBottom() {
nextTick(() => {
listRef?.scrollTo(0, listRef.scrollHeight)
})
}
function scrollToTop() {
nextTick(() => {
listRef?.scrollTo(0, 0)
})
}
function scrollToItem(index: number) {
nextTick(() => {
listRef?.children[index]?.scrollIntoView({ block: 'center', behavior: 'smooth' })
})
}
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) {
selectIds.splice(rIndex, 1)
} else {
selectIds.push(item.id)
}
}
function toggleSelectAll() {
if (selectAll) {
selectIds = []
} else {
selectIds = currentList.map(v => v.id)
}
}
let searchKey = $ref('')
let showSortDialog = $ref(false)
let showSearchInput = $ref(false)
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))
}
showSortDialog = false
}
function handleBatchDel() {
props.batchDel(selectIds)
selectIds = []
}
function handlePageNo(e) {
pageNo = e
scrollToTop()
}
const s = useSlots()
defineExpose({
scrollToBottom,
scrollToItem,
closeImportDialog
})
defineRender(
() => {
const d = (item) => <Checkbox
modelValue={selectIds.includes(item.id)}
onChange={() => toggleSelect(item)}
size="large"/>
return (
<div class="flex flex-col gap-3">
{
props.showToolbar && <div>
{
showSearchInput ? (
<div class="flex gap-4">
<BaseInput
clearable
modelValue={searchKey}
onUpdate:modelValue={debounce(e => searchKey = e)}
class="flex-1"
autofocus>
{{
subfix: () => <IconFluentSearch24Regular
class="text-lg text-gray"
/>
}}
</BaseInput>
<BaseButton onClick={() => (showSearchInput = false, searchKey = '')}>取消</BaseButton>
</div>
) : (
<div class="flex justify-between">
<div class="flex gap-2 items-center">
<Checkbox
disabled={!currentList.length}
onChange={() => toggleSelectAll()}
modelValue={selectAll}
size="large"/>
<span>{selectIds.length} / {list.value.length}</span>
</div>
<div class="flex gap-2 relative">
{
selectIds.length ?
<PopConfirm title="确认删除所有选中数据?"
onConfirm={handleBatchDel}
>
<BaseIcon
class="del"
title="删除">
<DeleteIcon/>
</BaseIcon>
</PopConfirm>
: null
}
<BaseIcon
onClick={() => showImportDialog = true}
title="导入">
<IconSystemUiconsImport/>
</BaseIcon>
<BaseIcon
onClick={() => emit('exportData')}
title="导出">
{props.exportLoading ? <IconEosIconsLoading/> : <IconPhExportLight/>}
</BaseIcon>
<BaseIcon
onClick={props.add}
title="添加单词">
<IconFluentAdd20Regular/>
</BaseIcon>
<BaseIcon
disabled={!currentList.length}
title="改变顺序"
onClick={() => showSortDialog = !showSortDialog}
>
<IconFluentArrowSort20Regular/>
</BaseIcon>
<BaseIcon
disabled={!currentList.length}
onClick={() => showSearchInput = !showSearchInput}
title="搜索">
<IconFluentSearch20Regular/>
</BaseIcon>
<MiniDialog
modelValue={showSortDialog}
onUpdate:modelValue={e => showSortDialog = e}
style="width: 8rem;"
>
<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>
</MiniDialog>
</div>
</div>
)
}
</div>
}
{
props.loading ?
<div class="h-full w-full center text-4xl">
<IconEosIconsLoading color="gray"/>
</div>
: currentList.length ? (
<>
<div class="flex-1 overflow-auto"
ref={e => listRef = e}>
{currentList.map((item, index) => {
return (
<div class="list-item-wrapper"
key={item.word}
>
{s.default({ checkbox: d, item, index: (pageSize * (pageNo - 1)) + index + 1 })}
</div>
)
})}
</div>
{
props.showPagination && <div class="flex justify-end">
<Pagination
currentPage={pageNo}
onUpdate:current-page={handlePageNo}
pageSize={pageSize}
onUpdate:page-size={(e) => pageSize = e}
pageSizes={[20, 50, 100, 200]}
layout="prev, pager, next"
total={props.total}/>
</div>
}
</>
) : <Empty/>
}
<Dialog modelValue={showImportDialog}
onUpdate:modelValue={closeImportDialog}
title="导入教程"
>
<div className="w-100 p-4 pt-0">
<div>请按照模板的格式来填写数据</div>
<div class="color-red">单词项为必填其他项可不填</div>
<div>翻译一行一个翻译前面词性后面内容如n.取消多个翻译请换行</div>
<div>例句一行原文一行译文多个请换<span class="color-red"></span></div>
<div>短语一行原文一行译文多个请换<span class="color-red"></span></div>
<div>同义词同根词词源请前往官方字典然后编辑其中某个单词参考其格式</div>
<div class="mt-6">
模板下载地址<a href={`https://${Host}/libs/单词导入模板.xlsx`}>单词导入模板</a>
</div>
<div class="mt-4">
<BaseButton
onClick={() => {
let d: HTMLDivElement = document.querySelector('#upload-trigger')
d.click()
}}
loading={props.importLoading}>导入</BaseButton>
<input
id="upload-trigger"
type="file"
accept=".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
onChange={e => emit('importData', e)}
class="w-0 h-0 opacity-0"/>
</div>
</div>
</Dialog>
</div>
)
}
)
</script>
<style scoped lang="scss">
</style>

View File

@@ -19,7 +19,7 @@ export const ENV = Object.assign(map['DEV'], common)
export let AppEnv = {
TOKEN: localStorage.getItem('token') ?? '',
IS_OFFICIAL: true,
IS_OFFICIAL: false,
IS_LOGIN: false,
CAN_REQUEST: false
}

View File

@@ -34,7 +34,6 @@ const base = useBaseStore()
const router = useRouter()
const route = useRoute()
const isMob = isMobile()
let loading = $ref(false)
let list = $computed({
@@ -65,8 +64,8 @@ let wordForm = $ref(getDefaultFormWord())
let wordFormRef = $ref()
const wordRules = reactive({
word: [
{required: true, message: '请输入单词', trigger: 'blur'},
{max: 100, message: '名称不能超过100个字符', trigger: 'blur'},
{ required: true, message: '请输入单词', trigger: 'blur' },
{ max: 100, message: '名称不能超过100个字符', trigger: 'blur' },
],
})
let studyLoading = $ref(false)
@@ -201,10 +200,9 @@ onMounted(async () => {
let r = await _getDictDataByUrl(runtimeStore.editDict)
runtimeStore.editDict = r
}
if (base.word.bookList.find(book => book.id === runtimeStore.editDict.id)) {
if (AppEnv.CAN_REQUEST) {
let res = await detail({id: runtimeStore.editDict.id})
let res = await detail({ id: runtimeStore.editDict.id })
if (res.success) {
runtimeStore.editDict.statistics = res.data.statistics
if (res.data.words.length) {
@@ -213,6 +211,7 @@ onMounted(async () => {
}
}
}
list2 = runtimeStore.editDict.words
loading = false
}
}
@@ -227,7 +226,7 @@ let showPracticeSettingDialog = $ref(false)
const store = useBaseStore()
const settingStore = useSettingStore()
const {nav} = useNav()
const { nav } = useNav()
//todo 可以和首页合并
async function startPractice(query = {}) {
@@ -244,7 +243,7 @@ async function startPractice(query = {}) {
wordPracticeMode: settingStore.wordPracticeMode
})
let currentStudy = getCurrentStudyWord()
nav('practice-words/' + store.sdict.id, query, {taskWords: currentStudy})
nav('practice-words/' + store.sdict.id, query, { taskWords: currentStudy })
}
async function addMyStudyList() {
@@ -263,7 +262,6 @@ async function startTest() {
await base.changeDict(runtimeStore.editDict)
loading = false
nav('word-test/' + store.sdict.id)
}
let exportLoading = $ref(false)
@@ -279,7 +277,7 @@ function importData(e) {
let data = s.target.result;
importLoading = true
const XLSX = await loadJsLib('XLSX', LIB_JS_URL.XLSX);
let workbook = XLSX.read(data, {type: 'binary'});
let workbook = XLSX.read(data, { type: 'binary' });
let res: any[] = XLSX.utils.sheet_to_json(workbook.Sheets['Sheet1']);
if (res.length) {
let words = res.map(v => {
@@ -386,7 +384,6 @@ function searchWord() {
console.log('wordForm.word', wordForm.word)
}
watch(() => loading, (val) => {
if (!val) return
_nextTick(async () => {
@@ -398,7 +395,7 @@ watch(() => loading, (val) => {
tour.addStep({
id: 'step3',
text: '点击这里开始学习',
attachTo: {element: '#study', on: 'bottom'},
attachTo: { element: '#study', on: 'bottom' },
buttons: [
{
text: `下一步3/${TourConfig.total}`,
@@ -413,7 +410,7 @@ watch(() => loading, (val) => {
tour.addStep({
id: 'step4',
text: '这里可以选择学习模式、设置学习数量、修改学习进度',
attachTo: {element: '#mode', on: 'bottom'},
attachTo: { element: '#mode', on: 'bottom' },
beforeShowPromise() {
return new Promise((resolve) => {
const timer = setInterval(() => {
@@ -429,7 +426,7 @@ watch(() => loading, (val) => {
text: `下一步4/${TourConfig.total}`,
action() {
tour.next()
startPractice({guide: 1})
startPractice({ guide: 1 })
}
}
]
@@ -442,6 +439,18 @@ watch(() => loading, (val) => {
}, 500)
})
let pageNo = $ref(1)
let pageSize = $ref(50)
let list2 = $ref([])
async function requestList({ pageNo, pageSize }) {
if (AppEnv.CAN_REQUEST) {
} else {
return list2.slice((pageNo - 1) * pageSize, (pageNo - 1) * pageSize + pageSize)
}
}
defineRender(() => {
return (
@@ -484,7 +493,9 @@ defineRender(() => {
<BaseTable
ref={tableRef}
class="h-full"
request={requestList}
list={list}
total={runtimeStore.editDict.length}
loading={loading}
onUpdate:list={e => list = e}
del={delWord}
@@ -564,42 +575,42 @@ defineRender(() => {
modelValue={wordForm.trans}
onUpdate:modelValue={e => wordForm.trans = e}
placeholder="一行一个翻译前面词性后面内容如n.取消);多个翻译请换行"
autosize={{minRows: 6, maxRows: 10}}/>
autosize={{ minRows: 6, maxRows: 10 }}/>
</FormItem>
<FormItem label="例句">
<Textarea
modelValue={wordForm.sentences}
onUpdate:modelValue={e => wordForm.sentences = e}
placeholder="一行原文,一行译文;多个请换两行"
autosize={{minRows: 6, maxRows: 10}}/>
autosize={{ minRows: 6, maxRows: 10 }}/>
</FormItem>
<FormItem label="短语">
<Textarea
modelValue={wordForm.phrases}
onUpdate:modelValue={e => wordForm.phrases = e}
placeholder="一行原文,一行译文;多个请换两行"
autosize={{minRows: 6, maxRows: 10}}/>
autosize={{ minRows: 6, maxRows: 10 }}/>
</FormItem>
<FormItem label="同义词">
<Textarea
modelValue={wordForm.synos}
onUpdate:modelValue={e => wordForm.synos = e}
placeholder="请参考已有单词格式"
autosize={{minRows: 6, maxRows: 20}}/>
autosize={{ minRows: 6, maxRows: 20 }}/>
</FormItem>
<FormItem label="同根词">
<Textarea
modelValue={wordForm.relWords}
onUpdate:modelValue={e => wordForm.relWords = e}
placeholder="请参考已有单词格式"
autosize={{minRows: 6, maxRows: 20}}/>
autosize={{ minRows: 6, maxRows: 20 }}/>
</FormItem>
<FormItem label="词源">
<Textarea
modelValue={wordForm.etymology}
onUpdate:modelValue={e => wordForm.etymology = e}
placeholder="请参考已有单词格式"
autosize={{minRows: 6, maxRows: 10}}/>
autosize={{ minRows: 6, maxRows: 10 }}/>
</FormItem>
</Form>
<div class="center">

View File

@@ -245,8 +245,7 @@ let isNewHost = $ref(window.location.host === Host)
2study.top 域名将在不久后停止使用
</div>
<div class="card flex flex-col md:flex-row gap-8">
<div class="card flex flex-col md:flex-row gap-4">
<div class="flex-1 w-full flex flex-col justify-between">
<div class="flex gap-3">
<div class="p-1 center rounded-full bg-white">