This commit is contained in:
Zyronon
2025-12-17 19:50:45 +08:00
committed by GitHub
parent a464b754a5
commit 75e956b46d
17 changed files with 911 additions and 1191 deletions

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ import { defineAsyncComponent, onMounted, watch } from "vue";
import { useSettingStore } from "@/stores/setting.ts";
import { jump2Feedback } from "@/utils";
import { useDisableEventListener } from "@/hooks/event.ts";
import ConflictNoticeText from "@/components/ConflictNoticeText.vue";
const Dialog = defineAsyncComponent(() => import('@/components/dialog/Dialog.vue'))
@@ -27,29 +28,14 @@ useDisableEventListener(() => show)
v-model="show"
title="重要提示"
footer
padding
:closeOnClickBg="false"
cancel-button-text="不再提醒"
confirm-button-text="关闭"
@cancel="settingStore.conflictNotice = false"
>
<div class="card w-150 center flex-col color-main py-0 mb-0">
<div class="text">
如果您安装了 <span class="font-bold text-red">调速 Vim</span> 等插件/脚本它们会拦截键盘按下事件<span
class="font-bold text-red">导致在本网站练习时按 'A' 'S' 'D' 等键无反应</span>您可以根据以下步骤解决冲突
</div>
<ul class="m-0">
<li>用浏览器无痕模式打开本网站确认能否正常输入</li>
<li>无痕模式下无法输入请给<span class="color-link mx-1 cp" @click="jump2Feedback">点此</span>反馈</li>
<li>无痕模式下可以输入则是插件/脚本导致的冲突</li>
<li>临时禁用对应插件/脚本或在对应插件/脚本的设置里面排除本网站</li>
<li>可安装此
<a href="https://chromewebstore.google.com/detail/one-click-extensions-mana/pbgjpgbpljobkekbhnnmlikbbfhbhmem"
target="_blank">插件(Chrome版本需翻墙)</a>,
<a href="https://microsoftedge.microsoft.com/addons/detail/%E5%BF%AB%E6%8D%B7%E6%89%A9%E5%B1%95%E7%AE%A1%E7%90%86/jdodenbllldnoogfmbmmgpieafbnaogm"
target="_blank">插件(Edge版本无需翻墙)</a>,
来快速激活禁用其他插件
</li>
</ul>
<div class="w-150 center flex-col color-main">
<ConflictNoticeText/>
</div>
</Dialog>
</template>

View File

@@ -0,0 +1,31 @@
<script setup lang="ts">
import { jump2Feedback } from "@/utils";
import WeChat from "@/components/ChannelIcons/WeChat.vue";
</script>
<template>
<div class="text">
如果您安装了 <span class="font-bold text-red">调速 Vim 音视频增强</span> 等插件/脚本它们会拦截键盘按下事件<span
class="font-bold text-red">导致在本网站练习时按 'A' 'S' 'D' 等键无反应</span>您可以根据以下步骤解决冲突
</div>
<ul class="m-0">
<li>用浏览器无痕模式打开本网站确认能否正常输入</li>
<li>
无痕模式下无法输入<span class="color-link mx-1 cp" @click="jump2Feedback">点此</span>反馈或者加微信群反馈<WeChat/>
</li>
<li>无痕模式下可以输入则是插件/脚本导致的冲突</li>
<li>临时禁用对应插件/脚本或在对应插件/脚本的设置里面排除本网站</li>
<li>可安装此
<a href="https://chromewebstore.google.com/detail/one-click-extensions-mana/pbgjpgbpljobkekbhnnmlikbbfhbhmem"
target="_blank">插件(Chrome版本需翻墙)</a>,
<a href="https://microsoftedge.microsoft.com/addons/detail/%E5%BF%AB%E6%8D%B7%E6%89%A9%E5%B1%95%E7%AE%A1%E7%90%86/jdodenbllldnoogfmbmmgpieafbnaogm"
target="_blank">插件(Edge版本无需翻墙)</a>,
来快速激活禁用其他插件
</li>
</ul>
</template>
<style scoped lang="scss">
</style>

View File

