feat:add update prompts to dynamically introduce the xlsx library

This commit is contained in:
zyronon
2025-09-15 00:16:31 +08:00
parent fadcf0fff6
commit 12a22baf2b
22 changed files with 488 additions and 202 deletions

3
components.d.ts vendored
View File

@@ -46,7 +46,6 @@ declare module 'vue' {
IconFluentEyeOff16Regular: typeof import('~icons/fluent/eye-off16-regular')['default']
IconFluentHome20Regular: typeof import('~icons/fluent/home20-regular')['default']
IconFluentKeyboardLayoutFloat20Regular: typeof import('~icons/fluent/keyboard-layout-float20-regular')['default']
IconFluentMailEdit20Regular: typeof import('~icons/fluent/mail-edit20-regular')['default']
IconFluentMyLocation20Regular: typeof import('~icons/fluent/my-location20-regular')['default']
IconFluentPaddingLeft20Regular: typeof import('~icons/fluent/padding-left20-regular')['default']
IconFluentPerson20Regular: typeof import('~icons/fluent/person20-regular')['default']
@@ -73,10 +72,12 @@ declare module 'vue' {
IconFluentWeatherSunny16Regular: typeof import('~icons/fluent/weather-sunny16-regular')['default']
IconIconParkOutlineAddMusic: typeof import('~icons/icon-park-outline/add-music')['default']
IconMaterialSymbolsMail: typeof import('~icons/material-symbols/mail')['default']
IconPhExportLight: typeof import('~icons/ph/export-light')['default']
IconRiTwitterFill: typeof import('~icons/ri/twitter-fill')['default']
IconSimpleIconsGithub: typeof import('~icons/simple-icons/github')['default']
IconSimpleIconsWechat: typeof import('~icons/simple-icons/wechat')['default']
IconSimpleIconsXiaohongshu: typeof import('~icons/simple-icons/xiaohongshu')['default']
IconSystemUiconsImport: typeof import('~icons/system-uicons/import')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SlideHorizontal: typeof import('./src/components/slide/SlideHorizontal.vue')['default']

View File

@@ -41,8 +41,12 @@
"@iconify-json/icon-park-outline": "^1.2.4",
"@iconify-json/icon-park-solid": "^1.2.4",
"@iconify-json/material-symbols": "^1.2.33",
"@iconify-json/oui": "^1.2.6",
"@iconify-json/ph": "^1.2.2",
"@iconify-json/qlementine-icons": "^1.2.11",
"@iconify-json/ri": "^1.2.5",
"@iconify-json/simple-icons": "^1.2.48",
"@iconify-json/system-uicons": "^1.2.4",
"@types/file-saver": "^2.0.7",
"@types/lodash-es": "^4.17.12",
"@types/md5": "^2.1.33",

91
pnpm-lock.yaml generated
View File

@@ -29,9 +29,6 @@ importers:
idb-keyval:
specifier: ^6.2.2
version: 6.2.2
libarchive-wasm:
specifier: ^1.2.0
version: 1.2.0
md5:
specifier: ^2.2.1
version: 2.3.0
@@ -78,18 +75,27 @@ importers:
'@iconify-json/material-symbols':
specifier: ^1.2.33
version: 1.2.33
'@iconify-json/oui':
specifier: ^1.2.6
version: 1.2.6
'@iconify-json/ph':
specifier: ^1.2.2
version: 1.2.2
'@iconify-json/qlementine-icons':
specifier: ^1.2.11
version: 1.2.11
'@iconify-json/ri':
specifier: ^1.2.5
version: 1.2.5
'@iconify-json/simple-icons':
specifier: ^1.2.48
version: 1.2.48
'@iconify-json/system-uicons':
specifier: ^1.2.4
version: 1.2.4
'@types/file-saver':
specifier: ^2.0.7
version: 2.0.7
'@types/jszip':
specifier: ^3.4.1
version: 3.4.1
'@types/lodash-es':
specifier: ^4.17.12
version: 4.17.12
@@ -508,12 +514,24 @@ packages:
'@iconify-json/material-symbols@1.2.33':
resolution: {integrity: sha512-Bs0X1+/vpJydW63olrGh60zkR8/Y70sI14AIWaP7Z6YQXukzWANH4q3I0sIPklbIn1oL6uwLvh0zQyd6Vh79LQ==}
'@iconify-json/oui@1.2.6':
resolution: {integrity: sha512-dBqxbLKztTtb0Cq3kEyLeYAdyJT2un+FzIZB0ei3busps/OwNIHjqowsVqPwRtHXiXTjiwOHUPbxgcVB0SCIsQ==}
'@iconify-json/ph@1.2.2':
resolution: {integrity: sha512-PgkEZNtqa8hBGjHXQa4pMwZa93hmfu8FUSjs/nv4oUU6yLsgv+gh9nu28Kqi8Fz9CCVu4hj1MZs9/60J57IzFw==}
'@iconify-json/qlementine-icons@1.2.11':
resolution: {integrity: sha512-ipCO0hd9z/SgmzCRsxCB9NGph1rcEp4aQBKGy9YOuDpQc9pwtgbB+yAJkGDfO4CzMDIEBSS6z7tmjk4cE9eHCw==}
'@iconify-json/ri@1.2.5':
resolution: {integrity: sha512-kWGimOXMZrlYusjBKKXYOWcKhbOHusFsmrmRGmjS7rH0BpML5A9/fy8KHZqFOwZfC4M6amObQYbh8BqO5cMC3w==}
'@iconify-json/simple-icons@1.2.48':
resolution: {integrity: sha512-EACOtZMoPJtERiAbX1De0asrrCtlwI27+03c9OJlYWsly9w1O5vcD8rTzh+kDPjo+K8FOVnq2Qy+h/CzljSKDA==}
'@iconify-json/system-uicons@1.2.4':
resolution: {integrity: sha512-9WB9dmEm+TRCXI5Ml2IY8zQAPZES8euKxY0VOaf8D6E6ZaEr7ztO6DChMlGg7qWECs3m3FjFUqNgBx8ZpB+djw==}
'@iconify/types@2.0.0':
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
@@ -830,10 +848,6 @@ packages:
'@types/file-saver@2.0.7':
resolution: {integrity: sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==}
'@types/jszip@3.4.1':
resolution: {integrity: sha512-TezXjmf3lj+zQ651r6hPqvSScqBLvyPI9FxdXBqpEwBijNGQ2NXpaFW/7joGzveYkKQUil7iiDHLo6LV71Pc0A==}
deprecated: This is a stub types definition. jszip provides its own type definitions, so you do not need this installed.
'@types/lodash-es@4.17.12':
resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==}
@@ -2272,9 +2286,6 @@ packages:
ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
immediate@3.0.6:
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
immutable@5.1.3:
resolution: {integrity: sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==}
@@ -2506,9 +2517,6 @@ packages:
jstoxml@2.2.9:
resolution: {integrity: sha512-OYWlK0j+roh+eyaMROlNbS5cd5R25Y+IUpdl7cNdB8HNrkgwQzIS7L9MegxOiWNBj9dQhA/yAxiMwCC5mwNoBw==}
jszip@3.10.1:
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
just-debounce@1.1.0:
resolution: {integrity: sha512-qpcRocdkUmf+UTNBYx5w6dexX5J31AKK1OmPwH630a83DdVVUIngk55RSAiIGpQyoH0dlr872VHfPjnQnK1qDQ==}
@@ -2550,12 +2558,6 @@ packages:
resolution: {integrity: sha512-IpSVCk9AYvLHo5ctcIXxOBpMWUe+4TKN3VPWAKUbJikkmsGp0VrSM8IttVc32D6J4WUsiPE6aEFRNmIoF/gdow==}
engines: {node: '>= 0.10'}
libarchive-wasm@1.2.0:
resolution: {integrity: sha512-aunFn8oL9VwGRj+brRvdOv8BRUD4Ea1WxJW45IdiuXE2Vp/m/X+M1UxSU+yPzXfc1mPPC8AARaflg/CtF11u8g==}
lie@3.3.0:
resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
liftoff@3.1.0:
resolution: {integrity: sha512-DlIPlJUkCV0Ips2zf2pJP0unEoT1kwYhiiPUGF3s/jtxTCjziNLoiVVh+jqWOWeFi6mmwQ5fNxvAUyPad4Dfog==}
engines: {node: '>= 0.8'}
@@ -2857,9 +2859,6 @@ packages:
package-manager-detector@1.3.0:
resolution: {integrity: sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==}
pako@1.0.11:
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
parent-module@1.0.1:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
@@ -3181,9 +3180,6 @@ packages:
resolution: {integrity: sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==}
engines: {node: '>=0.10.0'}
setimmediate@1.0.5:
resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
side-channel-list@1.0.0:
resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
engines: {node: '>= 0.4'}
@@ -4178,6 +4174,18 @@ snapshots:
dependencies:
'@iconify/types': 2.0.0
'@iconify-json/oui@1.2.6':
dependencies:
'@iconify/types': 2.0.0
'@iconify-json/ph@1.2.2':
dependencies:
'@iconify/types': 2.0.0
'@iconify-json/qlementine-icons@1.2.11':
dependencies:
'@iconify/types': 2.0.0
'@iconify-json/ri@1.2.5':
dependencies:
'@iconify/types': 2.0.0
@@ -4186,6 +4194,10 @@ snapshots:
dependencies:
'@iconify/types': 2.0.0
'@iconify-json/system-uicons@1.2.4':
dependencies:
'@iconify/types': 2.0.0
'@iconify/types@2.0.0': {}
'@iconify/utils@2.3.0':
@@ -4426,10 +4438,6 @@ snapshots:
'@types/file-saver@2.0.7': {}
'@types/jszip@3.4.1':
dependencies:
jszip: 3.10.1
'@types/lodash-es@4.17.12':
dependencies:
'@types/lodash': 4.17.20
@@ -6226,8 +6234,6 @@ snapshots:
ieee754@1.2.1: {}
immediate@3.0.6: {}
immutable@5.1.3: {}
import-fresh@3.3.1:
@@ -6431,13 +6437,6 @@ snapshots:
jstoxml@2.2.9: {}
jszip@3.10.1:
dependencies:
lie: 3.3.0
pako: 1.0.11
readable-stream: 2.3.8
setimmediate: 1.0.5
just-debounce@1.1.0: {}
kind-of@3.2.2:
@@ -6473,12 +6472,6 @@ snapshots:
dependencies:
flush-write-stream: 1.1.1
libarchive-wasm@1.2.0: {}
lie@3.3.0:
dependencies:
immediate: 3.0.6
liftoff@3.1.0:
dependencies:
extend: 3.0.2
@@ -6830,8 +6823,6 @@ snapshots:
package-manager-detector@1.3.0: {}
pako@1.0.11: {}
parent-module@1.0.1:
dependencies:
callsites: 3.1.0
@@ -7169,8 +7160,6 @@ snapshots:
is-plain-object: 2.0.4
split-string: 3.1.0
setimmediate@1.0.5: {}
side-channel-list@1.0.0:
dependencies:
es-errors: 1.3.0

View File

@@ -4,7 +4,7 @@ import {BaseState, useBaseStore} from "@/stores/base.ts";
import {useRuntimeStore} from "@/stores/runtime.ts";
import {useSettingStore} from "@/stores/setting.ts";
import useTheme from "@/hooks/theme.ts";
import {LOCAL_FILE_KEY, SAVE_DICT_KEY, SAVE_SETTING_KEY} from "@/utils/const.ts";
import {APP_VERSION, LOCAL_FILE_KEY, SAVE_DICT_KEY, SAVE_SETTING_KEY} from "@/utils/const.ts";
import {shakeCommonDict} from "@/utils";
import {routes} from "@/router.ts";
import {get, set} from 'idb-keyval'
@@ -36,12 +36,14 @@ watch(store.$state, (n: BaseState) => {
let result = []
//删除未使用到的文件
get(LOCAL_FILE_KEY).then((fileList: Array<{ id: string, file: Blob }>) => {
audioFileIdList.forEach(a => {
let item = fileList.find(b => b.id === a)
item && result.push(item)
})
set(LOCAL_FILE_KEY, result)
lastAudioFileIdList = audioFileIdList
if (fileList && fileList.length > 0) {
audioFileIdList.forEach(a => {
let item = fileList.find(b => b.id === a)
item && result.push(item)
})
set(LOCAL_FILE_KEY, result)
lastAudioFileIdList = audioFileIdList
}
})
}
})
@@ -55,6 +57,10 @@ async function init() {
await settingStore.init()
store.load = true
setTheme(settingStore.theme)
get(APP_VERSION.key).then(r => {
runtimeStore.isNew = r ? (APP_VERSION.version > Number(r)) : true
})
}
onMounted(init)
@@ -83,13 +89,13 @@ watch(() => route.path, (to, from) => {
</script>
<template>
<!-- <router-view v-slot="{ Component }">-->
<!-- <transition :name="transitionName">-->
<!-- <keep-alive :exclude="runtimeStore.excludeRoutes">-->
<!-- <component :is="Component"/>-->
<!-- </keep-alive>-->
<!-- </transition>-->
<!-- </router-view>-->
<!-- <router-view v-slot="{ Component }">-->
<!-- <transition :name="transitionName">-->
<!-- <keep-alive :exclude="runtimeStore.excludeRoutes">-->
<!-- <component :is="Component"/>-->
<!-- </keep-alive>-->
<!-- </transition>-->
<!-- </router-view>-->
<router-view></router-view>
</template>

View File

@@ -441,3 +441,7 @@ a {
.page-title {
@apply text-2xl;
}
.red-point {
@apply bg-red w-3 h-3 rounded-full absolute right-5;
}

View File

@@ -1,5 +1,7 @@
export const GITHUB = 'https://github.com/zyronon/TypeWords'
export const ProjectName = 'Type Words'
export const Host = '2study.top'
export const Origin = `https://${Host}`
const common = {
word_dict_list_version: 1

View File

@@ -1,12 +1,10 @@
<script setup lang="ts">
import {onMounted, onUnmounted} from "vue";
import {Article, DictId} from "@/types/types.ts";
import BaseButton from "@/components/BaseButton.vue";
import {_nextTick, cloneDeep} from "@/utils";
import {_nextTick, cloneDeep, loadJsLib} from "@/utils";
import {useBaseStore} from "@/stores/base.ts";
import List from "@/pages/pc/components/list/List.vue";
import {emitter, EventKey} from "@/utils/eventBus.ts";
import {useWindowClick} from "@/hooks/event.ts";
import {MessageBox} from "@/utils/MessageBox.tsx";
import {useRuntimeStore} from "@/stores/runtime.ts";
@@ -16,11 +14,10 @@ import Toast from '@/pages/pc/components/base/toast/Toast.ts'
import {getDefaultArticle} from "@/types/func.ts";
import BackIcon from "@/pages/pc/components/BackIcon.vue";
import MiniDialog from "@/pages/pc/components/dialog/MiniDialog.vue";
import * as XLSX from "xlsx";
import {onMounted} from "vue";
import {Origin} from "@/config/ENV.ts";
defineEmits<{
importData: [val: Event]
exportData: [val: string]
}>()
const base = useBaseStore()
const runtimeStore = useRuntimeStore()
@@ -138,10 +135,122 @@ function saveAndNext(val: Article) {
let showExport = $ref(false)
useWindowClick(() => showExport = false)
function importData() {
onMounted(() => {
article = runtimeStore.editDict.articles[0]
})
let exportLoading = $ref(false)
let importLoading = $ref(false)
function importData(e: any) {
let file = e.target.files[0]
if (!file) return
// no()
let reader = new FileReader();
reader.onload = async function (s) {
importLoading = true
const XLSX = await loadJsLib('XLSX', `${Origin}/libs/xlsx.full.min.js`);
let data = s.target.result;
let workbook = XLSX.read(data, {type: 'binary'});
let res: any[] = XLSX.utils.sheet_to_json(workbook.Sheets['Sheet1']);
if (res.length) {
let articles = res.map(v => {
if (v['原文标题'] && v['原文正文']) {
return getDefaultArticle({
id: nanoid(6),
title: String(v['原文标题']),
titleTranslate: String(v['译文标题']),
text: String(v['原文正文']),
textTranslate: String(v['译文正文']),
audioSrc: String(v['音频地址']),
})
}
}).filter(v => v)
let repeat = []
let noRepeat = []
articles.map((v: any) => {
let rIndex = runtimeStore.editDict.articles.findIndex(s => s.title === v.title)
if (rIndex > -1) {
v.index = rIndex
repeat.push(v)
} else {
noRepeat.push(v)
}
})
runtimeStore.editDict.articles = runtimeStore.editDict.articles.concat(noRepeat)
if (repeat.length) {
MessageBox.confirm(
'文章"' + repeat.map(v => v.title).join(', ') + '" 已存在,是否覆盖原有文章?',
'检测到重复文章',
() => {
repeat.map(v => {
runtimeStore.editDict.articles[v.index] = v
delete runtimeStore.editDict.articles[v.index]["index"]
})
setTimeout(listEl?.scrollToBottom, 100)
},
null,
() => {
e.target.value = ''
importLoading = false
syncBookInMyStudyList()
Toast.success('导入成功!')
}
)
} else {
syncBookInMyStudyList()
Toast.success('导入成功!')
}
} else {
Toast.success('导入失败!原因:没有数据')
}
e.target.value = ''
importLoading = false
};
reader.readAsBinaryString(file);
}
async function exportData(val: { type: string, data?: Article }) {
exportLoading = true
const XLSX = await loadJsLib('XLSX', `${Origin}/libs/xlsx.full.min.js`);
const {type, data} = val
let list = []
let filename = ''
if (type === 'item') {
if (!data.id) {
return Toast.error('请选择文章')
}
list = [data]
filename = runtimeStore.editDict.name + `-${data.title}`
} else {
list = runtimeStore.editDict.articles
filename = runtimeStore.editDict.name
}
let wb = XLSX.utils.book_new()
let sheetData = list.map(v => {
return {
原文标题: v.title,
原文正文: v.text,
译文标题: v.titleTranslate,
译文正文: v.textTranslate,
音频地址: v.audioSrc,
}
})
wb.Sheets['Sheet1'] = XLSX.utils.json_to_sheet(sheetData)
wb.SheetNames = ['Sheet1']
XLSX.writeFile(wb, `${filename}.xlsx`);
Toast.success(filename + ' 导出成功!')
showExport = false
exportLoading = false
}
function updateList(e) {
runtimeStore.editDict.articles = e
syncBookInMyStudyList()
}
</script>
<template>
@@ -153,7 +262,8 @@ function importData() {
</header>
<List
ref="listEl"
v-model:list="runtimeStore.editDict.articles"
:list="runtimeStore.editDict.articles"
@update:list="updateList"
:select-item="article"
@del-select-item="article = getDefaultArticle()"
@select-item="selectArticle"
@@ -168,7 +278,7 @@ function importData() {
</div>
<div class="footer">
<div class="import">
<BaseButton>导入</BaseButton>
<BaseButton :loading="importLoading">导入</BaseButton>
<input type="file"
accept=".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
@change="importData">
@@ -184,11 +294,11 @@ function importData() {
<div class="mini-row-title">
导出选项
</div>
<div class="mini-row">
<BaseButton @click="emit('exportData',{type:'all',data:[]})">全部文章</BaseButton>
</div>
<div class="mini-row">
<BaseButton @click="emit('exportData',{type:'chapter',data:article})">当前章节</BaseButton>
<div class="flex">
<BaseButton :loading="exportLoading" @click="exportData({type:'all'})">全部</BaseButton>
<BaseButton :loading="exportLoading" :disabled="!article.id"
@click="exportData({type:'item',data:article})">当前
</BaseButton>
</div>
</MiniDialog>
</div>

View File

@@ -21,6 +21,7 @@ import {update} from "idb-keyval";
import {LOCAL_FILE_KEY} from "@/utils/const.ts";
import ArticleAudio from "@/pages/pc/article/components/ArticleAudio.vue";
import BaseInput from "@/pages/pc/components/base/BaseInput.vue";
import Textarea from "@/pages/pc/components/base/Textarea.vue";
const Dialog = defineAsyncComponent(() => import('@/pages/pc/components/dialog/Dialog.vue'))
@@ -79,21 +80,11 @@ function apply(isHandle: boolean = true) {
//分句原文
function splitText() {
editArticle.text = splitEnArticle2(editArticle.text.trim())
return
let text = editArticle.text.trim();
if (text) {
editArticle.text = splitEnArticle2(text)
}
}
//分句翻译
function splitTranslateText() {
editArticle.textTranslate = splitCNArticle2(editArticle.textTranslate.trim())
return
let text = editArticle.textTranslate.trim();
if (text) {
editArticle.textTranslate = splitCNArticle2(text)
}
}
//TODO
@@ -104,6 +95,8 @@ async function startNetworkTranslate() {
if (!editArticle.text.trim()) {
return Toast.error('请填写正文!')
}
editArticle.titleTranslate = ''
editArticle.textTranslate = ''
apply()
//注意!!!
//这里需要用异步因为watch了article.networkTranslate改变networkTranslate了之后会重新设置article.sections
@@ -327,19 +320,17 @@ function setStartTime(val: Sentence, i: number, j: number) {
<div class="shrink-0">标题</div>
<BaseInput
v-model="editArticle.title"
:disabled="![100,0].includes(progress)"
type="text"
placeholder="请填写原文标题"
/>
</div>
<div class="">正文<span class="text-sm color-gray">一行一句段落间空一行</span></div>
<textarea
v-model="editArticle.text"
:readonly="![100,0].includes(progress)"
type="textarea"
class="base-textarea"
placeholder="请复制原文"
>
</textarea>
<Textarea v-model="editArticle.text"
class="h-full"
:disabled="![100,0].includes(progress)"
placeholder="请复制原文"
:autosize="false"/>
<div class="justify-end items-center flex">
<Tooltip>
<IconFluentQuestionCircle20Regular class="mr-3" width="20"/>
@@ -368,22 +359,19 @@ function setStartTime(val: Sentence, i: number, j: number) {
<div class="shrink-0">标题</div>
<BaseInput
v-model="editArticle.titleTranslate"
:disabled="![100,0].includes(progress)"
type="text"
placeholder="请填写翻译标题"
/>
</div>
<div class="">正文<span class="text-sm color-gray">一行一句段落间空一行</span></div>
<textarea
v-model="editArticle.textTranslate"
:readonly="![100,0].includes(progress)"
type="textarea"
class="base-textarea"
placeholder="请填写翻译"
ref="textareaRef"
>
</textarea>
<Textarea v-model="editArticle.textTranslate"
class="h-full"
:disabled="![100,0].includes(progress)"
placeholder="请填写翻译"
:autosize="false"/>
<div class="justify-between items-center flex">
<div class="flex gap-space items-center w-50 ">
<div class="flex gap-space items-center w-50">
<BaseButton @click="startNetworkTranslate"
:loading="progress!==0 && progress !== 100">翻译
</BaseButton>
@@ -448,12 +436,14 @@ function setStartTime(val: Sentence, i: number, j: number) {
<div class="sentence" v-for="(sentence,indexJ) in item">
<div class="flex-[7]">
<EditAbleText
:disabled="![100,0].includes(progress)"
:value="sentence.text"
@save="(e:string) => saveSentenceText(sentence,e)"
/>
<EditAbleText
class="text-lg!"
v-if="sentence.translate"
:disabled="![100,0].includes(progress)"
:value="sentence.translate"
@save="(e:string) => saveSentenceTranslate(sentence,e)"
/>

View File

@@ -1,6 +1,6 @@
<script setup lang="tsx">
import {nextTick, useSlots} from "vue";
import {nextTick, useSlots, withDirectives} from "vue";
import {Sort} from "@/types/types.ts";
import MiniDialog from "@/pages/pc/components/dialog/MiniDialog.vue";
import BaseIcon from "@/components/BaseIcon.vue";
@@ -13,18 +13,23 @@ import Pagination from '@/pages/pc/components/base/Pagination.vue'
import Toast from '@/pages/pc/components/base/toast/Toast.ts'
import Checkbox from "@/pages/pc/components/base/checkbox/Checkbox.vue";
import DeleteIcon from "@/components/icon/DeleteIcon.vue";
import loadingDirective from "@/directives/loading.tsx";
let list = defineModel('list')
const props = withDefaults(defineProps<{
loading?: boolean
showToolbar?: boolean
exportLoading?: boolean
importLoading?: boolean
del?: Function
batchDel?: Function
add?: Function
}>(), {
loading: true,
showToolbar: true,
exportLoading: false,
importLoading: false,
del: () => void 0,
add: () => void 0,
batchDel: () => void 0
@@ -35,6 +40,8 @@ const emit = defineEmits<{
item: any,
index: number
}],
importData: [e: Event]
exportData: []
}>()
let listRef: any = $ref()
@@ -167,6 +174,29 @@ defineRender(
</PopConfirm>
: null
}
<div>
<BaseIcon
onClick={() => {
let d: HTMLDivElement = document.querySelector('#update-dict')
d.click()
}}
icon="fluent:add-20-filled"
title="导入">
{props.importLoading ? <IconEosIconsLoading/> : <IconSystemUiconsImport/>}
</BaseIcon>
<input
id="update-dict"
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>
<BaseIcon
onClick={() => emit('exportData')}
icon="fluent:add-20-filled"
title="导出">
{props.exportLoading ? <IconEosIconsLoading/> : <IconPhExportLight/>}
</BaseIcon>
<BaseIcon
onClick={props.add}
icon="fluent:add-20-filled"

View File

@@ -6,7 +6,7 @@ import {watch} from "vue";
import {useSettingStore} from "@/stores/setting.ts";
import {isMobile} from "@/utils";
import {ProjectName} from "@/config/ENV.ts";
import {ProjectName, Host} from "@/config/ENV.ts";
let settingStore = useSettingStore()
let showNotice = $ref(false)
@@ -32,9 +32,9 @@ function close() {
watch(() => settingStore.load, (n) => {
if (n && settingStore.first) {
setTimeout(()=>{
show = true
},1000)
setTimeout(() => {
show = true
}, 1000)
}
}, {immediate: true})
@@ -55,7 +55,7 @@ watch(() => settingStore.load, (n) => {
<div class="collect" v-if="showNotice">
<div class="href-wrapper">
<div class="round">
<div class="href">2study.top</div>
<div class="href">{{ Host }}</div>
<IconFluentStar12Regular width="22"/>
</div>
<div class="right">

View File

@@ -4,13 +4,16 @@ import BaseButton from "@/components/BaseButton.vue";
import {watchEffect} from "vue";
import Textarea from "@/pages/pc/components/base/Textarea.vue";
import Toast from "@/pages/pc/components/base/toast/Toast.ts";
interface IProps {
value: string,
disabled: boolean,
}
const props = withDefaults(defineProps<IProps>(), {
value: '',
disabled: false,
})
const emit = defineEmits([
@@ -30,6 +33,7 @@ function save() {
}
function toggle() {
if (props.disabled) return Toast.info('请等候翻译完成')
edit = !edit
editVal = props.value
}

View File

@@ -1,11 +1,14 @@
<template>
<div class="inline-flex w-full relative">
<div class="inline-flex w-full relative"
:class="[disabled && 'disabled']"
>
<textarea
ref="textareaRef"
v-model="innerValue"
:placeholder="placeholder"
:maxlength="maxlength"
:rows="rows"
:disabled="disabled"
:style="textareaStyle"
class="w-full px-3 py-2 border border-gray-300 rounded-md outline-none resize-none transition-colors duration-200 box-border"
@input="handleInput"
@@ -31,6 +34,7 @@ const props = defineProps<{
rows?: number,
autosize: boolean | { minRows?: number; maxRows?: number }
showWordLimit?: boolean
disabled?: boolean
}>()
const emit = defineEmits(["update:modelValue"])
@@ -84,6 +88,14 @@ watch(innerValue, () => {
</script>
<style>
.disabled {
opacity: 0.5;
textarea {
cursor: not-allowed !important;
}
}
textarea {
font-family: var(--font-family);
color: var(--color-input-color);

View File

@@ -167,7 +167,7 @@ async function cancel() {
</div>
<div class="modal-body" :class="{padding}">
<slot></slot>
<div v-if="content" class="content">{{ content }}</div>
<div v-if="content" class="content max-h-60vh">{{ content }}</div>
</div>
<div class="modal-footer" v-if="footer">
<div class="left flex items-end">

View File

@@ -79,6 +79,8 @@ function delItem(item: T) {
let rIndex = props.list.findIndex(v => v.id === item.id)
if (rIndex > -1) {
localList.splice(rIndex, 1)
//触发set
localList = localList
}
}
@@ -93,7 +95,6 @@ function scrollBottom() {
}
defineExpose({scrollBottom})
</script>
<template>
@@ -124,7 +125,7 @@ defineExpose({scrollBottom})
</div>
<div class="right">
<BaseIcon
@click="delItem(item)"
@click.stop="delItem(item)"
title="删除">
<DeleteIcon/>
</BaseIcon>

View File

@@ -6,9 +6,11 @@ import {useSettingStore} from "@/stores/setting.ts";
import {useRouter} from "vue-router";
import useTheme from "@/hooks/theme.ts";
import BaseIcon from "@/components/BaseIcon.vue";
import {useRuntimeStore} from "@/stores/runtime.ts";
const settingStore = useSettingStore()
const runtimeStore = useRuntimeStore()
const router = useRouter()
const {toggleTheme} = useTheme()
@@ -38,6 +40,7 @@ const {toggleTheme} = useTheme()
<div class="row" @click="router.push('/setting')">
<IconFluentSettings20Regular/>
<span v-if="settingStore.sideExpand">设置</span>
<div class="red-point" v-if="runtimeStore.isNew"></div>
</div>
</div>
<div class="bottom flex justify-evenly ">
@@ -83,9 +86,8 @@ const {toggleTheme} = useTheme()
z-index: 2;
.row {
@apply cursor-pointer rounded-md text p-2 my-2 flex items-center gap-2;
@apply cursor-pointer rounded-md text p-2 my-2 flex items-center gap-2 relative shrink-0;
transition: all .5s;
flex-shrink: 0;
&:hover {
background: var(--color-select-bg);

View File

@@ -1,13 +1,14 @@
<script setup lang="ts">
import {ref, watch, nextTick} from "vue";
import {nextTick, ref, watch} from "vue";
import {useSettingStore} from "@/stores/setting.ts";
import {getAudioFileUrl, usePlayAudio} from "@/hooks/sound.ts";
import {getShortcutKey, useEventListener} from "@/hooks/event.ts";
import {checkAndUpgradeSaveDict, checkAndUpgradeSaveSetting, cloneDeep, shakeCommonDict} from "@/utils";
import {checkAndUpgradeSaveDict, checkAndUpgradeSaveSetting, cloneDeep, loadJsLib, shakeCommonDict} from "@/utils";
import {DefaultShortcutKeyMap, ShortcutKey} from "@/types/types.ts";
import BaseButton from "@/components/BaseButton.vue";
import {
APP_NAME,
APP_VERSION,
EXPORT_DATA_KEY,
LOCAL_FILE_KEY,
SAVE_DICT_KEY,
@@ -17,7 +18,7 @@ import {
import VolumeIcon from "@/components/icon/VolumeIcon.vue";
import {useBaseStore} from "@/stores/base.ts";
import {saveAs} from "file-saver";
import {GITHUB} from "@/config/ENV.ts";
import {Origin} from "@/config/ENV.ts";
import dayjs from "dayjs";
import BasePage from "@/pages/pc/components/BasePage.vue";
import Toast from '@/pages/pc/components/base/toast/Toast.ts'
@@ -30,8 +31,8 @@ import InputNumber from "@/pages/pc/components/base/InputNumber.vue";
import PopConfirm from "@/pages/pc/components/PopConfirm.vue";
import Textarea from "@/pages/pc/components/base/Textarea.vue";
import SettingItem from "@/pages/pc/setting/SettingItem.vue";
// import {ArchiveReader, libarchiveWasm} from "libarchive-wasm";
import {get, set} from "idb-keyval";
import {useRuntimeStore} from "@/stores/runtime.ts";
const emit = defineEmits<{
toggleDisabledDialogEscKey: [val: boolean]
@@ -39,6 +40,7 @@ const emit = defineEmits<{
const tabIndex = $ref(0)
const settingStore = useSettingStore()
const runtimeStore = useRuntimeStore()
const store = useBaseStore()
//@ts-ignore
const gitLastCommitHash = ref(LATEST_COMMIT_HASH);
@@ -162,24 +164,12 @@ function resetShortcutKeyMap() {
Toast.success('恢复成功')
}
async function loadJSZip() {
if (window.JSZip) return window.JSZip;
return new Promise((resolve, reject) => {
const script = document.createElement("script");
// script.src = "https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js";
script.src = "https://2study.top/libs/jszip.min.js";
script.onload = () => resolve(window.JSZip);
script.onerror = reject;
document.head.appendChild(script);
});
}
let exportLoading = $ref(false)
let importLoading = $ref(false)
async function exportData(notice = '导出成功!') {
exportLoading = true
const JSZip = await loadJSZip();
const JSZip = await loadJsLib('JSZip', `${Origin}/libs/jszip.min.js`);
let data = {
version: EXPORT_DATA_KEY.version,
val: {
@@ -198,7 +188,7 @@ async function exportData(notice = '导出成功!') {
const mp3 = zip.folder("mp3");
const allRecords = await get(LOCAL_FILE_KEY);
for (const rec of allRecords) {
for (const rec of allRecords ?? []) {
mp3.file(rec.id + ".mp3", rec.file);
}
exportLoading = false
@@ -246,7 +236,7 @@ async function importData(e) {
} else if (file.name.endsWith(".zip")) {
try {
importLoading = true
const JSZip = await loadJSZip();
const JSZip = await loadJsLib('JSZip', `${Origin}/libs/jszip.min.js`);
const zip = await JSZip.loadAsync(file);
const dataFile = zip.file("data.json");
@@ -332,13 +322,14 @@ function importOldData() {
<IconFluentDatabasePerson20Regular width="20"/>
<span>数据管理</span>
</div>
<div class="tab" :class="tabIndex === 7 && 'active'" @click="tabIndex = 7">
<div class="tab" :class="tabIndex === 5 && 'active'" @click="()=>{
tabIndex = 5
runtimeStore.isNew = false
set(APP_VERSION.key,APP_VERSION.version)
}">
<IconFluentTextBulletListSquare20Regular width="20"/>
<span>更新日志</span>
</div>
<div class="tab" :class="tabIndex === 5 && 'active'" @click="tabIndex = 5">
<IconFluentMailEdit20Regular width="20"/>
<span>反馈</span>
<div class="red-point" v-if="runtimeStore.isNew"></div>
</div>
<div class="tab" :class="tabIndex === 6 && 'active'" @click="tabIndex = 6">
<IconFluentPerson20Regular width="20"/>
@@ -599,9 +590,10 @@ function importOldData() {
</div>
</div>
</div>
<div v-if="tabIndex === 4">
<div>
目前用户的所有数据(自定义设置自定义词典自定义文章学习进度等)
目前用户的所有数据
<b class="text-red">仅保存在本地</b>如果您需要在不同的设备浏览器或者其他非官方部署上使用 {{ APP_NAME }}
您需要手动进行数据同步和保存
</div>
@@ -609,7 +601,7 @@ function importOldData() {
<div class="line my-3"></div>
<div>请注意导入数据后将<b class="text-red"> 完全覆盖 </b>当前所有数据(自定义设置自定义词典自定义文章学习进度等)请谨慎操作
<div>请注意导入数据后将<b class="text-red"> 完全覆盖 </b>当前所有数据请谨慎操作
</div>
<div class="flex gap-space mt-3">
<div class="import hvr-grow">
@@ -625,14 +617,24 @@ function importOldData() {
</PopConfirm>
</div>
</div>
<div v-if="tabIndex === 5">
<div>
给我发Email<a href="mailto:zyronon@163.com">zyronon@163.com</a>
<div class="item p-2">
<div class="mb-2">
<div>
<span>2025/9/14</span>
<span>完善文章编辑导入导出等功能</span>
</div>
<div class="text-base mt-1">
<div>1文章的音频管理功能目前已可添加音频设置句子与音频的对应位置</div>
<div>2文章可导入导出</div>
<div>3单词可导入导出</div>
</div>
</div>
<div class="line"></div>
</div>
<span><a :href="GITHUB" target="_blank"> Github </a>上给作者提一个
<a :href="`${GITHUB}/issues`" target="_blank"> Issue </a>
</span>
</div>
<div v-if="tabIndex === 6" class="center flex-col">
<h1>Type Words</h1>
<p class="w-100 text-xl">
@@ -645,26 +647,14 @@ function importOldData() {
反馈<a
href="https://github.com/zyronon/TypeWords/issues" target="_blank">https://github.com/zyronon/TypeWords/issues</a>
</p>
<div class="text-md color-gray">
<p>
作者邮箱<a href="mailto:zyronon@163.com">zyronon@163.com</a>
</p>
<div class="text-md color-gray mt-10">
Build {{ gitLastCommitHash }}
</div>
</div>
<div v-if="tabIndex === 7">
<div class="item p-2" v-for="i in 10">
<div class="mb-2">
<div>
<span>2025/9/14</span>
<span>完善文章编辑功能翻译不可用</span>
</div>
<div class="text-base">
<p>除了翻译不可用前端直接调百度接口会跨域有能力可以把浏览器的跨域检测关掉就可以用了</p>
<p>1完善音频管理功能目前已可添加音频设置句子与音频的对应位置</p>
</div>
</div>
<div class="line"></div>
</div>
</div>
</div>
</div>
</BasePage>
@@ -692,12 +682,16 @@ function importOldData() {
//color: #0C8CE9;
.tab {
cursor: pointer;
@apply cursor-pointer flex items-center relative;
padding: .6rem .9rem;
border-radius: .5rem;
display: flex;
align-items: center;
gap: .6rem;
transition: all .5s;
&:hover {
background: var(--color-select-bg);
color: var(--color-select-text);
}
&.active {
background: var(--color-select-bg);

View File

@@ -2,9 +2,9 @@
import {DictId} from "@/types/types.ts";
import BasePage from "@/pages/pc/components/BasePage.vue";
import {computed, onMounted, reactive, shallowReactive, watch} from "vue";
import {computed, onMounted, reactive, shallowReactive} from "vue";
import {useRuntimeStore} from "@/stores/runtime.ts";
import {_getDictDataByUrl, _nextTick, cloneDeep, convertToWord, useNav} from "@/utils";
import {_getDictDataByUrl, _nextTick, cloneDeep, convertToWord, loadJsLib, useNav} from "@/utils";
import {nanoid} from "nanoid";
import BaseIcon from "@/components/BaseIcon.vue";
import BaseTable from "@/pages/pc/components/BaseTable.vue";
@@ -25,6 +25,9 @@ import DeleteIcon from "@/components/icon/DeleteIcon.vue";
import {getCurrentStudyWord} from "@/hooks/dict.ts";
import PracticeSettingDialog from "@/pages/pc/word/components/PracticeSettingDialog.vue";
import {useSettingStore} from "@/stores/setting.ts";
import * as XLSX from "xlsx";
import {MessageBox} from "@/utils/MessageBox.tsx";
import {Origin} from "@/config/ENV.ts";
const runtimeStore = useRuntimeStore()
const base = useBaseStore()
@@ -135,19 +138,26 @@ function batchDel(ids: string[]) {
syncDictInMyStudyList()
}
//把word对象的字段全转成字符串
function word2Str(word) {
let res = getDefaultFormWord()
res.id = word.id
res.word = word.word
res.phonetic1 = word.phonetic1
res.phonetic0 = word.phonetic0
res.trans = word.trans.map(v => (v.pos + v.cn).replaceAll('"', '')).join('\n')
res.sentences = word.sentences.map(v => (v.c + "\n" + v.cn).replaceAll('"', '')).join('\n\n')
res.phrases = word.phrases.map(v => (v.c + "\n" + v.cn).replaceAll('"', '')).join('\n\n')
res.synos = word.synos.map(v => (v.pos + v.cn + "\n" + v.ws.join('/')).replaceAll('"', '')).join('\n\n')
res.relWords = '词根:' + word.relWords.root + '\n\n' +
word.relWords.rels.map(v => (v.pos + "\n" + v.words.map(v => (v.c + ':' + v.cn)).join('\n')).replaceAll('"', '')).join('\n\n')
res.etymology = word.etymology.map(v => (v.t + '\n' + v.d).replaceAll('"', '')).join('\n\n')
return res
}
function editWord(word) {
isOperate = true
wordForm.id = word.id
wordForm.word = word.word
wordForm.phonetic1 = word.phonetic1
wordForm.phonetic0 = word.phonetic0
wordForm.trans = word.trans.map(v => (v.pos + v.cn).replaceAll('"', '')).join('\n')
wordForm.sentences = word.sentences.map(v => (v.c + "\n" + v.cn).replaceAll('"', '')).join('\n\n')
wordForm.phrases = word.phrases.map(v => (v.c + "\n" + v.cn).replaceAll('"', '')).join('\n\n')
wordForm.synos = word.synos.map(v => (v.pos + v.cn + "\n" + v.ws.join('/')).replaceAll('"', '')).join('\n\n')
wordForm.relWords = '词根:' + word.relWords.root + '\n\n' +
word.relWords.rels.map(v => (v.pos + "\n" + v.words.map(v => (v.c + ':' + v.cn)).join('\n')).replaceAll('"', '')).join('\n\n')
wordForm.etymology = word.etymology.map(v => (v.t + '\n' + v.d).replaceAll('"', '')).join('\n\n')
wordForm = word2Str(word)
}
function addWord() {
@@ -197,7 +207,6 @@ function formClose() {
let showPracticeSettingDialog = $ref(false)
const store = useBaseStore()
const settingStore = useSettingStore()
const {nav} = useNav()
@@ -235,6 +244,116 @@ async function addMyStudyList() {
startPractice()
}
let exportLoading = $ref(false)
let importLoading = $ref(false)
function importData(e) {
let file = e.target.files[0];
if (!file) return;
let reader = new FileReader();
reader.onload = async function (s) {
let data = s.target.result;
importLoading = true
const XLSX = await loadJsLib('XLSX', `${Origin}/libs/xlsx.full.min.js`);
let workbook = XLSX.read(data, {type: 'binary'});
let res: any[] = XLSX.utils.sheet_to_json(workbook.Sheets['Sheet1']);
if (res.length) {
let words = res.map(v => {
if (v['单词']) {
let data = null
try {
data = convertToWord({
word: v['单词'],
phonetic0: v['音标①'] ?? '',
phonetic1: v['音标②'] ?? '',
trans: v['释义'] ?? '',
sentences: v['例句'] ?? '',
phrases: v['短语'] ?? '',
synos: v['近义词'] ?? '',
relWords: v['同根词'] ?? '',
etymology: v['词源'] ?? '',
});
} catch (e) {
console.error('导入单词报错' + v['单词'], e.message)
}
return data
}
}).filter(v => v);
let repeat = []
let noRepeat = []
words.map((v: any) => {
let rIndex = runtimeStore.editDict.words.findIndex(s => s.word === v.word)
if (rIndex > -1) {
v.index = rIndex
repeat.push(v)
} else {
noRepeat.push(v)
}
})
runtimeStore.editDict.words = runtimeStore.editDict.words.concat(noRepeat)
if (repeat.length) {
MessageBox.confirm(
'单词"' + repeat.map(v => v.word).join(', ') + '" 已存在,是否覆盖原单词?',
'检测到重复单词',
() => {
repeat.map(v => {
runtimeStore.editDict.words[v.index] = v
delete runtimeStore.editDict.words[v.index]["index"]
})
},
null,
() => {
e.target.value = ''
importLoading = false
syncDictInMyStudyList()
Toast.success('导入成功!')
}
)
} else {
syncDictInMyStudyList()
Toast.success('导入成功!')
}
} else {
Toast.warning('导入失败!原因:没有数据');
}
e.target.value = ''
importLoading = false
};
reader.readAsBinaryString(file);
}
async function exportData() {
exportLoading = true
const XLSX = await loadJsLib('XLSX', `${Origin}/libs/xlsx.full.min.js`);
let list = runtimeStore.editDict.words
let filename = runtimeStore.editDict.name
let wb = XLSX.utils.book_new()
let sheetData = list.map(v => {
let t = word2Str(v)
return {
单词: t.word,
'音标①': t.phonetic0,
'音标②': t.phonetic1,
'释义': t.trans,
'例句': t.sentences,
'短语': t.phrases,
'近义词': t.synos,
'同根词': t.relWords,
'词源': t.etymology,
}
})
wb.Sheets['Sheet1'] = XLSX.utils.json_to_sheet(sheetData)
wb.SheetNames = ['Sheet1']
XLSX.writeFile(wb, `${filename}.xlsx`);
Toast.success(filename + ' 导出成功!')
exportLoading = false
}
defineRender(() => {
return (
<BasePage>
@@ -262,6 +381,10 @@ defineRender(() => {
del={delWord}
batchDel={batchDel}
add={addWord}
onImportData={importData}
onExportData={exportData}
exportLoading={exportLoading}
importLoading={importLoading}
>
{
(val) =>

View File

@@ -9,6 +9,7 @@ export interface RuntimeState {
showDictModal: boolean
excludeRoutes: any[]
routeData: any,
isNew: boolean,
}
export const useRuntimeStore = defineStore('runtime', {
@@ -20,6 +21,7 @@ export const useRuntimeStore = defineStore('runtime', {
editDict: getDefaultDict(),
showDictModal: false,
excludeRoutes: [],
isNew: false,
}
},
actions: {

View File

@@ -1,25 +1,24 @@
import {defineStore} from "pinia"
import {checkAndUpgradeSaveSetting, cloneDeep} from "@/utils";
import {DefaultShortcutKeyMap} from "@/types/types.ts";
import {SAVE_SETTING_KEY} from "@/utils/const.ts";
import {APP_VERSION, SAVE_SETTING_KEY} from "@/utils/const.ts";
import {get} from "idb-keyval";
export interface SettingState {
allSound: boolean,
soundType: string,
wordSound: boolean,
wordSoundVolume: number,
wordSoundSpeed: number,
soundType: string,
articleSound: boolean,
articleSoundVolume: number,
articleSoundSpeed: number,
// articleSound: boolean,
keyboardSound: boolean,
keyboardSoundVolume: number,
keyboardSoundFile: string,
effectSound: boolean,
effectSoundVolume: number,
@@ -51,15 +50,10 @@ export interface SettingState {
disableShowPracticeSettingDialog: boolean // 不默认显示练习设置弹框
autoNextWord: boolean //自动切换下一个单词
inputWrongClear: boolean //单词输入错误,清空已输入内容
}
export const getDefaultSettingState = (): SettingState => ({
showToolbar: true,
showPanel: true,
sideExpand: false,
allSound: true,
soundType: 'us',
wordSound: true,
wordSoundVolume: 100,
@@ -69,27 +63,30 @@ export const getDefaultSettingState = (): SettingState => ({
articleSoundVolume: 100,
articleSoundSpeed: 1,
soundType: 'us',
keyboardSound: true,
keyboardSoundVolume: 100,
keyboardSoundFile: '机械键盘2',
effectSound: true,
effectSoundVolume: 100,
repeatCount: 1,
repeatCustomCount: null,
dictation: false,
translate: true,
showNearWord: true,
ignoreCase: true,
allowWordTip: true,
waitTimeForChangeWord: 300,
fontSize: {
articleForeignFontSize: 48,
articleTranslateFontSize: 20,
wordForeignFontSize: 48,
wordTranslateFontSize: 20,
},
waitTimeForChangeWord: 300,
showToolbar: true,
showPanel: true,
sideExpand: false,
theme: 'auto',
shortcutKeyMap: cloneDeep(DefaultShortcutKeyMap),
first: true,
@@ -100,7 +97,7 @@ export const getDefaultSettingState = (): SettingState => ({
wordPracticeMode: 0,
disableShowPracticeSettingDialog: false,
autoNextWord: true,
inputWrongClear: false
inputWrongClear: false,
})
export const useSettingStore = defineStore('setting', {

View File

@@ -6,7 +6,6 @@ import {nanoid} from "nanoid";
export function getDefaultWord(val: Partial<Word> = {}): Word {
return {
custom: false,
id: nanoid(6),
"word": "",
"phonetic0": "",
"phonetic1": "",
@@ -19,7 +18,8 @@ export function getDefaultWord(val: Partial<Word> = {}): Word {
"rels": []
},
"etymology": [],
...val
...val,
id: val?.id ? val.id : nanoid(6),
}
}

View File

@@ -7,6 +7,10 @@ export const SoundFileOptions = [
]
export const APP_NAME = 'Type Words'
export const APP_VERSION = {
key: 'type-words-app-version',
version: 1
}
export const SAVE_DICT_KEY = {
key: 'typing-word-dict',

View File

@@ -619,3 +619,14 @@ export function splitIntoN(arr: any[], n: number) {
}
return result
}
export async function loadJsLib(key: string, url: string) {
if (window[key]) return window[key];
return new Promise((resolve, reject) => {
const script = document.createElement("script");
script.src = url;
script.onload = () => resolve(window[key]);
script.onerror = reject;
document.head.appendChild(script);
});
}