2
components.d.ts
vendored
2
components.d.ts
vendored
@@ -80,6 +80,7 @@ declare module 'vue' {
|
||||
IconFluentDismiss20Regular: typeof import('~icons/fluent/dismiss20-regular')['default']
|
||||
IconFluentDismissCircle16Regular: typeof import('~icons/fluent/dismiss-circle16-regular')['default']
|
||||
IconFluentDismissCircle20Filled: typeof import('~icons/fluent/dismiss-circle20-filled')['default']
|
||||
IconFluentDocument20Regular: typeof import('~icons/fluent/document20-regular')['default']
|
||||
IconFluentErrorCircle20Filled: typeof import('~icons/fluent/error-circle20-filled')['default']
|
||||
IconFluentErrorCircle20Regular: typeof import('~icons/fluent/error-circle20-regular')['default']
|
||||
IconFluentEye16Regular: typeof import('~icons/fluent/eye16-regular')['default']
|
||||
@@ -145,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']
|
||||
|
||||
@@ -25,7 +25,6 @@ let isInitializing = true // 标记是否正在初始化
|
||||
watch(store.$state, (n: BaseState) => {
|
||||
// 如果正在初始化,不保存数据,避免覆盖
|
||||
if (isInitializing) return
|
||||
console.log('watch')
|
||||
let data = shakeCommonDict(n)
|
||||
set(SAVE_DICT_KEY.key, JSON.stringify({val: data, version: SAVE_DICT_KEY.version}))
|
||||
|
||||
@@ -132,4 +131,4 @@ onMounted(() => {
|
||||
v-model="showTransfer"
|
||||
@ok="init"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
10
src/apis/dict.ts
Normal file
10
src/apis/dict.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import http from '@/utils/http.ts'
|
||||
import { Dict } from '@/types/types.ts'
|
||||
|
||||
export function copyOfficialDict(params?, data?) {
|
||||
return http<Dict>('dict/copyOfficialDict', data, params, 'post')
|
||||
}
|
||||
|
||||
export function deleteDict(params?, data?) {
|
||||
return http<Dict>('dict/delete', data, params, 'post')
|
||||
}
|
||||
6
src/apis/words.ts
Normal file
6
src/apis/words.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import http from '@/utils/http.ts'
|
||||
import { Dict } from '@/types/types.ts'
|
||||
|
||||
export function wordDelete(params?, data?) {
|
||||
return http<Dict>('word/delete', data, params, 'post')
|
||||
}
|
||||
@@ -1,45 +1,49 @@
|
||||
<script setup lang="tsx">
|
||||
|
||||
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 { debounce } from "@/utils";
|
||||
import PopConfirm from "@/components/PopConfirm.vue";
|
||||
import Empty from "@/components/Empty.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'
|
||||
import BaseButton from '@/components/BaseButton.vue'
|
||||
import { debounce } from '@/utils'
|
||||
import PopConfirm from '@/components/PopConfirm.vue'
|
||||
import Empty from '@/components/Empty.vue'
|
||||
import Pagination from '@/components/base/Pagination.vue'
|
||||
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 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'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
loading?: boolean
|
||||
showToolbar?: boolean
|
||||
showPagination?: boolean
|
||||
exportLoading?: boolean
|
||||
importLoading?: boolean
|
||||
request?: Function
|
||||
list?: any[]
|
||||
}>(), {
|
||||
loading: true,
|
||||
showToolbar: true,
|
||||
showPagination: true,
|
||||
exportLoading: false,
|
||||
importLoading: false,
|
||||
})
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
loading?: boolean
|
||||
showToolbar?: boolean
|
||||
showPagination?: boolean
|
||||
exportLoading?: boolean
|
||||
importLoading?: boolean
|
||||
request?: Function
|
||||
list?: any[]
|
||||
}>(),
|
||||
{
|
||||
loading: true,
|
||||
showToolbar: true,
|
||||
showPagination: true,
|
||||
exportLoading: false,
|
||||
importLoading: false,
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
add: []
|
||||
click: [val: {
|
||||
item: any,
|
||||
index: number
|
||||
}],
|
||||
click: [
|
||||
val: {
|
||||
item: any
|
||||
index: number
|
||||
},
|
||||
]
|
||||
import: [e: Event]
|
||||
export: []
|
||||
del: [ids: number[]],
|
||||
del: [ids: number[]]
|
||||
sort: [type: Sort, pageNo: number, pageSize: number]
|
||||
}>()
|
||||
|
||||
@@ -89,7 +93,7 @@ let showSortDialog = $ref(false)
|
||||
let showSearchInput = $ref(false)
|
||||
let showImportDialog = $ref(false)
|
||||
|
||||
const closeImportDialog = () => showImportDialog = false
|
||||
const closeImportDialog = () => (showImportDialog = false)
|
||||
|
||||
function sort(type: Sort) {
|
||||
if ([Sort.reverse, Sort.random].includes(type)) {
|
||||
@@ -111,7 +115,7 @@ defineExpose({
|
||||
scrollToBottom,
|
||||
scrollToItem,
|
||||
closeImportDialog,
|
||||
getData
|
||||
getData,
|
||||
})
|
||||
|
||||
let loading2 = $ref(false)
|
||||
@@ -122,7 +126,7 @@ let params = $ref({
|
||||
total: 0,
|
||||
list: [],
|
||||
sortType: null,
|
||||
searchKey: ''
|
||||
searchKey: '',
|
||||
})
|
||||
|
||||
function search(key: string) {
|
||||
@@ -159,182 +163,181 @@ function handlePageNo(e) {
|
||||
|
||||
onMounted(async () => {
|
||||
getData()
|
||||
|
||||
})
|
||||
|
||||
defineRender(
|
||||
() => {
|
||||
const d = (item) => <Checkbox
|
||||
modelValue={selectIds.includes(item.id)}
|
||||
onChange={() => toggleSelect(item)}
|
||||
size="large"/>
|
||||
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={params.searchKey}
|
||||
onUpdate:modelValue={debounce(e => search(e), 500)}
|
||||
class="flex-1"
|
||||
autofocus>
|
||||
{{
|
||||
subfix: () => <IconFluentSearch24Regular
|
||||
class="text-lg text-gray"
|
||||
/>
|
||||
}}
|
||||
</BaseInput>
|
||||
<BaseButton onClick={cancelSearch}>取消</BaseButton>
|
||||
</div>
|
||||
) : (
|
||||
<div class="flex justify-between">
|
||||
<div class="flex gap-2 items-center">
|
||||
<Checkbox
|
||||
disabled={!params.list.length}
|
||||
onChange={() => toggleSelectAll()}
|
||||
modelValue={selectAll}
|
||||
size="large"/>
|
||||
<span>{selectIds.length} / {params.total}</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('export')}
|
||||
title="导出">
|
||||
{props.exportLoading ? <IconEosIconsLoading/> : <IconPhExportLight/>}
|
||||
</BaseIcon>
|
||||
<BaseIcon
|
||||
onClick={() => emit('add')}
|
||||
title="添加单词">
|
||||
<IconFluentAdd20Regular/>
|
||||
</BaseIcon>
|
||||
<BaseIcon
|
||||
disabled={!params.list.length}
|
||||
title="改变顺序"
|
||||
onClick={() => showSortDialog = !showSortDialog}
|
||||
>
|
||||
<IconFluentArrowSort20Regular/>
|
||||
</BaseIcon>
|
||||
<BaseIcon
|
||||
disabled={!params.list.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="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>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
}
|
||||
{
|
||||
loading2 ?
|
||||
<div class="h-full w-full center text-4xl">
|
||||
<IconEosIconsLoading color="gray"/>
|
||||
</div>
|
||||
: params.list.length ? (
|
||||
<>
|
||||
<div class="flex-1 overflow-auto"
|
||||
ref={e => listRef = e}>
|
||||
{params.list.map((item, index) => {
|
||||
return (
|
||||
<div class="list-item-wrapper"
|
||||
key={item.word}
|
||||
>
|
||||
{s.default({
|
||||
checkbox: d,
|
||||
item,
|
||||
index: (params.pageSize * (params.pageNo - 1)) + index + 1
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{
|
||||
props.showPagination && <div class="flex justify-end">
|
||||
<Pagination
|
||||
currentPage={params.pageNo}
|
||||
onUpdate:current-page={handlePageNo}
|
||||
pageSize={params.pageSize}
|
||||
onUpdate:page-size={(e) => params.pageSize = e}
|
||||
pageSizes={[20, 50, 100, 200]}
|
||||
layout="total,sizes"
|
||||
total={params.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('import', e)}
|
||||
class="w-0 h-0 opacity-0"/>
|
||||
</div>
|
||||
return (
|
||||
<div class="flex flex-col gap-3">
|
||||
{props.showToolbar && (
|
||||
<div>
|
||||
{showSearchInput ? (
|
||||
<div class="flex gap-4">
|
||||
<BaseInput
|
||||
clearable
|
||||
modelValue={params.searchKey}
|
||||
onUpdate:modelValue={debounce(e => search(e), 500)}
|
||||
class="flex-1"
|
||||
autofocus
|
||||
>
|
||||
{{
|
||||
subfix: () => <IconFluentSearch24Regular class="text-lg text-gray" />,
|
||||
}}
|
||||
</BaseInput>
|
||||
<BaseButton onClick={cancelSearch}>取消</BaseButton>
|
||||
</div>
|
||||
) : (
|
||||
<div class="flex justify-between">
|
||||
<div class="flex gap-2 items-center">
|
||||
<Checkbox
|
||||
disabled={!params.list.length}
|
||||
onChange={() => toggleSelectAll()}
|
||||
modelValue={selectAll}
|
||||
size="large"
|
||||
/>
|
||||
<span>
|
||||
{selectIds.length} / {params.total}
|
||||
</span>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
<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('export')} title="导出">
|
||||
{props.exportLoading ? <IconEosIconsLoading /> : <IconPhExportLight />}
|
||||
</BaseIcon>
|
||||
<BaseIcon onClick={() => emit('add')} title="添加单词">
|
||||
<IconFluentAdd20Regular />
|
||||
</BaseIcon>
|
||||
<BaseIcon
|
||||
disabled={!params.list.length}
|
||||
title="改变顺序"
|
||||
onClick={() => (showSortDialog = !showSortDialog)}
|
||||
>
|
||||
<IconFluentArrowSort20Regular />
|
||||
</BaseIcon>
|
||||
<BaseIcon
|
||||
disabled={!params.list.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="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>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div class="relative flex-1 overflow-hidden">
|
||||
{params.list.length ? (
|
||||
<div class="overflow-auto h-full" ref={e => (listRef = e)}>
|
||||
{params.list.map((item, index) => {
|
||||
return (
|
||||
<div class="list-item-wrapper" key={item.word}>
|
||||
{s.default({
|
||||
checkbox: d,
|
||||
item,
|
||||
index: params.pageSize * (params.pageNo - 1) + index + 1,
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
) : !loading2 ? (
|
||||
<Empty />
|
||||
) : null}
|
||||
{loading2 && (
|
||||
<div class="absolute top-0 left-0 bottom-0 right-0 bg-black bg-op-10 center text-4xl">
|
||||
<IconEosIconsLoading color="gray" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{props.showPagination && (
|
||||
<div class="flex justify-end">
|
||||
<Pagination
|
||||
currentPage={params.pageNo}
|
||||
onUpdate:current-page={handlePageNo}
|
||||
pageSize={params.pageSize}
|
||||
onUpdate:page-size={e => (params.pageSize = e)}
|
||||
pageSizes={[20, 50, 100, 200]}
|
||||
layout="total,sizes"
|
||||
total={params.total}
|
||||
/>
|
||||
</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('import', e)}
|
||||
class="w-0 h-0 opacity-0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
</script>
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
75
src/components/ResourceCard.vue
Normal file
75
src/components/ResourceCard.vue
Normal 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>
|
||||
@@ -1,4 +1,4 @@
|
||||
import { offset } from "@floating-ui/dom";
|
||||
import { offset } from '@floating-ui/dom'
|
||||
|
||||
export const GITHUB = 'https://github.com/zyronon/TypeWords'
|
||||
export const Host = 'typewords.cc'
|
||||
@@ -7,25 +7,27 @@ export const Origin = `https://${Host}`
|
||||
export const APP_NAME = 'Type Words'
|
||||
|
||||
const common = {
|
||||
word_dict_list_version: 1
|
||||
word_dict_list_version: 1,
|
||||
}
|
||||
const map = {
|
||||
DEV: {
|
||||
API: 'http://localhost/',
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export const ENV = Object.assign(map['DEV'], common)
|
||||
|
||||
export let AppEnv = {
|
||||
TOKEN: localStorage.getItem('token') ?? '',
|
||||
IS_OFFICIAL: false,
|
||||
IS_OFFICIAL: true,
|
||||
IS_LOGIN: false,
|
||||
CAN_REQUEST: false
|
||||
CAN_REQUEST: false,
|
||||
}
|
||||
|
||||
AppEnv.IS_LOGIN = !!AppEnv.TOKEN
|
||||
AppEnv.CAN_REQUEST = AppEnv.IS_LOGIN && AppEnv.IS_OFFICIAL
|
||||
// AppEnv.IS_OFFICIAL = true
|
||||
// AppEnv.CAN_REQUEST = true
|
||||
// console.log('AppEnv.CAN_REQUEST',AppEnv.CAN_REQUEST)
|
||||
|
||||
export const RESOURCE_PATH = ENV.API + 'static'
|
||||
@@ -38,41 +40,41 @@ export const DICT_LIST = {
|
||||
ARTICLE: {
|
||||
ALL: `/list/article.json`,
|
||||
RECOMMENDED: `/list/article.json`,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export const SoundFileOptions = [
|
||||
{value: '机械键盘', label: '机械键盘'},
|
||||
{value: '机械键盘1', label: '机械键盘1'},
|
||||
{value: '机械键盘2', label: '机械键盘2'},
|
||||
{value: '老式机械键盘', label: '老式机械键盘'},
|
||||
{value: '笔记本键盘', label: '笔记本键盘'},
|
||||
{ value: '机械键盘', label: '机械键盘' },
|
||||
{ value: '机械键盘1', label: '机械键盘1' },
|
||||
{ value: '机械键盘2', label: '机械键盘2' },
|
||||
{ value: '老式机械键盘', label: '老式机械键盘' },
|
||||
{ value: '笔记本键盘', label: '笔记本键盘' },
|
||||
]
|
||||
export const APP_VERSION = {
|
||||
key: 'type-words-app-version',
|
||||
version: 2
|
||||
version: 2,
|
||||
}
|
||||
export const SAVE_DICT_KEY = {
|
||||
key: 'typing-word-dict',
|
||||
version: 4
|
||||
version: 4,
|
||||
}
|
||||
export const SAVE_SETTING_KEY = {
|
||||
key: 'typing-word-setting',
|
||||
version: 17
|
||||
version: 17,
|
||||
}
|
||||
export const EXPORT_DATA_KEY = {
|
||||
key: 'typing-word-export',
|
||||
version: 4
|
||||
version: 4,
|
||||
}
|
||||
export const LOCAL_FILE_KEY = 'typing-word-files'
|
||||
|
||||
export const PracticeSaveWordKey = {
|
||||
key: 'PracticeSaveWord',
|
||||
version: 1
|
||||
version: 1,
|
||||
}
|
||||
export const PracticeSaveArticleKey = {
|
||||
key: 'PracticeSaveArticle',
|
||||
version: 1
|
||||
version: 1,
|
||||
}
|
||||
|
||||
export const TourConfig = {
|
||||
@@ -80,21 +82,22 @@ export const TourConfig = {
|
||||
defaultStepOptions: {
|
||||
canClickTarget: false,
|
||||
classes: 'shadow-md bg-purple-dark',
|
||||
cancelIcon: {enabled: true},
|
||||
cancelIcon: { enabled: true },
|
||||
modalOverlayOpeningPadding: 10,
|
||||
modalOverlayOpeningRadius: 6,
|
||||
floatingUIOptions: {
|
||||
middleware: [offset({mainAxis: 30})]
|
||||
middleware: [offset({ mainAxis: 30 })],
|
||||
},
|
||||
},
|
||||
total: 7
|
||||
total: 7,
|
||||
}
|
||||
|
||||
export const LIB_JS_URL = {
|
||||
SHEPHERD: import.meta.env.MODE === 'development' ?
|
||||
'https://cdn.jsdelivr.net/npm/shepherd.js@14.5.1/dist/esm/shepherd.mjs'
|
||||
: Origin + '/libs/Shepherd.14.5.1.mjs',
|
||||
SHEPHERD:
|
||||
import.meta.env.MODE === 'development'
|
||||
? 'https://cdn.jsdelivr.net/npm/shepherd.js@14.5.1/dist/esm/shepherd.mjs'
|
||||
: Origin + '/libs/Shepherd.14.5.1.mjs',
|
||||
SNAPDOM: `${Origin}/libs/snapdom.min.js`,
|
||||
JSZIP: `${Origin}/libs/jszip.min.js`,
|
||||
XLSX: `${Origin}/libs/xlsx.full.min.js`,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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('修改成功并加入我的词典')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,363 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import BasePage from '@/components/BasePage.vue'
|
||||
import BaseButton from '@/components/BaseButton.vue'
|
||||
import ResourceCard from '@/components/ResourceCard.vue'
|
||||
|
||||
import BasePage from "@/components/BasePage.vue";
|
||||
// 类型定义
|
||||
interface Resource {
|
||||
name: string
|
||||
description?: string
|
||||
difficulty?: string
|
||||
link: string
|
||||
author?: string
|
||||
features?: string
|
||||
suitable?: string
|
||||
}
|
||||
|
||||
interface Subcategory {
|
||||
name: string
|
||||
description?: string
|
||||
resources: Resource[]
|
||||
}
|
||||
|
||||
interface Category {
|
||||
id: string
|
||||
name: string
|
||||
icon?: string
|
||||
description?: string
|
||||
resources?: Resource[]
|
||||
subcategories?: Subcategory[]
|
||||
}
|
||||
|
||||
// 资源分类
|
||||
const categories = ref<Category[]>([
|
||||
{
|
||||
id: 'new-concept',
|
||||
name: '新概念英语',
|
||||
description: '经典英语教材,适合系统学习',
|
||||
resources: [
|
||||
{
|
||||
name: '新概念英语第一册',
|
||||
description: '适合英语初学者',
|
||||
difficulty: '入门',
|
||||
link: 'https://pan.quark.cn/s/0d2ece46983f',
|
||||
},
|
||||
{
|
||||
name: '新概念英语第二册',
|
||||
description: '基础英语学习,巩固语法和词汇',
|
||||
difficulty: '基础',
|
||||
link: 'https://pan.quark.cn/s/df29bb396728',
|
||||
},
|
||||
{
|
||||
name: '新概念英语第三册',
|
||||
description: '提高英语水平,增强阅读能力',
|
||||
difficulty: '进阶',
|
||||
link: 'https://pan.quark.cn/s/ec39dc7cbe5b',
|
||||
},
|
||||
{
|
||||
name: '新概念英语第四册',
|
||||
description: '高级英语学习,提升综合能力',
|
||||
difficulty: '高级',
|
||||
link: 'https://pan.quark.cn/s/207a2cc8a320',
|
||||
},
|
||||
{
|
||||
name: '新概念英青少年版',
|
||||
description: '儿童读物',
|
||||
difficulty: '7岁至14岁',
|
||||
link: 'https://pan.quark.cn/s/4628b00b39c0',
|
||||
},
|
||||
{
|
||||
name: '新概念英语1-4 教材高清 PDF',
|
||||
description: '仅 1-4 册的教材高清扫描版 PDF',
|
||||
difficulty: '',
|
||||
link: 'https://pan.quark.cn/s/f1e7739ed806',
|
||||
},
|
||||
{
|
||||
name: '新概念讲解视频',
|
||||
description: '多家机构/个人的讲解视频',
|
||||
difficulty: '',
|
||||
link: 'https://pan.quark.cn/s/07e25ee6de9f',
|
||||
},
|
||||
{
|
||||
name: '新概念资源合集',
|
||||
description: '',
|
||||
difficulty: '包含前面所有的内容',
|
||||
link: 'https://pan.quark.cn/s/6b12da160020',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'exam',
|
||||
name: '电视/电影',
|
||||
description: '一些不错的美/英剧,可练听力和口语',
|
||||
resources: [
|
||||
{
|
||||
name: '老友记',
|
||||
description: '',
|
||||
difficulty: '喜剧/爱情',
|
||||
link: 'https://pan.quark.cn/s/c17770edfa15',
|
||||
},
|
||||
{
|
||||
name: '生活大爆炸',
|
||||
description: '',
|
||||
difficulty: '喜剧/爱情',
|
||||
link: 'https://pan.quark.cn/s/3e66da8ce1c4',
|
||||
},
|
||||
{
|
||||
name: '是大臣/是首相',
|
||||
description: '',
|
||||
difficulty: '喜剧/讽刺',
|
||||
link: 'https://pan.quark.cn/s/2c62ce3e220d',
|
||||
},
|
||||
{
|
||||
name: '破产姐妹',
|
||||
description: '',
|
||||
difficulty: '喜剧',
|
||||
link: 'https://pan.quark.cn/s/018600971998',
|
||||
},
|
||||
{
|
||||
name: '绝望主妇',
|
||||
description: '',
|
||||
difficulty: '悬疑',
|
||||
link: 'https://pan.quark.cn/s/1c67ae200c2e',
|
||||
},
|
||||
{
|
||||
name: '纸牌屋',
|
||||
description: '',
|
||||
difficulty: '纸牌屋',
|
||||
link: 'https://pan.quark.cn/s/5ba146c46180',
|
||||
},
|
||||
{
|
||||
name: '电视/电影资源合集',
|
||||
difficulty: '包含前面所有的内容',
|
||||
link: 'https://pan.quark.cn/s/84ecb30b700b', //159
|
||||
// link: 'https://pan.quark.cn/s/e9b62b79c48c',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'grammar',
|
||||
name: '语法学习',
|
||||
description: '',
|
||||
subcategories: [
|
||||
{
|
||||
name: '经典教材',
|
||||
description: '',
|
||||
resources: [
|
||||
{
|
||||
name: '英语语法新思维',
|
||||
author: '张满胜',
|
||||
features:
|
||||
'从思维角度讲解语法,注重理解而非死记硬背,分为初级、中级、高级三册,循序渐进',
|
||||
suitable: '希望系统建立语法体系的学习者',
|
||||
difficulty: '',
|
||||
link: 'https://pan.quark.cn/s/d06abef6c737',
|
||||
},
|
||||
{
|
||||
name: '薄冰英语语法',
|
||||
author: '薄冰',
|
||||
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.quark.cn/s/0d0de559794e',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '进阶提升',
|
||||
description: '',
|
||||
resources: [
|
||||
{
|
||||
name: '剑桥英语语法(English Grammar in Use)',
|
||||
author: '剑桥大学出版',
|
||||
features: '分为初级、中级、高级三册,经典畅销语法自学书,解释简明且有大量练习',
|
||||
suitable: '需要结合国际考试的学习者',
|
||||
description: '',
|
||||
difficulty: '中文版',
|
||||
link: 'https://pan.quark.cn/s/d4a6ef53c04d',
|
||||
},
|
||||
{
|
||||
name: 'Oxford English Grammar(牛津英语语法)',
|
||||
author: 'Sidney Greenbaum & Gerald Nelson',
|
||||
features:
|
||||
'分为基础、提升、高级三册,英式语法权威,解释清晰、例句地道,适合备考雅思/托福',
|
||||
suitable: '想全面系统梳理语法体系的人',
|
||||
difficulty: '英文版',
|
||||
link: 'https://pan.quark.cn/s/ca505875e68c',
|
||||
},
|
||||
{
|
||||
name: '实用英语用法(Practical English Usage)',
|
||||
author: 'Michael Swan',
|
||||
features: '解释非常细致,尤其适合纠正常见错误和困惑',
|
||||
suitable: '中高级学习者,适合作为语法问题的工具书',
|
||||
difficulty: '中文版/英文版',
|
||||
link: 'https://pan.quark.cn/s/05006e705a77',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'listening',
|
||||
name: '听力训练',
|
||||
description: '提升英语听力水平',
|
||||
resources: [
|
||||
{
|
||||
name: 'VOA慢速英语合集',
|
||||
description: '新闻类听力材料,语速适中,内容丰富',
|
||||
difficulty: '初级',
|
||||
link: 'https://pan.quark.cn/s/681794bffc6e',
|
||||
},
|
||||
// {
|
||||
// name: 'BBC Learning English',
|
||||
// description: 'BBC官方英语学习资源,涵盖多方面内容',
|
||||
// difficulty: '中高级',
|
||||
// link: 'https://pan.baidu.com/s/xxx',
|
||||
// },
|
||||
{
|
||||
name: 'TED-ED 科普动画',
|
||||
description: 'TED-Ed 是一个专为初高中生所设计的在3到5分钟长的科普动画課程',
|
||||
difficulty: '初级',
|
||||
link: 'https://pan.quark.cn/s/d3d83038afb9',
|
||||
},
|
||||
{
|
||||
name: '哈弗演讲',
|
||||
description: '高质量演讲,锻炼听力同时开拓视野',
|
||||
difficulty: '中高级',
|
||||
link: 'https://pan.quark.cn/s/62e8d536a34f',
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
// 当前选中的分类
|
||||
const selectedCategory = ref('all')
|
||||
|
||||
// 筛选后的资源
|
||||
const filteredResources = computed(() => {
|
||||
if (selectedCategory.value === 'all') {
|
||||
return categories.value
|
||||
}
|
||||
return categories.value.filter(cat => cat.id === selectedCategory.value)
|
||||
})
|
||||
|
||||
// 跳转到网盘链接
|
||||
const openLink = (url: string) => {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasePage>
|
||||
<div class="center">
|
||||
<div class="card qa w-2/3">
|
||||
<div class="font-bold text-2xl mb-6">分享个人收藏的一些学习资料</div>
|
||||
<div class="list">
|
||||
<div class="title">新概念相关</div>
|
||||
<div class="line"></div>
|
||||
<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>
|
||||
<p class="text-lg text-gray-600 dark:text-gray-300 max-w-3xl mx-auto">
|
||||
以下是整理的一些英语学习资源,希望对大家有所帮助!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 分类筛选 -->
|
||||
<div class="card-white flex flex-wrap justify-center gap-2 mb-8 p-4">
|
||||
<BaseButton
|
||||
:type="selectedCategory === 'all' ? 'primary' : 'info'"
|
||||
@click="selectedCategory = 'all'"
|
||||
>
|
||||
全部资源
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
v-for="category in categories"
|
||||
:key="category.id"
|
||||
:type="selectedCategory === category.id ? 'primary' : 'info'"
|
||||
@click="selectedCategory = category.id"
|
||||
>
|
||||
{{ category.name }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
|
||||
<!-- 资源列表 -->
|
||||
<div class="w-full">
|
||||
<div v-for="category in filteredResources" :key="category.id" class="mb-12">
|
||||
<div class="text-center mb-6">
|
||||
<h2 class="text-2xl font-bold mb-2">{{ category.icon }} {{ category.name }}</h2>
|
||||
<p v-if="category.description" class="text-gray-600 dark:text-gray-300">
|
||||
{{ category.description }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 如果有子分类,显示子分类 -->
|
||||
<template v-if="category.subcategories">
|
||||
<div
|
||||
v-for="subcategory in category.subcategories"
|
||||
:key="subcategory.name"
|
||||
class="mb-10"
|
||||
>
|
||||
<div class="text-center mb-4">
|
||||
<h3 class="text-xl font-semibold mb-2 text-gray-800 dark:text-gray-200">
|
||||
{{ subcategory.name }}
|
||||
</h3>
|
||||
<p v-if="subcategory.description" class="text-gray-600 dark:text-gray-300">
|
||||
{{ subcategory.description }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||
<ResourceCard
|
||||
v-for="resource in subcategory.resources"
|
||||
:key="resource.name"
|
||||
:resource="resource"
|
||||
@openLink="openLink"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 如果没有子分类,直接显示资源 -->
|
||||
<template v-else>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||
<ResourceCard
|
||||
v-for="resource in category.resources"
|
||||
:key="resource.name"
|
||||
:resource="resource"
|
||||
@openLink="openLink"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 页面底部 -->
|
||||
<div class="mt-12 pt-8 border-t border-gray-200 dark:border-gray-700">
|
||||
<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>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BasePage>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
</style>
|
||||
@@ -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>
|
||||
|
||||
@@ -1,111 +1,128 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {ShortcutKey} from "@/types/types.ts";
|
||||
import Logo from "@/components/Logo.vue";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {useRouter} from "vue-router";
|
||||
import useTheme from "@/hooks/theme.ts";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import { jump2Feedback } from "@/utils";
|
||||
import { ShortcutKey } from '@/types/types.ts'
|
||||
import Logo from '@/components/Logo.vue'
|
||||
import { useSettingStore } from '@/stores/setting.ts'
|
||||
import { useRouter } from 'vue-router'
|
||||
import useTheme from '@/hooks/theme.ts'
|
||||
import BaseIcon from '@/components/BaseIcon.vue'
|
||||
import { useRuntimeStore } from '@/stores/runtime.ts'
|
||||
import { jump2Feedback } from '@/utils'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const runtimeStore = useRuntimeStore()
|
||||
const router = useRouter()
|
||||
const {toggleTheme, getTheme} = useTheme()
|
||||
const { toggleTheme, getTheme } = useTheme()
|
||||
|
||||
//首页为了seo被剥离出去了,现在是一个静态页面,用nginx 重定向控制对应的跳转
|
||||
function goHome() {
|
||||
window.location.href = '/';
|
||||
window.location.href = '/'
|
||||
}
|
||||
</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" :class="{ expand: settingStore.sideExpand }"></div>
|
||||
<div class="aside anim fixed" :class="{ expand: settingStore.sideExpand }">
|
||||
<div class="top">
|
||||
<Logo v-if="settingStore.sideExpand"/>
|
||||
<Logo v-if="settingStore.sideExpand" />
|
||||
<div class="row" @click="goHome">
|
||||
<IconFluentHome20Regular/>
|
||||
<IconFluentHome20Regular />
|
||||
<span v-if="settingStore.sideExpand">主页</span>
|
||||
</div>
|
||||
<div class="row" @click="router.push('/words')">
|
||||
<IconFluentTextUnderlineDouble20Regular/>
|
||||
<IconFluentTextUnderlineDouble20Regular />
|
||||
<span v-if="settingStore.sideExpand">单词</span>
|
||||
</div>
|
||||
<div id="article" class="row" @click="router.push('/articles')">
|
||||
<div id="article" class="row" @click="router.push('/articles')">
|
||||
<!-- <IconPhArticleNyTimes/>-->
|
||||
<IconFluentBookLetter20Regular/>
|
||||
<IconFluentBookLetter20Regular />
|
||||
<span v-if="settingStore.sideExpand">文章</span>
|
||||
</div>
|
||||
<div class="row" @click="router.push('/setting')">
|
||||
<IconFluentSettings20Regular/>
|
||||
<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/>
|
||||
<IconFluentCommentEdit20Regular />
|
||||
<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/>
|
||||
<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('/user')">-->
|
||||
<!-- <IconFluentPerson20Regular/>-->
|
||||
<!-- <span v-if="settingStore.sideExpand">用户</span>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="row" @click="router.push('/user')">-->
|
||||
<!-- <IconFluentPerson20Regular/>-->
|
||||
<!-- <span v-if="settingStore.sideExpand">用户</span>-->
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
<div class="bottom flex justify-evenly ">
|
||||
<BaseIcon
|
||||
@click="settingStore.sideExpand = !settingStore.sideExpand">
|
||||
<IconFluentChevronLeft20Filled v-if="settingStore.sideExpand"/>
|
||||
<IconFluentChevronLeft20Filled class="transform-rotate-180" v-else/>
|
||||
<div class="bottom flex justify-evenly">
|
||||
<BaseIcon @click="settingStore.sideExpand = !settingStore.sideExpand">
|
||||
<IconFluentChevronLeft20Filled v-if="settingStore.sideExpand" />
|
||||
<IconFluentChevronLeft20Filled class="transform-rotate-180" v-else />
|
||||
</BaseIcon>
|
||||
<BaseIcon
|
||||
v-if="settingStore.sideExpand"
|
||||
:title="`切换主题(${settingStore.shortcutKeyMap[ShortcutKey.ToggleTheme]})`"
|
||||
@click="toggleTheme"
|
||||
>
|
||||
<IconFluentWeatherMoon16Regular v-if="getTheme() === 'light'"/>
|
||||
<IconFluentWeatherSunny16Regular v-else/>
|
||||
<IconFluentWeatherMoon16Regular v-if="getTheme() === 'light'" />
|
||||
<IconFluentWeatherSunny16Regular v-else />
|
||||
</BaseIcon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 移动端顶部菜单栏 -->
|
||||
<div class="mobile-top-nav" :class="{'collapsed': settingStore.mobileNavCollapsed}">
|
||||
<div class="mobile-top-nav" :class="{ collapsed: settingStore.mobileNavCollapsed }">
|
||||
<div class="nav-items">
|
||||
<div class="nav-item" @click="router.push('/')" :class="{'active': $route.path === '/'}">
|
||||
<IconFluentHome20Regular/>
|
||||
<div class="nav-item" @click="router.push('/')" :class="{ active: $route.path === '/' }">
|
||||
<IconFluentHome20Regular />
|
||||
<span>主页</span>
|
||||
</div>
|
||||
<div class="nav-item" @click="router.push('/words')" :class="{'active': $route.path.includes('/words')}">
|
||||
<IconFluentTextUnderlineDouble20Regular/>
|
||||
<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')}">
|
||||
<IconFluentBookLetter20Regular/>
|
||||
<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'}">
|
||||
<IconFluentSettings20Regular/>
|
||||
<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">
|
||||
<IconFluentChevronDown20Filled v-if="!settingStore.mobileNavCollapsed"/>
|
||||
<IconFluentChevronUp20Filled v-else/>
|
||||
<div
|
||||
class="nav-toggle"
|
||||
@click="settingStore.mobileNavCollapsed = !settingStore.mobileNavCollapsed"
|
||||
>
|
||||
<IconFluentChevronDown20Filled v-if="!settingStore.mobileNavCollapsed" />
|
||||
<IconFluentChevronUp20Filled v-else />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="flex-1 z-1 relative main-content">
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
@@ -134,7 +151,7 @@ function goHome() {
|
||||
|
||||
.row {
|
||||
@apply cursor-pointer rounded-md text p-2 my-2 flex items-center gap-2 relative shrink-0;
|
||||
transition: all .5s;
|
||||
transition: all 0.5s;
|
||||
|
||||
&:hover {
|
||||
background: var(--btn-primary);
|
||||
@@ -167,12 +184,12 @@ function goHome() {
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
|
||||
.nav-items {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding: 0.5rem 0;
|
||||
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -184,29 +201,30 @@ function goHome() {
|
||||
min-width: 44px;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
|
||||
|
||||
svg {
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 0.2rem;
|
||||
color: var(--color-main-text);
|
||||
}
|
||||
|
||||
|
||||
span {
|
||||
font-size: 0.7rem;
|
||||
color: var(--color-main-text);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
&.active {
|
||||
svg, span {
|
||||
svg,
|
||||
span {
|
||||
color: var(--color-select-bg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
|
||||
.red-point {
|
||||
position: absolute;
|
||||
top: 0.2rem;
|
||||
@@ -218,7 +236,7 @@ function goHome() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.nav-toggle {
|
||||
position: absolute;
|
||||
bottom: -1.5rem;
|
||||
@@ -231,20 +249,20 @@ function goHome() {
|
||||
padding: 0.3rem 0.8rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
|
||||
svg {
|
||||
font-size: 1rem;
|
||||
color: var(--color-main-text);
|
||||
}
|
||||
|
||||
|
||||
&:active {
|
||||
transform: translateX(-50%) scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&.collapsed {
|
||||
transform: translateY(calc(-100% + 1.5rem));
|
||||
|
||||
|
||||
.nav-items {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
@@ -264,11 +282,11 @@ function goHome() {
|
||||
.aside {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
.aside.space {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
.main-content {
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -387,6 +387,7 @@ function transferOk() {
|
||||
:loading="importLoading">导入数据恢复</BaseButton>
|
||||
<input type="file"
|
||||
id="import"
|
||||
class="w-0 h-0 opacity-0"
|
||||
accept="application/json,.zip,application/zip"
|
||||
@change="importData">
|
||||
</div>
|
||||
|
||||
@@ -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 { add2MyDict, 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,14 @@ 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, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { wordDelete } from '@/apis/words.ts'
|
||||
import { copyOfficialDict } from '@/apis/dict.ts'
|
||||
|
||||
const runtimeStore = useRuntimeStore()
|
||||
const base = useBaseStore()
|
||||
@@ -65,8 +67,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 +79,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 +102,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对比
|
||||
@@ -128,18 +133,53 @@ async function onSubmitWord() {
|
||||
})
|
||||
}
|
||||
|
||||
function batchDel(ids: string[]) {
|
||||
ids.map(id => {
|
||||
let rIndex2 = allList.findIndex(v => v.id === id)
|
||||
if (rIndex2 > -1) {
|
||||
if (id === wordForm.id) {
|
||||
wordForm = getDefaultFormWord()
|
||||
async function batchDel(ids: string[]) {
|
||||
let localHandle = () => {
|
||||
ids.map(id => {
|
||||
let rIndex2 = allList.findIndex(v => v.id === id)
|
||||
if (rIndex2 > -1) {
|
||||
if (id === wordForm.id) {
|
||||
wordForm = getDefaultFormWord()
|
||||
}
|
||||
allList.splice(rIndex2, 1)
|
||||
}
|
||||
allList.splice(rIndex2, 1)
|
||||
})
|
||||
tableRef.value.getData()
|
||||
syncDictInMyStudyList()
|
||||
}
|
||||
|
||||
let cloudHandle = async dictId => {
|
||||
let res = await wordDelete(null, {
|
||||
wordIds: ids,
|
||||
dictId,
|
||||
})
|
||||
if (res.success) {
|
||||
tableRef.value.getData()
|
||||
} else {
|
||||
return Toast.error(res.msg ?? '删除失败')
|
||||
}
|
||||
})
|
||||
tableRef.value.getData()
|
||||
syncDictInMyStudyList()
|
||||
}
|
||||
|
||||
if (AppEnv.CAN_REQUEST) {
|
||||
if (dict.custom) {
|
||||
if (dict.sync) {
|
||||
await cloudHandle(dict.id)
|
||||
} else {
|
||||
localHandle()
|
||||
}
|
||||
} else {
|
||||
let r = await copyOfficialDict(null, { id: dict.id })
|
||||
if (r.success) {
|
||||
await cloudHandle(r.data.id)
|
||||
getDetail(r.data.id)
|
||||
} else {
|
||||
//todo 权限判断,能否复制
|
||||
return Toast.error(r.msg)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
localHandle()
|
||||
}
|
||||
}
|
||||
|
||||
//把word对象的字段全转成字符串
|
||||
@@ -150,11 +190,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 +233,7 @@ let isAdd = $ref(false)
|
||||
let activeTab = $ref<'list' | 'edit'>('list') // 移动端标签页状态
|
||||
|
||||
const showBookDetail = computed(() => {
|
||||
return !(isAdd || isEdit);
|
||||
return !(isAdd || isEdit)
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -192,11 +242,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,13 +257,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})
|
||||
if (res.success) {
|
||||
runtimeStore.editDict.statistics = res.data.statistics
|
||||
if (res.data.words.length) {
|
||||
runtimeStore.editDict.words = res.data.words
|
||||
}
|
||||
}
|
||||
getDetail(runtimeStore.editDict.id)
|
||||
}
|
||||
}
|
||||
loading = false
|
||||
@@ -221,6 +268,14 @@ onMounted(async () => {
|
||||
tableRef.value.getData()
|
||||
})
|
||||
|
||||
async function getDetail(id) {
|
||||
//todo 优化:这里只返回详情
|
||||
let res = await detail({ id })
|
||||
if (res.success) {
|
||||
runtimeStore.editDict = res.data
|
||||
}
|
||||
}
|
||||
|
||||
function formClose() {
|
||||
if (isEdit) isEdit = false
|
||||
else router.back()
|
||||
@@ -230,7 +285,7 @@ let showPracticeSettingDialog = $ref(false)
|
||||
|
||||
const store = useBaseStore()
|
||||
const settingStore = useSettingStore()
|
||||
const {nav} = useNav()
|
||||
const { nav } = useNav()
|
||||
|
||||
//todo 可以和首页合并
|
||||
async function startPractice(query = {}) {
|
||||
@@ -244,10 +299,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 +328,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 +385,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,
|
||||
@@ -352,20 +409,20 @@ function importData(e) {
|
||||
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()
|
||||
@@ -375,88 +432,125 @@ 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();
|
||||
const r = localStorage.getItem('tour-guide')
|
||||
if (settingStore.first && !r && !isMobile()) {
|
||||
tour.start()
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
)
|
||||
|
||||
const dict = $computed(() => runtimeStore.editDict)
|
||||
|
||||
//获取本地单词列表
|
||||
function getLocalList({ pageNo, pageSize, searchKey }) {
|
||||
let list = allList
|
||||
let total = allList.length
|
||||
if (searchKey.trim()) {
|
||||
list = allList.filter(v => v.word.toLowerCase().includes(searchKey.trim().toLowerCase()))
|
||||
total = list.length
|
||||
}
|
||||
list = list.slice((pageNo - 1) * pageSize, (pageNo - 1) * pageSize + pageSize)
|
||||
return { list, total }
|
||||
}
|
||||
|
||||
async function requestList({ pageNo, pageSize, searchKey }) {
|
||||
if (
|
||||
!dict.custom &&
|
||||
![DictId.wordCollect, DictId.wordWrong, DictId.wordKnown].includes(dict.en_name || dict.id)
|
||||
) {
|
||||
// 非自定义词典,直接请求json
|
||||
|
||||
//如果没数据则请求
|
||||
if (!allList.length) {
|
||||
let r = await _getDictDataByUrl(dict)
|
||||
allList = r.words
|
||||
}
|
||||
}, 500)
|
||||
})
|
||||
|
||||
async function requestList({pageNo, pageSize, searchKey}) {
|
||||
if (AppEnv.CAN_REQUEST) {
|
||||
|
||||
return getLocalList({ pageNo, pageSize, searchKey })
|
||||
} else {
|
||||
let list = allList
|
||||
let total = allList.length
|
||||
if (searchKey.trim()) {
|
||||
list = allList.filter(v => v.word.toLowerCase().includes(searchKey.trim().toLowerCase()))
|
||||
total = list.length
|
||||
// 自定义词典
|
||||
|
||||
//如果登录了,则请求后端数据
|
||||
if (AppEnv.CAN_REQUEST) {
|
||||
//todo 加上sync标记
|
||||
if (dict.sync || true) {
|
||||
//todo 优化:这里应该只返回列表
|
||||
let res = await detail({ id: dict.id, pageNo, pageSize })
|
||||
if (res.success) {
|
||||
return { list: res.data.words, total: res.data.length }
|
||||
}
|
||||
return { list: [], total: 0 }
|
||||
}
|
||||
} else {
|
||||
//未登录则用本地保存的数据
|
||||
allList = dict.words
|
||||
}
|
||||
list = list.slice((pageNo - 1) * pageSize, (pageNo - 1) * pageSize + pageSize)
|
||||
return {list, total}
|
||||
return getLocalList({ pageNo, pageSize, searchKey })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -469,7 +563,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
|
||||
@@ -482,205 +577,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>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,38 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import { useBaseStore } from "@/stores/base.ts";
|
||||
import { useRouter } from "vue-router";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import { useBaseStore } from '@/stores/base.ts'
|
||||
import { useRouter } from 'vue-router'
|
||||
import BaseIcon from '@/components/BaseIcon.vue'
|
||||
import {
|
||||
_getAccomplishDate,
|
||||
_getDictDataByUrl, _getStudyProgress,
|
||||
_getDictDataByUrl,
|
||||
_nextTick,
|
||||
isMobile,
|
||||
loadJsLib,
|
||||
resourceWrap,
|
||||
shuffle,
|
||||
useNav
|
||||
} from "@/utils";
|
||||
import BasePage from "@/components/BasePage.vue";
|
||||
import { DictResource, WordPracticeMode } from "@/types/types.ts";
|
||||
import { watch } from "vue";
|
||||
import { getCurrentStudyWord } from "@/hooks/dict.ts";
|
||||
import { useRuntimeStore } from "@/stores/runtime.ts";
|
||||
import Book from "@/components/Book.vue";
|
||||
import PopConfirm from "@/components/PopConfirm.vue";
|
||||
import Progress from '@/components/base/Progress.vue';
|
||||
import Toast from '@/components/base/toast/Toast.ts';
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import { getDefaultDict } from "@/types/func.ts";
|
||||
import DeleteIcon from "@/components/icon/DeleteIcon.vue";
|
||||
import PracticeSettingDialog from "@/pages/word/components/PracticeSettingDialog.vue";
|
||||
import ChangeLastPracticeIndexDialog from "@/pages/word/components/ChangeLastPracticeIndexDialog.vue";
|
||||
import { useSettingStore } from "@/stores/setting.ts";
|
||||
import { useFetch } from "@vueuse/core";
|
||||
import { AppEnv, DICT_LIST, Host, LIB_JS_URL, PracticeSaveWordKey, TourConfig } from "@/config/env.ts";
|
||||
import { myDictList } from "@/apis";
|
||||
import PracticeWordListDialog from "@/pages/word/components/PracticeWordListDialog.vue";
|
||||
import ShufflePracticeSettingDialog from "@/pages/word/components/ShufflePracticeSettingDialog.vue";
|
||||
|
||||
useNav,
|
||||
} from '@/utils'
|
||||
import BasePage from '@/components/BasePage.vue'
|
||||
import { DictResource, WordPracticeMode } from '@/types/types.ts'
|
||||
import { watch } from 'vue'
|
||||
import { getCurrentStudyWord } from '@/hooks/dict.ts'
|
||||
import { useRuntimeStore } from '@/stores/runtime.ts'
|
||||
import Book from '@/components/Book.vue'
|
||||
import PopConfirm from '@/components/PopConfirm.vue'
|
||||
import Progress from '@/components/base/Progress.vue'
|
||||
import Toast from '@/components/base/toast/Toast.ts'
|
||||
import BaseButton from '@/components/BaseButton.vue'
|
||||
import { getDefaultDict } from '@/types/func.ts'
|
||||
import DeleteIcon from '@/components/icon/DeleteIcon.vue'
|
||||
import PracticeSettingDialog from '@/pages/word/components/PracticeSettingDialog.vue'
|
||||
import ChangeLastPracticeIndexDialog from '@/pages/word/components/ChangeLastPracticeIndexDialog.vue'
|
||||
import { useSettingStore } from '@/stores/setting.ts'
|
||||
import { useFetch } from '@vueuse/core'
|
||||
import {
|
||||
AppEnv,
|
||||
DICT_LIST,
|
||||
Host,
|
||||
LIB_JS_URL,
|
||||
PracticeSaveWordKey,
|
||||
TourConfig,
|
||||
} from '@/config/env.ts'
|
||||
import { myDictList } from '@/apis'
|
||||
import PracticeWordListDialog from '@/pages/word/components/PracticeWordListDialog.vue'
|
||||
import ShufflePracticeSettingDialog from '@/pages/word/components/ShufflePracticeSettingDialog.vue'
|
||||
import { deleteDict } from '@/apis/dict.ts'
|
||||
|
||||
const store = useBaseStore()
|
||||
const settingStore = useSettingStore()
|
||||
@@ -49,41 +56,45 @@ let currentStudy = $ref({
|
||||
shuffle: [],
|
||||
})
|
||||
|
||||
watch(() => store.load, n => {
|
||||
if (n) {
|
||||
init()
|
||||
_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: 'step1',
|
||||
text: '点击这里选择一本词典开始学习',
|
||||
attachTo: {
|
||||
element: '#step1',
|
||||
on: 'bottom'
|
||||
},
|
||||
buttons: [
|
||||
{
|
||||
text: `下一步(1/${TourConfig.total})`,
|
||||
action() {
|
||||
tour.next()
|
||||
router.push('/dict-list')
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
const r = localStorage.getItem('tour-guide');
|
||||
if (settingStore.first && !r && !isMobile()) tour.start();
|
||||
}, 500)
|
||||
}
|
||||
}, { immediate: true })
|
||||
watch(
|
||||
() => store.load,
|
||||
n => {
|
||||
if (n) {
|
||||
init()
|
||||
_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: 'step1',
|
||||
text: '点击这里选择一本词典开始学习',
|
||||
attachTo: {
|
||||
element: '#step1',
|
||||
on: 'bottom',
|
||||
},
|
||||
buttons: [
|
||||
{
|
||||
text: `下一步(1/${TourConfig.total})`,
|
||||
action() {
|
||||
tour.next()
|
||||
router.push('/dict-list')
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
const r = localStorage.getItem('tour-guide')
|
||||
if (settingStore.first && !r && !isMobile()) tour.start()
|
||||
}, 500)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
async function init() {
|
||||
if (AppEnv.CAN_REQUEST) {
|
||||
let res = await myDictList({ type: "word" })
|
||||
let res = await myDictList({ type: 'word' })
|
||||
if (res.success) {
|
||||
store.setState(Object.assign(store.$state, res.data))
|
||||
}
|
||||
@@ -122,7 +133,7 @@ function startPractice() {
|
||||
perDayStudyNumber: store.sdict.perDayStudyNumber,
|
||||
custom: store.sdict.custom,
|
||||
complete: store.sdict.complete,
|
||||
wordPracticeMode: settingStore.wordPracticeMode
|
||||
wordPracticeMode: settingStore.wordPracticeMode,
|
||||
})
|
||||
//把是否是第一次设置为false
|
||||
settingStore.first = false
|
||||
@@ -147,21 +158,30 @@ async function goDictDetail(val: DictResource) {
|
||||
let isManageDict = $ref(false)
|
||||
let selectIds = $ref([])
|
||||
|
||||
function handleBatchDel() {
|
||||
selectIds.forEach(id => {
|
||||
let r = store.word.bookList.findIndex(v => v.id === id)
|
||||
if (r !== -1) {
|
||||
if (store.word.studyIndex === r) {
|
||||
store.word.studyIndex = -1
|
||||
}
|
||||
if (store.word.studyIndex > r) {
|
||||
store.word.studyIndex--
|
||||
}
|
||||
store.word.bookList.splice(r, 1)
|
||||
async function handleBatchDel() {
|
||||
if (AppEnv.CAN_REQUEST) {
|
||||
let res = await deleteDict(null, selectIds)
|
||||
if (res.success) {
|
||||
init()
|
||||
} else {
|
||||
Toast.error(res.msg)
|
||||
}
|
||||
})
|
||||
selectIds = []
|
||||
Toast.success("删除成功!")
|
||||
} else {
|
||||
selectIds.forEach(id => {
|
||||
let r = store.word.bookList.findIndex(v => v.id === id)
|
||||
if (r !== -1) {
|
||||
if (store.word.studyIndex === r) {
|
||||
store.word.studyIndex = -1
|
||||
}
|
||||
if (store.word.studyIndex > r) {
|
||||
store.word.studyIndex--
|
||||
}
|
||||
store.word.bookList.splice(r, 1)
|
||||
}
|
||||
})
|
||||
selectIds = []
|
||||
Toast.success('删除成功!')
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSelect(item) {
|
||||
@@ -212,11 +232,17 @@ async function onShufflePracticeSettingOk(total) {
|
||||
localStorage.removeItem(PracticeSaveWordKey.key)
|
||||
|
||||
let ignoreList = [store.allIgnoreWords, store.knownWords][settingStore.ignoreSimpleWord ? 0 : 1]
|
||||
currentStudy.shuffle = shuffle(store.sdict.words.slice(0, store.sdict.lastLearnIndex).filter(v => !ignoreList.includes(v.word))).slice(0, total)
|
||||
nav('practice-words/' + store.sdict.id, {}, {
|
||||
taskWords: currentStudy,
|
||||
total //用于再来一组时,随机出正确的长度,因为练习中可能会点击已掌握,导致重学一遍之后长度变少,如果再来一组,此时长度就不正确
|
||||
})
|
||||
currentStudy.shuffle = shuffle(
|
||||
store.sdict.words.slice(0, store.sdict.lastLearnIndex).filter(v => !ignoreList.includes(v.word))
|
||||
).slice(0, total)
|
||||
nav(
|
||||
'practice-words/' + store.sdict.id,
|
||||
{},
|
||||
{
|
||||
taskWords: currentStudy,
|
||||
total, //用于再来一组时,随机出正确的长度,因为练习中可能会点击已掌握,导致重学一遍之后长度变少,如果再来一组,此时长度就不正确
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async function saveLastPracticeIndex(e) {
|
||||
@@ -229,20 +255,19 @@ async function saveLastPracticeIndex(e) {
|
||||
currentStudy = getCurrentStudyWord()
|
||||
}
|
||||
|
||||
const {
|
||||
data: recommendDictList,
|
||||
isFetching
|
||||
} = useFetch(resourceWrap(DICT_LIST.WORD.RECOMMENDED)).json()
|
||||
const { data: recommendDictList, isFetching } = useFetch(
|
||||
resourceWrap(DICT_LIST.WORD.RECOMMENDED)
|
||||
).json()
|
||||
|
||||
let isNewHost = $ref(window.location.host === Host)
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasePage>
|
||||
<div class="mb-4" v-if="!isNewHost">
|
||||
新域名已启用,后续请访问 <a href="https://typewords.cc/words?from_old_site=1">https://typewords.cc</a>。当前
|
||||
2study.top 域名将在不久后停止使用
|
||||
新域名已启用,后续请访问
|
||||
<a href="https://typewords.cc/words?from_old_site=1">https://typewords.cc</a>。当前 2study.top
|
||||
域名将在不久后停止使用
|
||||
</div>
|
||||
|
||||
<div class="card flex flex-col md:flex-row gap-4">
|
||||
@@ -259,11 +284,17 @@ let isNewHost = $ref(window.location.host === Host)
|
||||
<template v-if="store.sdict.id">
|
||||
<div class="mt-4 flex flex-col gap-2">
|
||||
<div class="">当前进度:{{ progressTextLeft }}</div>
|
||||
<Progress size="large" :percentage="store.currentStudyProgress" :show-text="false"></Progress>
|
||||
<Progress
|
||||
size="large"
|
||||
:percentage="store.currentStudyProgress"
|
||||
:show-text="false"
|
||||
></Progress>
|
||||
<div class="text-sm flex justify-between">
|
||||
<span>已完成 {{ progressTextRight }} 词 / 共 {{ store.sdict.words.length }} 词</span>
|
||||
<span v-if="store.sdict.id">
|
||||
预计完成日期:{{ _getAccomplishDate(store.sdict.words.length, store.sdict.perDayStudyNumber) }}
|
||||
预计完成日期:{{
|
||||
_getAccomplishDate(store.sdict.words.length, store.sdict.perDayStudyNumber)
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -274,8 +305,11 @@ let isNewHost = $ref(window.location.host === Host)
|
||||
<span>选择词典</span>
|
||||
</div>
|
||||
</BaseButton>
|
||||
<PopConfirm :disabled="!isSaveData" title="当前存在未完成的学习任务,修改会重新生成学习任务,是否继续?"
|
||||
@confirm="check(() => showChangeLastPracticeIndexDialog = true)">
|
||||
<PopConfirm
|
||||
:disabled="!isSaveData"
|
||||
title="当前存在未完成的学习任务,修改会重新生成学习任务,是否继续?"
|
||||
@confirm="check(() => (showChangeLastPracticeIndexDialog = true))"
|
||||
>
|
||||
<BaseButton type="info" size="small" v-if="store.sdict.id">
|
||||
<div class="center gap-1">
|
||||
<IconFluentSlideTextTitleEdit20Regular />
|
||||
@@ -297,29 +331,37 @@ let isNewHost = $ref(window.location.host === Host)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 w-full mt-4 md:mt-0" :class="!store.sdict.id && 'opacity-30 cursor-not-allowed'">
|
||||
<div
|
||||
class="flex-1 w-full mt-4 md:mt-0"
|
||||
:class="!store.sdict.id && 'opacity-30 cursor-not-allowed'"
|
||||
>
|
||||
<div class="flex justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="p-2 center rounded-full bg-white ">
|
||||
<div class="p-2 center rounded-full bg-white">
|
||||
<IconFluentStar20Filled class="text-lg color-amber" />
|
||||
</div>
|
||||
<div class="text-xl font-bold">
|
||||
{{ isSaveData ? '上次任务' : '今日任务' }}
|
||||
</div>
|
||||
<span class="color-link cursor-pointer" v-if="store.sdict.id"
|
||||
@click="showPracticeWordListDialog = true">词表</span>
|
||||
|
||||
<span
|
||||
class="color-link cursor-pointer"
|
||||
v-if="store.sdict.id"
|
||||
@click="showPracticeWordListDialog = true"
|
||||
>词表</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex gap-1 items-center" v-if="store.sdict.id">
|
||||
每日目标
|
||||
<div style="color:#ac6ed1;" class="bg-third px-2 h-10 flex center text-2xl rounded">
|
||||
<div style="color: #ac6ed1" class="bg-third px-2 h-10 flex center text-2xl rounded">
|
||||
{{ store.sdict.id ? store.sdict.perDayStudyNumber : 0 }}
|
||||
</div>
|
||||
个单词
|
||||
<PopConfirm :disabled="!isSaveData" title="当前存在未完成的学习任务,修改会重新生成学习任务,是否继续?"
|
||||
@confirm="check(() => showPracticeSettingDialog = true)">
|
||||
<BaseButton type="info" size="small">更改
|
||||
</BaseButton>
|
||||
<PopConfirm
|
||||
:disabled="!isSaveData"
|
||||
title="当前存在未完成的学习任务,修改会重新生成学习任务,是否继续?"
|
||||
@confirm="check(() => (showPracticeSettingDialog = true))"
|
||||
>
|
||||
<BaseButton type="info" size="small">更改 </BaseButton>
|
||||
</PopConfirm>
|
||||
</div>
|
||||
</div>
|
||||
@@ -340,32 +382,45 @@ let isNewHost = $ref(window.location.host === Host)
|
||||
</template>
|
||||
</div>
|
||||
<div class="flex items-end mt-4">
|
||||
<BaseButton size="large" class="flex-1" :disabled="!store.sdict.id" :loading="loading" @click="startPractice">
|
||||
<BaseButton
|
||||
size="large"
|
||||
class="flex-1"
|
||||
:disabled="!store.sdict.id"
|
||||
:loading="loading"
|
||||
@click="startPractice"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="line-height-[2]">{{ isSaveData ? '继续学习' : '开始学习' }}</span>
|
||||
<IconFluentArrowCircleRight16Regular class="text-xl" />
|
||||
</div>
|
||||
</BaseButton>
|
||||
|
||||
<div v-if="false" class="w-full flex box-border cp color-white">
|
||||
<div @click="startPractice"
|
||||
class="flex-1 rounded-l-lg center gap-2 py-1 bg-[var(--btn-primary)] hover:opacity-50">
|
||||
<div v-if="false" class="w-full flex box-border cp color-white">
|
||||
<div
|
||||
@click="startPractice"
|
||||
class="flex-1 rounded-l-lg center gap-2 py-1 bg-[var(--btn-primary)] hover:opacity-50"
|
||||
>
|
||||
<span class="line-height-[2]">{{ isSaveData ? '继续学习' : '开始学习' }}</span>
|
||||
<IconFluentArrowCircleRight16Regular class="text-xl" />
|
||||
</div>
|
||||
|
||||
<div class="relative group">
|
||||
<div
|
||||
class="w-10 rounded-r-lg h-full center bg-[var(--btn-primary)] hover:bg-gray border-solid border-2 border-l-gray border-transparent box-border">
|
||||
class="w-10 rounded-r-lg h-full center bg-[var(--btn-primary)] hover:bg-gray border-solid border-2 border-l-gray border-transparent box-border"
|
||||
>
|
||||
<IconFluentChevronDown20Regular />
|
||||
</div>
|
||||
|
||||
<div class="space-y-2 pt-2 absolute z-2 right-0 border rounded opacity-0 scale-95
|
||||
group-hover:opacity-100 group-hover:scale-100
|
||||
transition-all duration-150 pointer-events-none group-hover:pointer-events-auto">
|
||||
<div
|
||||
class="space-y-2 pt-2 absolute z-2 right-0 border rounded opacity-0 scale-95 group-hover:opacity-100 group-hover:scale-100 transition-all duration-150 pointer-events-none group-hover:pointer-events-auto"
|
||||
>
|
||||
<div>
|
||||
<BaseButton size="large" type="orange" :loading="loading"
|
||||
@click="check(() => showShufflePracticeSettingDialog = true)">
|
||||
<BaseButton
|
||||
size="large"
|
||||
type="orange"
|
||||
:loading="loading"
|
||||
@click="check(() => (showShufflePracticeSettingDialog = true))"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="line-height-[2]">随机复习</span>
|
||||
<IconFluentArrowShuffle20Filled class="text-xl" />
|
||||
@@ -373,8 +428,12 @@ let isNewHost = $ref(window.location.host === Host)
|
||||
</BaseButton>
|
||||
</div>
|
||||
<div>
|
||||
<BaseButton size="large" type="orange" :loading="loading"
|
||||
@click="check(() => showShufflePracticeSettingDialog = true)">
|
||||
<BaseButton
|
||||
size="large"
|
||||
type="orange"
|
||||
:loading="loading"
|
||||
@click="check(() => (showShufflePracticeSettingDialog = true))"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="line-height-[2]">重新学习</span>
|
||||
<IconFluentArrowShuffle20Filled class="text-xl" />
|
||||
@@ -385,8 +444,13 @@ let isNewHost = $ref(window.location.host === Host)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BaseButton v-if="store.sdict.id && store.sdict.lastLearnIndex" size="large" type="orange" :loading="loading"
|
||||
@click="check(() => showShufflePracticeSettingDialog = true)">
|
||||
<BaseButton
|
||||
v-if="store.sdict.id && store.sdict.lastLearnIndex"
|
||||
size="large"
|
||||
type="orange"
|
||||
:loading="loading"
|
||||
@click="check(() => (showShufflePracticeSettingDialog = true))"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="line-height-[2]">随机复习</span>
|
||||
<IconFluentArrowShuffle20Filled class="text-xl" />
|
||||
@@ -400,22 +464,44 @@ let isNewHost = $ref(window.location.host === Host)
|
||||
<div class="flex justify-between">
|
||||
<div class="title">我的词典</div>
|
||||
<div class="flex gap-4 items-center">
|
||||
<PopConfirm title="确认删除所有选中词典?" @confirm="handleBatchDel" v-if="selectIds.length">
|
||||
<PopConfirm
|
||||
title="确认删除所有选中词典?"
|
||||
@confirm="handleBatchDel"
|
||||
v-if="selectIds.length"
|
||||
>
|
||||
<BaseIcon class="del" title="删除">
|
||||
<DeleteIcon />
|
||||
</BaseIcon>
|
||||
</PopConfirm>
|
||||
|
||||
<div class="color-link cursor-pointer" v-if="store.word.bookList.length > 3"
|
||||
@click="isManageDict = !isManageDict; selectIds = []">{{ isManageDict ? '取消' : '管理词典' }}
|
||||
<div
|
||||
class="color-link cursor-pointer"
|
||||
v-if="store.word.bookList.length > 3"
|
||||
@click="
|
||||
() => {
|
||||
isManageDict = !isManageDict
|
||||
selectIds = []
|
||||
}
|
||||
"
|
||||
>
|
||||
{{ isManageDict ? '取消' : '管理词典' }}
|
||||
</div>
|
||||
<div class="color-link cursor-pointer" @click="nav('dict-detail', { isAdd: true })">
|
||||
创建个人词典
|
||||
</div>
|
||||
<div class="color-link cursor-pointer" @click="nav('dict-detail', { isAdd: true })">创建个人词典</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-4 flex-wrap mt-4">
|
||||
<Book :is-add="false" quantifier="个词" :item="item" :checked="selectIds.includes(item.id)"
|
||||
@check="() => toggleSelect(item)" :show-checkbox="isManageDict && j >= 3"
|
||||
v-for="(item, j) in store.word.bookList" @click="goDictDetail(item)" />
|
||||
<div class="flex gap-4 flex-wrap mt-4">
|
||||
<Book
|
||||
:is-add="false"
|
||||
quantifier="个词"
|
||||
:item="item"
|
||||
:checked="selectIds.includes(item.id)"
|
||||
@check="() => toggleSelect(item)"
|
||||
:show-checkbox="isManageDict && j >= 3"
|
||||
v-for="(item, j) in store.word.bookList"
|
||||
@click="goDictDetail(item)"
|
||||
/>
|
||||
<Book :is-add="true" @click="router.push('/dict-list')" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -428,21 +514,35 @@ let isNewHost = $ref(window.location.host === Host)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 flex-wrap mt-4 min-h-50">
|
||||
<Book :is-add="false" quantifier="个词" :item="item as any" v-for="(item, j) in recommendDictList"
|
||||
@click="goDictDetail(item as any)" />
|
||||
<div class="flex gap-4 flex-wrap mt-4 min-h-50">
|
||||
<Book
|
||||
:is-add="false"
|
||||
quantifier="个词"
|
||||
:item="item as any"
|
||||
v-for="(item, j) in recommendDictList"
|
||||
@click="goDictDetail(item as any)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</BasePage>
|
||||
|
||||
<PracticeSettingDialog :show-left-option="false" v-model="showPracticeSettingDialog" @ok="savePracticeSetting" />
|
||||
<PracticeSettingDialog
|
||||
:show-left-option="false"
|
||||
v-model="showPracticeSettingDialog"
|
||||
@ok="savePracticeSetting"
|
||||
/>
|
||||
|
||||
<ChangeLastPracticeIndexDialog v-model="showChangeLastPracticeIndexDialog" @ok="saveLastPracticeIndex" />
|
||||
<ChangeLastPracticeIndexDialog
|
||||
v-model="showChangeLastPracticeIndexDialog"
|
||||
@ok="saveLastPracticeIndex"
|
||||
/>
|
||||
|
||||
<PracticeWordListDialog :data="currentStudy" v-model="showPracticeWordListDialog" />
|
||||
|
||||
<ShufflePracticeSettingDialog v-model="showShufflePracticeSettingDialog" @ok="onShufflePracticeSettingOk" />
|
||||
|
||||
<ShufflePracticeSettingDialog
|
||||
v-model="showShufflePracticeSettingDialog"
|
||||
@ok="onShufflePracticeSettingOk"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
Reference in New Issue
Block a user