feat:add update prompts to dynamically introduce the xlsx library
This commit is contained in:
3
components.d.ts
vendored
3
components.d.ts
vendored
@@ -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']
|
||||
|
||||
@@ -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
91
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
34
src/App.vue
34
src/App.vue
@@ -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>
|
||||
|
||||
|
||||
@@ -441,3 +441,7 @@ a {
|
||||
.page-title {
|
||||
@apply text-2xl;
|
||||
}
|
||||
|
||||
.red-point {
|
||||
@apply bg-red w-3 h-3 rounded-full absolute right-5;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)"
|
||||
/>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user