@@ -1,349 +0,0 @@
<script setup lang="ts">
import {useSettingStore} from "@/stores/setting.ts";
import {getAudioFileUrl, usePlayAudio} from "@/hooks/sound.ts";
import {ShortcutKey} from "@/types/types.ts";
import VolumeIcon from "@/components/icon/VolumeIcon.vue";
import {useBaseStore} from "@/stores/base.ts";
import {SoundFileOptions} from "@/config/env.ts";
import {Option, Select} from "@/components/base/select";
import Switch from "@/components/base/Switch.vue";
import Slider from "@/components/base/Slider.vue";
import RadioGroup from "@/components/base/radio/RadioGroup.vue";
import Radio from "@/components/base/radio/Radio.vue";
import InputNumber from "@/components/base/InputNumber.vue";
import Textarea from "@/components/base/Textarea.vue";
import SettingItem from "@/pages/setting/SettingItem.vue";
import {defineAsyncComponent} from "vue";
import BaseIcon from "@/components/BaseIcon.vue";
const Dialog = defineAsyncComponent(() => import('@/components/dialog/Dialog.vue'))
const props = defineProps<{
type: 'article' | 'word'
}>()
const tabIndex = $ref(props.type === 'word' ? 1 : 2)
const settingStore = useSettingStore()
const store = useBaseStore()
let show = $ref(false)
const simpleWords = $computed({
get: () => store.simpleWords.join(','),
set: v => {
try {
store.simpleWords = v.split(',');
} catch (e) {
}
}
})
</script>
<template>
<Dialog v-model="show" title="设置">
<div class="setting text-lg w-200 h-[60vh] text-md flex flex-col">
<div class="flex flex-1 overflow-hidden">
<div class="left">
<div class="tabs">
<div class="tab" :class="tabIndex === 1 && 'active'" @click="tabIndex = 1" v-if="type === 'word'">
<IconFluentTextUnderlineDouble20Regular width="20"/>
<span>单词</span>
</div>
<div class="tab" :class="tabIndex === 2 && 'active'" @click="tabIndex = 2" v-if="type === 'article'">
<IconFluentBookLetter20Regular width="20"/>
<span>文章</span>
</div>
<div class="tab" :class="tabIndex === 0 && 'active'" @click="tabIndex = 0">
<IconFluentSettings20Regular width="20"/>
<span>通用</span>
</div>
</div>
</div>
<div class="content">
<!-- 通用练习设置-->
<!-- 通用练习设置-->
<!-- 通用练习设置-->
<div v-if="tabIndex === 0">
<SettingItem title="忽略大小写"
desc="开启后输入时不区分大小写如输入“hello”和“Hello”都会被认为是正确的"
>
<Switch v-model="settingStore.ignoreCase"/>
</SettingItem>
<SettingItem title="允许默写模式下显示提示"
:desc="`开启后,可以通过将鼠标移动到单词上或者按快捷键 ${settingStore.shortcutKeyMap[ShortcutKey.ShowWord]} 显示正确答案`"
>
<Switch v-model="settingStore.allowWordTip"/>
</SettingItem>
<div class="line"></div>
<SettingItem title="简单词过滤"
desc="开启后,练习的单词中不会包含简单词;文章统计的总词数中不会包含简单词"
>
<Switch v-model="settingStore.ignoreSimpleWord"/>
</SettingItem>
<SettingItem title="简单词列表"
class="items-start!"
v-if="settingStore.ignoreSimpleWord"
>
<Textarea
placeholder="多个单词用英文逗号隔号"
v-model="simpleWords" :autosize="{minRows: 6, maxRows: 10}"/>
</SettingItem>
<!-- 音效-->
<!-- 音效-->
<!-- 音效-->
<div class="line"></div>
<SettingItem main-title="音效"/>
<SettingItem title="单词/句子发音口音"
desc="仅单词生效,文章固定美音"
>
<Select v-model="settingStore.soundType"
placeholder="请选择"
class="w-50!"
>
<Option label="美音" value="us"/>
<Option label="英音" value="uk"/>
</Select>
</SettingItem>
<div class="line"></div>
<SettingItem title="按键音">
<Switch v-model="settingStore.keyboardSound"/>
</SettingItem>
<SettingItem title="按键音效">
<Select v-model="settingStore.keyboardSoundFile"
placeholder="请选择"
class="w-50!"
>
<Option
v-for="item in SoundFileOptions"
:key="item.value"
:label="item.label"
:value="item.value"
>
<div class="flex justify-between items-center w-full">
<span>{{ item.label }}</span>
<VolumeIcon
:time="100"
@click="usePlayAudio(getAudioFileUrl(item.value)[0])"/>
</div>
</Option>
</Select>
</SettingItem>
<SettingItem title="音量">
<Slider v-model="settingStore.keyboardSoundVolume" showText showValue unit="%"/>
</SettingItem>
</div>
<!-- 单词练习设置-->
<!-- 单词练习设置-->
<!-- 单词练习设置-->
<div v-if="tabIndex === 1">
<!-- <SettingItem title="练习模式">-->
<!-- <RadioGroup v-model="settingStore.wordPracticeMode" class="flex-col gap-0!">-->
<!-- <Radio :value="WordPracticeMode.System" label="智能模式:自动规划学习、复习、听写、默写"/>-->
<!-- <Radio :value="WordPracticeMode.Free" label="自由模式:系统不强制复习与默写"/>-->
<!-- </RadioGroup>-->
<!-- </SettingItem>-->
<SettingItem title="显示上一个/下一个单词"
desc="开启后,练习中会在上方显示上一个/下一个单词"
>
<Switch v-model="settingStore.showNearWord"/>
</SettingItem>
<SettingItem title="不默认显示练习设置弹框"
desc="在词典详情页面,点击学习按钮后,是否显示练习设置弹框"
>
<Switch v-model="settingStore.disableShowPracticeSettingDialog"/>
</SettingItem>
<SettingItem title="输入错误时,清空已输入内容"
>
<Switch v-model="settingStore.inputWrongClear"/>
</SettingItem>
<SettingItem title="单词循环设置" class="gap-0!">
<RadioGroup v-model="settingStore.repeatCount">
<Radio :value="1" size="default">1</Radio>
<Radio :value="2" size="default">2</Radio>
<Radio :value="3" size="default">3</Radio>
<Radio :value="5" size="default">5</Radio>
<Radio :value="100" size="default">自定义</Radio>
</RadioGroup>
<div class="ml-2 center gap-space" v-if="settingStore.repeatCount === 100">
<span>循环次数</span>
<InputNumber v-model="settingStore.repeatCustomCount"
:min="6"
:max="15"
type="number"
/>
</div>
</SettingItem>
<SettingItem title="复习比"
desc="复习词与新词的比例,修改后下次学习生效"
>
<InputNumber :min="0" :max="10" v-model="settingStore.wordReviewRatio"/>
</SettingItem>
<!-- 发音-->
<!-- 发音-->
<!-- 发音-->
<div class="line"></div>
<SettingItem mainTitle="音效"/>
<SettingItem title="单词自动发音">
<Switch v-model="settingStore.wordSound"/>
</SettingItem>
<SettingItem title="音量">
<Slider v-model="settingStore.wordSoundVolume" showText showValue unit="%"/>
</SettingItem>
<SettingItem title="倍速">
<Slider v-model="settingStore.wordSoundSpeed" :step="0.1" :min="0.5" :max="3" showText showValue/>
</SettingItem>
<div class="line"></div>
<SettingItem title="效果音(输入错误、完成时的音效)">
<Switch v-model="settingStore.effectSound"/>
</SettingItem>
<SettingItem title="音量">
<Slider v-model="settingStore.effectSoundVolume" showText showValue unit="%"/>
</SettingItem>
<!-- 自动切换-->
<!-- 自动切换-->
<!-- 自动切换-->
<div class="line"></div>
<SettingItem mainTitle="自动切换"/>
<SettingItem title="自动切换下一个单词"
desc="仅在 **跟写** 时生效,听写、自测、默写均不会自动切换,需要手动按 **空格键** 切换"
>
<Switch v-model="settingStore.autoNextWord"/>
</SettingItem>
<SettingItem title="自动切换下一个单词时间"
desc="正确输入单词后,自动跳转下一个单词的时间"
>
<InputNumber v-model="settingStore.waitTimeForChangeWord"
:disabled="!settingStore.autoNextWord"
:min="0"
:max="10000"
:step="100"
type="number"
/>
<span class="ml-4">毫秒</span>
</SettingItem>
<!-- 字体设置-->
<!-- 字体设置-->
<!-- 字体设置-->
<div class="line"></div>
<SettingItem mainTitle="字体设置"/>
<SettingItem title="外语字体">
<Slider
:min="10"
:max="100"
v-model="settingStore.fontSize.wordForeignFontSize" showText showValue unit="px"/>
</SettingItem>
<SettingItem title="中文字体">
<Slider
:min="10"
:max="100"
v-model="settingStore.fontSize.wordTranslateFontSize" showText showValue unit="px"/>
</SettingItem>
</div>
<!-- 文章练习设置-->
<!-- 文章练习设置-->
<!-- 文章练习设置-->
<div v-if="tabIndex === 2">
<!-- 发音-->
<!-- 发音-->
<!-- 发音-->
<SettingItem mainTitle="音效"/>
<SettingItem title="自动播放句子">
<Switch v-model="settingStore.articleSound"/>
</SettingItem>
<SettingItem title="自动播放下一篇">
<Switch v-model="settingStore.articleAutoPlayNext"/>
</SettingItem>
<SettingItem title="音量">
<Slider v-model="settingStore.articleSoundVolume" showText showValue unit="%"/>
</SettingItem>
<SettingItem title="倍速">
<Slider v-model="settingStore.articleSoundSpeed" :step="0.1" :min="0.5" :max="3" showText showValue/>
</SettingItem>
<div class="line"></div>
<SettingItem title="输入时忽略符号/数字/人名">
<Switch v-model="settingStore.ignoreSymbol"/>
</SettingItem>
</div>
</div>
</div>
</div>
</Dialog>
<BaseIcon title="设置" @click="show = true;tabIndex = props.type === 'word' ? 1 : 2">
<IconFluentSettings20Regular/>
</BaseIcon>
</template>
<style scoped lang="scss">
.setting {
.left {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
border-right: 1px solid gainsboro;
.tabs {
padding: 1rem;
display: flex;
flex-direction: column;
gap: .6rem;
//color: #0C8CE9;
.tab {
@apply cursor-pointer flex items-center relative;
padding: .6rem .9rem;
border-radius: .5rem;
width: 8rem;
gap: .6rem;
transition: all .5s;
&:hover {
background: var(--btn-primary);
color: white;
}
&.active {
background: var(--btn-primary);
color: white;
}
}
}
}
.content {
flex: 1;
height: 100%;
overflow: auto;
padding: 0 1.6rem;
.line {
border-bottom: 1px solid #c4c3c3;
}
}
}
</style>

