feat:拆分页面

This commit is contained in:
zyronon
2025-07-30 01:47:28 +08:00
parent a484184d48
commit dc120ff375
17 changed files with 827 additions and 313 deletions

2
components.d.ts vendored
View File

@@ -7,7 +7,7 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
BackIcon: typeof import('./src/components/BackIcon.vue')['default']
BackIcon: typeof import('./src/components/icon/BackIcon.vue')['default']
BaseButton: typeof import('./src/components/BaseButton.vue')['default']
BaseIcon: typeof import('./src/components/BaseIcon.vue')['default']
Close: typeof import('./src/components/icon/Close.vue')['default']

View File

@@ -5,7 +5,6 @@ import {useRuntimeStore} from "@/stores/runtime.ts";
import {useSettingStore} from "@/stores/setting.ts";
import useTheme from "@/hooks/theme.ts";
import * as localforage from "localforage";
import SettingDialog from "@/pages/pc/components/dialog/SettingDialog.vue";
import CollectNotice from "@/pages/pc/components/CollectNotice.vue";
import {SAVE_DICT_KEY, SAVE_SETTING_KEY} from "@/utils/const.ts";
import {isMobile, shakeCommonDict} from "@/utils";
@@ -76,7 +75,6 @@ watch(() => route.path, (to, from) => {
</transition>
</router-view>
<CollectNotice/>
<SettingDialog/>
</template>
<style scoped lang="scss">

View File

@@ -64,10 +64,6 @@
}
.side-expand {
//--panel-margin-left: 10px !important;
}
html.dark {
--color-main-bg: rgba(14, 18, 23, 1);
--color-second-bg: rgb(30, 31, 34);
@@ -467,7 +463,7 @@ footer {
}
.card {
@apply rounded-xl bg-white p-4 mb-5 box-border;
@apply rounded-xl bg-white p-4 mb-5 box-border relative;
}
.center {
@@ -492,3 +488,7 @@ footer {
width: 100%;
border-bottom: 1px dashed #ababab;
}
.page-title {
@apply text-2xl;
}

646
src/pages/pc/Setting.vue Normal file
View File

@@ -0,0 +1,646 @@
<script setup lang="ts">
import {Icon} from '@iconify/vue';
import {ref, watch} from "vue";
import {useSettingStore} from "@/stores/setting.ts";
import {getAudioFileUrl, useChangeAllSound, usePlayAudio, useWatchAllSound} from "@/hooks/sound.ts";
import {getShortcutKey, useDisableEventListener, useEventListener} from "@/hooks/event.ts";
import {cloneDeep} from "lodash-es";
import {DefaultShortcutKeyMap, ShortcutKey} from "@/types.ts";
import BaseButton from "@/components/BaseButton.vue";
import {APP_NAME, EXPORT_DATA_KEY, SAVE_DICT_KEY, SAVE_SETTING_KEY, SoundFileOptions} from "@/utils/const.ts";
import VolumeIcon from "@/components/icon/VolumeIcon.vue";
import {useBaseStore} from "@/stores/base.ts";
import {saveAs} from "file-saver";
import {checkAndUpgradeSaveDict, checkAndUpgradeSaveSetting, shakeCommonDict} from "@/utils";
import {GITHUB} from "@/config/ENV.ts";
import dayjs from "dayjs";
import BasePage from "@/pages/pc/components/BasePage.vue";
const emit = defineEmits<{
toggleDisabledDialogEscKey: [val: boolean]
}>()
const tabIndex = $ref(0)
const settingStore = useSettingStore()
const store = useBaseStore()
//@ts-ignore
const gitLastCommitHash = ref(LATEST_COMMIT_HASH);
useDisableEventListener(() => undefined)
useWatchAllSound()
let editShortcutKey = $ref('')
const disabledDefaultKeyboardEvent = $computed(() => {
return editShortcutKey && tabIndex === 2
})
watch(() => disabledDefaultKeyboardEvent, v => {
emit('toggleDisabledDialogEscKey', !!v)
})
useEventListener('keydown', (e: KeyboardEvent) => {
if (!disabledDefaultKeyboardEvent) return
e.preventDefault()
let shortcutKey = getShortcutKey(e)
// console.log('e', e, e.keyCode, e.ctrlKey, e.altKey, e.shiftKey)
// console.log('key', shortcutKey)
// if (shortcutKey[shortcutKey.length-1] === '+') {
// settingStore.shortcutKeyMap[editShortcutKey] = DefaultShortcutKeyMap[editShortcutKey]
// return ElMessage.warning('设备失败!')
// }
if (editShortcutKey) {
if (shortcutKey === 'Delete') {
settingStore.shortcutKeyMap[editShortcutKey] = ''
} else {
for (const [k, v] of Object.entries(settingStore.shortcutKeyMap)) {
if (v === shortcutKey && k !== editShortcutKey) {
settingStore.shortcutKeyMap[editShortcutKey] = DefaultShortcutKeyMap[editShortcutKey]
return ElMessage.warning('快捷键重复!')
}
}
settingStore.shortcutKeyMap[editShortcutKey] = shortcutKey
}
}
})
function resetShortcutKeyMap() {
editShortcutKey = ''
settingStore.shortcutKeyMap = cloneDeep(DefaultShortcutKeyMap)
ElMessage.success('恢复成功')
}
function exportData() {
let data = {
version: EXPORT_DATA_KEY.version,
val: {
setting: {
version: SAVE_SETTING_KEY.version,
val: settingStore.$state
},
dict: {
version: SAVE_DICT_KEY.version,
val: shakeCommonDict(store.$state)
}
}
}
let blob = new Blob([JSON.stringify(data)], {type: "text/plain;charset=utf-8"});
saveAs(blob, `${APP_NAME}-User-Data-${dayjs().format('YYYY-MM-DD HH-mm-ss')}.json`);
ElMessage.success('导出成功!')
}
function importData(e) {
let file = e.target.files[0]
if (!file) return
// no()
let reader = new FileReader();
reader.onload = function (v) {
let str: any = v.target.result;
if (str) {
let obj = {
version: -1,
val: {
setting: {},
dict: {},
}
}
try {
obj = JSON.parse(str)
} catch (err) {
ElMessage.error('导入失败!')
}
if (obj.version === EXPORT_DATA_KEY.version) {
} else {
//TODO
}
let data = obj.val
let settingState = checkAndUpgradeSaveSetting(data.setting)
settingStore.setState(settingState)
let dictState = checkAndUpgradeSaveDict(data.dict)
store.init(dictState)
ElMessage.success('导入成功!')
}
}
reader.readAsText(file);
}
</script>
<template>
<BasePage>
<div class="page-title text-align-center">设置</div>
<div class="setting">
<div class="left">
<div class="tabs">
<div class="tab" :class="tabIndex === 0 && 'active'" @click="tabIndex = 0">
<Icon icon="bx:headphone" width="20" color="#0C8CE9"/>
<span>音效设置</span>
</div>
<div class="tab" :class="tabIndex === 2 && 'active'" @click="tabIndex = 2">
<Icon icon="material-symbols:keyboard-outline" width="20" color="#0C8CE9"/>
<span>快捷键设置</span>
</div>
<div class="tab" :class="tabIndex === 1 && 'active'" @click="tabIndex = 1">
<Icon icon="icon-park-outline:setting-config" width="20" color="#0C8CE9"/>
<span>其他设置</span>
</div>
<div class="tab" :class="tabIndex === 3 && 'active'" @click="tabIndex = 3">
<Icon icon="mdi:database-cog-outline" width="20" color="#0C8CE9"/>
<span>数据管理</span>
</div>
<div class="tab" :class="tabIndex === 4 && 'active'" @click="tabIndex = 4">
<Icon icon="mingcute:service-fill" width="20" color="#0C8CE9"/>
<span>反馈</span>
</div>
<div class="tab" :class="tabIndex === 5 && 'active'" @click="tabIndex = 5">
<Icon icon="mdi:about-circle-outline" width="20" color="#0C8CE9"/>
<span>关于</span>
</div>
</div>
<div class="git-log">
Build {{ gitLastCommitHash }}
</div>
</div>
<div class="content">
<div v-if="tabIndex === 0">
<div class="row">
<label class="main-title">所有音效</label>
<div class="wrapper">
<el-switch v-model="settingStore.allSound"
@change="useChangeAllSound"
inline-prompt
active-text=""
inactive-text=""
/>
</div>
</div>
<div class="line"></div>
<div class="row">
<label class="item-title">单词/句子自动发音</label>
<div class="wrapper">
<el-switch v-model="settingStore.wordSound"
inline-prompt
active-text=""
inactive-text=""
/>
</div>
</div>
<div class="row">
<label class="sub-title">单词/句子发音口音</label>
<div class="wrapper">
<el-select v-model="settingStore.wordSoundType"
placeholder="请选择"
>
<el-option label="美音" value="us"/>
<el-option label="英音" value="uk"/>
</el-select>
</div>
</div>
<div class="row">
<label class="sub-title">音量</label>
<div class="wrapper">
<el-slider v-model="settingStore.wordSoundVolume"/>
<span>{{ settingStore.wordSoundVolume }}%</span>
</div>
</div>
<div class="row">
<label class="sub-title">倍速</label>
<div class="wrapper">
<el-slider v-model="settingStore.wordSoundSpeed" :step="0.1" :min="0.5" :max="3"/>
<span>{{ settingStore.wordSoundSpeed }}</span>
</div>
</div>
<div class="line"></div>
<div class="row">
<label class="item-title">按键音</label>
<div class="wrapper">
<el-switch v-model="settingStore.keyboardSound"
inline-prompt
active-text=""
inactive-text=""
/>
</div>
</div>
<div class="row">
<label class="item-title">按键音效</label>
<div class="wrapper">
<el-select v-model="settingStore.keyboardSoundFile"
placeholder="请选择"
>
<el-option
v-for="item in SoundFileOptions"
:key="item.value"
:label="item.label"
:value="item.value"
>
<div class="el-option-row">
<span>{{ item.label }}</span>
<VolumeIcon
:time="100"
@click="usePlayAudio(getAudioFileUrl(item.value)[0])"/>
</div>
</el-option>
</el-select>
</div>
</div>
<div class="row">
<label class="sub-title">音量</label>
<div class="wrapper">
<el-slider v-model="settingStore.keyboardSoundVolume"/>
<span>{{ settingStore.keyboardSoundVolume }}%</span>
</div>
</div>
<div class="line"></div>
<div class="row">
<label class="item-title">效果音输入错误完成时的音效</label>
<div class="wrapper">
<el-switch v-model="settingStore.effectSound"
inline-prompt
active-text=""
inactive-text=""
/>
</div>
</div>
<div class="row">
<label class="sub-title">音量</label>
<div class="wrapper">
<el-slider v-model="settingStore.effectSoundVolume"/>
<span>{{ settingStore.effectSoundVolume }}%</span>
</div>
</div>
</div>
<div v-if="tabIndex === 1">
<div class="row">
<label class="item-title">显示上一个/下一个单词</label>
<div class="wrapper">
<el-switch v-model="settingStore.showNearWord"
inline-prompt
active-text=""
inactive-text=""
/>
</div>
</div>
<div class="desc">
开启后练习中会在上方显示上一个/下一个单词
</div>
<div class="line"></div>
<div class="row">
<label class="item-title">忽略大小写</label>
<div class="wrapper">
<el-switch v-model="settingStore.ignoreCase"
inline-prompt
active-text=""
inactive-text=""
/>
</div>
</div>
<div class="desc">
开启后输入时不区分大小写如输入helloHello都会被认为是正确的
</div>
<div class="line"></div>
<div class="row">
<label class="item-title">允许默写模式下显示提示</label>
<div class="wrapper">
<el-switch v-model="settingStore.allowWordTip"
inline-prompt
active-text=""
inactive-text=""
/>
</div>
</div>
<div class="desc">
开启后可以通过鼠标 hover 单词或者按 {{ settingStore.shortcutKeyMap[ShortcutKey.ShowWord] }} 显示正确答案
</div>
<div class="line"></div>
<div class="row">
<label class="item-title">字体设置(仅可调整单词练习)</label>
</div>
<div class="row">
<label class="sub-title">外语字体</label>
<div class="wrapper">
<el-slider
:min="10"
:max="100"
v-model="settingStore.fontSize.wordForeignFontSize"/>
<span>{{ settingStore.fontSize.wordForeignFontSize }}</span>
</div>
</div>
<div class="row">
<label class="sub-title">中文字体</label>
<div class="wrapper">
<el-slider
:min="10"
:max="100"
v-model="settingStore.fontSize.wordTranslateFontSize"/>
<span>{{ settingStore.fontSize.wordTranslateFontSize }}</span>
</div>
</div>
<div class="line"></div>
<div class="row">
<label class="item-title">其他设置</label>
</div>
<div class="row">
<label class="sub-title">切换下一个单词时间</label>
<div class="wrapper">
<el-input-number v-model="settingStore.waitTimeForChangeWord"
:min="6"
:max="100"
type="number"
/>
<span>毫秒</span>
</div>
</div>
</div>
<div class="body" v-if="tabIndex === 2">
<div class="row">
<label class="main-title">功能</label>
<div class="wrapper">快捷键(点击可修改)</div>
</div>
<div class="scroll">
<div class="row" v-for="item of Object.entries(settingStore.shortcutKeyMap)">
<label class="item-title">{{ $t(item[0]) }}</label>
<div class="wrapper" @click="editShortcutKey = item[0]">
<div class="set-key" v-if="editShortcutKey === item[0]">
<input :value="item[1]?item[1]:'未设置快捷键'" readonly type="text" @blur="editShortcutKey = ''">
<span @click.stop="editShortcutKey = ''">直接按键盘进行设置</span>
</div>
<div v-else>
<div v-if="item[1]">{{ item[1] }}</div>
<span v-else>未设置快捷键</span>
</div>
</div>
</div>
</div>
<div class="row footer">
<label class="item-title"></label>
<div class="wrapper">
<BaseButton @click="resetShortcutKeyMap">恢复默认</BaseButton>
</div>
</div>
</div>
<div v-if="tabIndex === 3">
<div class="row">
<div class="main-title">数据导出</div>
</div>
<div class="row">
<label class="sub-title">
目前用户的所有数据(自定义设置自定义词典练习进度等)
<b>仅保存在本地</b>
如果您需要在不同的设备浏览器或者其他非官方部署上使用 {{ APP_NAME }} 您需要手动进行数据同步和保存
</label>
</div>
<div class="row">
<BaseButton @click="exportData">数据导出</BaseButton>
</div>
<div class="row">
<div class="main-title">数据导入</div>
</div>
<div class="row">
<label class="sub-title">
请注意导入数据将
<b style="color: red"> 完全覆盖 </b>
当前数据请谨慎操作
</label>
</div>
<div class="row">
<div class="import hvr-grow">
<BaseButton>数据导入</BaseButton>
<input type="file"
accept="application/json"
@change="importData">
</div>
</div>
</div>
<div v-if="tabIndex === 4" class="feedback-modal">
<div>
给我发Email<a href="mailto:zyronon@163.com">zyronon@163.com</a>
</div>
<p>or</p>
<div class="github">
<span><a :href="GITHUB" target="_blank">Github</a>上给我提一个
<a :href="`${GITHUB}/issues`" target="_blank">Issue</a>
</span>
<div class="options">
<BaseButton>
<a :href="`${GITHUB}/issues/new?assignees=&labels=&projects=&template=%E5%8D%95%E8%AF%8D%E9%94%99%E8%AF%AF---word-error.md&title=%E5%8D%95%E8%AF%8D%E9%94%99%E8%AF%AF+%7C+Word+error`"
target="_blank">词典错误</a>
</BaseButton>
<BaseButton>
<a :href="`${GITHUB}/issues/new?assignees=&labels=&projects=&template=问题报告---bug-report-.md&title=问题报告+%7C+Bug+report+`"
target="_blank">反馈BUG</a>
</BaseButton>
<BaseButton>
<a :href="`${GITHUB}/issues/new?assignees=&labels=&projects=&template=功能请求---feature-request.md&title=功能请求+%7C+Feature+request`"
target="_blank">功能请求</a>
</BaseButton>
</div>
</div>
</div>
<div v-if="tabIndex === 5" class="about">
<h1>Type Words</h1>
<p>
本项目完全开源好用请大家多多点Star
</p>
<p>
GitHub地址<a href="https://github.com/zyronon/typing-word">https://github.com/zyronon/typing-word</a>
</p>
<p>
反馈<a
href="https://github.com/zyronon/typing-word/issues">https://github.com/zyronon/typing-word/issues</a>
</p>
</div>
</div>
</div>
</BasePage>
</template>
<style scoped lang="scss">
.setting {
display: flex;
color: var(--color-font-1);
.left {
height: 93vh;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
border-right: 2px solid gainsboro;
.tabs {
padding: .6rem 1.6rem;
display: flex;
flex-direction: column;
gap: .6rem;
.tab {
cursor: pointer;
padding: .6rem .9rem;
border-radius: .5rem;
display: flex;
align-items: center;
gap: .6rem;
&.active {
background: var(--color-item-bg);
}
}
}
.git-log {
font-size: .6rem;
color: gray;
margin-bottom: .3rem;
}
}
.content {
flex: 1;
height: 100%;
overflow: auto;
padding: var(--space);
padding: 0 2.6rem;
.row {
min-height: 2.6rem;
display: flex;
justify-content: space-between;
align-items: center;
gap: calc(var(--space) * 5);
.wrapper {
height: 2rem;
flex: 1;
display: flex;
justify-content: flex-end;
gap: var(--space);
span {
text-align: right;
//width: 30rem;
font-size: .7rem;
color: gray;
}
.set-key {
align-items: center;
input {
width: 9rem;
box-sizing: border-box;
margin-right: .6rem;
height: 1.8rem;
outline: none;
font-size: 1rem;
border: 1px solid gray;
border-radius: .2rem;
padding: 0 .3rem;
background: var(--color-second-bg);
color: var(--color-font-1);
}
}
}
.main-title {
font-size: 1.1rem;
font-weight: bold;
}
.item-title {
font-size: 1rem;
}
.sub-title {
font-size: .9rem;
}
}
.body {
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
}
.scroll {
flex: 1;
padding-right: .6rem;
overflow: auto;
}
.footer {
margin-bottom: 1.3rem;
}
.desc {
margin-bottom: .6rem;
font-size: .8rem;
}
.line {
border-bottom: 1px solid #c4c3c3;
}
}
}
.el-option-row {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
.icon-wrapper {
transform: translateX(10rem);
}
}
.import {
display: inline-flex;
position: relative;
input {
position: absolute;
height: 100%;
width: 100%;
opacity: 0;
}
}
.feedback-modal {
//height: 80vh;
display: flex;
flex-direction: column;
align-items: center;
padding: var(--space);
//justify-content: center;
color: var(--color-font-1);
p {
font-size: 2.4rem;
}
.github {
display: flex;
align-items: center;
gap: var(--space);
.options {
display: flex;
flex-direction: column;
gap: .6rem;
}
}
}
.about {
text-align: center;
}
</style>

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import BasePage from "@/pages/pc/components/BasePage.vue";
import BackIcon from "@/components/BackIcon.vue";
import BackIcon from "@/pages/pc/components/BackIcon.vue";
import Empty from "@/components/Empty.vue";
import ArticleList from "@/pages/pc/components/list/ArticleList.vue";
import {useBaseStore} from "@/stores/base.ts";

View File

@@ -4,12 +4,15 @@
<template>
<div class="flex justify-center">
<div class="w-[70vw] 2xl:w-[50vw] my-5">
<div class="w-[70vw] 2xl:w-[50vw] page">
<slot></slot>
</div>
</div>
</template>
<style scoped lang="scss">
.page{
min-height: calc(100vh - 1.2rem);
margin-top: 1.2rem;
}
</style>

View File

@@ -54,7 +54,7 @@ watch(() => settingStore.load, (n) => {
<div class="collect" v-if="showNotice">
<div class="href-wrapper">
<div class="round">
<div class="href">typing-word.ttentau.top</div>
<div class="href">2study.top</div>
<Icon
width="22"
icon="mdi:star-outline"/>
@@ -115,7 +115,7 @@ watch(() => settingStore.load, (n) => {
box-shadow: var(--shadow);
box-sizing: border-box;
&.mobile{
&.mobile {
width: 95%;
padding: 0.6rem;
}

View File

@@ -1,96 +0,0 @@
<script setup lang="ts">
import {DictResource} from "@/types.ts";
import {dictionaryResources} from "@/assets/dictionary.ts";
import {groupBy} from "lodash-es";
import {useBaseStore} from "@/stores/base.ts";
import DictGroup from "@/pages/pc/components/list/DictGroup.vue";
import BaseIcon from "@/components/BaseIcon.vue";
import Input from "@/pages/pc/components/Input.vue";
import BaseButton from "@/components/BaseButton.vue";
import {computed} from "vue";
import DictList from "@/pages/pc/components/list/DictList.vue";
import Empty from "@/components/Empty.vue";
const emit = defineEmits<{
add: [],
selectDict: [val: { dict: any, index: number }]
}>()
const store = useBaseStore()
function groupByDictTags(dictList: DictResource[]) {
return dictList.reduce<Record<string, DictResource[]>>((result, dict) => {
dict.tags.forEach((tag) => {
if (result[tag]) {
result[tag].push(dict)
} else {
result[tag] = [dict]
}
})
return result
}, {})
}
const groupedByCategoryAndTag = $computed(() => {
const groupByCategory = groupBy(dictionaryResources, 'category')
let data = []
for (const [key, value] of Object.entries(groupByCategory)) {
data.push([key, groupByDictTags(value)])
}
return data
})
let showSearchInput = $ref(false)
let searchKey = $ref('')
const searchList = computed(() => {
if (searchKey) {
let s = searchKey.toLowerCase()
return dictionaryResources.filter((item) => {
return item.name.toLowerCase().includes(s)
|| item.category.toLowerCase().includes(s)
|| item.tags.join('').replace('所有', '').toLowerCase().includes(s)
|| item?.url?.toLowerCase?.().includes?.(s)
})
}
return []
})
defineExpose({startSearch: () => showSearchInput = true})
</script>
<template>
<div class="card">
<div class="flex gap-4" v-if="showSearchInput">
<Input placeholder="请输入词典名称/缩写/类别" v-model="searchKey" class="flex-1" autofocus/>
<BaseButton @click="showSearchInput = false, searchKey = ''">取消</BaseButton>
</div>
<div class="title flex justify-between" v-else>
<span>词典列表</span>
<BaseIcon @click="showSearchInput = true" icon="fluent:search-24-regular"/>
</div>
<div class="mt-4" v-if="searchKey">
<DictList
v-if="searchList.length "
@selectDict="e => emit('selectDict',e)"
:list="searchList"
:select-id="'-1'"/>
<Empty v-else text="没有相关词典"/>
</div>
<div class="w-full" v-else>
<DictGroup
v-for="item in groupedByCategoryAndTag"
:select-id="store.currentStudyWordDict.id"
@selectDict="e => emit('selectDict',e)"
:groupByTag="item[1]"
:category="item[0]"
/>
</div>
</div>
</template>
<style scoped lang="scss">
</style>

View File

@@ -1,154 +0,0 @@
<script setup lang="ts">
import Dialog from "@/pages/pc/components/dialog/Dialog.vue"
import Setting from "@/pages/pc/components/Setting.vue";
import {useRuntimeStore} from "@/stores/runtime.ts";
const runtimeStore = useRuntimeStore()
let disabledDialogEscKey = $ref(true)
</script>
<template>
<Dialog
v-model="runtimeStore.showSettingModal"
:keyboard="disabledDialogEscKey"
title="设置">
<Setting @toggle-disabled-dialog-esc-key="e => disabledDialogEscKey = !e"/>
</Dialog>
</template>
<style scoped lang="scss">
.setting-modal {
width: 40vw;
height: 70vh;
display: flex;
color: var(--color-font-1);
.left {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
.tabs {
padding: .6rem 1.6rem;
display: flex;
flex-direction: column;
//align-items: center;
//justify-content: center;
gap: .6rem;
.tab {
cursor: pointer;
padding: .6rem .9rem;
border-radius: .5rem;
display: flex;
align-items: center;
gap: .6rem;
&.active {
background: var(--color-item-bg);
}
}
}
.git-log {
font-size: .6rem;
color: gray;
margin-bottom: .3rem;
}
}
.content {
background: var(--color-header-bg);
flex: 1;
height: 100%;
overflow: auto;
padding: 0 var(--space);
.row {
height: 2.6rem;
display: flex;
justify-content: space-between;
align-items: center;
gap: calc(var(--space) * 5);
.wrapper {
height: 2rem;
flex: 1;
display: flex;
justify-content: flex-end;
gap: var(--space);
span {
text-align: right;
//width: 30rem;
font-size: .7rem;
color: gray;
}
.set-key {
align-items: center;
input {
width: 9rem;
box-sizing: border-box;
margin-right: .6rem;
height: 1.8rem;
outline: none;
font-size: 1rem;
border: 1px solid gray;
border-radius: .2rem;
padding: 0 .3rem;
background: var(--color-second-bg);
color: var(--color-font-1);
}
}
}
.main-title {
font-size: 1.1rem;
font-weight: bold;
}
.item-title {
font-size: 1rem;
}
.sub-title {
font-size: .9rem;
}
}
.body {
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
}
.scroll {
flex: 1;
padding-right: .6rem;
overflow: auto;
}
.footer {
margin-bottom: 1.3rem;
}
.desc {
margin-bottom: .6rem;
font-size: .8rem;
}
.line {
border-bottom: 1px solid #c4c3c3;
}
}
}
</style>

View File

@@ -42,7 +42,7 @@ const {toggleTheme} = useTheme()
</div>
<div class="row"
:title="`设置(${settingStore.shortcutKeyMap[ShortcutKey.OpenSetting]})`"
@click="runtimeStore.showSettingModal = true"
@click="router.push('/setting')"
>
<Icon icon="uil:setting"/>
<span v-if="settingStore.sideExpand">设置</span>

View File

@@ -1,5 +1,5 @@
<script setup lang="tsx">
import {getDefaultDict, getDefaultWord} from "@/types";
import {DictId, getDefaultDict, getDefaultWord} from "@/types";
import type {Word} from "@/types";
import BasePage from "@/pages/pc/components/BasePage.vue";
@@ -12,7 +12,7 @@ import BaseTable from "@/pages/pc/components/BaseTable.vue";
import WordItem from "@/pages/pc/components/WordItem.vue";
import type {FormInstance, FormRules} from "element-plus";
import PopConfirm from "@/pages/pc/components/PopConfirm.vue";
import BackIcon from "@/components/BackIcon.vue";
import BackIcon from "@/pages/pc/components/BackIcon.vue";
import BaseButton from "@/components/BaseButton.vue";
import {useRoute, useRouter} from "vue-router";
import {useBaseStore} from "@/stores/base.ts";
@@ -278,7 +278,10 @@ onMounted(() => {
if (!runtimeStore.editDict.id) {
router.push("/word")
} else {
if (!runtimeStore.editDict.words.length && !runtimeStore.editDict.custom) {
if (!runtimeStore.editDict.words.length
&& !runtimeStore.editDict.custom
&& ![DictId.wordCollect, DictId.wordWrong, DictId.wordKnown].includes(runtimeStore.editDict.id)
) {
loading = true
_getDictDataByUrl(runtimeStore.editDict).then(r => {
loading = false
@@ -295,8 +298,13 @@ function formClose() {
}
async function addMyStudyList() {
base.changeDict(runtimeStore.editDict)
if (route.query?.from) {
router.back()
}
router.back()
requestIdleCallback(()=>{
base.changeDict(runtimeStore.editDict)
})
}
defineRender(() => {
@@ -306,7 +314,7 @@ defineRender(() => {
showBookDetail.value ? <div className="card mb-0 h-[95vh] flex flex-col">
<div class="flex justify-between items-center relative">
<BackIcon class="z-2" onClick={() => router.back()}/>
<div class="absolute text-2xl text-align-center w-full">{runtimeStore.editDict.name}</div>
<div class="absolute page-title text-align-center w-full">{runtimeStore.editDict.name}</div>
<div class="flex gap-2">
<BaseButton type="info" onClick={() => isEdit = true}>编辑</BaseButton>
<BaseButton onClick={addMyStudyList}>学习</BaseButton>
@@ -450,7 +458,7 @@ defineRender(() => {
<div class="card mb-0 h-[95vh]">
<div class="flex justify-between items-center relative">
<BackIcon class="z-2" onClick={() => isAdd ? router.back() : (isEdit = false)}/>
<div class="absolute text-2xl text-align-center w-full">
<div class="absolute page-title text-align-center w-full">
{runtimeStore.editDict.id ? '修改' : '创建'}词典
</div>
</div>

View File

@@ -0,0 +1,114 @@
<script setup lang="ts">
import "vue-activity-calendar/style.css";
import {useNav} from "@/utils";
import BasePage from "@/pages/pc/components/BasePage.vue";
import {DictResource, getDefaultDict} from "@/types.ts";
import {useRuntimeStore} from "@/stores/runtime.ts";
import BaseIcon from "@/components/BaseIcon.vue";
import Empty from "@/components/Empty.vue";
import Input from "@/pages/pc/components/Input.vue";
import BaseButton from "@/components/BaseButton.vue";
import DictList from "@/pages/pc/components/list/DictList.vue";
import BackIcon from "@/pages/pc/components/BackIcon.vue";
import DictGroup from "@/pages/pc/components/list/DictGroup.vue";
import {useBaseStore} from "@/stores/base.ts";
import {useRouter} from "vue-router";
import {groupBy} from "lodash-es";
import {dictionaryResources} from "@/assets/dictionary.ts";
import {computed} from "vue";
const {nav} = useNav()
const runtimeStore = useRuntimeStore()
const store = useBaseStore()
const router = useRouter()
function selectDict(e) {
console.log(e.dict)
getDictDetail(e.dict)
}
async function getDictDetail(val: DictResource) {
runtimeStore.editDict = getDefaultDict(val)
nav('dict-detail', {from: 'list'})
}
function groupByDictTags(dictList: DictResource[]) {
return dictList.reduce<Record<string, DictResource[]>>((result, dict) => {
dict.tags.forEach((tag) => {
if (result[tag]) {
result[tag].push(dict)
} else {
result[tag] = [dict]
}
})
return result
}, {})
}
const groupedByCategoryAndTag = $computed(() => {
const groupByCategory = groupBy(dictionaryResources, 'category')
let data = []
for (const [key, value] of Object.entries(groupByCategory)) {
data.push([key, groupByDictTags(value)])
}
return data
})
let showSearchInput = $ref(false)
let searchKey = $ref('')
const searchList = computed<any[]>(() => {
if (searchKey) {
let s = searchKey.toLowerCase()
return dictionaryResources.filter((item) => {
return item.name.toLowerCase().includes(s)
|| item.category.toLowerCase().includes(s)
|| item.tags.join('').replace('所有', '').toLowerCase().includes(s)
|| item?.url?.toLowerCase?.().includes?.(s)
})
}
return []
})
</script>
<template>
<BasePage>
<div class="card">
<div class="flex items-center relative gap-2">
<BackIcon class="z-2" @Click='router.back()'/>
<div class="flex flex-1 gap-4" v-if="showSearchInput">
<Input placeholder="请输入词典名称/缩写/类别" v-model="searchKey" class="flex-1" autofocus/>
<BaseButton @click="showSearchInput = false, searchKey = ''">取消</BaseButton>
</div>
<div class="py-1 flex flex-1 justify-end" v-else>
<span class="page-title absolute w-full center">词典列表</span>
<BaseIcon @click="showSearchInput = true"
class="z-1"
icon="fluent:search-24-regular"/>
</div>
</div>
<div class="mt-4" v-if="searchKey">
<DictList
v-if="searchList.length "
@selectDict="selectDict"
:list="searchList"
:select-id="'-1'"/>
<Empty v-else text="没有相关词典"/>
</div>
<div class="w-full" v-else>
<DictGroup
v-for="item in groupedByCategoryAndTag"
:select-id="store.currentStudyWordDict.id"
@selectDict="selectDict"
:groupByTag="item[1]"
:category="item[0]"
/>
</div>
</div>
</BasePage>
</template>
<style scoped lang="scss">
</style>

View File

@@ -6,12 +6,11 @@ import "vue-activity-calendar/style.css";
import {useRouter} from "vue-router";
import BaseIcon from "@/components/BaseIcon.vue";
import Dialog from "@/pages/pc/components/dialog/Dialog.vue";
import {_getAccomplishDate, _getAccomplishDays, _getDictDataByUrl, useNav, _dateFormat} from "@/utils";
import {_dateFormat, _getAccomplishDate, _getAccomplishDays, _getDictDataByUrl, useNav} from "@/utils";
import BasePage from "@/pages/pc/components/BasePage.vue";
import {DictResource, getDefaultDict} from "@/types.ts";
import {onMounted, watch} from "vue";
import {getCurrentStudyWord} from "@/hooks/dict.ts";
import DictListPanel from "@/pages/pc/components/DictListPanel.vue";
import {useRuntimeStore} from "@/stores/runtime.ts";
import Book from "@/pages/pc/components/Book.vue";
import PopConfirm from "@/pages/pc/components/PopConfirm.vue";
@@ -28,13 +27,8 @@ let currentStudy = $ref({
write: []
})
onMounted(() => {
init()
})
watch(() => store.load, () => {
init()
})
onMounted(init)
watch(() => store.load, init)
async function init() {
if (store.word.studyIndex >= 3) {
@@ -46,7 +40,6 @@ async function init() {
if (!currentStudy.new.length && store.sdict.words.length) {
currentStudy = getCurrentStudyWord()
}
}
function startStudy() {
@@ -64,7 +57,6 @@ function startStudy() {
nav('study-word', {}, currentStudy)
} else {
ElMessage.warning('请先选择一本词典')
dictListRef.startSearch()
}
}
@@ -74,7 +66,6 @@ function setPerDayStudyNumber() {
tempPerDayStudyNumber = store.sdict.perDayStudyNumber
} else {
ElMessage.warning('请先选择一本词典')
dictListRef.startSearch()
}
}
@@ -86,17 +77,11 @@ function changePerDayStudyNumber() {
currentStudy = getCurrentStudyWord()
}
function selectDict(e) {
console.log(e.dict)
getDictDetail(e.dict)
}
async function getDictDetail(val: DictResource) {
async function goDictDetail(val: DictResource) {
runtimeStore.editDict = getDefaultDict(val)
nav('dict-detail', {})
}
let dictListRef = $ref<any>()
let isMultiple = $ref(false)
let selectIds = $ref([])
@@ -113,6 +98,7 @@ function handleBatchDel() {
store.word.bookList.splice(r, 1)
}
})
selectIds = []
ElMessage.success("删除成功!")
}
@@ -190,7 +176,7 @@ const progressTextRight = $computed(() => {
<span class="text-xl font-bold">{{ store.sdict.name || '请选择书籍开始学习' }}</span>
<BaseIcon title="切换词典" :icon="store.sdict.name ? 'gg:arrows-exchange' : 'fluent:add-20-filled'"
class="ml-4"
@click="dictListRef.startSearch()"/>
@click="router.push('/dict-list')"/>
</div>
</div>
<div class="">
@@ -224,14 +210,14 @@ const progressTextRight = $computed(() => {
</div>
</div>
<div class="flex flex-col items-end justify-around">
<div class="flex flex-col items-end justify-around ">
<div class="flex gap-1 items-center">
每日目标
<div style="color:#ac6ed1;" @click="setPerDayStudyNumber"
class="bg-slate-200 px-2 h-10 flex center text-2xl rounded cursor-pointer">
{{ store.sdict.id ? store.sdict.perDayStudyNumber : 0 }}
</div>
个单词
个单词 <span class="color-blue cursor-pointer" @click="setPerDayStudyNumber">更改</span>
</div>
<div class="rounded-xl bg-slate-800 flex items-center gap-2 py-3 px-5 text-white cursor-pointer"
:class="store.sdict.name || 'opacity-70 cursor-not-allowed'" @click="startStudy">
@@ -258,12 +244,11 @@ const progressTextRight = $computed(() => {
<div class="grid grid-cols-6 gap-4 mt-4">
<Book :is-add="false" quantifier="个词" :item="item" :checked="selectIds.includes(item.id)"
@check="() => toggleSelect(item)" :show-checkbox="isMultiple && j >= 3"
v-for="(item, j) in store.word.bookList" @click="getDictDetail(item)"/>
<Book :is-add="true" @click="dictListRef.startSearch()"/>
v-for="(item, j) in store.word.bookList" @click="goDictDetail(item)"/>
<Book :is-add="true" @click="router.push('/dict-list')"/>
</div>
</div>
<DictListPanel ref="dictListRef" @selectDict="selectDict"/>
<div class="card">
<div class="title">

View File

@@ -12,6 +12,8 @@ import StudyWord from "@/pages/pc/word/StudyWord.vue";
import EditArticlePage from "@/pages/pc/article/EditArticlePage.vue";
import BookDetail from "@/pages/pc/article/BookDetail.vue";
import BatchEditArticlePage from "@/pages/pc/article/BatchEditArticlePage.vue";
import DictList from "@/pages/pc/word/DictList.vue";
import Setting from "@/pages/pc/Setting.vue";
export const routes: RouteRecordRaw[] = [
{
@@ -20,6 +22,7 @@ export const routes: RouteRecordRaw[] = [
children: [
{path: 'home', component: HomeIndex},
{path: 'word', component: WordHomePage},
{path: 'dict-list', component: DictList},
{path: 'study-word', component: StudyWord},
{path: 'dict-detail', component: DictDetail},
{path: 'article', component: ArticleHomePage},
@@ -27,6 +30,7 @@ export const routes: RouteRecordRaw[] = [
{path: 'edit-article', component: EditArticlePage},
{path: 'batch-edit-article', component: BatchEditArticlePage},
{path: 'book-detail', component: BookDetail},
{path: 'setting', component: Setting},
]
},

View File

@@ -1,5 +1,5 @@
import {defineStore} from 'pinia'
import {Article, Dict, DictType, getDefaultDict, Sort, Word} from "../types.ts"
import {Article, Dict, DictId, DictType, getDefaultDict, Sort, Word} from "../types.ts"
import {cloneDeep, merge, reverse, shuffle} from "lodash-es";
import {emitter, EventKey} from "@/utils/eventBus.ts"
import * as localforage from "localforage";
@@ -32,11 +32,9 @@ export const DefaultBaseState = (): BaseState => ({
load: false,
word: {
bookList: [
getDefaultDict({
id: 'word-collect', name: '收藏', words: []
}),
getDefaultDict({id: 'word-wrong', name: '错词'}),
getDefaultDict({id: 'word-known', name: '已掌握'}),
getDefaultDict({id: DictId.wordCollect, name: '收藏', words: []}),
getDefaultDict({id: DictId.wordWrong, name: '错词'}),
getDefaultDict({id: DictId.wordKnown, name: '已掌握'}),
// getDefaultDict({
// id: 'nce-new-2',
// name: '新概念英语(新版)-2',
@@ -54,7 +52,7 @@ export const DefaultBaseState = (): BaseState => ({
},
article: {
bookList: [
getDefaultDict({id: 'article-collect', name: '收藏'})
getDefaultDict({id: DictId.articleCollect, name: '收藏'})
],
studyIndex: -1,
}

View File

@@ -74,7 +74,6 @@ export enum DictType {
simple = 'simple',
wrong = 'wrong',
known = 'known',
collectWord = 'collect-word',
word = 'word',
article = 'article',
}
@@ -240,10 +239,21 @@ export type DictResource = {
category: string
tags: string[]
translateLanguage: TranslateLanguageType
//todo 可以考虑删除了
type: DictType
language: LanguageType
}
export interface Dict extends DictResource {
lastLearnIndex: number,
perDayStudyNumber: number,
words: Word[],
articles: Article[],
statistics: Statistics[],
custom: boolean,//是否是自定义词典
complete: boolean,//是否学习完成学完了设为true然后lastLearnIndex重置
}
export function getDefaultDict(val: Partial<Dict> = {}): Dict {
return {
id: '',
@@ -262,20 +272,11 @@ export function getDefaultDict(val: Partial<Dict> = {}): Dict {
articles: [],
statistics: [],
custom: false,
complete: false,
...val
}
}
export interface Dict extends DictResource {
lastLearnIndex: number,
perDayStudyNumber: number,
words: Word[],
articles: Article[],
statistics: Statistics[],
custom: boolean,//是否是自定义词典
complete: boolean,//是否学习完成学完了设为true然后lastLearnIndex重置
}
export interface ArticleItem {
item: Article,
index: number
@@ -291,3 +292,10 @@ export interface StudyData {
words: any[],
wrongWords: any[],
}
export class DictId {
static wordCollect = 'wordCollect'
static wordWrong = 'wordWrong'
static wordKnown = 'wordKnown'
static articleCollect = 'articleCollect'
}