feat:add resource share page

This commit is contained in:
Zyronon
2025-12-20 23:12:54 +08:00
parent 43721e4f78
commit fac03bf0a4
9 changed files with 530 additions and 481 deletions

1
components.d.ts vendored
View File

@@ -146,6 +146,7 @@ declare module 'vue' {
Progress: typeof import('./src/components/base/Progress.vue')['default']
Radio: typeof import('./src/components/base/radio/Radio.vue')['default']
RadioGroup: typeof import('./src/components/base/radio/RadioGroup.vue')['default']
ResourceCard: typeof import('./src/components/ResourceCard.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
Select: typeof import('./src/components/base/select/Select.vue')['default']

View File

@@ -0,0 +1,75 @@
<script setup lang="ts">
import BaseButton from "./BaseButton.vue";
interface Resource {
name: string;
description?: string;
difficulty?: string;
author?: string;
features?: string;
suitable?: string;
link: string;
}
const props = defineProps<{
resource: Resource;
}>();
const emit = defineEmits(['openLink']);
// 根据难度获取对应的样式类
const getDifficultyClass = (difficulty: string) => {
switch (difficulty) {
case '入门':
return 'bg-green-500';
case '基础':
return 'bg-blue-500';
case '中级':
return 'bg-purple-500';
case '进阶':
return 'bg-amber-500';
case '高级':
return 'bg-red-500';
case '全级别':
return 'bg-gray-500';
default:
return 'bg-blue-500';
}
};
</script>
<template>
<div class="card-white min-h-45 mb-0 flex flex-col justify-between">
<div>
<div class="text-xl font-semibold mb-3 text-gray-800 dark:text-gray-100">
{{ resource.name }}
</div>
<div class="space-y-2 mb-4">
<div v-if="resource.author" class="text-sm text-gray-600 dark:text-gray-300">
<span class="font-medium">作者</span>{{ resource.author }}
</div>
<div v-if="resource.features" class="text-sm text-gray-600 dark:text-gray-300">
<span class="font-medium">🌟 特点</span>{{ resource.features }}
</div>
<div v-if="resource.suitable" class="text-sm text-gray-600 dark:text-gray-300">
<span class="font-medium">📌 适合</span>{{ resource.suitable }}
</div>
<div v-if="resource.description" class="text-sm text-gray-600 dark:text-gray-300">
{{ resource.description }}
</div>
<span
v-if="resource.difficulty"
class="inline-block px-3 py-1 rounded-full text-xs font-medium text-white"
:class="getDifficultyClass(resource.difficulty)"
>
{{ resource.difficulty }}
</span>
</div>
</div>
<div class="flex flex-col gap-3">
<BaseButton type="primary" @click="emit('openLink', resource.link)">
打开链接
</BaseButton>
</div>
</div>
</template>

View File

@@ -54,6 +54,7 @@ async function onSubmit() {
//todo 可以检查的更准确些比如json对比
if (props.isAdd) {
data.id = 'custom-dict-' + Date.now()
data.custom = true
if (source.bookList.find(v => v.name === data.name)) {
Toast.warning('已有相同名称!')
return
@@ -84,11 +85,11 @@ async function onSubmit() {
}
runtimeStore.editDict = data
if (rIndex > -1) {
source.bookList[rIndex] = cloneDeep(data)
source.bookList[rIndex] = getDefaultDict(data)
emit('submit')
Toast.success('修改成功')
} else {
source.bookList.push(cloneDeep(data))
source.bookList.push(getDefaultDict(data))
Toast.success('修改成功并加入我的词典')
}
}

View File

@@ -2,6 +2,7 @@
import { computed, ref } from 'vue'
import BasePage from '@/components/BasePage.vue'
import BaseButton from '@/components/BaseButton.vue'
import ResourceCard from '@/components/ResourceCard.vue'
// 类型定义
interface Resource {
@@ -23,7 +24,7 @@ interface Subcategory {
interface Category {
id: string
name: string
icon: string
icon?: string
description?: string
resources?: Resource[]
subcategories?: Subcategory[]
@@ -34,113 +35,111 @@ const categories = ref<Category[]>([
{
id: 'new-concept',
name: '新概念英语',
icon: '📚',
description: '经典英语教材,适合系统学习',
resources: [
{
name: '新概念英语第一册',
description: '适合英语初学者',
difficulty: '入门',
link: 'https://pan.quark.cn/s/92a317cf1a16',
link: 'https://pan.quark.cn/s/0d2ece46983f',
},
{
name: '新概念英语第二册',
description: '基础英语学习,巩固语法和词汇',
difficulty: '基础',
link: 'https://pan.quark.cn/s/1ee9c8a7e8e2',
link: 'https://pan.quark.cn/s/df29bb396728',
},
{
name: '新概念英语第三册',
description: '提高英语水平,增强阅读能力',
difficulty: '进阶',
link: 'https://pan.quark.cn/s/b35c2859812a',
link: 'https://pan.quark.cn/s/ec39dc7cbe5b',
},
{
name: '新概念英语第四册',
description: '高级英语学习,提升综合能力',
difficulty: '高级',
link: 'https://pan.quark.cn/s/a56713cafbc5',
link: 'https://pan.quark.cn/s/207a2cc8a320',
},
{
name: '新概念英青少年版',
description: '儿童读物',
difficulty: '7岁至14岁',
link: 'https://pan.quark.cn/s/9de8d7967de2',
link: 'https://pan.quark.cn/s/4628b00b39c0',
},
{
name: '新概念英语1-4 教材高清 PDF',
description: '仅 1-4 册的教材高清扫描版 PDF',
difficulty: '',
link: 'https://pan.quark.cn/s/ec49145d6b00',
link: 'https://pan.quark.cn/s/f1e7739ed806',
},
{
name: '新概念讲解视频',
description: '多家机构/个人的讲解视频',
difficulty: '',
link: 'https://pan.quark.cn/s/09e98acd55b4',
link: 'https://pan.quark.cn/s/07e25ee6de9f',
},
{
name: '新概念合集',
description: '包含前面的内容',
difficulty: '',
link: 'https://pan.quark.cn/s/667e2e48eba4',
link: 'https://pan.quark.cn/s/6b12da160020',
},
],
},
{
id: 'exam',
name: '电视/电影',
icon: '🎯',
description: '一些不错的美/英剧,可练听力和口语',
resources: [
{
name: '老友记',
description: '',
difficulty: '喜剧/爱情',
link: 'https://pan.quark.cn/s/674834e7a5b1',
link: 'https://pan.quark.cn/s/c17770edfa15',
},
{
name: '生活大爆炸',
description: '',
difficulty: '喜剧/爱情',
link: 'https://pan.quark.cn/s/0539c10704ba',
link: 'https://pan.quark.cn/s/3e66da8ce1c4',
},
{
name: '是大臣/是首相',
description: '',
difficulty: '喜剧/讽刺',
link: 'https://pan.quark.cn/s/316323ce51d5',
link: 'https://pan.quark.cn/s/2c62ce3e220d',
},
{
name: '破产姐妹',
description: '',
difficulty: '喜剧',
link: 'https://pan.quark.cn/s/40af6faaa19a',
link: 'https://pan.quark.cn/s/018600971998',
},
{
name: '绝望主妇',
description: '',
difficulty: '悬疑',
link: 'https://pan.quark.cn/s/bcccdf9e788b',
link: 'https://pan.quark.cn/s/1c67ae200c2e',
},
{
name: '纸牌屋',
description: '',
difficulty: '纸牌屋',
link: 'https://pan.quark.cn/s/d2f3082508fd',
link: 'https://pan.quark.cn/s/5ba146c46180',
},
{
name: '电视/电影合集',
description: '包含前面的内容',
difficulty: '',
link: 'https://pan.quark.cn/s/84ecb30b700b',
link: 'https://pan.quark.cn/s/84ecb30b700b', //159
// link: 'https://pan.quark.cn/s/e9b62b79c48c',
},
],
},
{
id: 'grammar',
name: '语法学习',
icon: '📝',
description: '',
subcategories: [
{
@@ -150,32 +149,33 @@ const categories = ref<Category[]>([
{
name: '英语语法新思维',
author: '张满胜',
features:'从思维角度讲解语法,注重理解而非死记硬背,分为初级、中级、高级三册,循序渐进',
suitable:'希望系统建立语法体系的学习者',
features:
'从思维角度讲解语法,注重理解而非死记硬背,分为初级、中级、高级三册,循序渐进',
suitable: '希望系统建立语法体系的学习者',
difficulty: '',
link: 'https://pan.quark.cn/s/d06abef6c737',
},
{
name: '薄冰英语语法',
author: '薄冰',
features:'老牌经典,体系完整,分类非常细,查语法点方便',
suitable:'中学生或基础较弱的学习者',
features: '老牌经典,体系完整,分类非常细,查语法点方便',
suitable: '中学生或基础较弱的学习者',
difficulty: '',
link: 'https://pan.quark.cn/s/30777ceba5b9',
},
{
name: '实用英语语法',
author: '张道真',
features:'国内经典语法教材,内容详实全面,例句丰富,适合作为工具书查阅',
suitable:'需要权威参考书的学生或教师',
difficulty: '',
link: 'https://pan.baidu.com/s/xxx',
},
// {
// name: '实用英语语法',
// author: '张道真',
// features: '国内经典语法教材,内容详实全面,例句丰富,适合作为工具书查阅',
// suitable: '需要权威参考书的学生或教师',
// difficulty: '',
// link: 'https://pan.baidu.com/s/xxx',
// },
{
name: '旋元估文法',
author: '旋元估',
features:'以通俗易懂的语言解析复杂语法,强调“理解逻辑”,适合突破语法难点',
suitable:'对传统语法教学感到枯燥,想轻松掌握核心逻辑的学习者',
features: '以通俗易懂的语言解析复杂语法,强调“理解逻辑”,适合突破语法难点',
suitable: '对传统语法教学感到枯燥,想轻松掌握核心逻辑的学习者',
difficulty: '繁体中文版',
link: 'https://pan.quark.cn/s/0d0de559794e',
},
@@ -188,8 +188,8 @@ const categories = ref<Category[]>([
{
name: '剑桥英语语法(English Grammar in Use)',
author: '剑桥大学出版',
features:'分为初级、中级、高级三册,经典畅销语法自学书,解释简明且有大量练习',
suitable:'需要结合国际考试的学习者',
features: '分为初级、中级、高级三册,经典畅销语法自学书,解释简明且有大量练习',
suitable: '需要结合国际考试的学习者',
description: '',
difficulty: '中文版',
link: 'https://pan.quark.cn/s/d4a6ef53c04d',
@@ -197,16 +197,17 @@ const categories = ref<Category[]>([
{
name: 'Oxford English Grammar(牛津英语语法)',
author: 'Sidney Greenbaum & Gerald Nelson',
features:'分为基础、提升、高级三册,英式语法权威,解释清晰、例句地道,适合备考雅思/托福',
suitable:'想全面系统梳理语法体系的人',
features:
'分为基础、提升、高级三册,英式语法权威,解释清晰、例句地道,适合备考雅思/托福',
suitable: '想全面系统梳理语法体系的人',
difficulty: '英文版',
link: 'https://pan.quark.cn/s/ca505875e68c',
},
{
name: '实用英语用法(Practical English Usage)',
author: 'Michael Swan',
features:'解释非常细致,尤其适合纠正常见错误和困惑',
suitable:'中高级学习者,适合作为语法问题的工具书',
features: '解释非常细致,尤其适合纠正常见错误和困惑',
suitable: '中高级学习者,适合作为语法问题的工具书',
difficulty: '中文版/英文版',
link: 'https://pan.quark.cn/s/05006e705a77',
},
@@ -217,14 +218,13 @@ const categories = ref<Category[]>([
{
id: 'listening',
name: '听力训练',
icon: '🎧',
description: '提升英语听力水平',
resources: [
{
name: 'VOA慢速英语合集',
description: '新闻类听力材料,语速适中,内容丰富',
difficulty: '初级',
link: 'https://pan.quark.cn/s/23fc83043ca4',
link: 'https://pan.quark.cn/s/681794bffc6e',
},
// {
// name: 'BBC Learning English',
@@ -236,13 +236,13 @@ const categories = ref<Category[]>([
name: 'TED-ED 科普动画',
description: 'TED-Ed 是一个专为初高中生所设计的在3到5分钟长的科普动画課程',
difficulty: '初级',
link: 'https://pan.quark.cn/s/e4c0182833ba',
link: 'https://pan.quark.cn/s/d3d83038afb9',
},
{
name: '哈弗演讲',
description: '高质量演讲,锻炼听力同时开拓视野',
difficulty: '中高级',
link: 'https://pan.quark.cn/s/f2bfa8a50d25',
link: 'https://pan.quark.cn/s/62e8d536a34f',
},
],
},
@@ -263,26 +263,6 @@ const filteredResources = computed(() => {
const openLink = (url: string) => {
window.open(url, '_blank')
}
// 根据难度获取对应的样式类
const getDifficultyClass = (difficulty: string) => {
switch (difficulty) {
case '入门':
return 'bg-green-500'
case '基础':
return 'bg-blue-500'
case '中级':
return 'bg-purple-500'
case '进阶':
return 'bg-amber-500'
case '高级':
return 'bg-red-500'
case '全级别':
return 'bg-gray-500'
default:
return 'bg-blue-500'
}
}
</script>
<template>
@@ -290,16 +270,14 @@ const getDifficultyClass = (difficulty: string) => {
<div class="flex flex-col items-center justify-center px-4 py-8">
<!-- 页面标题 -->
<div class="text-center mb-8">
<h1 class="text-4xl font-bold mb-4">📚 英语学习资源分享</h1>
<h1 class="text-4xl font-bold mb-4">英语学习资源分享</h1>
<p class="text-lg text-gray-600 dark:text-gray-300 max-w-3xl mx-auto">
以下是我整理的个人收藏的优质英语学习资源希望对大家有所帮助
以下是我整理的英语学习资源希望对大家有所帮助
</p>
</div>
<!-- 分类筛选 -->
<div
class="flex flex-wrap justify-center gap-2 mb-8 p-4 bg-white dark:bg-gray-800 rounded-lg shadow-md"
>
<div class="card-white flex flex-wrap justify-center gap-2 mb-8 p-4">
<BaseButton
:type="selectedCategory === 'all' ? 'primary' : 'info'"
@click="selectedCategory = 'all'"
@@ -312,7 +290,7 @@ const getDifficultyClass = (difficulty: string) => {
:type="selectedCategory === category.id ? 'primary' : 'info'"
@click="selectedCategory = category.id"
>
{{ category.icon }} {{ category.name }}
{{ category.name }}
</BaseButton>
</div>
@@ -343,43 +321,12 @@ const getDifficultyClass = (difficulty: string) => {
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
<div
<ResourceCard
v-for="resource in subcategory.resources"
:key="resource.name"
class="card-white min-h-45 mb-0 hover:shadow-xl transition-all duration-300 hover:-translate-y-1 flex flex-col justify-between"
>
<div class="">
<div class="text-xl font-semibold mb-3 text-gray-800 dark:text-gray-100">
{{ resource.name }}
</div>
<div class="space-y-2 mb-4">
<p v-if="resource.author" class="text-sm text-gray-600 dark:text-gray-300">
<span class="font-medium">📍 作者</span>{{ resource.author }}
</p>
<p v-if="resource.features" class="text-sm text-gray-600 dark:text-gray-300">
<span class="font-medium">🌟 特点</span>{{ resource.features }}
</p>
<p v-if="resource.suitable" class="text-sm text-gray-600 dark:text-gray-300">
<span class="font-medium">📌 适合</span>{{ resource.suitable }}
</p>
<p v-if="resource.description" class="text-sm text-gray-600 dark:text-gray-300">
{{ resource.description }}
</p>
<span
v-if="resource.difficulty"
class="inline-block px-3 py-1 rounded-full text-xs font-medium text-white"
:class="getDifficultyClass(resource.difficulty)"
>
{{ resource.difficulty }}
</span>
</div>
</div>
<div class="flex flex-col gap-3">
<BaseButton type="primary" @click="openLink(resource.link)">
打开链接
</BaseButton>
</div>
</div>
:resource="resource"
@openLink="openLink"
/>
</div>
</div>
</template>
@@ -387,41 +334,12 @@ const getDifficultyClass = (difficulty: string) => {
<!-- 如果没有子分类直接显示资源 -->
<template v-else>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
<div
<ResourceCard
v-for="resource in category.resources"
:key="resource.name"
class="card-white min-h-45 mb-0 hover:shadow-xl transition-all duration-300 hover:-translate-y-1 flex flex-col justify-between"
>
<div class="">
<div class="text-xl font-semibold mb-3 text-gray-800 dark:text-gray-100">
{{ resource.name }}
</div>
<div class="space-y-2 mb-4">
<p v-if="resource.author" class="text-sm text-gray-600 dark:text-gray-300">
<span class="font-medium">📍 作者</span>{{ resource.author }}
</p>
<p v-if="resource.features" class="text-sm text-gray-600 dark:text-gray-300">
<span class="font-medium">🌟 特点</span>{{ resource.features }}
</p>
<p v-if="resource.suitable" class="text-sm text-gray-600 dark:text-gray-300">
<span class="font-medium">📌 适合</span>{{ resource.suitable }}
</p>
<p v-if="resource.description" class="text-sm text-gray-600 dark:text-gray-300">
{{ resource.description }}
</p>
<span
v-if="resource.difficulty"
class="inline-block px-3 py-1 rounded-full text-xs font-medium text-white"
:class="getDifficultyClass(resource.difficulty)"
>
{{ resource.difficulty }}
</span>
</div>
</div>
<div class="flex flex-col gap-3">
<BaseButton type="primary" @click="openLink(resource.link)"> 打开链接 </BaseButton>
</div>
</div>
:resource="resource"
@openLink="openLink"
/>
</div>
</template>
</div>
@@ -429,12 +347,15 @@ const getDifficultyClass = (difficulty: string) => {
<!-- 页面底部 -->
<div class="mt-12 pt-8 border-t border-gray-200 dark:border-gray-700">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h3 class="text-xl font-bold mb-4">💡 温馨提示</h3>
<ul class="list-disc list-inside space-y-2 text-gray-600 dark:text-gray-300">
<div class="card-white">
<div class="text-xl font-bold mb-4">温馨提示</div>
<ul class="space-y-2 text-gray-600 dark:text-gray-300">
<li>所有资源均来自互联网收集仅供学习交流使用</li>
<li>如果链接失效请及时<a :href="`https://v.wjx.cn/vm/ev0W7fv.aspx#`"
target="_blank">告知</a>我会尽快更新</li>
<li>
如果链接失效请及时<a :href="`https://v.wjx.cn/vm/ev0W7fv.aspx#`" target="_blank"
>告知</a
>我会尽快更新
</li>
</ul>
</div>
</div>

View File

@@ -7,7 +7,7 @@ import About from "@/components/About.vue";
<template>
<BasePage>
<div class="center">
<div class="card w-2/3 center-col pb-20">
<div class="card-white w-2/3 center-col pb-20">
<About/>
</div>
</div>

View File

@@ -52,14 +52,14 @@ function goHome() {
<IconFluentCommentEdit20Regular />
<span v-if="settingStore.sideExpand">反馈</span>
</div>
<div class="row" @click="router.push('/qa')">
<IconFluentQuestionCircle20Regular />
<span v-if="settingStore.sideExpand">帮助</span>
</div>
<div class="row" @click="router.push('/doc')">
<IconFluentDocument20Regular />
<span v-if="settingStore.sideExpand">资料</span>
</div>
<div class="row" @click="router.push('/qa')">
<IconFluentQuestionCircle20Regular />
<span v-if="settingStore.sideExpand">帮助</span>
</div>
<!-- <div class="row" @click="router.push('/user')">-->
<!-- <IconFluentPerson20Regular/>-->
<!-- <span v-if="settingStore.sideExpand">用户</span>-->

View File

@@ -9,7 +9,7 @@ import ConflictNoticeText from '@/components/ConflictNoticeText.vue'
<template>
<BasePage>
<div class="center">
<div class="card qa w-2/3">
<div class="card-white qa w-2/3">
<div class="font-bold text-2xl mb-6">常见问题解答</div>
<div class="list">
<Collapse

View File

@@ -4,6 +4,14 @@
<template>
<div>
<div class="log-item">
<div class="mb-2">
<div>
<div>日期2025/12/20</div>
<div>内容新增资源分享页面</div>
</div>
</div>
</div>
<div class="log-item">
<div class="mb-2">
<div>
@@ -274,4 +282,4 @@
margin-bottom: 1rem;
}
</style>
</style>

View File

@@ -1,28 +1,28 @@
<script setup lang="tsx">
import { DictId, Sort } from "@/types/types.ts";
import { DictId, Sort } from '@/types/types.ts'
import { detail } from "@/apis";
import BackIcon from "@/components/BackIcon.vue";
import BaseButton from "@/components/BaseButton.vue";
import BaseIcon from "@/components/BaseIcon.vue";
import BasePage from "@/components/BasePage.vue";
import BaseTable from "@/components/BaseTable.vue";
import PopConfirm from "@/components/PopConfirm.vue";
import WordItem from "@/components/WordItem.vue";
import BaseInput from "@/components/base/BaseInput.vue";
import Textarea from "@/components/base/Textarea.vue";
import Form from "@/components/base/form/Form.vue";
import FormItem from "@/components/base/form/FormItem.vue";
import Toast from '@/components/base/toast/Toast.ts';
import DeleteIcon from "@/components/icon/DeleteIcon.vue";
import { AppEnv, LIB_JS_URL, PracticeSaveWordKey, TourConfig } from "@/config/env.ts";
import { getCurrentStudyWord } from "@/hooks/dict.ts";
import EditBook from "@/pages/article/components/EditBook.vue";
import PracticeSettingDialog from "@/pages/word/components/PracticeSettingDialog.vue";
import { useBaseStore } from "@/stores/base.ts";
import { useRuntimeStore } from "@/stores/runtime.ts";
import { useSettingStore } from "@/stores/setting.ts";
import { getDefaultDict } from "@/types/func.ts";
import { detail } from '@/apis'
import BackIcon from '@/components/BackIcon.vue'
import BaseButton from '@/components/BaseButton.vue'
import BaseIcon from '@/components/BaseIcon.vue'
import BasePage from '@/components/BasePage.vue'
import BaseTable from '@/components/BaseTable.vue'
import PopConfirm from '@/components/PopConfirm.vue'
import WordItem from '@/components/WordItem.vue'
import BaseInput from '@/components/base/BaseInput.vue'
import Textarea from '@/components/base/Textarea.vue'
import Form from '@/components/base/form/Form.vue'
import FormItem from '@/components/base/form/FormItem.vue'
import Toast from '@/components/base/toast/Toast.ts'
import DeleteIcon from '@/components/icon/DeleteIcon.vue'
import { AppEnv, LIB_JS_URL, PracticeSaveWordKey, TourConfig } from '@/config/env.ts'
import { getCurrentStudyWord } from '@/hooks/dict.ts'
import EditBook from '@/pages/article/components/EditBook.vue'
import PracticeSettingDialog from '@/pages/word/components/PracticeSettingDialog.vue'
import { useBaseStore } from '@/stores/base.ts'
import { useRuntimeStore } from '@/stores/runtime.ts'
import { useSettingStore } from '@/stores/setting.ts'
import { getDefaultDict } from '@/types/func.ts'
import {
_getDictDataByUrl,
_nextTick,
@@ -31,12 +31,12 @@ import {
loadJsLib,
reverse,
shuffle,
useNav
} from "@/utils";
import { MessageBox } from "@/utils/MessageBox.tsx";
import { nanoid } from "nanoid";
import { computed, onMounted, reactive, ref, shallowReactive, shallowRef, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
useNav,
} from '@/utils'
import { MessageBox } from '@/utils/MessageBox.tsx'
import { nanoid } from 'nanoid'
import { computed, onMounted, reactive, ref, shallowReactive, shallowRef, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
const runtimeStore = useRuntimeStore()
const base = useBaseStore()
@@ -65,8 +65,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)
@@ -77,8 +77,11 @@ function syncDictInMyStudyList(study = false) {
let rIndex = base.word.bookList.findIndex(v => v.id === runtimeStore.editDict.id)
runtimeStore.editDict.words = allList
let temp = runtimeStore.editDict;
if (!temp.custom && ![DictId.wordKnown, DictId.wordWrong, DictId.wordCollect].includes(temp.id)) {
let temp = runtimeStore.editDict
if (
!temp.custom &&
![DictId.wordKnown, DictId.wordWrong, DictId.wordCollect].includes(temp.id)
) {
temp.custom = true
if (!temp.id.includes('_custom')) {
temp.id += '_custom'
@@ -97,7 +100,7 @@ function syncDictInMyStudyList(study = false) {
async function onSubmitWord() {
// return console.log('wordFormRef',wordFormRef,wordFormRef.validate)
await wordFormRef.validate((valid) => {
await wordFormRef.validate(valid => {
if (valid) {
let data: any = convertToWord(wordForm)
//todo 可以检查的更准确些比如json对比
@@ -150,11 +153,21 @@ function word2Str(word) {
res.phonetic1 = word.phonetic1
res.phonetic0 = word.phonetic0
res.trans = word.trans.map(v => (v.pos + v.cn).replaceAll('"', '')).join('\n')
res.sentences = word.sentences.map(v => (v.c + "\n" + v.cn).replaceAll('"', '')).join('\n\n')
res.phrases = word.phrases.map(v => (v.c + "\n" + v.cn).replaceAll('"', '')).join('\n\n')
res.synos = word.synos.map(v => (v.pos + v.cn + "\n" + v.ws.join('/')).replaceAll('"', '')).join('\n\n')
res.relWords = word.relWords.root ? ('词根:' + word.relWords.root + '\n\n' +
word.relWords.rels.map(v => (v.pos + "\n" + v.words.map(v => (v.c + ':' + v.cn)).join('\n')).replaceAll('"', '')).join('\n\n')) : ''
res.sentences = word.sentences.map(v => (v.c + '\n' + v.cn).replaceAll('"', '')).join('\n\n')
res.phrases = word.phrases.map(v => (v.c + '\n' + v.cn).replaceAll('"', '')).join('\n\n')
res.synos = word.synos
.map(v => (v.pos + v.cn + '\n' + v.ws.join('/')).replaceAll('"', ''))
.join('\n\n')
res.relWords = word.relWords.root
? '词根:' +
word.relWords.root +
'\n\n' +
word.relWords.rels
.map(v =>
(v.pos + '\n' + v.words.map(v => v.c + ':' + v.cn).join('\n')).replaceAll('"', '')
)
.join('\n\n')
: ''
res.etymology = word.etymology.map(v => (v.t + '\n' + v.d).replaceAll('"', '')).join('\n\n')
return res
}
@@ -183,7 +196,7 @@ let isAdd = $ref(false)
let activeTab = $ref<'list' | 'edit'>('list') // 移动端标签页状态
const showBookDetail = computed(() => {
return !(isAdd || isEdit);
return !(isAdd || isEdit)
})
onMounted(async () => {
@@ -192,11 +205,14 @@ onMounted(async () => {
runtimeStore.editDict = getDefaultDict()
} else {
if (!runtimeStore.editDict.id) {
return router.push("/word")
return router.push('/word')
} else {
if (!runtimeStore.editDict.words.length
&& !runtimeStore.editDict.custom
&& ![DictId.wordCollect, DictId.wordWrong, DictId.wordKnown].includes(runtimeStore.editDict.en_name || runtimeStore.editDict.id)
if (
!runtimeStore.editDict.words.length &&
!runtimeStore.editDict.custom &&
![DictId.wordCollect, DictId.wordWrong, DictId.wordKnown].includes(
runtimeStore.editDict.en_name || runtimeStore.editDict.id
)
) {
loading = true
let r = await _getDictDataByUrl(runtimeStore.editDict)
@@ -204,7 +220,7 @@ onMounted(async () => {
}
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) {
@@ -230,7 +246,7 @@ let showPracticeSettingDialog = $ref(false)
const store = useBaseStore()
const settingStore = useSettingStore()
const {nav} = useNav()
const { nav } = useNav()
//todo 可以和首页合并
async function startPractice(query = {}) {
@@ -244,10 +260,10 @@ async function startPractice(query = {}) {
perDayStudyNumber: store.sdict.perDayStudyNumber,
custom: store.sdict.custom,
complete: store.sdict.complete,
wordPracticeMode: settingStore.wordPracticeMode
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() {
@@ -273,39 +289,41 @@ let importLoading = $ref(false)
let tableRef = ref()
function importData(e) {
let file = e.target.files[0];
if (!file) return;
let file = e.target.files[0]
if (!file) return
let reader = new FileReader();
let reader = new FileReader()
reader.onload = async function (s) {
let data = s.target.result;
let data = s.target.result
importLoading = true
const XLSX = await loadJsLib('XLSX', LIB_JS_URL.XLSX);
let workbook = XLSX.read(data, {type: 'binary'});
let res: any[] = XLSX.utils.sheet_to_json(workbook.Sheets['Sheet1']);
const XLSX = await loadJsLib('XLSX', LIB_JS_URL.XLSX)
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 => {
if (v['单词']) {
let data = null
try {
data = convertToWord({
id: nanoid(6),
word: v['单词'],
phonetic0: v['音标①'] ?? '',
phonetic1: v['音标'] ?? '',
trans: v['翻译'] ?? '',
sentences: v['例句'] ?? '',
phrases: v['短语'] ?? '',
synos: v['近义词'] ?? '',
relWords: v['同根词'] ?? '',
etymology: v['词'] ?? '',
});
} catch (e) {
console.error('导入单词报错' + v['单词'], e.message)
let words = res
.map(v => {
if (v['单词']) {
let data = null
try {
data = convertToWord({
id: nanoid(6),
word: v['单词'],
phonetic0: v['音标'] ?? '',
phonetic1: v['音标②'] ?? '',
trans: v['翻译'] ?? '',
sentences: v['例句'] ?? '',
phrases: v['短语'] ?? '',
synos: v['近义词'] ?? '',
relWords: v['同根词'] ?? '',
etymology: v['词源'] ?? '',
})
} catch (e) {
console.error('导入单词报错' + v['单词'], e.message)
}
return data
}
return data
}
}).filter(v => v);
})
.filter(v => v)
if (words.length) {
let repeat = []
let noRepeat = []
@@ -328,7 +346,7 @@ function importData(e) {
() => {
repeat.map(v => {
runtimeStore.editDict.words[v.index] = v
delete runtimeStore.editDict.words[v.index]["index"]
delete runtimeStore.editDict.words[v.index]['index']
})
},
null,
@@ -336,30 +354,36 @@ function importData(e) {
tableRef.value.closeImportDialog()
e.target.value = ''
importLoading = false
allList = runtimeStore.editDict.words
tableRef.value.getData()
syncDictInMyStudyList()
Toast.success('导入成功!')
}
)
} else {
tableRef.value.closeImportDialog()
syncDictInMyStudyList()
Toast.success('导入成功!')
e.target.value = ''
importLoading = false
allList = runtimeStore.editDict.words
tableRef.value.getData()
syncDictInMyStudyList()
Toast.success('导入成功!')
}
} else {
Toast.warning('导入失败!原因:没有数据/未认别到数据');
Toast.warning('导入失败!原因:没有数据/未认别到数据')
}
} else {
Toast.warning('导入失败!原因:没有数据');
Toast.warning('导入失败!原因:没有数据')
}
e.target.value = ''
importLoading = false
};
reader.readAsBinaryString(file);
}
reader.readAsBinaryString(file)
}
async function exportData() {
exportLoading = true
const XLSX = await loadJsLib('XLSX', LIB_JS_URL.XLSX);
const XLSX = await loadJsLib('XLSX', LIB_JS_URL.XLSX)
let list = runtimeStore.editDict.words
let filename = runtimeStore.editDict.name
let wb = XLSX.utils.book_new()
@@ -369,79 +393,81 @@ async function exportData() {
单词: t.word,
'音标①': t.phonetic0,
'音标②': t.phonetic1,
'翻译': t.trans,
'例句': t.sentences,
'短语': t.phrases,
'近义词': t.synos,
'同根词': t.relWords,
'词源': t.etymology,
翻译: t.trans,
例句: t.sentences,
短语: t.phrases,
近义词: t.synos,
同根词: t.relWords,
词源: t.etymology,
}
})
wb.Sheets['Sheet1'] = XLSX.utils.json_to_sheet(sheetData)
wb.SheetNames = ['Sheet1']
XLSX.writeFile(wb, `${filename}.xlsx`);
XLSX.writeFile(wb, `${filename}.xlsx`)
Toast.success(filename + ' 导出成功!')
exportLoading = false
}
watch(() => loading, (val) => {
if (!val) return
_nextTick(async () => {
const Shepherd = await loadJsLib('Shepherd', LIB_JS_URL.SHEPHERD);
const tour = new Shepherd.Tour(TourConfig);
tour.on('cancel', () => {
localStorage.setItem('tour-guide', '1');
});
tour.addStep({
id: 'step3',
text: '点击这里开始学习',
attachTo: {element: '#study', on: 'bottom'},
buttons: [
{
text: `下一步3/${TourConfig.total}`,
action() {
tour.next()
addMyStudyList()
}
}
]
});
watch(
() => loading,
val => {
if (!val) return
_nextTick(async () => {
const Shepherd = await loadJsLib('Shepherd', LIB_JS_URL.SHEPHERD)
const tour = new Shepherd.Tour(TourConfig)
tour.on('cancel', () => {
localStorage.setItem('tour-guide', '1')
})
tour.addStep({
id: 'step3',
text: '点击这里开始学习',
attachTo: { element: '#study', on: 'bottom' },
buttons: [
{
text: `下一步3/${TourConfig.total}`,
action() {
tour.next()
addMyStudyList()
},
},
],
})
tour.addStep({
id: 'step4',
text: '这里可以选择学习模式、设置学习数量、修改学习进度',
attachTo: {element: '#mode', on: 'bottom'},
beforeShowPromise() {
return new Promise((resolve) => {
const timer = setInterval(() => {
if (document.querySelector('#mode')) {
clearInterval(timer);
setTimeout(resolve, 500)
}
}, 100);
});
},
buttons: [
{
text: `下一步4/${TourConfig.total}`,
action() {
tour.next()
startPractice({guide: 1})
}
}
]
});
tour.addStep({
id: 'step4',
text: '这里可以选择学习模式、设置学习数量、修改学习进度',
attachTo: { element: '#mode', on: 'bottom' },
beforeShowPromise() {
return new Promise(resolve => {
const timer = setInterval(() => {
if (document.querySelector('#mode')) {
clearInterval(timer)
setTimeout(resolve, 500)
}
}, 100)
})
},
buttons: [
{
text: `下一步4/${TourConfig.total}`,
action() {
tour.next()
startPractice({ guide: 1 })
},
},
],
})
const r = localStorage.getItem('tour-guide');
if (settingStore.first && !r && !isMobile()) {
tour.start();
}
}, 500)
})
const r = localStorage.getItem('tour-guide')
if (settingStore.first && !r && !isMobile()) {
tour.start()
}
}, 500)
}
)
async function requestList({pageNo, pageSize, searchKey}) {
async function requestList({ pageNo, pageSize, searchKey }) {
if (AppEnv.CAN_REQUEST) {
} else {
let list = allList
let total = allList.length
@@ -450,7 +476,7 @@ async function requestList({pageNo, pageSize, searchKey}) {
total = list.length
}
list = list.slice((pageNo - 1) * pageSize, (pageNo - 1) * pageSize + pageSize)
return {list, total}
return { list, total }
}
}
@@ -463,7 +489,8 @@ function onSort(type: Sort, pageNo: number, pageSize: number) {
} else if ([Sort.random, Sort.randomAll].includes(type)) {
fun = shuffle
}
allList = allList.slice(0, pageSize * (pageNo - 1))
allList = allList
.slice(0, pageSize * (pageNo - 1))
.concat(fun(allList.slice(pageSize * (pageNo - 1), pageSize * (pageNo - 1) + pageSize)))
.concat(allList.slice(pageSize * (pageNo - 1) + pageSize))
runtimeStore.editDict.words = allList
@@ -476,205 +503,221 @@ function onSort(type: Sort, pageNo: number, pageSize: number) {
defineRender(() => {
return (
<BasePage>
{
showBookDetail.value ? <div className="card mb-0 dict-detail-card flex flex-col">
<div class="dict-header flex justify-between items-center relative">
<BackIcon class="dict-back z-2"/>
<div class="dict-title absolute page-title text-align-center w-full">{runtimeStore.editDict.name}</div>
<div class="dict-actions flex">
<BaseButton loading={studyLoading || loading} type="info"
onClick={() => isEdit = true}>编辑</BaseButton>
<BaseButton id="study" loading={studyLoading || loading} onClick={addMyStudyList}>学习</BaseButton>
<BaseButton loading={studyLoading || loading} onClick={startTest}>测试</BaseButton>
{showBookDetail.value ? (
<div className="card mb-0 dict-detail-card flex flex-col">
<div class="dict-header flex justify-between items-center relative">
<BackIcon class="dict-back z-2" />
<div class="dict-title absolute page-title text-align-center w-full">
{runtimeStore.editDict.name}
</div>
<div class="dict-actions flex">
<BaseButton
loading={studyLoading || loading}
type="info"
onClick={() => (isEdit = true)}
>
编辑
</BaseButton>
<BaseButton id="study" loading={studyLoading || loading} onClick={addMyStudyList}>
学习
</BaseButton>
<BaseButton loading={studyLoading || loading} onClick={startTest}>
测试
</BaseButton>
</div>
</div>
<div class="text-lg mt-2">介绍{runtimeStore.editDict.description}</div>
<div class="line my-3"></div>
{/* 移动端标签页导航 */}
{isMob && isOperate && (
<div class="tab-navigation mb-3">
<div
class={`tab-item ${activeTab === 'list' ? 'active' : ''}`}
onClick={() => (activeTab = 'list')}
>
单词列表
</div>
<div
class={`tab-item ${activeTab === 'edit' ? 'active' : ''}`}
onClick={() => (activeTab = 'edit')}
>
{wordForm.id ? '编辑' : '添加'}单词
</div>
</div>
<div class="text-lg mt-2">介绍{runtimeStore.editDict.description}</div>
<div class="line my-3"></div>
)}
{/* 移动端标签页导航 */}
{isMob && isOperate && (
<div class="tab-navigation mb-3">
<div
class={`tab-item ${activeTab === 'list' ? 'active' : ''}`}
onClick={() => activeTab = 'list'}
>
单词列表
</div>
<div
class={`tab-item ${activeTab === 'edit' ? 'active' : ''}`}
onClick={() => activeTab = 'edit'}
>
{wordForm.id ? '编辑' : '添加'}单词
</div>
</div>
)}
<div class="flex flex-1 overflow-hidden content-area">
<div class={`word-list-section ${isMob && isOperate && activeTab !== 'list' ? 'mobile-hidden' : ''}`}>
<BaseTable
ref={tableRef}
class="h-full"
request={requestList}
onDel={batchDel}
onSort={onSort}
onAdd={addWord}
onImport={importData}
onExport={exportData}
exportLoading={exportLoading}
importLoading={importLoading}
>
{
(val) =>
<WordItem
showTransPop={false}
showCollectIcon={false}
showMarkIcon={false}
item={val.item}
>
{{
prefix: () => val.checkbox(val.item),
suffix: () => (
<div class='flex flex-col'>
<BaseIcon
class="option-icon"
onClick={() => editWord(val.item)}
title="编辑">
<IconFluentTextEditStyle20Regular/>
</BaseIcon>
<PopConfirm title="确认删除?"
onConfirm={() => batchDel([val.item.id])}
>
<BaseIcon
class="option-icon"
title="删除">
<DeleteIcon/>
</BaseIcon>
</PopConfirm>
</div>
)
}}
</WordItem>
}
</BaseTable>
</div>
{
isOperate ? (
<div
class={`edit-section flex-1 flex flex-col ${isMob && activeTab !== 'edit' ? 'mobile-hidden' : ''}`}>
<div class="common-title">
{wordForm.id ? '修改' : '添加'}单词
</div>
<Form
class="flex-1 overflow-auto pr-2"
ref={e => wordFormRef = e}
rules={wordRules}
model={wordForm}
label-width="7rem">
<FormItem label="单词" prop="word">
<BaseInput
modelValue={wordForm.word}
onUpdate:modelValue={e => wordForm.word = e}
>
</BaseInput>
</FormItem>
<FormItem label="英音音标">
<BaseInput
modelValue={wordForm.phonetic0}
onUpdate:modelValue={e => wordForm.phonetic0 = e}
/>
</FormItem>
<FormItem label="美音音标">
<BaseInput
modelValue={wordForm.phonetic1}
onUpdate:modelValue={e => wordForm.phonetic1 = e}/>
</FormItem>
<FormItem label="翻译">
<Textarea
modelValue={wordForm.trans}
onUpdate:modelValue={e => wordForm.trans = e}
placeholder="一行一个翻译前面词性后面内容如n.取消);多个翻译请换行"
autosize={{minRows: 6, maxRows: 10}}/>
</FormItem>
<FormItem label="例句">
<Textarea
modelValue={wordForm.sentences}
onUpdate:modelValue={e => wordForm.sentences = e}
placeholder="一行原文,一行译文;多个请换两行"
autosize={{minRows: 6, maxRows: 10}}/>
</FormItem>
<FormItem label="短语">
<Textarea
modelValue={wordForm.phrases}
onUpdate:modelValue={e => wordForm.phrases = e}
placeholder="一行原文,一行译文;多个请换两行"
autosize={{minRows: 6, maxRows: 10}}/>
</FormItem>
<FormItem label="同义词">
<Textarea
modelValue={wordForm.synos}
onUpdate:modelValue={e => wordForm.synos = e}
placeholder="请参考已有单词格式"
autosize={{minRows: 6, maxRows: 20}}/>
</FormItem>
<FormItem label="同根词">
<Textarea
modelValue={wordForm.relWords}
onUpdate:modelValue={e => wordForm.relWords = e}
placeholder="请参考已有单词格式"
autosize={{minRows: 6, maxRows: 20}}/>
</FormItem>
<FormItem label="词源">
<Textarea
modelValue={wordForm.etymology}
onUpdate:modelValue={e => wordForm.etymology = e}
placeholder="请参考已有单词格式"
autosize={{minRows: 6, maxRows: 10}}/>
</FormItem>
</Form>
<div class="center">
<BaseButton
type="info"
onClick={closeWordForm}>关闭
</BaseButton>
<BaseButton type="primary"
onClick={onSubmitWord}>保存
</BaseButton>
</div>
</div>
) : null
}
<div class="flex flex-1 overflow-hidden content-area">
<div
class={`word-list-section ${isMob && isOperate && activeTab !== 'list' ? 'mobile-hidden' : ''}`}
>
<BaseTable
ref={tableRef}
class="h-full"
request={requestList}
onDel={batchDel}
onSort={onSort}
onAdd={addWord}
onImport={importData}
onExport={exportData}
exportLoading={exportLoading}
importLoading={importLoading}
>
{val => (
<WordItem
showTransPop={false}
showCollectIcon={false}
showMarkIcon={false}
item={val.item}
>
{{
prefix: () => val.checkbox(val.item),
suffix: () => (
<div class="flex flex-col">
<BaseIcon
class="option-icon"
onClick={() => editWord(val.item)}
title="编辑"
>
<IconFluentTextEditStyle20Regular />
</BaseIcon>
<PopConfirm title="确认删除?" onConfirm={() => batchDel([val.item.id])}>
<BaseIcon class="option-icon" title="删除">
<DeleteIcon />
</BaseIcon>
</PopConfirm>
</div>
),
}}
</WordItem>
)}
</BaseTable>
</div>
</div> :
<div class="card mb-0 dict-detail-card">
<div class="dict-header flex justify-between items-center relative">
<BackIcon class="dict-back z-2" onClick={() => {
{isOperate ? (
<div
class={`edit-section flex-1 flex flex-col ${isMob && activeTab !== 'edit' ? 'mobile-hidden' : ''}`}
>
<div class="common-title">{wordForm.id ? '修改' : '添加'}单词</div>
<Form
class="flex-1 overflow-auto pr-2"
ref={e => (wordFormRef = e)}
rules={wordRules}
model={wordForm}
label-width="7rem"
>
<FormItem label="单词" prop="word">
<BaseInput
modelValue={wordForm.word}
onUpdate:modelValue={e => (wordForm.word = e)}
></BaseInput>
</FormItem>
<FormItem label="英音音标">
<BaseInput
modelValue={wordForm.phonetic0}
onUpdate:modelValue={e => (wordForm.phonetic0 = e)}
/>
</FormItem>
<FormItem label="美音音标">
<BaseInput
modelValue={wordForm.phonetic1}
onUpdate:modelValue={e => (wordForm.phonetic1 = e)}
/>
</FormItem>
<FormItem label="翻译">
<Textarea
modelValue={wordForm.trans}
onUpdate:modelValue={e => (wordForm.trans = e)}
placeholder="一行一个翻译前面词性后面内容如n.取消);多个翻译请换行"
autosize={{ minRows: 6, maxRows: 10 }}
/>
</FormItem>
<FormItem label="例句">
<Textarea
modelValue={wordForm.sentences}
onUpdate:modelValue={e => (wordForm.sentences = e)}
placeholder="一行原文,一行译文;多个请换两行"
autosize={{ minRows: 6, maxRows: 10 }}
/>
</FormItem>
<FormItem label="短语">
<Textarea
modelValue={wordForm.phrases}
onUpdate:modelValue={e => (wordForm.phrases = e)}
placeholder="一行原文,一行译文;多个请换两行"
autosize={{ minRows: 6, maxRows: 10 }}
/>
</FormItem>
<FormItem label="同义词">
<Textarea
modelValue={wordForm.synos}
onUpdate:modelValue={e => (wordForm.synos = e)}
placeholder="请参考已有单词格式"
autosize={{ minRows: 6, maxRows: 20 }}
/>
</FormItem>
<FormItem label="同根词">
<Textarea
modelValue={wordForm.relWords}
onUpdate:modelValue={e => (wordForm.relWords = e)}
placeholder="请参考已有单词格式"
autosize={{ minRows: 6, maxRows: 20 }}
/>
</FormItem>
<FormItem label="词源">
<Textarea
modelValue={wordForm.etymology}
onUpdate:modelValue={e => (wordForm.etymology = e)}
placeholder="请参考已有单词格式"
autosize={{ minRows: 6, maxRows: 10 }}
/>
</FormItem>
</Form>
<div class="center">
<BaseButton type="info" onClick={closeWordForm}>
关闭
</BaseButton>
<BaseButton type="primary" onClick={onSubmitWord}>
保存
</BaseButton>
</div>
</div>
) : null}
</div>
</div>
) : (
<div class="card mb-0 dict-detail-card">
<div class="dict-header flex justify-between items-center relative">
<BackIcon
class="dict-back z-2"
onClick={() => {
if (isAdd) {
router.back()
} else {
isEdit = false
}
}}/>
<div class="dict-title absolute page-title text-align-center w-full">
{runtimeStore.editDict.id ? '修改' : '创建'}词典
</div>
</div>
<div class="center">
<EditBook
isAdd={isAdd}
isBook={false}
onClose={formClose}
onSubmit={() => isEdit = isAdd = false}
/>
}}
/>
<div class="dict-title absolute page-title text-align-center w-full">
{runtimeStore.editDict.id ? '修改' : '创建'}词典
</div>
</div>
}
<div class="center">
<EditBook
isAdd={isAdd}
isBook={false}
onClose={formClose}
onSubmit={() => (isEdit = isAdd = false)}
/>
</div>
</div>
)}
<PracticeSettingDialog
showLeftOption
modelValue={showPracticeSettingDialog}
onUpdate:modelValue={val => (showPracticeSettingDialog = val)}
onOk={startPractice}/>
onOk={startPractice}
/>
</BasePage>
)
})