View File

@@ -0,0 +1,28 @@
<script setup lang="ts">
const props = defineProps<{
q: string,
a?: string | string[],
}>()
let show = $ref(false)
let isArray = $computed(() => typeof props.a !== 'string')
</script>
<template>
<div class="qa-item my-6">
<header class="flex justify-between items-center cp font-bold text-lg" @click="show = !show">
<span>{{ q }}</span>
<IconFluentChevronLeft20Filled class="anim" :class="show?'transform-rotate-270':'transform-rotate-180'"/>
</header>
<div class="content mt-4 text-base" v-if="show">
<template v-if="isArray">
<p v-for="(v,i) in a">{{a.length>1?`${i+1}. `:''}}{{v}}</p>
</template>
<span v-else>{{a}}</span>
<slot></slot>
</div>
</div>
</template>
<style scoped lang="scss">
</style>

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import {computed, onMounted, onUnmounted, ref} from 'vue';
import { computed, onMounted, onUnmounted, ref } from 'vue';
import BaseInput from "@/components/base/BaseInput.vue";
interface IProps {
currentPage?: number;
@@ -8,7 +9,6 @@ interface IProps {
layout?: string;
total: number;
hideOnSinglePage?: boolean;
// background property removed as per requirements
}
const props = withDefaults(defineProps<IProps>(), {
@@ -37,59 +37,18 @@ const pageCount = computed(() => {
// 可用于显示的页码数量,会根据容器宽度动态计算
const availablePagerCount = ref(5); // 默认值
// 计算显示的页码
const pagers = computed(() => {
const pagerCount = availablePagerCount.value; // 动态计算的页码数量
const halfPagerCount = Math.floor(pagerCount / 2);
const currentPage = internalCurrentPage.value;
const pageCountValue = pageCount.value;
let showPrevMore = false;
let showNextMore = false;
if (pageCountValue > pagerCount) {
if (currentPage > pagerCount - halfPagerCount) {
showPrevMore = true;
}
if (currentPage < pageCountValue - halfPagerCount) {
showNextMore = true;
}
}
const array = [];
if (showPrevMore && !showNextMore) {
const startPage = pageCountValue - (pagerCount - 2);
for (let i = startPage; i < pageCountValue; i++) {
array.push(i);
}
} else if (!showPrevMore && showNextMore) {
for (let i = 2; i < pagerCount; i++) {
array.push(i);
}
} else if (showPrevMore && showNextMore) {
const offset = Math.floor(pagerCount / 2) - 1;
for (let i = currentPage - offset; i <= currentPage + offset; i++) {
array.push(i);
}
} else {
for (let i = 2; i < pageCountValue; i++) {
array.push(i);
}
}
return array;
});
// 是否显示分页
const shouldShow = computed(() => {
return props.hideOnSinglePage ? pageCount.value > 1 : true;
});
// 处理页码变化
function handleCurrentChange(val: number) {
function jumpPage(val: number) {
if (Number(val) > pageCount.value) val = pageCount.value;
if (Number(val) <= 0) val = 1;
internalCurrentPage.value = val;
emit('update:currentPage', val);
emit('current-change', val);
emit('update:currentPage', Number(val));
emit('current-change', Number(val));
}
// 处理每页条数变化
@@ -143,7 +102,7 @@ onUnmounted(() => {
function prev() {
const newPage = internalCurrentPage.value - 1;
if (newPage >= 1) {
handleCurrentChange(newPage);
jumpPage(newPage);
}
}
@@ -151,32 +110,10 @@ function prev() {
function next() {
const newPage = internalCurrentPage.value + 1;
if (newPage <= pageCount.value) {
handleCurrentChange(newPage);
jumpPage(newPage);
}
}
// 跳转到指定页
function jumpPage(page: number) {
if (page !== internalCurrentPage.value) {
handleCurrentChange(page);
}
}
// 快速向前跳转
function quickPrevPage() {
const newPage = Math.max(1, internalCurrentPage.value - 5);
if (newPage !== internalCurrentPage.value) {
handleCurrentChange(newPage);
}
}
// 快速向后跳转
function quickNextPage() {
const newPage = Math.min(pageCount.value, internalCurrentPage.value + 5);
if (newPage !== internalCurrentPage.value) {
handleCurrentChange(newPage);
}
}
</script>
<template>
@@ -184,71 +121,29 @@ function quickNextPage() {
<div class="pagination-container">
<!-- 上一页 -->
<button
v-if="layout.includes('prev')"
class="btn-prev"
:disabled="internalCurrentPage <= 1"
@click="prev"
class="btn-prev"
:disabled="internalCurrentPage <= 1"
@click="prev"
>
<IconFluentChevronLeft20Filled/>
</button>
<!-- 页码 -->
<ul v-if="layout.includes('pager')" class="pager">
<!-- 第一页 -->
<li
class="number"
:class="{ active: internalCurrentPage === 1 }"
@click="jumpPage(1)"
>
1
</li>
<!-- 快速向前 -->
<li
v-if="pageCount > availablePagerCount && internalCurrentPage > (availablePagerCount - Math.floor(availablePagerCount / 2))"
class="more btn-quickprev"
@click="quickPrevPage"
>
...
</li>
<!-- 中间页码 -->
<li
v-for="pager in pagers"
:key="pager"
class="number"
:class="{ active: internalCurrentPage === pager }"
@click="jumpPage(pager)"
>
{{ pager }}
</li>
<!-- 快速向后 -->
<li
v-if="pageCount > availablePagerCount && internalCurrentPage < pageCount - Math.floor(availablePagerCount / 2)"
class="more btn-quicknext"
@click="quickNextPage"
>
...
</li>
<!-- 最后一页 -->
<li
v-if="pageCount > 1"
class="number"
:class="{ active: internalCurrentPage === pageCount }"
@click="jumpPage(pageCount)"
>
{{ pageCount }}
</li>
</ul>
<div class="flex items-center">
<div class="w-12">
<BaseInput v-model="internalCurrentPage"
@enter="jumpPage(internalCurrentPage)"
class="text-center"/>
</div>
<span class="mx-2">/</span>
<span class="text-base">{{ pageCount }}</span>
</div>
<!-- 下一页 -->
<button
v-if="layout.includes('next')"
class="btn-next"
:disabled="internalCurrentPage >= pageCount"
@click="next"
class="btn-next"
:disabled="internalCurrentPage >= pageCount"
@click="next"
>
<IconFluentChevronLeft20Filled class="transform-rotate-180"/>
</button>
@@ -256,18 +151,18 @@ function quickNextPage() {
<!-- 每页条数选择器 -->
<div v-if="layout.includes('sizes')" class="sizes">
<select
:value="internalPageSize"
@change="handleSizeChange(Number($event.target.value))"
:value="internalPageSize"
@change="handleSizeChange(Number($event.target.value))"
>
<option v-for="item in pageSizes" :key="item" :value="item">
{{ item }} /
{{ item }}/
</option>
</select>
</div>
<!-- 总数 -->
<span v-if="layout.includes('total')" class="total">
{{ total }}
<span v-if="layout.includes('total')" class="total text-base">
{{ total }}
</span>
</div>
</div>
@@ -298,69 +193,40 @@ function quickNextPage() {
font-size: 1rem;
min-width: 1.9375rem;
height: 1.9375rem;
border-radius: 0.125rem;
border-radius: 0.2rem;
cursor: pointer;
background-color: var(--color-third);
color: #606266;
border: none;
padding: 0 0.375rem;
margin: 0.25rem 0.25rem;
background-color: transparent;
transition: all .3s;
&:disabled {
opacity: 0.3;
cursor: not-allowed;
}
&:hover:not(:disabled) {
background-color: var(--color-third);
color: var(--color-select-bg);
}
}
.pager {
display: inline-flex;
list-style: none;
margin: 0;
padding: 0;
flex-wrap: wrap;
li {
display: inline-flex;
justify-content: center;
align-items: center;
font-size: 0.875rem;
min-width: 1.9375rem;
height: 1.9375rem;
line-height: 1.9375rem;
border-radius: 0.125rem;
margin: 0.25rem 0.25rem;
cursor: pointer;
background-color: var(--color-third);
border: none;
&.active {
background-color: var(--el-color-primary, #409eff);
color: #fff;
}
&.more {
color: #606266;
}
&:hover:not(.active) {
color: var(--el-color-primary, #409eff);
}
}
}
.sizes {
margin: 0.25rem 0.5rem;
border: 1px solid var(--color-input-border);
border-radius: 0.25rem;
padding-right: .2rem;
background-color: var(--color-bg);
overflow: hidden;
select {
height: 1.9375rem;
padding: 0 0.5rem;
font-size: 0.875rem;
border-radius: 0.125rem;
border: 1px solid #dcdfe6;
background-color: #fff;
border: none;
background-color: transparent;
color: var(--color-main-text);
&:focus {
outline: none;
@@ -377,8 +243,7 @@ function quickNextPage() {
.total {
margin: 0.25rem 0.5rem;
font-weight: normal;
color: #606266;
color: var(--color-main-text);
}
}
</style>

View File

@@ -24,15 +24,15 @@ let show = $ref(false)
<div class="tabs">
<div class="tab" :class="tabIndex === 1 && 'active'" @click="tabIndex = 1" v-if="type === 'word'">
<IconFluentTextUnderlineDouble20Regular width="20"/>
<span>单词</span>
<span>单词设置</span>
</div>
<div class="tab" :class="tabIndex === 2 && 'active'" @click="tabIndex = 2" v-if="type === 'article'">
<IconFluentBookLetter20Regular width="20"/>
<span>文章</span>
<span>文章设置</span>
</div>
<div class="tab" :class="tabIndex === 0 && 'active'" @click="tabIndex = 0">
<IconFluentSettings20Regular width="20"/>
<span>通用</span>
<span>通用设置</span>
</div>
</div>
</div>

21
src/pages/doc.vue Normal file
View File

@@ -0,0 +1,21 @@
<script setup lang="ts">
import BasePage from "@/components/BasePage.vue";
</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>
</div>
</div>
</BasePage>
</template>
<style scoped lang="scss">
</style>

View File

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

View File

@@ -49,6 +49,14 @@ function goHome() {
<IconFluentCommentEdit20Regular/>
<span v-if="settingStore.sideExpand">反馈</span>
</div>
<div class="row" @click="router.push('/qa')">
<IconFluentQuestionCircle20Regular/>
<span v-if="settingStore.sideExpand">帮助</span>
</div>
<!-- <div class="row" @click="router.push('/doc')">-->
<!-- <IconFluentDocument20Regular/>-->
<!-- <span v-if="settingStore.sideExpand">资料</span>-->
<!-- </div>-->
<!-- <div class="row" @click="router.push('/user')">-->
<!-- <IconFluentPerson20Regular/>-->
<!-- <span v-if="settingStore.sideExpand">用户</span>-->
@@ -129,7 +137,7 @@ function goHome() {
transition: all .5s;
&:hover {
background: var(--color-select-bg);
background: var(--btn-primary);
color: white;
}

140
src/pages/qa.vue Normal file
View File

@@ -0,0 +1,140 @@
<script setup lang="ts">
import BasePage from "@/components/BasePage.vue";
import Collapse from "@/components/base/Collapse.vue";
import WeChat from "@/components/ChannelIcons/WeChat.vue";
import { APP_NAME, GITHUB } from "@/config/env.ts";
import ConflictNoticeText from "@/components/ConflictNoticeText.vue";
</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">
<Collapse q="网站是免费的吗?" :a="[
'不完全免费,因为想要长久发展后续收费是必然的,但不会必须付费才可用,我们尽量在免费与收费之间找到一个平衡点',
// '不登录依然可以使用大部分功能,但数据需要自己管理,如需多设备使用则需要自行导入导出',
// '登录后提供官方词典/书籍的数据同步功能,如需要同步自定义词典/书籍,则需要开通会员,同时会提供更多的学习内容和功能',
'项目是开源的,可自行部署'
]"/>
<div class="line"></div>
<Collapse q="无法输入,按下键盘没有反应?">
<ConflictNoticeText/>
</Collapse>
<div class="line"></div>
<Collapse q="数据在哪?怎么在多台电脑/设备之间使用?">
<div>
1. 所有用户数据
<b class="text-red">保存在本地浏览器中</b>如果您需要在不同的设备浏览器上使用 {{ APP_NAME }}
您需要手动进行数据导出和导入
</div>
<p>
2. 设置 -> 数据设置 -> 在原电脑上导出数据 -> 通过社交软件发送给新电脑 -> 在新电脑上导入
</p>
<p>
3. 正在开发账户体系
</p>
</Collapse>
<div class="line"></div>
<Collapse q="网站自动规划的单词数量太多了,怎么修改?">
<p>
1. 默认复习词数量与新词数量是14如果新词40个那么会复习40个上次学习的复习120个之前学习的由近到远
</p>
<p>
2. 您可在通过 设置 -> 单词设置 -> 复习比 修改
</p>
</Collapse>
<div class="line"></div>
<Collapse q="完成一次学习要很久,流程是不是太冗长了?" :a="[
'这的确是个问题,冗长的流程容易让人失去背单词的积极性,我正在思考如何优化学习流程,如果您有好的建议欢迎反馈',
'错误单词会重新再来如果只是手误按错了后续重新练习时可以按Tab键跳过。无法判断用户是手误还是真的不会所以只能错词统统重来直到正确为止',
'复习时只有选择了不认识的单词才会要求听写与默写这是合理的不过目前会出现同一个单词复习了N遍的问题',
'上线了账户体系之后,会添加艾宾浩斯的记忆曲线功能,到时候规划的复习单词会比现在更智能'
]"
/>
<div class="line"></div>
<Collapse q="会添加艾宾浩斯的记忆曲线功能吗?" :a="[
'上线了账户体系之后,会添加艾宾浩斯的记忆曲线功能'
]"
/>
<div class="line"></div>
<Collapse q="能否 自行添加单词/自定义词典/导入自己的单词/修改单词内容?" :a="[
'可以',
'在单词界面,点击“创建个人词典”',
'创建完成之后,在词典详情页面,点击 “添加单词” 图标,即可添加自己的单词',
'也可以点击 “导入” 图标批量导入需要严格按照模板xlsx格式来'
]"/>
<div class="line"></div>
<Collapse q="能否 自行添加文章/自定义书籍/导入自己的文章/修改文章内容?" :a="[
'可以,操作步骤基本和添加单词的一样',
'在文章界面,点击“创建个人书籍”',
'创建完成之后,在书籍详情页面,点击顶部的 “文章管理” 按钮,即可添加自己的文章',
]"/>
<div class="line"></div>
<Collapse q="怎么关闭按键音?" :a="[
'设置 -> 通用设置 -> 音效 -> 按键音,关闭即可',
]"/>
<div class="line"></div>
<Collapse q="平板能用吗?" :a="[
'平板可以使用但使用蓝牙键盘体验会更好毕竟系统自带的虚拟键盘占了1/3的屏幕空间比较影响观感',
'连接蓝牙键盘',
'安卓平板,需要开启 “电脑模式”iPad无需此操作',
]"/>
<div class="line"></div>
<Collapse q="手机能用吗?" :a="[
'手机可以使用,但暂时未进行其针对优化,使用起来可能会有不方便的地方,还是建议在电脑或平板上用'
]"/>
<div class="line"></div>
<Collapse q="有APP、小程序吗" a="无,只有网站"/>
<div class="line"></div>
<Collapse q="如何向开发团队反馈问题和功能需求?" :a="[
'可以加入我们官方 微信 群, 详细的描述您想要的功能以及告知这个功能想要解决的问题是什么',
'如果您在应用中发现了错误或漏洞,请提供详细的描述和重现问题的步骤,当然最好提供一个小视频',
'也可以给我们提工单',
'也可以去 github/issues 提交'
]">
<div class="flex items-center">微信群
<WeChat/>
</div>
<p>
GitHub地址<a :href="GITHUB" target="_blank">{{ GITHUB }}</a>
</p>
<div class="">
工单反馈<a :href="`https://v.wjx.cn/vm/ev0W7fv.aspx#`"
target="_blank">https://v.wjx.cn/vm/ev0W7fv.aspx#</a>
</div>
</Collapse>
</div>
</div>
</div>
</BasePage>
</template>
<style scoped lang="scss">
</style>

View File

@@ -4,6 +4,22 @@
<template>
<div>
<div class="log-item">
<div class="mb-2">
<div>
<div>日期2025/12/17</div>
<div>内容新增帮助页面</div>
</div>
</div>
</div>
<div class="log-item">
<div class="mb-2">
<div>
<div>日期2025/12/16</div>
<div>内容修复弹框内边距太小单词文章通用设置在设置页面练习界面均可进行设置</div>
</div>
</div>
</div>
<div class="log-item">
<div class="mb-2">
<div>

View File

@@ -306,15 +306,15 @@ function transferOk() {
<div class="tabs">
<div class="tab" :class="tabIndex === 0 && 'active'" @click="tabIndex = 0">
<IconFluentSettings20Regular width="20"/>
<span>通用</span>
<span>通用设置</span>
</div>
<div class="tab" :class="tabIndex === 1 && 'active'" @click="tabIndex = 1">
<IconFluentTextUnderlineDouble20Regular width="20"/>
<span>单词</span>
<span>单词设置</span>
</div>
<div class="tab" :class="tabIndex === 2 && 'active'" @click="tabIndex = 2">
<IconFluentBookLetter20Regular width="20"/>
<span>文章</span>
<span>文章设置</span>
</div>
<div class="tab" :class="tabIndex === 4 && 'active'" @click="tabIndex = 4">
<IconFluentDatabasePerson20Regular width="20"/>

View File

@@ -4,7 +4,17 @@ import { DictId, Sort } from "@/types/types.ts";
import BasePage from "@/components/BasePage.vue";
import { computed, onMounted, reactive, ref, shallowReactive, watch } from "vue";
import { useRuntimeStore } from "@/stores/runtime.ts";
import { _getDictDataByUrl, _nextTick, cloneDeep, convertToWord, isMobile, loadJsLib, reverse, shuffle, useNav } from "@/utils";
import {
_getDictDataByUrl,
_nextTick,
cloneDeep,
convertToWord,
isMobile,
loadJsLib,
reverse,
shuffle,
useNav
} from "@/utils";
import { nanoid } from "nanoid";
import BaseIcon from "@/components/BaseIcon.vue";
import BaseTable from "@/components/BaseTable.vue";
@@ -35,15 +45,7 @@ const router = useRouter()
const route = useRoute()
const isMob = isMobile()
let loading = $ref(false)
let list = $computed({
get() {
return runtimeStore.editDict.words
},
set(v) {
runtimeStore.editDict.words = shallowReactive(v)
}
})
let list2 = $ref([])
const getDefaultFormWord = () => {
return {
@@ -64,8 +66,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)
@@ -98,7 +100,7 @@ async function onSubmitWord() {
let data: any = convertToWord(wordForm)
//todo 可以检查的更准确些比如json对比
if (data.id) {
let r = list.find(v => v.id === data.id)
let r = list2.find(v => v.id === data.id)
if (r) {
Object.assign(r, data)
Toast.success('修改成功')
@@ -109,11 +111,11 @@ async function onSubmitWord() {
} else {
data.id = nanoid(6)
data.checked = false
let r = list.find(v => v.word === wordForm.word)
let r = list2.find(v => v.word === wordForm.word)
if (r) {
Toast.warning('已有相同名称单词!')
return
} else list.push(data)
} else list2.push(data)
Toast.success('添加成功')
wordForm = getDefaultFormWord()
}
@@ -124,19 +126,16 @@ async function onSubmitWord() {
})
}
function delWord(id: string, isBatch = false) {
let rIndex2 = list.findIndex(v => v.id === id)
if (rIndex2 > -1) {
if (id === wordForm.id) {
wordForm = getDefaultFormWord()
}
list.splice(rIndex2, 1)
}
if (!isBatch) syncDictInMyStudyList()
}
function batchDel(ids: string[]) {
ids.map(v => delWord(v, true))
ids.map(id => {
let rIndex2 = list2.findIndex(v => v.id === id)
if (rIndex2 > -1) {
if (id === wordForm.id) {
wordForm = getDefaultFormWord()
}
list2.splice(rIndex2, 1)
}
})
syncDictInMyStudyList()
}
@@ -202,7 +201,7 @@ onMounted(async () => {
}
if (base.word.bookList.find(book => book.id === runtimeStore.editDict.id)) {
if (AppEnv.CAN_REQUEST) {
let res = await detail({ id: runtimeStore.editDict.id })
let res = await detail({id: runtimeStore.editDict.id})
if (res.success) {
runtimeStore.editDict.statistics = res.data.statistics
if (res.data.words.length) {
@@ -227,7 +226,7 @@ let showPracticeSettingDialog = $ref(false)
const store = useBaseStore()
const settingStore = useSettingStore()
const { nav } = useNav()
const {nav} = useNav()
//todo 可以和首页合并
async function startPractice(query = {}) {
@@ -244,7 +243,7 @@ async function startPractice(query = {}) {
wordPracticeMode: settingStore.wordPracticeMode
})
let currentStudy = getCurrentStudyWord()
nav('practice-words/' + store.sdict.id, query, { taskWords: currentStudy })
nav('practice-words/' + store.sdict.id, query, {taskWords: currentStudy})
}
async function addMyStudyList() {
@@ -278,7 +277,7 @@ function importData(e) {
let data = s.target.result;
importLoading = true
const XLSX = await loadJsLib('XLSX', LIB_JS_URL.XLSX);
let workbook = XLSX.read(data, { type: 'binary' });
let workbook = XLSX.read(data, {type: 'binary'});
let res: any[] = XLSX.utils.sheet_to_json(workbook.Sheets['Sheet1']);
if (res.length) {
let words = res.map(v => {
@@ -381,10 +380,6 @@ async function exportData() {
exportLoading = false
}
function searchWord() {
console.log('wordForm.word', wordForm.word)
}
watch(() => loading, (val) => {
if (!val) return
_nextTick(async () => {
@@ -396,7 +391,7 @@ watch(() => loading, (val) => {
tour.addStep({
id: 'step3',
text: '点击这里开始学习',
attachTo: { element: '#study', on: 'bottom' },
attachTo: {element: '#study', on: 'bottom'},
buttons: [
{
text: `下一步3/${TourConfig.total}`,
@@ -411,7 +406,7 @@ watch(() => loading, (val) => {
tour.addStep({
id: 'step4',
text: '这里可以选择学习模式、设置学习数量、修改学习进度',
attachTo: { element: '#mode', on: 'bottom' },
attachTo: {element: '#mode', on: 'bottom'},
beforeShowPromise() {
return new Promise((resolve) => {
const timer = setInterval(() => {
@@ -427,7 +422,7 @@ watch(() => loading, (val) => {
text: `下一步4/${TourConfig.total}`,
action() {
tour.next()
startPractice({ guide: 1 })
startPractice({guide: 1})
}
}
]
@@ -440,9 +435,7 @@ watch(() => loading, (val) => {
}, 500)
})
let list2 = $ref([])
async function requestList({ pageNo, pageSize, searchKey }) {
async function requestList({pageNo, pageSize, searchKey}) {
if (AppEnv.CAN_REQUEST) {
} else {
@@ -453,7 +446,7 @@ async function requestList({ pageNo, pageSize, searchKey }) {
total = list.length
}
list = list.slice((pageNo - 1) * pageSize, (pageNo - 1) * pageSize + pageSize)
return { list, total }
return {list, total}
}
}
@@ -481,177 +474,172 @@ defineRender(() => {
<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>
</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 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>
<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}
list={list}
total={runtimeStore.editDict.length}
loading={loading}
onUpdate:list={e => list = e}
del={delWord}
batchDel={batchDel}
onSort={onSort}
onAdd={addWord}
onImportData={importData}
onExportData={exportData}
exportLoading={exportLoading}
importLoading={importLoading}
>
{
(val) =>
<WordItem
showTransPop={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={() => delWord(val.item.id)}
>
{/* 移动端标签页导航 */}
{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}
item={val.item}>
{{
prefix: () => val.checkbox(val.item),
suffix: () => (
<div class='flex flex-col'>
<BaseIcon
class="option-icon"
title="删除">
<DeleteIcon />
onClick={() => editWord(val.item)}
title="编辑">
<IconFluentTextEditStyle20Regular/>
</BaseIcon>
</PopConfirm>
<PopConfirm title="确认删除?"
onConfirm={() => batchDel([val.item.id])}
>
<BaseIcon
class="option-icon"
title="删除">
<DeleteIcon/>
</BaseIcon>
</PopConfirm>
</div>
)
}}
</WordItem>
}
</BaseTable>
</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>
{
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> :
<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={() => {
@@ -660,7 +648,7 @@ defineRender(() => {
} else {
isEdit = false
}
}} />
}}/>
<div class="dict-title absolute page-title text-align-center w-full">
{runtimeStore.editDict.id ? '修改' : '创建'}词典
</div>
@@ -680,7 +668,7 @@ defineRender(() => {
showLeftOption
modelValue={showPracticeSettingDialog}
onUpdate:modelValue={val => (showPracticeSettingDialog = val)}
onOk={startPractice} />
onOk={startPractice}/>
</BasePage>
)
})

View File

@@ -17,7 +17,7 @@ defineEmits<{
<template>
<!-- todo 这里显示的时候可以选中并高亮当前index-->
<!-- todo 这个组件的分需要直接可跳转指定页面并显示一页有多少个-->
<!-- todo 这个组件的分需要直接可跳转指定页面并显示一页有多少个-->
<Dialog v-model="model"
padding
title="修改学习进度">

View File

@@ -15,6 +15,8 @@ import Login from "@/pages/user/login.vue";
import User from "@/pages/user/User.vue";
import VipIntro from "@/pages/user/VipIntro.vue";
import Feedback from "@/pages/feedback.vue";
import Qa from "@/pages/qa.vue";
import Doc from "@/pages/doc.vue";
// import { useAuthStore } from "@/stores/user.ts";
export const routes: RouteRecordRaw[] = [
@@ -44,7 +46,8 @@ export const routes: RouteRecordRaw[] = [
{path: 'setting', component: Setting},
{path: 'feedback', component: Feedback},
{path: 'qa', component: Qa},
{path: 'doc', component: Doc},
]
},
{path: '/batch-edit-article', component: () => import("@/pages/article/BatchEditArticlePage.vue")},