Develop mobile pages
This commit is contained in:
@@ -1,110 +0,0 @@
|
||||
<template>
|
||||
<div id="background" class="anim">
|
||||
<img src="@/assets/img/moon.png" alt="" id="moon" style="display:none">
|
||||
<Transition name="fade">
|
||||
<canvas ref="canvas" v-show="settingStore.theme === 'dark'"/>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import {onMounted} from "vue"
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
|
||||
const canvas = $ref<HTMLCanvasElement>()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
onMounted(() => {
|
||||
// console.log('canvas;', canvas)
|
||||
let ctx = canvas.getContext("2d");
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
let maxRadius = 1,
|
||||
stars = [];
|
||||
|
||||
|
||||
let Star = function (x: number, y: number, r: number) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.r = r;
|
||||
};
|
||||
Star.prototype = {
|
||||
paint: function () {
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI);
|
||||
ctx.fillStyle = "rgba(255,255,255," + this.r + ")";
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
},
|
||||
};
|
||||
|
||||
function drawBg() {
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
let r = Math.random() * maxRadius;
|
||||
let x = Math.random() * canvas.width;
|
||||
let y = Math.random() * 2 * canvas.height - canvas.height;
|
||||
let star = new Star(x, y, r);
|
||||
stars.push(star);
|
||||
star.paint();
|
||||
}
|
||||
}
|
||||
|
||||
drawBg()
|
||||
|
||||
function drawMoon() {
|
||||
let moon: HTMLImageElement = document.getElementById("moon");
|
||||
let centerX = canvas.width - 200,
|
||||
centerY = 100,
|
||||
width = 80;
|
||||
|
||||
let index = 0;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.arc(
|
||||
centerX + width / 2,
|
||||
centerY + width / 2,
|
||||
width / 2 + index,
|
||||
0,
|
||||
2 * Math.PI
|
||||
);
|
||||
ctx.fillStyle = "rgba(240,219,120,0.05)";
|
||||
index += 2;
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
if (moon.complete) {
|
||||
ctx.drawImage(moon, centerX, centerY, width, width);
|
||||
} else {
|
||||
moon.onload = function () {
|
||||
ctx.drawImage(moon, centerX, centerY, width, width);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
drawMoon()
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
#background {
|
||||
position: fixed;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
left: 0;
|
||||
top: 0;
|
||||
background-color: var(--color-main-bg);
|
||||
|
||||
canvas {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import Tooltip from "@/components/Tooltip.vue";
|
||||
import Tooltip from "@/pages/pc/components/Tooltip.vue";
|
||||
import {Icon} from "@iconify/vue";
|
||||
|
||||
interface IProps {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import Tooltip from "@/components/Tooltip.vue";
|
||||
import IconWrapper from "@/components/IconWrapper.vue";
|
||||
import Tooltip from "@/pages/pc/components/Tooltip.vue";
|
||||
import IconWrapper from "@/pages/pc/components/IconWrapper.vue";
|
||||
import {Icon} from "@iconify/vue";
|
||||
|
||||
defineProps<{
|
||||
|
||||
@@ -1,193 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {Icon} from "@iconify/vue";
|
||||
import Close from "@/components/icon/Close.vue";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import {watch} from "vue";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {$ref} from "vue/macros";
|
||||
import {isMobile} from "@/utils";
|
||||
|
||||
let settingStore = useSettingStore()
|
||||
let showNotice = $ref(false)
|
||||
let show = $ref(false)
|
||||
let num = $ref(5)
|
||||
let timer = -1
|
||||
let mobile = $ref(isMobile())
|
||||
const isMac = /macintosh|mac os x/i.test(navigator.userAgent);
|
||||
|
||||
function toggleNotice() {
|
||||
showNotice = true
|
||||
settingStore.first = false
|
||||
timer = setInterval(() => {
|
||||
num--
|
||||
if (num <= 0) close()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
function close() {
|
||||
clearInterval(timer)
|
||||
show = settingStore.first = false
|
||||
}
|
||||
|
||||
watch(() => settingStore.load, (n) => {
|
||||
if (n && settingStore.first) {
|
||||
show = true
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<transition name="right">
|
||||
<div class="CollectNotice"
|
||||
:class="{mobile}"
|
||||
v-if="show">
|
||||
<div class="notice">
|
||||
坚持练习,提高外语能力。将
|
||||
<span class="active">「Typing Word」</span>
|
||||
保存为书签,永不迷失!
|
||||
</div>
|
||||
<div class="wrapper">
|
||||
<transition name="fade">
|
||||
<div class="collect" v-if="showNotice">
|
||||
<div class="href-wrapper">
|
||||
<div class="round">
|
||||
<div class="href">typing-word.ttentau.top</div>
|
||||
<Icon
|
||||
width="22"
|
||||
icon="mdi:star-outline"/>
|
||||
</div>
|
||||
<div class="right">
|
||||
👈
|
||||
<Icon
|
||||
class="star"
|
||||
width="22"
|
||||
icon="mdi:star"/>
|
||||
点亮它!
|
||||
</div>
|
||||
</div>
|
||||
<div class="collect-keyboard" v-if="!mobile">或使用收藏快捷键<span
|
||||
class="active">{{ isMac ? 'Command' : 'Ctrl' }} + D</span></div>
|
||||
</div>
|
||||
<BaseButton v-else size="large" @click="toggleNotice">我想收藏</BaseButton>
|
||||
</transition>
|
||||
</div>
|
||||
<div class="close-wrapper">
|
||||
<span v-show="showNotice"><span class="active">{{ num }}s</span> 后自动关闭</span>
|
||||
<Close @click="close" title="关闭"/>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.right-enter-active,
|
||||
.right-leave-active {
|
||||
transition: all .5s ease;
|
||||
}
|
||||
|
||||
.right-enter-from,
|
||||
.right-leave-to {
|
||||
transform: translateX(110%);
|
||||
}
|
||||
|
||||
.CollectNotice {
|
||||
position: fixed;
|
||||
right: var(--space);
|
||||
top: var(--space);
|
||||
z-index: 2;
|
||||
font-size: 20rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background: var(--color-second-bg);
|
||||
padding: 30rem;
|
||||
border-radius: 12rem;
|
||||
width: 500rem;
|
||||
gap: 40rem;
|
||||
color: var(--color-font-1);
|
||||
line-height: 1.5;
|
||||
border: 1px solid var(--color-item-border);
|
||||
box-shadow: var(--shadow);
|
||||
box-sizing: border-box;
|
||||
|
||||
&.mobile{
|
||||
width: 95%;
|
||||
padding: 10rem;
|
||||
}
|
||||
|
||||
.notice {
|
||||
margin-top: 30rem;
|
||||
}
|
||||
|
||||
.active {
|
||||
color: var(--color-main-active);
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
.collect {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.href-wrapper {
|
||||
display: flex;
|
||||
font-size: 16rem;
|
||||
align-items: center;
|
||||
gap: 10rem;
|
||||
|
||||
.round {
|
||||
color: var(--color-font-1);
|
||||
border-radius: 50rem;
|
||||
padding: 10rem 10rem;
|
||||
padding-left: 20rem;
|
||||
gap: 30rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: var(--color-main-bg);
|
||||
|
||||
.href {
|
||||
font-size: 14rem;
|
||||
}
|
||||
}
|
||||
|
||||
.star {
|
||||
color: var(--color-main-active);
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.collect-keyboard {
|
||||
margin-top: 20rem;
|
||||
font-size: 16rem;
|
||||
|
||||
span {
|
||||
margin-left: 10rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.close-wrapper {
|
||||
right: var(--space);
|
||||
top: var(--space);
|
||||
position: absolute;
|
||||
font-size: 14rem;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
color: var(--color-font-1);
|
||||
gap: 10rem;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,177 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {DictResource, languageCategoryOptions} from "@/types.ts";
|
||||
import {$computed, $ref} from "vue/macros";
|
||||
import {dictionaryResources} from "@/assets/dictionary.ts";
|
||||
import {groupBy} from "lodash-es";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import DictList from "@/components/list/DictList.vue";
|
||||
import DictGroup from "@/components/list/DictGroup.vue";
|
||||
|
||||
const emit = defineEmits<{
|
||||
add: [],
|
||||
selectDict: [val: { dict: any, index: number }]
|
||||
}>()
|
||||
const store = useBaseStore()
|
||||
|
||||
let currentLanguage = $ref('my')
|
||||
let currentTranslateLanguage = $ref('common')
|
||||
let groupByLanguage = groupBy(dictionaryResources, 'language')
|
||||
let translateLanguageList = $ref([])
|
||||
|
||||
function groupByDictTags(dictList: DictResource[]) {
|
||||
return dictList.reduce<Record<string, DictResource[]>>((result, dict) => {
|
||||
dict.tags.forEach((tag) => {
|
||||
if (Object.prototype.hasOwnProperty.call(result, tag)) {
|
||||
result[tag].push(dict)
|
||||
} else {
|
||||
result[tag] = [dict]
|
||||
}
|
||||
})
|
||||
return result
|
||||
}, {})
|
||||
}
|
||||
|
||||
const groupByTranslateLanguage = $computed(() => {
|
||||
let data: any
|
||||
if (currentLanguage === 'article') {
|
||||
let articleList = dictionaryResources.filter(v => v.type === 'article')
|
||||
data = groupBy(articleList, 'translateLanguage')
|
||||
} else if (currentLanguage === 'my') {
|
||||
data = {
|
||||
common: store.myDictList.concat([{id: '',} as any])
|
||||
}
|
||||
} else {
|
||||
data = groupBy(groupByLanguage[currentLanguage], 'translateLanguage')
|
||||
}
|
||||
// console.log('groupByTranslateLanguage', data)
|
||||
translateLanguageList = Object.keys(data)
|
||||
currentTranslateLanguage = translateLanguageList[0]
|
||||
return data
|
||||
})
|
||||
|
||||
const groupedByCategoryAndTag = $computed(() => {
|
||||
const currentTranslateLanguageDictList = groupByTranslateLanguage[currentTranslateLanguage]
|
||||
const groupByCategory = groupBy(currentTranslateLanguageDictList, 'category')
|
||||
|
||||
let data = []
|
||||
for (const [key, value] of Object.entries(groupByCategory)) {
|
||||
data.push([key, groupByDictTags(value)])
|
||||
}
|
||||
// console.log('groupedByCategoryAndTag', data)
|
||||
return data
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dict-list-panel">
|
||||
<header>
|
||||
<div class="tabs">
|
||||
<div class="tab"
|
||||
:class="currentLanguage === item.id && 'active'"
|
||||
@click="currentLanguage = item.id"
|
||||
v-for="item in languageCategoryOptions">
|
||||
<img :src='item.flag' alt=""/>
|
||||
<span>{{ item.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="page-content">
|
||||
<div class="dict-list-wrapper">
|
||||
<template v-if="currentLanguage === 'my'">
|
||||
<DictList
|
||||
@add="emit('add')"
|
||||
@selectDict="e => emit('selectDict',e)"
|
||||
:select-id="store.currentDict.id"
|
||||
:list="groupByTranslateLanguage['common']"/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="translate">
|
||||
<span>翻译:</span>
|
||||
<el-radio-group v-model="currentTranslateLanguage">
|
||||
<el-radio-button border v-for="i in translateLanguageList" :label="i">{{ $t(i) }}</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<DictGroup
|
||||
v-for="item in groupedByCategoryAndTag"
|
||||
:select-id="store.currentDict.id"
|
||||
@selectDict="e => emit('selectDict',e)"
|
||||
:groupByTag="item[1]"
|
||||
:category="item[0]"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/assets/css/variable";
|
||||
|
||||
.dict-list-panel {
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
$header-height: 60rem;
|
||||
padding: var(--space);
|
||||
padding-top: 0;
|
||||
box-sizing: border-box;
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: $header-height;
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 20rem;
|
||||
|
||||
.tab {
|
||||
color: var(--color-font-1);
|
||||
cursor: pointer;
|
||||
padding: 10rem;
|
||||
padding-bottom: 5rem;
|
||||
transition: all .5s;
|
||||
border-bottom: 2px solid transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6rem;
|
||||
|
||||
&.active {
|
||||
$main: rgb(64, 158, 255);
|
||||
border-bottom: 2px solid $main;
|
||||
}
|
||||
|
||||
img {
|
||||
height: 30rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page-content {
|
||||
display: flex;
|
||||
height: calc(100% - $header-height);
|
||||
|
||||
.dict-list-wrapper {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
padding-right: 10rem;
|
||||
|
||||
.translate {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--color-font-1);
|
||||
margin-bottom: 30rem;
|
||||
|
||||
& > span {
|
||||
font-size: 22rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,79 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import {$ref} from "vue/macros";
|
||||
import {watchEffect} from "vue";
|
||||
|
||||
interface IProps {
|
||||
value: string,
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<IProps>(), {
|
||||
value: '',
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'save'
|
||||
])
|
||||
|
||||
let editVal = $ref('')
|
||||
let edit = $ref(false)
|
||||
|
||||
watchEffect(() => {
|
||||
editVal = props.value
|
||||
})
|
||||
|
||||
function save() {
|
||||
emit('save', editVal)
|
||||
edit = false
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
edit = !edit
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="edit"
|
||||
class="edit-text">
|
||||
<el-input
|
||||
v-model="editVal"
|
||||
ref="inputRef"
|
||||
autosize
|
||||
autofocus
|
||||
type="textarea"
|
||||
:input-style="`color: var(--color-font-1);font-size: 16rem;`"
|
||||
/>
|
||||
<div class="options">
|
||||
<BaseButton @click="toggle">取消</BaseButton>
|
||||
<BaseButton @click="save">保存</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="text"
|
||||
:style="`font-size: 16rem;`"
|
||||
@click="toggle">
|
||||
{{ value }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.edit-text {
|
||||
margin-top: 10rem;
|
||||
color: var(--color-font-1);
|
||||
|
||||
.options {
|
||||
margin-top: 10rem;
|
||||
gap: 10rem;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
color: var(--color-font-1);
|
||||
min-height: 18rem;
|
||||
}
|
||||
</style>
|
||||
@@ -1,262 +0,0 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<canvas ref="canvas"/>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {onMounted} from "vue";
|
||||
import {getRandom} from "@/utils/index.ts";
|
||||
import boom from '@/assets/sound/boom.mp3'
|
||||
import shotfire from '@/assets/sound/shotfire.mp3'
|
||||
import {useSound} from "@/hooks/sound.ts";
|
||||
|
||||
const canvas = $ref()
|
||||
const {play: playBoom} = useSound([boom], 3)
|
||||
const {play: playShotFire} = useSound([shotfire], 3)
|
||||
|
||||
onMounted(() => {
|
||||
let ctx = canvas.getContext("2d");
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
let bigBooms = [];
|
||||
let lastTime;
|
||||
|
||||
let raf = window.requestAnimationFrame
|
||||
let count = 0
|
||||
let isBreak = false
|
||||
|
||||
function initAnimate() {
|
||||
lastTime = new Date();
|
||||
animate();
|
||||
}
|
||||
|
||||
initAnimate()
|
||||
|
||||
function animate() {
|
||||
ctx.save();
|
||||
ctx.globalCompositeOperation = "destination-out";
|
||||
ctx.globalAlpha = 0.1;
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.restore();
|
||||
|
||||
let newTime = new Date();
|
||||
if (newTime - lastTime > 200 + (window.innerHeight - 767) / 2 && count < 11) {
|
||||
let boomArea
|
||||
let startX
|
||||
if (count % 2 === 0) {
|
||||
startX = getRandom(0, canvas.width * 0.1);
|
||||
// startX = getRandom(500, 700);
|
||||
boomArea = {
|
||||
x: getRandom(canvas.width * 0.1, canvas.width * 0.3),
|
||||
y: getRandom(50, 200)
|
||||
}
|
||||
} else {
|
||||
startX = getRandom(canvas.width * 0.9, canvas.width);
|
||||
boomArea = {
|
||||
x: getRandom(canvas.width * 0.7, canvas.width * 0.9),
|
||||
y: getRandom(50, 200)
|
||||
}
|
||||
}
|
||||
|
||||
let bigBoom = new Boom(
|
||||
startX,
|
||||
2,
|
||||
"#FFF",
|
||||
boomArea
|
||||
);
|
||||
bigBooms.push(bigBoom);
|
||||
lastTime = newTime;
|
||||
count++
|
||||
}
|
||||
|
||||
bigBooms.map((itemI) => {
|
||||
if (!itemI.dead) {
|
||||
itemI._move();
|
||||
} else {
|
||||
itemI.booms.map((itemJ, index) => {
|
||||
if (!itemJ.dead) {
|
||||
itemJ.moveTo();
|
||||
} else if (index === itemI.booms.length - 1) {
|
||||
bigBooms.splice(bigBooms.indexOf(itemI), 1);
|
||||
}
|
||||
});
|
||||
if (bigBooms.length === 0) {
|
||||
setTimeout(() => {
|
||||
isBreak = true
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!isBreak) {
|
||||
raf(animate);
|
||||
} else {
|
||||
canvas.style.display = 'none'
|
||||
}
|
||||
}
|
||||
|
||||
class Boom {
|
||||
booms = [];
|
||||
x;
|
||||
y;
|
||||
r;
|
||||
color;
|
||||
shape;
|
||||
boomArea;
|
||||
theta;
|
||||
dead;
|
||||
ba;
|
||||
|
||||
constructor(x, r, color, boomArea, shape) {
|
||||
this.x = x;
|
||||
this.y = canvas.height + r;
|
||||
this.r = r;
|
||||
// console.log(this.x, this.y, this.r, boomArea)
|
||||
this.color = color;
|
||||
this.shape = shape || false;
|
||||
this.boomArea = boomArea;
|
||||
this.theta = 0;
|
||||
this.dead = false;
|
||||
this.ba = getRandom(80, 200);
|
||||
|
||||
// playShotFire()
|
||||
}
|
||||
|
||||
_move() {
|
||||
let dx = this.boomArea.x - this.x,
|
||||
dy = this.boomArea.y - this.y;
|
||||
this.x = this.x + dx * 0.01;
|
||||
this.y = this.y + dy * 0.01;
|
||||
// console.log(this.x, this.y, dx, this.ba)
|
||||
|
||||
if (Math.abs(dx) <= this.ba && Math.abs(dy) <= this.ba) {
|
||||
this._boom();
|
||||
this.dead = true;
|
||||
} else {
|
||||
this._paint();
|
||||
this._drawLight();
|
||||
}
|
||||
}
|
||||
|
||||
_paint() {
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI);
|
||||
ctx.fillStyle = this.color;
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
_drawLight() {
|
||||
ctx.save();
|
||||
ctx.fillStyle = "rgba(255,228,150,0.3)";
|
||||
ctx.beginPath();
|
||||
ctx.arc(
|
||||
this.x,
|
||||
this.y,
|
||||
this.r + 3 * Math.random() + 1,
|
||||
0,
|
||||
2 * Math.PI
|
||||
);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
_boom() {
|
||||
let fireNum = getRandom(100, 300);
|
||||
let style = getRandom(0, 10) >= 5 ? 1 : 2;
|
||||
let color;
|
||||
if (style === 1) {
|
||||
color = {
|
||||
a: getRandom(128, 255),
|
||||
b: getRandom(128, 255),
|
||||
c: getRandom(128, 255),
|
||||
};
|
||||
}
|
||||
|
||||
let fanwei = fireNum;
|
||||
// playBoom()
|
||||
|
||||
for (let i = 0; i < fireNum; i++) {
|
||||
if (style === 2) {
|
||||
color = {
|
||||
a: getRandom(128, 255),
|
||||
b: getRandom(128, 255),
|
||||
c: getRandom(128, 255),
|
||||
};
|
||||
}
|
||||
let a = getRandom(-Math.PI, Math.PI);
|
||||
let x = getRandom(0, fanwei) * Math.cos(a) + this.x;
|
||||
let y = getRandom(0, fanwei) * Math.sin(a) + this.y;
|
||||
let radius = getRandom(0, 2);
|
||||
let frag = new Firework(this.x, this.y, radius, color, x, y);
|
||||
this.booms.push(frag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Firework {
|
||||
tx = 0;
|
||||
ty = 0;
|
||||
x = 0;
|
||||
y = 0;
|
||||
dead = false;
|
||||
centerX = 0;
|
||||
centerY = 0;
|
||||
radius = 0;
|
||||
color = 0;
|
||||
|
||||
constructor(x, y, radius, color, tx, ty) {
|
||||
this.tx = tx;
|
||||
this.ty = ty;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.dead = false;
|
||||
this.centerX = x;
|
||||
this.centerY = y;
|
||||
this.radius = radius;
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
paint() {
|
||||
ctx.fillStyle = `rgba(${this.color.a},${this.color.b},${this.color.c})`;
|
||||
ctx.fillRect(
|
||||
this.x - this.radius,
|
||||
this.y - this.radius,
|
||||
this.radius * 2,
|
||||
this.radius * 2
|
||||
);
|
||||
}
|
||||
|
||||
moveTo() {
|
||||
this.ty = this.ty + 0.3;
|
||||
let dx = this.tx - this.x,
|
||||
dy = this.ty - this.y;
|
||||
this.x = Math.abs(dx) < 0.1 ? this.tx : this.x + dx * 0.1;
|
||||
this.y = Math.abs(dy) < 0.1 ? this.ty : this.y + dy * 0.1;
|
||||
if (dx === 0 && Math.abs(dy) <= 80) {
|
||||
this.dead = true;
|
||||
}
|
||||
this.paint();
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
canvas {
|
||||
z-index: 99999;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: transparent;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,33 +0,0 @@
|
||||
<template>
|
||||
<div class="icon-wrapper">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
$w: 22rem;
|
||||
.icon-wrapper {
|
||||
cursor: pointer;
|
||||
//padding: 2rem;
|
||||
width: 26rem;
|
||||
height: 26rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 3rem;
|
||||
background: transparent;
|
||||
transition: all .3s;
|
||||
color: var(--color-main-active);
|
||||
|
||||
&:hover {
|
||||
background: var(--color-main-active);
|
||||
color: white;
|
||||
}
|
||||
|
||||
:deep(svg) {
|
||||
width: $w;
|
||||
height: $w;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,86 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import {$ref} from "vue/macros";
|
||||
import {Icon} from "@iconify/vue";
|
||||
import Close from "@/components/icon/Close.vue";
|
||||
import {useDisableEventListener, useWindowClick} from "@/hooks/event.ts";
|
||||
import {watch} from "vue";
|
||||
|
||||
defineProps<{
|
||||
modelValue: string
|
||||
}>()
|
||||
|
||||
defineEmits(['update:modelValue'])
|
||||
let focus = $ref(false)
|
||||
let inputEl = $ref<HTMLDivElement>()
|
||||
|
||||
useWindowClick((e: PointerEvent) => {
|
||||
if (!e) return
|
||||
focus = inputEl.contains(e.target as any);
|
||||
})
|
||||
|
||||
useDisableEventListener(() => focus)
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="base-input"
|
||||
:class="{focus}"
|
||||
ref="inputEl"
|
||||
>
|
||||
<Icon icon="fluent:search-24-regular"
|
||||
width="20"/>
|
||||
<input type="text"
|
||||
:value="modelValue"
|
||||
@input="e=>$emit('update:modelValue',e.target.value)"
|
||||
>
|
||||
<transition name="fade">
|
||||
<Close v-if="modelValue" @click="$emit('update:modelValue','')"/>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/assets/css/style.scss";
|
||||
|
||||
.base-input {
|
||||
border: 1px solid var(--color-second-bg);
|
||||
border-radius: 6rem;
|
||||
overflow: hidden;
|
||||
padding: 3rem 5rem;
|
||||
transition: all .3s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: all .3s;
|
||||
background: var(--color-input-bg);
|
||||
|
||||
:deep(svg) {
|
||||
transition: all .3s;
|
||||
color: var(--color-input-icon);
|
||||
}
|
||||
|
||||
&.focus {
|
||||
border: 1px solid var(--color-main-active);
|
||||
|
||||
:deep(svg) {
|
||||
color: gray;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
font-family: var(--font-family);
|
||||
font-size: 18rem;
|
||||
outline: none;
|
||||
min-height: 20rem;
|
||||
flex: 1;
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
border: none;
|
||||
background: transparent;
|
||||
|
||||
&[readonly] {
|
||||
cursor: not-allowed;
|
||||
opacity: .7;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,30 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import router from "@/router.ts";
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
function goHome(){
|
||||
router.push('/')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="logo" @click="goHome">
|
||||
<img v-show="settingStore.theme === 'dark'" src="/logo-text-white.png" alt="">
|
||||
<img v-show="settingStore.theme !== 'dark'" src="/logo-text-black.png" alt="">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.logo {
|
||||
position: fixed;
|
||||
left: var(--space);
|
||||
top: var(--space);
|
||||
z-index: 1;
|
||||
|
||||
img {
|
||||
cursor: pointer;
|
||||
height: 35rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,123 +0,0 @@
|
||||
<script lang="jsx">
|
||||
import {nextTick, Teleport, Transition} from "vue";
|
||||
|
||||
export default {
|
||||
name: "PopConfirm",
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default() {
|
||||
return ''
|
||||
}
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('click', () => {
|
||||
this.show = false
|
||||
})
|
||||
window.addEventListener('keydown', () => {
|
||||
this.show = false
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
showPop(e) {
|
||||
if (this.disabled) return
|
||||
e?.stopPropagation()
|
||||
let rect = e.target.getBoundingClientRect()
|
||||
this.show = true
|
||||
nextTick(() => {
|
||||
let tip = this.$refs?.tip?.getBoundingClientRect()
|
||||
console.log('rect', rect, tip)
|
||||
if (!tip) return
|
||||
if (rect.top < 150) {
|
||||
this.$refs.tip.style.top = rect.top + rect.height + tip.height + 30 + 'px'
|
||||
} else {
|
||||
this.$refs.tip.style.top = rect.top - 10 + 'px'
|
||||
}
|
||||
this.$refs.tip.style.left = rect.left + rect.width / 2 - 50 + 'px'
|
||||
})
|
||||
},
|
||||
confirm() {
|
||||
this.show = false
|
||||
this.$emit('confirm')
|
||||
}
|
||||
},
|
||||
render() {
|
||||
let Vnode = this.$slots.default()[0]
|
||||
return (
|
||||
<div class="pop-confirm">
|
||||
<Teleport to="body">
|
||||
<Transition>
|
||||
{
|
||||
this.show && (
|
||||
<div ref="tip" className="pop-confirm-content">
|
||||
<div className="text">
|
||||
{this.title}
|
||||
</div>
|
||||
<div className="options">
|
||||
<div onClick={() => this.show = false}>取消</div>
|
||||
<div className="main" onClick={() => this.confirm()}>确认</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</Transition>
|
||||
</Teleport>
|
||||
<Vnode onClick={(e) => this.showPop(e)}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
$bg-color: rgb(226, 226, 226);
|
||||
|
||||
.pop-confirm-content {
|
||||
position: fixed;
|
||||
background: var(--color-tooltip-bg);
|
||||
padding: 15rem;
|
||||
border-radius: 4rem;
|
||||
transform: translate(-50%, calc(-100% - 10rem));
|
||||
box-shadow: 0 0 6px 1px var(--color-tooltip-shadow);
|
||||
z-index: 999;
|
||||
|
||||
.text {
|
||||
color: var(--color-font-1);
|
||||
text-align: start;
|
||||
font-size: 14rem;
|
||||
width: 150rem;
|
||||
min-width: 150rem;
|
||||
}
|
||||
|
||||
.options {
|
||||
margin-top: 15rem;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 12rem;
|
||||
font-size: 12rem;
|
||||
|
||||
div {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.main {
|
||||
color: gray;
|
||||
background: $bg-color;
|
||||
padding: 3rem 10rem;
|
||||
border-radius: 4rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,50 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {ShortcutKey} from "@/types.ts";
|
||||
import {$ref} from "vue/macros";
|
||||
import FeedbackModal from "@/components/toolbar/FeedbackModal.vue";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import Tooltip from "@/components/Tooltip.vue";
|
||||
import IconWrapper from "@/components/IconWrapper.vue";
|
||||
import {Icon} from "@iconify/vue";
|
||||
import useTheme from "@/hooks/theme.ts";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
|
||||
let showFeedbackModal = $ref(false)
|
||||
const {toggleTheme} = useTheme()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="right-bar">
|
||||
<Tooltip
|
||||
:title="`切换主题(快捷键:${settingStore.shortcutKeyMap[ShortcutKey.ToggleTheme]})`"
|
||||
>
|
||||
<IconWrapper>
|
||||
<Icon icon="ep:moon" v-if="settingStore.theme === 'dark'"
|
||||
@click="toggleTheme"/>
|
||||
<Icon icon="tabler:sun" v-else @click="toggleTheme"/>
|
||||
</IconWrapper>
|
||||
</Tooltip>
|
||||
|
||||
<a href="https://github.com/zyronon/typing-word" target="_blank">
|
||||
<BaseIcon
|
||||
title="Github地址"
|
||||
icon="mdi:github"/>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.right-bar {
|
||||
position: fixed;
|
||||
right: var(--space);
|
||||
top: var(--space);
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
gap: 10rem;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,87 +0,0 @@
|
||||
<template>
|
||||
<div class="ring">
|
||||
<svg height="100%" width="100%">
|
||||
<circle class="circle-full"
|
||||
cx="40rem"
|
||||
cy="40rem"
|
||||
r="35rem"
|
||||
fill="none"
|
||||
stroke-width="6rem"
|
||||
stroke-linecap="round"></circle>
|
||||
<circle v-if="props.percentage" ref="circleEl"
|
||||
class="circle-detail"
|
||||
cx="40rem"
|
||||
cy="40rem"
|
||||
r="35rem"
|
||||
fill="none"
|
||||
stroke-width="6rem"
|
||||
stroke-linecap="round"
|
||||
stroke-dasharray="0,10000"></circle>
|
||||
</svg>
|
||||
<span class="value">{{ props.value }}</span>
|
||||
<span class="desc">{{ props.desc }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {onMounted} from "vue"
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
percentage?: number,
|
||||
value: string | number,
|
||||
desc: string,
|
||||
}>(), {
|
||||
percentage: 90,
|
||||
})
|
||||
|
||||
const circleEl = $ref(null)
|
||||
|
||||
onMounted(() => {
|
||||
if (props.percentage) {
|
||||
let circleLength = Math.floor(2 * Math.PI * 40);
|
||||
let val = Number(props.percentage.toFixed(0));
|
||||
circleEl.setAttribute("stroke-dasharray", "" + circleLength * val / 100 + "rem,10000");
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@import "@/assets/css/variable.scss";
|
||||
|
||||
$w: 80rem;
|
||||
$w2: calc($w / 2);
|
||||
|
||||
.ring {
|
||||
font-size: 16rem;
|
||||
width: $w;
|
||||
height: $w;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
margin-bottom: 6rem;
|
||||
|
||||
svg {
|
||||
position: absolute;
|
||||
|
||||
.circle-full {
|
||||
$item-hover: rgb(75, 75, 75);
|
||||
stroke: $item-hover;
|
||||
}
|
||||
|
||||
.circle-detail {
|
||||
transform-origin: $w2 $w2;
|
||||
transform: rotate(-90deg);
|
||||
stroke: var(--color-main-active);
|
||||
}
|
||||
}
|
||||
|
||||
.value {
|
||||
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 12rem;
|
||||
opacity: .6;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,577 +0,0 @@
|
||||
<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 {$computed, $ref} from "vue/macros";
|
||||
import {cloneDeep} from "lodash-es";
|
||||
import {DefaultShortcutKeyMap, Dict, DictType, 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 {BaseState, useBaseStore} from "@/stores/base.ts";
|
||||
import * as copy from "copy-to-clipboard";
|
||||
import {saveAs} from "file-saver";
|
||||
import {checkAndUpgradeSaveDict, checkAndUpgradeSaveSetting, shakeCommonDict} from "@/utils";
|
||||
import {dayjs} from "element-plus";
|
||||
|
||||
|
||||
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"});
|
||||
let date = new Date()
|
||||
let dateStr = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()} ${date.getHours()}-${date.getMinutes()}-${date.getSeconds()}`
|
||||
saveAs(blob, `${APP_NAME}-User-Data-${dateStr}.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 = v.target.result;
|
||||
if (str) {
|
||||
let obj = JSON.parse(str)
|
||||
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>
|
||||
<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>
|
||||
<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.translateSound"-->
|
||||
<!-- inline-prompt-->
|
||||
<!-- active-text="开"-->
|
||||
<!-- inactive-text="关"-->
|
||||
<!-- />-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="row">-->
|
||||
<!-- <label class="sub-title">音量</label>-->
|
||||
<!-- <div class="wrapper">-->
|
||||
<!-- <el-slider v-model="settingStore.translateSoundVolume"/>-->
|
||||
<!-- <span>{{ settingStore.translateSoundVolume }}%</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">
|
||||
开启后,输入时不区分大小写,如输入“hello”和“Hello”都会被认为是正确的
|
||||
</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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/assets/css/variable";
|
||||
|
||||
.setting {
|
||||
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: 10rem 20rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
//align-items: center;
|
||||
//justify-content: center;
|
||||
gap: 10rem;
|
||||
|
||||
.tab {
|
||||
cursor: pointer;
|
||||
padding: 10rem 15rem;
|
||||
border-radius: 8rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rem;
|
||||
|
||||
&.active {
|
||||
background: var(--color-item-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.git-log {
|
||||
font-size: 10rem;
|
||||
color: gray;
|
||||
margin-bottom: 5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
background: var(--color-header-bg);
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding: 10rem var(--space);
|
||||
|
||||
.row {
|
||||
min-height: 40rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: calc(var(--space) * 5);
|
||||
|
||||
.wrapper {
|
||||
height: 30rem;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--space);
|
||||
|
||||
span {
|
||||
text-align: right;
|
||||
//width: 30rem;
|
||||
font-size: 12rem;
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.set-key {
|
||||
align-items: center;
|
||||
|
||||
input {
|
||||
width: 150rem;
|
||||
box-sizing: border-box;
|
||||
margin-right: 10rem;
|
||||
height: 28rem;
|
||||
outline: none;
|
||||
font-size: 16rem;
|
||||
border: 1px solid gray;
|
||||
border-radius: 3rem;
|
||||
padding: 0 5rem;
|
||||
background: var(--color-second-bg);
|
||||
color: var(--color-font-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main-title {
|
||||
font-size: 22rem;
|
||||
}
|
||||
|
||||
.item-title {
|
||||
font-size: 16rem;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
font-size: 14rem;
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.scroll {
|
||||
flex: 1;
|
||||
padding-right: 10rem;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-bottom: 20rem;
|
||||
}
|
||||
|
||||
.desc {
|
||||
margin-bottom: 10rem;
|
||||
font-size: 12rem;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,44 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {$computed} from "vue/macros";
|
||||
|
||||
const props = defineProps<{
|
||||
width?: string,
|
||||
height?: string,
|
||||
slideCount: number,
|
||||
step: number
|
||||
}>()
|
||||
|
||||
const style = $computed(() => {
|
||||
return {
|
||||
width: props.slideCount * 100 + '%',
|
||||
transform: `translate3d(-${100 / props.slideCount * props.step}%, 0, 0)`
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="slide">
|
||||
<div class="slide-list"
|
||||
:style="style">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.slide {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
.slide-list {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
transition: all .3s;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,84 +0,0 @@
|
||||
<script lang="jsx">
|
||||
import {nextTick, Teleport, Transition} from "vue";
|
||||
|
||||
export default {
|
||||
name: "Tooltip",
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default() {
|
||||
return ''
|
||||
}
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showPop(e) {
|
||||
if (this.disabled) return
|
||||
if (!this.title) return
|
||||
e.stopPropagation()
|
||||
let rect = e.target.getBoundingClientRect()
|
||||
this.show = true
|
||||
nextTick(() => {
|
||||
let tip = this.$refs?.tip?.getBoundingClientRect()
|
||||
if (!tip) return
|
||||
if (rect.top < 50) {
|
||||
this.$refs.tip.style.top = rect.top + rect.height + 10 + 'px'
|
||||
} else {
|
||||
this.$refs.tip.style.top = rect.top - tip.height - 10 + 'px'
|
||||
}
|
||||
let tipWidth = tip.width
|
||||
let rectWidth = rect.width
|
||||
this.$refs.tip.style.left = rect.left - (tipWidth - rectWidth) / 2 + 'px'
|
||||
// onmouseleave={() => this.show = false}
|
||||
})
|
||||
},
|
||||
},
|
||||
render() {
|
||||
let Vnode = this.$slots.default()[0]
|
||||
return <>
|
||||
{
|
||||
this.show && this.title && (
|
||||
<Teleport to="body">
|
||||
<Transition name="fade">
|
||||
<div ref="tip" className="tip">
|
||||
{this.title}
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
)
|
||||
}
|
||||
<Vnode
|
||||
onClick={() => this.show = false}
|
||||
onmouseenter={(e) => this.showPop(e)}
|
||||
onmouseleave={() => this.show = false}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "@/assets/css/style.scss";
|
||||
|
||||
.tip {
|
||||
position: fixed;
|
||||
font-size: 14rem;
|
||||
z-index: 9999;
|
||||
border-radius: 4rem;
|
||||
padding: 10rem;
|
||||
color: var(--color-font-1);
|
||||
background: var(--color-tooltip-bg);
|
||||
//box-shadow: 1px 1px 6px #bbbbbb;
|
||||
box-shadow: 0 0 6px 1px var(--color-tooltip-shadow);
|
||||
}
|
||||
</style>
|
||||
@@ -1,504 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {Article, DefaultArticle, Sentence, TranslateEngine, TranslateType} from "@/types.ts";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import EditAbleText from "@/components/EditAbleText.vue";
|
||||
import {Icon} from "@iconify/vue";
|
||||
import {
|
||||
getNetworkTranslate,
|
||||
getSentenceAllText,
|
||||
getSentenceAllTranslateText,
|
||||
renewSectionTexts,
|
||||
renewSectionTranslates
|
||||
} from "@/hooks/translate.ts";
|
||||
import * as copy from "copy-to-clipboard";
|
||||
import {$ref} from "vue/macros";
|
||||
import {MessageBox} from "@/utils/MessageBox.tsx";
|
||||
import {getSplitTranslateText} from "@/hooks/article.ts";
|
||||
import {cloneDeep} from "lodash-es";
|
||||
import {watch} from "vue";
|
||||
import Empty from "@/components/Empty.vue";
|
||||
|
||||
interface IProps {
|
||||
article?: Article,
|
||||
type?: 'single' | 'batch'
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<IProps>(), {
|
||||
article: () => cloneDeep(DefaultArticle),
|
||||
type: 'single'
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
save: [val: Article],
|
||||
saveAndNext: [val: Article]
|
||||
}>()
|
||||
|
||||
let networkTranslateEngine = $ref('baidu')
|
||||
let progress = $ref(0)
|
||||
let failCount = $ref(0)
|
||||
let textareaRef = $ref<HTMLTextAreaElement>()
|
||||
const TranslateEngineOptions = [
|
||||
{value: 'baidu', label: '百度'},
|
||||
{value: 'youdao', label: '有道'},
|
||||
]
|
||||
|
||||
let editArticle = $ref<Article>(cloneDeep(DefaultArticle))
|
||||
|
||||
watch(() => props.article, val => {
|
||||
editArticle = cloneDeep(val)
|
||||
progress = 0
|
||||
failCount = 0
|
||||
if (editArticle.text.trim()) {
|
||||
if (editArticle.useTranslateType === TranslateType.custom) {
|
||||
if (editArticle.textCustomTranslate.trim()) {
|
||||
if (!editArticle.textCustomTranslateIsFormat) {
|
||||
let r = getSplitTranslateText(editArticle.textCustomTranslate)
|
||||
if (r) {
|
||||
editArticle.textCustomTranslate = r
|
||||
ElMessage({
|
||||
message: '检测到本地翻译未格式化,已自动格式化',
|
||||
type: 'success',
|
||||
duration: 3000
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
renewSections()
|
||||
// console.log('ar', article)
|
||||
}, {immediate: true})
|
||||
|
||||
function renewSections() {
|
||||
if (editArticle.text.trim()) {
|
||||
renewSectionTexts(editArticle)
|
||||
if (editArticle.useTranslateType === TranslateType.custom) {
|
||||
failCount = renewSectionTranslates(editArticle, editArticle.textCustomTranslate)
|
||||
}
|
||||
if (editArticle.useTranslateType === TranslateType.network) {
|
||||
failCount = renewSectionTranslates(editArticle, editArticle.textNetworkTranslate)
|
||||
}
|
||||
} else {
|
||||
editArticle.sections = []
|
||||
}
|
||||
}
|
||||
|
||||
function appendTranslate(str: string) {
|
||||
let selectionStart = textareaRef.selectionStart;
|
||||
let selectionEnd = textareaRef.selectionEnd;
|
||||
if (editArticle.useTranslateType === TranslateType.custom) {
|
||||
editArticle.textCustomTranslate = editArticle.textCustomTranslate.slice(0, selectionStart) + str + editArticle.textCustomTranslate.slice(selectionEnd)
|
||||
}
|
||||
if (editArticle.useTranslateType === TranslateType.network) {
|
||||
editArticle.textNetworkTranslate = editArticle.textNetworkTranslate.slice(0, selectionStart) + str + editArticle.textNetworkTranslate.slice(selectionEnd)
|
||||
}
|
||||
}
|
||||
|
||||
function onPaste(event: ClipboardEvent) {
|
||||
event.preventDefault()
|
||||
// @ts-ignore
|
||||
let paste = (event.clipboardData || window.clipboardData).getData("text");
|
||||
return MessageBox.confirm(
|
||||
'是否需要自动分句',
|
||||
'提示',
|
||||
() => {
|
||||
let r = getSplitTranslateText(paste)
|
||||
if (r) {
|
||||
appendTranslate(r)
|
||||
renewSections()
|
||||
}
|
||||
},
|
||||
() => {
|
||||
appendTranslate(paste)
|
||||
renewSections()
|
||||
}, null,
|
||||
{
|
||||
confirmButtonText: '需要',
|
||||
cancelButtonText: '关闭',
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function onBlur() {
|
||||
document.removeEventListener('paste', onPaste);
|
||||
}
|
||||
|
||||
function onFocus() {
|
||||
document.addEventListener('paste', onPaste);
|
||||
}
|
||||
|
||||
async function startNetworkTranslate() {
|
||||
if (!editArticle.title.trim()) {
|
||||
return ElMessage.error('请填写标题!')
|
||||
}
|
||||
if (!editArticle.text.trim()) {
|
||||
return ElMessage.error('请填写正文!')
|
||||
}
|
||||
renewSectionTexts(editArticle)
|
||||
editArticle.textNetworkTranslate = ''
|
||||
//注意!!!
|
||||
//这里需要用异步,因为watch了article.networkTranslate,改变networkTranslate了之后,会重新设置article.sections
|
||||
//导致getNetworkTranslate里面拿到的article.sections是废弃的值
|
||||
setTimeout(async () => {
|
||||
await getNetworkTranslate(editArticle, TranslateEngine.Baidu, true, (v: number) => {
|
||||
progress = v
|
||||
})
|
||||
failCount = 0
|
||||
|
||||
copy(JSON.stringify(editArticle.sections))
|
||||
})
|
||||
}
|
||||
|
||||
function saveSentenceTranslate(sentence: Sentence, val: string) {
|
||||
sentence.translate = val
|
||||
if (editArticle.useTranslateType === TranslateType.custom) {
|
||||
editArticle.textCustomTranslate = getSentenceAllTranslateText(editArticle)
|
||||
}
|
||||
if (editArticle.useTranslateType === TranslateType.network) {
|
||||
editArticle.textNetworkTranslate = getSentenceAllTranslateText(editArticle)
|
||||
}
|
||||
renewSections()
|
||||
}
|
||||
|
||||
function saveSentenceText(sentence: Sentence, val: string) {
|
||||
sentence.text = val
|
||||
editArticle.text = getSentenceAllText(editArticle)
|
||||
renewSections()
|
||||
}
|
||||
|
||||
function save(option: 'save' | 'saveAndNext') {
|
||||
// return console.log(cloneDeep(editArticle))
|
||||
return new Promise((resolve: Function) => {
|
||||
// console.log('article', article)
|
||||
// copy(JSON.stringify(article))
|
||||
|
||||
editArticle.title = editArticle.title.trim()
|
||||
editArticle.titleTranslate = editArticle.titleTranslate.trim()
|
||||
editArticle.text = editArticle.text.trim()
|
||||
editArticle.textCustomTranslate = editArticle.textCustomTranslate.trim()
|
||||
editArticle.textNetworkTranslate = editArticle.textNetworkTranslate.trim()
|
||||
|
||||
if (!editArticle.title) {
|
||||
ElMessage.error('请填写标题!')
|
||||
return resolve(false)
|
||||
}
|
||||
if (!editArticle.text) {
|
||||
ElMessage.error('请填写正文!')
|
||||
return resolve(false)
|
||||
}
|
||||
|
||||
const saveTemp = () => {
|
||||
editArticle.textCustomTranslateIsFormat = true
|
||||
emit(option as any, editArticle)
|
||||
|
||||
return resolve(true)
|
||||
}
|
||||
|
||||
if (editArticle.useTranslateType === TranslateType.network) {
|
||||
if (!editArticle.textNetworkTranslate) {
|
||||
return MessageBox.confirm(
|
||||
'您选择了“网络翻译”,但译文内容却为空白,是否修改为“不需要翻译”并保存?',
|
||||
'提示',
|
||||
() => {
|
||||
editArticle.useTranslateType = TranslateType.none
|
||||
saveTemp()
|
||||
},
|
||||
() => void 0,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (editArticle.useTranslateType === TranslateType.custom) {
|
||||
if (!editArticle.textCustomTranslate) {
|
||||
return MessageBox.confirm(
|
||||
'您选择了“本地翻译”,但译文内容却为空白,是否修改为“不需要翻译”并保存?',
|
||||
'提示',
|
||||
() => {
|
||||
editArticle.useTranslateType = TranslateType.none
|
||||
saveTemp()
|
||||
},
|
||||
() => void 0,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
saveTemp()
|
||||
})
|
||||
}
|
||||
|
||||
//不知道为什么直接用editArticle,取到是空的默认值
|
||||
defineExpose({save, getEditArticle: () => cloneDeep(editArticle)})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="content">
|
||||
<div class="row">
|
||||
<div class="title">①原文</div>
|
||||
<div class="item">
|
||||
<div class="label">标题:</div>
|
||||
<textarea
|
||||
v-model="editArticle.title"
|
||||
type="textarea"
|
||||
class="base-textarea"
|
||||
placeholder="请填写原文标题"
|
||||
>
|
||||
</textarea>
|
||||
</div>
|
||||
<div class="item basic">
|
||||
<div class="label">正文:</div>
|
||||
<textarea
|
||||
v-model="editArticle.text"
|
||||
@input="renewSections"
|
||||
:readonly="![100,0].includes(progress)"
|
||||
type="textarea"
|
||||
class="base-textarea"
|
||||
placeholder="请填写原文正文"
|
||||
>
|
||||
</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="title">②译文</div>
|
||||
<div class="item">
|
||||
<div class="label">
|
||||
<span>标题:</span>
|
||||
<el-radio-group
|
||||
v-model="editArticle.useTranslateType"
|
||||
@change="renewSections"
|
||||
>
|
||||
<el-radio-button :label="TranslateType.custom">本地翻译</el-radio-button>
|
||||
<el-radio-button :label="TranslateType.network">网络翻译</el-radio-button>
|
||||
<el-radio-button :label="TranslateType.none">不需要翻译</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<textarea
|
||||
v-model="editArticle.titleTranslate"
|
||||
type="textarea"
|
||||
class="base-textarea"
|
||||
placeholder="请填写翻译标题"
|
||||
>
|
||||
</textarea>
|
||||
</div>
|
||||
<div class="item basic">
|
||||
<div class="label">
|
||||
<span>正文:</span>
|
||||
<div class="translate-item" v-if="editArticle.useTranslateType === TranslateType.network">
|
||||
<el-progress :percentage="progress"
|
||||
:duration="30"
|
||||
:striped="progress !== 100"
|
||||
:striped-flow="progress !== 100"
|
||||
:stroke-width="8"
|
||||
:show-text="true"/>
|
||||
<el-select v-model="networkTranslateEngine"
|
||||
style="width: 80rem;"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in TranslateEngineOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<BaseButton
|
||||
size="small"
|
||||
@click="startNetworkTranslate"
|
||||
:loading="progress!==0 && progress !== 100"
|
||||
>开始翻译
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
<textarea
|
||||
v-if="editArticle.useTranslateType === TranslateType.custom"
|
||||
v-model="editArticle.textCustomTranslate"
|
||||
@input="renewSections"
|
||||
@blur="onBlur"
|
||||
@focus="onFocus"
|
||||
type="textarea"
|
||||
class="base-textarea"
|
||||
placeholder="请填写翻译正文"
|
||||
ref="textareaRef"
|
||||
>
|
||||
</textarea>
|
||||
<textarea
|
||||
v-if="editArticle.useTranslateType === TranslateType.network"
|
||||
v-model="editArticle.textNetworkTranslate"
|
||||
:readonly="![100,0].includes(progress)"
|
||||
@input="renewSections"
|
||||
@blur="onBlur"
|
||||
@focus="onFocus"
|
||||
type="textarea"
|
||||
class="base-textarea"
|
||||
placeholder="等待网络翻译中..."
|
||||
ref="textareaRef"
|
||||
>
|
||||
</textarea>
|
||||
<Empty
|
||||
text="不需要翻译~"
|
||||
v-if="editArticle.useTranslateType === TranslateType.none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="title">③译文对照</div>
|
||||
<template v-if="editArticle.sections.length">
|
||||
<div class="article-translate">
|
||||
<div class="section" v-for="(item,indexI) in editArticle.sections">
|
||||
<div class="sentence" v-for="(sentence,indexJ) in item">
|
||||
<EditAbleText
|
||||
:value="sentence.text"
|
||||
@save="(e:string) => saveSentenceText(sentence,e)"
|
||||
/>
|
||||
<EditAbleText
|
||||
:value="sentence.translate"
|
||||
@save="(e:string) => saveSentenceTranslate(sentence,e)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="options" v-if="editArticle.text.trim()">
|
||||
<div class="status">
|
||||
<span>状态:</span>
|
||||
<div class="warning" v-if="failCount && editArticle.useTranslateType !== TranslateType.none">
|
||||
<Icon icon="typcn:warning-outline"/>
|
||||
共有{{ failCount }}句没有翻译!
|
||||
</div>
|
||||
<div class="success" v-else>
|
||||
<Icon icon="mdi:success-circle-outline"/>
|
||||
翻译完成!
|
||||
</div>
|
||||
</div>
|
||||
<div class="left">
|
||||
<BaseButton @click="save('save')">保存</BaseButton>
|
||||
<BaseButton v-if="type === 'batch'" @click="save('saveAndNext')">保存并添加下一篇</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<Empty v-else text="没有译文对照~"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/assets/css/style.scss";
|
||||
|
||||
.content {
|
||||
color: var(--color-font-1);
|
||||
flex: 1;
|
||||
display: flex;
|
||||
gap: var(--space);
|
||||
padding: var(--space);
|
||||
padding-top: 10rem;
|
||||
}
|
||||
|
||||
.row {
|
||||
flex: 10;
|
||||
width: 33%;
|
||||
//height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
//opacity: 0;
|
||||
|
||||
.basic {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&:nth-child(1) {
|
||||
flex: 7;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 22rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.item {
|
||||
width: 100%;
|
||||
//margin-bottom: 10rem;
|
||||
|
||||
.label {
|
||||
height: 45rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 16rem;
|
||||
}
|
||||
}
|
||||
|
||||
.translate-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: calc(var(--space) / 2);
|
||||
}
|
||||
|
||||
.el-progress {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.article-translate {
|
||||
margin-top: 10rem;
|
||||
margin-bottom: 20rem;
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
border-radius: 8rem;
|
||||
|
||||
.section {
|
||||
background: var(--color-textarea-bg);
|
||||
margin-bottom: 20rem;
|
||||
padding: var(--space);
|
||||
border-radius: 8rem;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.sentence {
|
||||
margin-bottom: 20rem;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: 18rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.options {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.warning {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 20rem;
|
||||
color: red;
|
||||
}
|
||||
|
||||
.success {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 20rem;
|
||||
color: #67C23A;
|
||||
}
|
||||
|
||||
.left {
|
||||
gap: var(--space);
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,295 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import {onMounted, onUnmounted} from "vue";
|
||||
import {Article, DefaultArticle} from "@/types.ts";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import {cloneDeep} from "lodash-es";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import {$ref} from "vue/macros";
|
||||
import List from "@/components/list/List.vue";
|
||||
import Dialog from "@/components/dialog/Dialog.vue";
|
||||
import EditArticle from "@/components/article/EditArticle.vue";
|
||||
import {emitter, EventKey} from "@/utils/eventBus.ts";
|
||||
import {useDisableEventListener, useWindowClick} from "@/hooks/event.ts";
|
||||
import {MessageBox} from "@/utils/MessageBox.tsx";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import {nanoid} from "nanoid";
|
||||
import {syncMyDictList} from "@/hooks/dict.ts";
|
||||
import MiniDialog from "@/components/dialog/MiniDialog.vue";
|
||||
|
||||
const emit = defineEmits<{
|
||||
importData: [val: Event]
|
||||
exportData: [val: string]
|
||||
}>()
|
||||
const base = useBaseStore()
|
||||
const runtimeStore = useRuntimeStore()
|
||||
|
||||
let article = $ref<Article>(cloneDeep(DefaultArticle))
|
||||
let show = $ref(false)
|
||||
let editArticleRef: any = $ref()
|
||||
let listEl: any = $ref()
|
||||
|
||||
onMounted(() => {
|
||||
emitter.on(EventKey.openArticleListModal, (val: Article) => {
|
||||
console.log('val', val)
|
||||
show = true
|
||||
if (val) {
|
||||
article = cloneDeep(val)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
emitter.off(EventKey.openArticleListModal)
|
||||
})
|
||||
|
||||
useDisableEventListener(() => show)
|
||||
|
||||
async function selectArticle(item: Article) {
|
||||
let r = await checkDataChange()
|
||||
if (r) {
|
||||
article = cloneDeep(item)
|
||||
}
|
||||
}
|
||||
|
||||
function checkDataChange() {
|
||||
return new Promise(resolve => {
|
||||
let editArticle: Article = editArticleRef.getEditArticle()
|
||||
|
||||
if (editArticle.id !== '-1') {
|
||||
editArticle.title = editArticle.title.trim()
|
||||
editArticle.titleTranslate = editArticle.titleTranslate.trim()
|
||||
editArticle.text = editArticle.text.trim()
|
||||
editArticle.textCustomTranslate = editArticle.textCustomTranslate.trim()
|
||||
editArticle.textNetworkTranslate = editArticle.textNetworkTranslate.trim()
|
||||
|
||||
if (
|
||||
editArticle.title !== article.title ||
|
||||
editArticle.titleTranslate !== article.titleTranslate ||
|
||||
editArticle.text !== article.text ||
|
||||
editArticle.textCustomTranslate !== article.textCustomTranslate ||
|
||||
editArticle.textNetworkTranslate !== article.textNetworkTranslate ||
|
||||
editArticle.useTranslateType !== article.useTranslateType
|
||||
) {
|
||||
return MessageBox.confirm(
|
||||
'检测到数据有变动,是否保存?',
|
||||
'提示',
|
||||
async () => {
|
||||
let r = await editArticleRef.save('save')
|
||||
if (r) resolve(true)
|
||||
},
|
||||
() => resolve(true),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (editArticle.title.trim() && editArticle.text.trim()) {
|
||||
return MessageBox.confirm(
|
||||
'检测到数据有变动,是否保存?',
|
||||
'提示',
|
||||
async () => {
|
||||
let r = await editArticleRef.save('save')
|
||||
if (r) resolve(true)
|
||||
},
|
||||
() => resolve(true),
|
||||
)
|
||||
}
|
||||
}
|
||||
resolve(true)
|
||||
})
|
||||
}
|
||||
|
||||
async function add() {
|
||||
let r = await checkDataChange()
|
||||
if (r) {
|
||||
article = cloneDeep(DefaultArticle)
|
||||
}
|
||||
}
|
||||
|
||||
function saveArticle(val: Article): boolean {
|
||||
console.log('saveArticle', val)
|
||||
if (val.id) {
|
||||
let rIndex = runtimeStore.editDict.articles.findIndex(v => v.id === val.id)
|
||||
if (rIndex > -1) {
|
||||
runtimeStore.editDict.articles[rIndex] = cloneDeep(val)
|
||||
}
|
||||
} else {
|
||||
let has = runtimeStore.editDict.articles.find((item: Article) => item.title === val.title)
|
||||
if (has) {
|
||||
ElMessage.error('已存在同名文章!')
|
||||
return false
|
||||
}
|
||||
val.id = nanoid(6)
|
||||
runtimeStore.editDict.articles.push(val)
|
||||
setTimeout(() => {
|
||||
listEl.scrollBottom()
|
||||
})
|
||||
}
|
||||
article = cloneDeep(val)
|
||||
//TODO 保存完成后滚动到对应位置
|
||||
ElMessage.success('保存成功!')
|
||||
syncMyDictList(runtimeStore.editDict)
|
||||
return true
|
||||
}
|
||||
|
||||
function saveAndNext(val: Article) {
|
||||
if (saveArticle(val)) {
|
||||
add()
|
||||
}
|
||||
}
|
||||
|
||||
let showExport = $ref(false)
|
||||
useWindowClick(() => showExport = false)
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
v-model="show"
|
||||
:full-screen="true"
|
||||
:header="false"
|
||||
>
|
||||
<div class="add-article">
|
||||
<div class="slide">
|
||||
<header>
|
||||
<div class="dict-name">{{ runtimeStore.editDict.name }}</div>
|
||||
</header>
|
||||
<List
|
||||
ref="listEl"
|
||||
v-model:list="runtimeStore.editDict.articles"
|
||||
:select-item="article"
|
||||
@del-select-item="article = cloneDeep(DefaultArticle)"
|
||||
@select-item="selectArticle"
|
||||
>
|
||||
<template v-slot="{item,index}">
|
||||
<div class="name"> {{ `${index + 1}. ${item.title}` }}</div>
|
||||
<div class="translate-name"> {{ ` ${item.titleTranslate}` }}</div>
|
||||
</template>
|
||||
</List>
|
||||
<div class="add" v-if="!article.title">
|
||||
正在添加新文章...
|
||||
</div>
|
||||
<div class="footer">
|
||||
<div class="import">
|
||||
<BaseButton size="small">导入</BaseButton>
|
||||
<input type="file"
|
||||
accept=".csv, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
|
||||
@change="e => emit('importData',e)">
|
||||
</div>
|
||||
<div class="export"
|
||||
style="position: relative"
|
||||
@click.stop="null">
|
||||
<BaseButton size="small" @click="showExport = true">导出</BaseButton>
|
||||
<MiniDialog
|
||||
v-model="showExport"
|
||||
style="width: 80rem;bottom: calc(100% + 10rem);top:unset;"
|
||||
>
|
||||
<div class="mini-row-title">
|
||||
导出选项
|
||||
</div>
|
||||
<div class="mini-row">
|
||||
<BaseButton size="small" @click="emit('exportData',{type:'all',data:[]})">全部文章</BaseButton>
|
||||
</div>
|
||||
<div class="mini-row">
|
||||
<BaseButton size="small" @click="emit('exportData',{type:'chapter',data:article})">当前章节</BaseButton>
|
||||
</div>
|
||||
</MiniDialog>
|
||||
</div>
|
||||
<BaseButton size="small" @click="add">新增</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
<EditArticle
|
||||
ref="editArticleRef"
|
||||
type="batch"
|
||||
@save="saveArticle"
|
||||
@saveAndNext="saveAndNext"
|
||||
:article="article"/>
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/assets/css/style.scss";
|
||||
|
||||
.add-article {
|
||||
//position: fixed;
|
||||
position: relative;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 9;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
color: var(--color-font-1);
|
||||
background: var(--color-second-bg);
|
||||
display: flex;
|
||||
|
||||
.close {
|
||||
position: absolute;
|
||||
right: 20rem;
|
||||
top: 20rem;
|
||||
}
|
||||
|
||||
.slide {
|
||||
width: 14vw;
|
||||
height: 100%;
|
||||
padding: 0 10rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
$height: 60rem;
|
||||
|
||||
header {
|
||||
height: $height;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
//opacity: 0;
|
||||
|
||||
.dict-name {
|
||||
font-size: 30rem;
|
||||
color: var(--color-font-1);
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 18rem;
|
||||
}
|
||||
|
||||
.translate-name {
|
||||
font-size: 16rem;
|
||||
}
|
||||
|
||||
.add {
|
||||
width: 260rem;
|
||||
box-sizing: border-box;
|
||||
border-radius: 8rem;
|
||||
margin-bottom: 10rem;
|
||||
padding: 10rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
transition: all .3s;
|
||||
color: var(--color-font-1);
|
||||
background: var(--color-item-active);
|
||||
}
|
||||
|
||||
.footer {
|
||||
height: $height;
|
||||
display: flex;
|
||||
gap: 10rem;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
.import {
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
|
||||
input {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,52 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {Article, DefaultArticle} from "@/types.ts";
|
||||
import {cloneDeep} from "lodash-es";
|
||||
import Dialog from "@/components/dialog/Dialog.vue";
|
||||
import EditArticle from "@/components/article/EditArticle.vue";
|
||||
import {useDisableEventListener} from "@/hooks/event.ts";
|
||||
|
||||
interface IProps {
|
||||
article?: Article
|
||||
modelValue: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<IProps>(), {
|
||||
article: () => cloneDeep(DefaultArticle),
|
||||
modelValue: false
|
||||
})
|
||||
const emit = defineEmits<{
|
||||
save: [val: Article]
|
||||
'update:modelValue': [val: boolean]
|
||||
}>()
|
||||
|
||||
useDisableEventListener(() => props.modelValue)
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
:header="false"
|
||||
:model-value="props.modelValue"
|
||||
@close="emit('update:modelValue',false)"
|
||||
:full-screen="true"
|
||||
>
|
||||
<div class="wrapper">
|
||||
<EditArticle
|
||||
:article="article"
|
||||
@save="val => emit('save',val)"
|
||||
/>
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/assets/css/style.scss";
|
||||
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
background: var(--color-main-bg);
|
||||
}
|
||||
</style>
|
||||
@@ -1,97 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import Dialog from "@/components/dialog/Dialog.vue";
|
||||
import {$ref} from "vue/macros";
|
||||
import {onMounted, onUnmounted, watch} from "vue";
|
||||
import {emitter, EventKey} from "@/utils/eventBus.ts";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import WordList from "@/components/list/WordList.vue";
|
||||
import {Article, DefaultArticle} from "@/types.ts";
|
||||
import {cloneDeep} from "lodash-es";
|
||||
import Empty from "@/components/Empty.vue";
|
||||
import {getTranslateText} from "@/hooks/article.ts";
|
||||
|
||||
let show = $ref(false)
|
||||
let loading = $ref(false)
|
||||
const runtimeStore = useRuntimeStore()
|
||||
let article: Article = $ref(cloneDeep(DefaultArticle))
|
||||
|
||||
onMounted(() => {
|
||||
emitter.on(EventKey.openArticleContentModal, (val: any) => {
|
||||
show = true
|
||||
article = cloneDeep(val)
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
emitter.off(EventKey.openArticleContentModal)
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
:header="false"
|
||||
v-model="show">
|
||||
<div class="content">
|
||||
<div class="article-content">
|
||||
<div class="title">
|
||||
<div>{{ article.title }}</div>
|
||||
</div>
|
||||
<div class="text" v-if="article.text">
|
||||
<div class="sentence" v-for="t in article.text.split('\n')">{{ t }}</div>
|
||||
</div>
|
||||
<Empty v-else/>
|
||||
</div>
|
||||
<div class="article-content">
|
||||
<div class="title">
|
||||
<div>{{ article.titleTranslate }}</div>
|
||||
</div>
|
||||
<div class="text" v-if="getTranslateText(article).length">
|
||||
<div class="sentence" v-for="t in getTranslateText(article)">{{ t }}</div>
|
||||
</div>
|
||||
<Empty v-else/>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/assets/css/style";
|
||||
|
||||
.content {
|
||||
width: 70vw;
|
||||
height: 75vh;
|
||||
display: flex;
|
||||
gap: var(--space);
|
||||
padding: var(--space);
|
||||
color: var(--color-font-1);
|
||||
|
||||
.article-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
font-size: 20rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.title {
|
||||
text-align: center;
|
||||
margin-bottom: var(--space);
|
||||
font-size: 24rem;
|
||||
}
|
||||
|
||||
.text {
|
||||
text-indent: 1.5em;
|
||||
line-height: 35rem;
|
||||
overflow: auto;
|
||||
padding-right: 10rem;
|
||||
padding-bottom: 50rem;
|
||||
|
||||
.sentence {
|
||||
margin-bottom: 30rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
@@ -1,372 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import {onMounted, onUnmounted, watch} from "vue";
|
||||
import Tooltip from "@/components/Tooltip.vue";
|
||||
import {Icon} from '@iconify/vue';
|
||||
import {useEventListener} from "@/hooks/event.ts";
|
||||
import {$ref} from "vue/macros";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
|
||||
export interface ModalProps {
|
||||
modelValue?: boolean,
|
||||
showClose?: boolean,
|
||||
title?: string,
|
||||
content?: string,
|
||||
fullScreen?: boolean;
|
||||
padding?: boolean
|
||||
footer?: boolean
|
||||
header?: boolean
|
||||
confirmButtonText?: string
|
||||
cancelButtonText?: string,
|
||||
keyboard?: boolean,
|
||||
confirm?: any
|
||||
beforeClose?: any
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<ModalProps>(), {
|
||||
modelValue: undefined,
|
||||
showClose: true,
|
||||
fullScreen: false,
|
||||
footer: false,
|
||||
header: true,
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消',
|
||||
keyboard: true
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'update:modelValue',
|
||||
'close',
|
||||
'ok',
|
||||
'cancel',
|
||||
])
|
||||
|
||||
let confirmButtonLoading = $ref(false)
|
||||
let zIndex = $ref(999)
|
||||
let visible = $ref(false)
|
||||
let openTime = $ref(Date.now())
|
||||
let maskRef = $ref<HTMLDivElement>(null)
|
||||
let modalRef = $ref<HTMLDivElement>(null)
|
||||
const runtimeStore = useRuntimeStore()
|
||||
let id = Date.now()
|
||||
|
||||
async function close() {
|
||||
if (!visible) {
|
||||
return
|
||||
}
|
||||
if (props.beforeClose) {
|
||||
if (!await props.beforeClose()) {
|
||||
return
|
||||
}
|
||||
}
|
||||
//记录停留时间,避免时间太短,弹框闪烁
|
||||
let stayTime = Date.now() - openTime;
|
||||
let closeTime = 300;
|
||||
if (stayTime < 500) {
|
||||
closeTime += 500 - stayTime;
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
maskRef?.classList.toggle('bounce-out');
|
||||
modalRef?.classList.toggle('bounce-out');
|
||||
}, 500 - stayTime);
|
||||
|
||||
setTimeout(() => {
|
||||
emit('update:modelValue', false)
|
||||
emit('close')
|
||||
visible = false
|
||||
resolve(true)
|
||||
let rIndex = runtimeStore.modalList.findIndex(item => item.id === id)
|
||||
if (rIndex > -1) {
|
||||
runtimeStore.modalList.splice(rIndex, 1)
|
||||
}
|
||||
}, closeTime)
|
||||
});
|
||||
}
|
||||
|
||||
watch(() => props.modelValue, n => {
|
||||
// console.log('n', n)
|
||||
if (n) {
|
||||
id = Date.now()
|
||||
runtimeStore.modalList.push({id, close})
|
||||
zIndex = 999 + runtimeStore.modalList.length
|
||||
visible = true
|
||||
} else {
|
||||
close()
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// console.log('props.modelValue', props.modelValue)
|
||||
if (props.modelValue === undefined) {
|
||||
visible = true
|
||||
id = Date.now()
|
||||
runtimeStore.modalList.push({id, close})
|
||||
zIndex = 999 + runtimeStore.modalList.length
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (props.modelValue === undefined) {
|
||||
visible = false
|
||||
let rIndex = runtimeStore.modalList.findIndex(item => item.id === id)
|
||||
if (rIndex > -1) {
|
||||
runtimeStore.modalList.splice(rIndex, 1)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
useEventListener('keyup', async (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape' && props.keyboard) {
|
||||
let lastItem = runtimeStore.modalList[runtimeStore.modalList.length - 1]
|
||||
if (lastItem?.id === id) {
|
||||
await cancel()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
async function ok() {
|
||||
if (props.confirm) {
|
||||
confirmButtonLoading = true
|
||||
await props.confirm()
|
||||
confirmButtonLoading = false
|
||||
}
|
||||
emit('ok')
|
||||
await close()
|
||||
}
|
||||
|
||||
async function cancel() {
|
||||
emit('cancel')
|
||||
await close()
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div class="modal-root" :style="{'z-index': zIndex}" v-if="visible">
|
||||
<div class="modal-mask"
|
||||
ref="maskRef"
|
||||
v-if="!fullScreen"
|
||||
@click.stop="close"></div>
|
||||
<div class="modal"
|
||||
ref="modalRef"
|
||||
:class="[
|
||||
fullScreen?'full':'window'
|
||||
]"
|
||||
>
|
||||
<Tooltip title="关闭">
|
||||
<Icon @click="close"
|
||||
v-if="showClose"
|
||||
class="close hvr-grow pointer"
|
||||
width="24" color="#929596"
|
||||
icon="ion:close-outline"/>
|
||||
</Tooltip>
|
||||
<div class="modal-header" v-if="header">
|
||||
<div class="title">{{ props.title }}</div>
|
||||
</div>
|
||||
<div class="modal-body" :class="{padding}">
|
||||
<slot></slot>
|
||||
<div v-if="content" class="content">{{ content }}</div>
|
||||
</div>
|
||||
<div class="modal-footer" v-if="footer">
|
||||
<div class="left">
|
||||
</div>
|
||||
<div class="right">
|
||||
<BaseButton type="link" @click="cancel">{{ cancelButtonText }}</BaseButton>
|
||||
<BaseButton
|
||||
:loading="confirmButtonLoading"
|
||||
@click="ok">{{ confirmButtonText }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/assets/css/variable.scss";
|
||||
|
||||
$modal-mask-bg: rgba(#000, .45);
|
||||
$radius: 24rem;
|
||||
$time: 0.3s;
|
||||
$header-height: 60rem;
|
||||
|
||||
@keyframes bounce-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.15);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bounce-in-full {
|
||||
0% {
|
||||
transform: scale(1.5);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-root {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
|
||||
.modal-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: $modal-mask-bg;
|
||||
transition: background 0.3s;
|
||||
animation: fade-in $time;
|
||||
|
||||
&.bounce-out {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.window {
|
||||
//width: 75vw;
|
||||
//height: 70vh;
|
||||
box-shadow: var(--shadow);
|
||||
border-radius: $radius;
|
||||
animation: bounce-in $time ease-out;
|
||||
|
||||
&.bounce-out {
|
||||
transform: scale(0);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.full {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
animation: bounce-in-full $time ease-out;
|
||||
|
||||
&.bounce-out {
|
||||
transform: scale(1.5);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: relative;
|
||||
background: var(--color-second-bg);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: transform $time, opacity $time;
|
||||
|
||||
.close {
|
||||
position: absolute;
|
||||
right: 20rem;
|
||||
top: 20rem;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 24rem 24rem 16rem;
|
||||
border-radius: $radius $radius 0 0;
|
||||
|
||||
.title {
|
||||
color: var(--color-font-1);
|
||||
font-weight: bold;
|
||||
font-size: 24rem;
|
||||
line-height: 33rem;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
box-sizing: border-box;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-weight: 400;
|
||||
font-size: 18rem;
|
||||
line-height: 27rem;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
|
||||
&.padding {
|
||||
padding: 4rem 24rem 24rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 350rem;
|
||||
color: var(--color-font-1);
|
||||
padding: 4rem 24rem 24rem;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16rem 24rem;
|
||||
color: #fff;
|
||||
font-size: 18rem;
|
||||
background: rgba(0, 0, 0, .2);
|
||||
border-radius: 0 0 24rem 24rem;
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
|
||||
.text {
|
||||
color: white;
|
||||
font-size: 16rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.active {
|
||||
.text {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
height: 100%;
|
||||
gap: var(--space);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,575 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import {useBaseStore} from "@/stores/base.ts"
|
||||
import {onMounted} from "vue"
|
||||
import {DefaultDict, Dict, DictResource, DictType, Sort, Word} from "@/types.ts"
|
||||
import {chunk, cloneDeep, reverse, shuffle} from "lodash-es";
|
||||
import {$computed, $ref} from "vue/macros";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import {Icon} from '@iconify/vue';
|
||||
import "vue-activity-calendar/style.css";
|
||||
import WordListDialog from "@/components/dialog/WordListDialog.vue";
|
||||
import {isArticle} from "@/hooks/article.ts";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {emitter, EventKey} from "@/utils/eventBus.ts";
|
||||
import Slide from "@/components/Slide.vue";
|
||||
import Empty from "@/components/Empty.vue";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import Dialog from "@/components/dialog/Dialog.vue";
|
||||
import EditBatchArticleModal from "@/components/article/EditBatchArticleModal.vue";
|
||||
import {nanoid} from "nanoid";
|
||||
import DictListPanel from "@/components/DictListPanel.vue";
|
||||
import {useRouter} from "vue-router";
|
||||
import ArticleList from "@/components/list/ArticleList.vue";
|
||||
import BaseList from "@/components/list/BaseList.vue";
|
||||
import {MessageBox} from "@/utils/MessageBox.tsx";
|
||||
import {ArchiveReader, libarchiveWasm} from 'libarchive-wasm';
|
||||
import {getDictFile} from "@/utils";
|
||||
|
||||
const store = useBaseStore()
|
||||
const settingStore = useSettingStore()
|
||||
const runtimeStore = useRuntimeStore()
|
||||
let router = useRouter()
|
||||
|
||||
let step = $ref(1)
|
||||
let loading = $ref(false)
|
||||
let show = $ref(false)
|
||||
let chapterList2 = $ref([])
|
||||
let chapterWordNumber = $ref(0)
|
||||
let toggleLoading = $ref(false)
|
||||
|
||||
const activeId = $computed(() => {
|
||||
if (dictIsArticle) {
|
||||
return runtimeStore.editDict.articles?.[runtimeStore.editDict.chapterIndex].id ?? ''
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
async function selectDict(val: { dict: DictResource | Dict, index: number }) {
|
||||
let item = val.dict
|
||||
// console.log('item', item)
|
||||
step = 1
|
||||
loading = true
|
||||
let find: Dict = store.myDictList.find((v: Dict) => v.id === item.id)
|
||||
if (find) {
|
||||
runtimeStore.editDict = cloneDeep(find)
|
||||
} else {
|
||||
runtimeStore.editDict = cloneDeep({
|
||||
...cloneDeep(DefaultDict),
|
||||
...item,
|
||||
})
|
||||
runtimeStore.editDict.id = nanoid(6)
|
||||
//设置默认章节单词数
|
||||
runtimeStore.editDict.chapterWordNumber = settingStore.chapterWordNumber
|
||||
}
|
||||
|
||||
if ([DictType.collect, DictType.simple, DictType.wrong].includes(runtimeStore.editDict.type)) {
|
||||
} else {
|
||||
//如果不是自定义词典,并且有url地址才去下载
|
||||
if (!runtimeStore.editDict.isCustom && runtimeStore.editDict.url) {
|
||||
let url = `./dicts/${runtimeStore.editDict.language}/${runtimeStore.editDict.type}/${runtimeStore.editDict.translateLanguage}/${runtimeStore.editDict.url}`;
|
||||
if (runtimeStore.editDict.type === DictType.word) {
|
||||
if (!runtimeStore.editDict.originWords.length) {
|
||||
let v = await getDictFile(url)
|
||||
v.map(s => {
|
||||
s.id = nanoid(6)
|
||||
})
|
||||
runtimeStore.editDict.originWords = cloneDeep(v)
|
||||
changeSort(runtimeStore.editDict.sort, false)
|
||||
} else {
|
||||
runtimeStore.editDict.length = runtimeStore.editDict.words.length
|
||||
}
|
||||
}
|
||||
if (runtimeStore.editDict.type === DictType.article) {
|
||||
if (!runtimeStore.editDict.articles.length) {
|
||||
let v = await getDictFile(url)
|
||||
v.map(s => {
|
||||
s.id = nanoid(6)
|
||||
})
|
||||
runtimeStore.editDict.articles = cloneDeep(v)
|
||||
} else {
|
||||
runtimeStore.editDict.length = runtimeStore.editDict.articles.length
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
chapterWordNumber = runtimeStore.editDict.chapterWordNumber
|
||||
chapterList2 = runtimeStore.editDict.chapterWords.map((v, i) => ({id: i}))
|
||||
loading = false
|
||||
}
|
||||
|
||||
|
||||
function close() {
|
||||
show = false
|
||||
}
|
||||
|
||||
//TODO 切大词典太卡了
|
||||
function changeDict() {
|
||||
close()
|
||||
store.changeDict(runtimeStore.editDict)
|
||||
setTimeout(() => {
|
||||
runtimeStore.editDict = cloneDeep(DefaultDict)
|
||||
})
|
||||
ElMessage.success('切换成功')
|
||||
}
|
||||
|
||||
const dictIsArticle = $computed(() => {
|
||||
return isArticle(runtimeStore.editDict.type)
|
||||
})
|
||||
|
||||
function showAllWordModal() {
|
||||
emitter.emit(EventKey.openWordListModal, {
|
||||
title: runtimeStore.editDict.name,
|
||||
translateLanguage: runtimeStore.editDict.translateLanguage,
|
||||
list: runtimeStore.editDict.words
|
||||
})
|
||||
}
|
||||
|
||||
function resetChapterList(v: number) {
|
||||
const temp = () => {
|
||||
runtimeStore.editDict.chapterWordNumber = v
|
||||
runtimeStore.editDict.chapterWords = chunk(runtimeStore.editDict.words, runtimeStore.editDict.chapterWordNumber)
|
||||
chapterList2 = runtimeStore.editDict.chapterWords.map((v, i) => ({id: i}))
|
||||
}
|
||||
if (runtimeStore.editDict.isCustom) {
|
||||
MessageBox.confirm(
|
||||
'检测到您已对这本词典自定义修改,修改“每章单词数”将会导致所有章节被重新分配,原有章节内容将被清除且不可恢复,是否继续?',
|
||||
'提示',
|
||||
() => temp(),
|
||||
() => {
|
||||
chapterWordNumber = runtimeStore.editDict.chapterWordNumber
|
||||
}
|
||||
)
|
||||
} else {
|
||||
temp()
|
||||
}
|
||||
}
|
||||
|
||||
function changeSort(v: Sort, notice: boolean = true) {
|
||||
const temp = () => {
|
||||
runtimeStore.editDict.sort = v
|
||||
if (v === Sort.normal) {
|
||||
runtimeStore.editDict.words = cloneDeep(runtimeStore.editDict.originWords)
|
||||
} else if (v === Sort.random) {
|
||||
runtimeStore.editDict.words = shuffle(cloneDeep(runtimeStore.editDict.originWords))
|
||||
} else {
|
||||
runtimeStore.editDict.words = reverse(cloneDeep(runtimeStore.editDict.originWords))
|
||||
}
|
||||
resetChapterList(runtimeStore.editDict.chapterWordNumber)
|
||||
notice && ElMessage.success('已重新排序')
|
||||
}
|
||||
if (runtimeStore.editDict.isCustom) {
|
||||
MessageBox.confirm(
|
||||
'检测到您已对这本词典自定义修改,修改“单词排序”将会导致所有章节被重新分配,原有章节内容将被清除且不可恢复,是否继续?',
|
||||
'提示',
|
||||
() => temp()
|
||||
)
|
||||
} else {
|
||||
temp()
|
||||
}
|
||||
}
|
||||
|
||||
function option(type: string) {
|
||||
show = false
|
||||
setTimeout(() => {
|
||||
router.push({path: '/pc/dict', query: {type: type}})
|
||||
}, 500)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
emitter.on(EventKey.openDictModal, (type: 'detail' | 'list' | 'my') => {
|
||||
if (type === "detail") {
|
||||
selectDict({dict: store.currentDict, index: 0})
|
||||
}
|
||||
if (type === "list") {
|
||||
// currentLanguage = 'en'
|
||||
step = 0
|
||||
}
|
||||
if (type === "my") {
|
||||
// currentLanguage = 'my'
|
||||
step = 0
|
||||
}
|
||||
show = true
|
||||
})
|
||||
})
|
||||
|
||||
function showWordListModal(val: { item: Word, index: number }) {
|
||||
emitter.emit(EventKey.openWordListModal, {
|
||||
title: `第${val.index + 1}章`,
|
||||
translateLanguage: runtimeStore.editDict.translateLanguage,
|
||||
list: runtimeStore.editDict.chapterWords[val.index]
|
||||
})
|
||||
}
|
||||
|
||||
function handleChangeArticleChapterIndex(val: any) {
|
||||
let rIndex = runtimeStore.editDict.articles.findIndex(v => v.id === val.item.id)
|
||||
if (rIndex > -1) {
|
||||
runtimeStore.editDict.chapterIndex = rIndex
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
:header="false"
|
||||
v-model="show"
|
||||
:show-close="false">
|
||||
<div id="DictDialog">
|
||||
<Slide :slide-count="2" :step="step">
|
||||
<DictListPanel
|
||||
@add="option('addDict')"
|
||||
@select-dict="selectDict"
|
||||
/>
|
||||
<div class="dict-detail-page">
|
||||
<header>
|
||||
<div class="left" @click.stop="step = 0">
|
||||
<Icon icon="octicon:arrow-left-24" class="go" width="20"/>
|
||||
<div class="title">
|
||||
词典详情
|
||||
</div>
|
||||
</div>
|
||||
<Icon @click="close"
|
||||
class="hvr-grow pointer"
|
||||
width="20" color="#929596"
|
||||
icon="ion:close-outline"/>
|
||||
</header>
|
||||
<div class="detail">
|
||||
<div class="page-content">
|
||||
<div class="left-column">
|
||||
<BaseIcon
|
||||
v-if="![DictType.collect,DictType.wrong,DictType.simple].includes(runtimeStore.editDict.type)"
|
||||
class="edit-icon"
|
||||
title="编辑词典"
|
||||
icon="tabler:edit"
|
||||
@click='option("editDict")'
|
||||
/>
|
||||
<div class="name">{{ runtimeStore.editDict.name }}</div>
|
||||
<div class="desc">{{ runtimeStore.editDict.description }}</div>
|
||||
<div class="text flex align-center gap10">
|
||||
<div v-if="dictIsArticle">总文章:{{ runtimeStore.editDict.articles.length }}篇
|
||||
</div>
|
||||
<div v-else>总词汇:
|
||||
<span class="count" @click="showAllWordModal">{{
|
||||
runtimeStore.editDict.originWords.length
|
||||
}}词</span>
|
||||
</div>
|
||||
<BaseIcon icon="mi:add"
|
||||
@click='option("addWordOrArticle")'
|
||||
:title="`添加${dictIsArticle?'文章':'单词'}`"
|
||||
/>
|
||||
</div>
|
||||
<template v-if="false">
|
||||
<div class="text">开始日期:-</div>
|
||||
<div class="text">花费时间:-</div>
|
||||
<div class="text">累积错误:-</div>
|
||||
<div class="text">进度:
|
||||
<el-progress :percentage="0"
|
||||
:stroke-width="8"
|
||||
:show-text="false"/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="center-column">
|
||||
<div class="common-title">学习设置</div>
|
||||
<div class="setting">
|
||||
<template v-if="!dictIsArticle">
|
||||
<div class="row">
|
||||
<div class="label">每章单词数</div>
|
||||
<el-slider
|
||||
class="my-slider"
|
||||
:min="10"
|
||||
:step="10"
|
||||
:max="runtimeStore.editDict.words.length < 10 ? 10 : runtimeStore.editDict.words.length"
|
||||
show-input
|
||||
:show-input-controls="false"
|
||||
size="small"
|
||||
v-model="chapterWordNumber"
|
||||
@change="resetChapterList"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="notice">
|
||||
<span class="text">最小:10</span>
|
||||
<span class="text">最大:{{ runtimeStore.editDict.words.length }}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="label">单词顺序</div>
|
||||
<div class="option">
|
||||
<el-radio-group :model-value="runtimeStore.editDict.sort"
|
||||
@change="changeSort"
|
||||
>
|
||||
<el-radio :label="Sort.normal" size="large">默认</el-radio>
|
||||
<el-radio :label="Sort.random" size="large">随机</el-radio>
|
||||
<el-radio :label="Sort.reverse" size="large">反转</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="row">
|
||||
<div class="label">学习模式</div>
|
||||
<div class="option">
|
||||
<el-radio-group v-model="settingStore.dictation">
|
||||
<el-radio :label="false" size="large">再认</el-radio>
|
||||
<el-radio :label="true" size="large">拼写</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="label">{{ dictIsArticle ? '句子' : '单词' }}发音</div>
|
||||
<div class="option">
|
||||
<el-radio-group v-model="settingStore.wordSoundType">
|
||||
<el-radio label="us" size="large">美音</el-radio>
|
||||
<el-radio label="uk" size="large">英音</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="label">{{ dictIsArticle ? '句子' : '单词' }}自动发音</div>
|
||||
<div class="option">
|
||||
<el-switch v-model="settingStore.wordSound"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="label">是否显示翻译</div>
|
||||
<div class="option">
|
||||
<el-switch v-model="settingStore.translate"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="label">忽略大小写</div>
|
||||
<div class="option">
|
||||
<el-switch v-model="settingStore.ignoreCase"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right-column">
|
||||
<div class="common-title">
|
||||
<span>{{ dictIsArticle ? '文章' : '章节' }}列表</span>
|
||||
<BaseIcon
|
||||
icon="fluent:notepad-edit-20-regular"
|
||||
@click='option("detail")'
|
||||
style="position: absolute;right: 20rem;"
|
||||
:title="`管理${dictIsArticle?'文章':'章节'}`"
|
||||
/>
|
||||
</div>
|
||||
<div class="list-content">
|
||||
<template v-if="dictIsArticle">
|
||||
<ArticleList
|
||||
v-if="runtimeStore.editDict.articles.length"
|
||||
:isActive="false"
|
||||
v-loading="loading"
|
||||
:show-border="true"
|
||||
@title="(val:any) => emitter.emit(EventKey.openArticleContentModal,val.item)"
|
||||
@click="handleChangeArticleChapterIndex"
|
||||
:active-id="activeId"
|
||||
:list="runtimeStore.editDict.articles">
|
||||
<template v-slot:prefix="{item,index}">
|
||||
<input type="radio" :checked="activeId === item.id">
|
||||
</template>
|
||||
</ArticleList>
|
||||
<Empty v-else/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<BaseList
|
||||
ref="chapterListRef"
|
||||
v-if="chapterList2.length"
|
||||
:list="chapterList2"
|
||||
:show-border="true"
|
||||
@click="(val:any) => runtimeStore.editDict.chapterIndex = val.index"
|
||||
:active-index="runtimeStore.editDict.chapterIndex"
|
||||
>
|
||||
<template v-slot:prefix="{ item, index }">
|
||||
<input type="radio" :checked="runtimeStore.editDict.chapterIndex === item.id">
|
||||
</template>
|
||||
<template v-slot="{ item, index }">
|
||||
<div class="item-title" @click.stop="showWordListModal({item,index})">
|
||||
<span>第{{ item.id + 1 }}章</span>
|
||||
<span>{{ runtimeStore.editDict.chapterWords[item.id]?.length }}词</span>
|
||||
</div>
|
||||
</template>
|
||||
</BaseList>
|
||||
<Empty v-else/>
|
||||
</template>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<!-- <BaseButton @click="step = 0">导出</BaseButton>-->
|
||||
<BaseButton @click="close">关闭</BaseButton>
|
||||
<BaseButton :loading="toggleLoading" @click="changeDict">切换</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Slide>
|
||||
</div>
|
||||
</Dialog>
|
||||
<WordListDialog/>
|
||||
<EditBatchArticleModal/>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/assets/css/variable";
|
||||
|
||||
$header-height: 60rem;
|
||||
|
||||
#DictDialog {
|
||||
//position: fixed;
|
||||
//left: 50%;
|
||||
//top: 50%;
|
||||
//transform: translate(-50%, -50%);
|
||||
background: var(--color-second-bg);
|
||||
z-index: 99999;
|
||||
width: 1030rem;
|
||||
height: 75vh;
|
||||
}
|
||||
|
||||
.dict-detail-page {
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
header {
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
height: $header-height;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
color: var(--color-font-3);
|
||||
padding: 0 var(--space);
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
gap: 10rem;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.detail {
|
||||
padding-left: var(--space);
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
.page-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
.column {
|
||||
background: var(--color-second-bg);
|
||||
color: var(--color-font-1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.left-column {
|
||||
flex: 5;
|
||||
gap: 10rem;
|
||||
position: relative;
|
||||
font-size: 14rem;
|
||||
padding-right: var(--space);
|
||||
@extend .column;
|
||||
|
||||
|
||||
.name {
|
||||
font-size: 24rem;
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 16rem;
|
||||
margin-bottom: 20rem;
|
||||
}
|
||||
|
||||
.count {
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid var(--color-item-active);
|
||||
}
|
||||
|
||||
:deep(.edit-icon) {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.center-column {
|
||||
overflow: auto;
|
||||
flex: 7;
|
||||
@extend .column;
|
||||
|
||||
.setting {
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 34rem;
|
||||
word-break: keep-all;
|
||||
gap: 10rem;
|
||||
|
||||
.el-radio {
|
||||
margin-right: 10rem;
|
||||
}
|
||||
}
|
||||
|
||||
.my-slider {
|
||||
:deep(.el-slider__input) {
|
||||
width: 55px;
|
||||
}
|
||||
}
|
||||
|
||||
.notice {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
transform: translate3d(0, -5rem, 0);
|
||||
padding-left: 100rem;
|
||||
font-size: 13rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right-column {
|
||||
flex: 7;
|
||||
@extend .column;
|
||||
|
||||
.list-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
box-sizing: content-box;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: flex-end;
|
||||
gap: var(--space);
|
||||
padding-right: var(--space);
|
||||
margin: var(--space) 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
interface IProps {
|
||||
modelValue?: boolean,
|
||||
width?: string
|
||||
}
|
||||
|
||||
withDefaults(defineProps<IProps>(), {
|
||||
modelValue: true,
|
||||
width: '180rem'
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Transition name="fade">
|
||||
<div v-if="modelValue" class="mini-modal" :style="{width}">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import "@/assets/css/style.scss";
|
||||
|
||||
.mini-row-title {
|
||||
min-height: 35rem;
|
||||
text-align: center;
|
||||
font-size: 16rem;
|
||||
font-weight: bold;
|
||||
color: var(--color-font-1);
|
||||
}
|
||||
|
||||
.mini-row {
|
||||
min-height: 35rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: var(--space);
|
||||
color: var(--color-font-1);
|
||||
word-break: keep-all;
|
||||
}
|
||||
|
||||
.mini-modal {
|
||||
position: absolute;
|
||||
z-index: 9;
|
||||
width: 180rem;
|
||||
background: var(--color-second-bg);
|
||||
border-radius: 8rem;
|
||||
box-shadow: 0 0 8px 2px var(--color-item-border);
|
||||
padding: 10rem var(--space);
|
||||
top: 40rem;
|
||||
left: 50%;
|
||||
transform: translate3d(-50%, 0, 0);
|
||||
//margin-top: 10rem;
|
||||
}
|
||||
</style>
|
||||
@@ -1,165 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import Dialog from "@/components/dialog/Dialog.vue"
|
||||
import {Icon} from '@iconify/vue';
|
||||
import {ref} 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 {$computed, $ref} from "vue/macros";
|
||||
import {cloneDeep} from "lodash-es";
|
||||
import {DefaultShortcutKeyMap, ShortcutKey} from "@/types.ts";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import {SoundFileOptions} from "@/utils/const.ts";
|
||||
import VolumeIcon from "@/components/icon/VolumeIcon.vue";
|
||||
import Setting from "@/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">
|
||||
@import "@/assets/css/variable";
|
||||
|
||||
.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: 10rem 20rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
//align-items: center;
|
||||
//justify-content: center;
|
||||
gap: 10rem;
|
||||
|
||||
.tab {
|
||||
cursor: pointer;
|
||||
padding: 10rem 15rem;
|
||||
border-radius: 8rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rem;
|
||||
|
||||
&.active {
|
||||
background: var(--color-item-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.git-log {
|
||||
font-size: 10rem;
|
||||
color: gray;
|
||||
margin-bottom: 5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
background: var(--color-header-bg);
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding: 0 var(--space);
|
||||
|
||||
.row {
|
||||
height: 40rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: calc(var(--space) * 5);
|
||||
|
||||
.wrapper {
|
||||
height: 30rem;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--space);
|
||||
|
||||
span {
|
||||
text-align: right;
|
||||
//width: 30rem;
|
||||
font-size: 12rem;
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.set-key {
|
||||
align-items: center;
|
||||
|
||||
input {
|
||||
width: 150rem;
|
||||
box-sizing: border-box;
|
||||
margin-right: 10rem;
|
||||
height: 28rem;
|
||||
outline: none;
|
||||
font-size: 16rem;
|
||||
border: 1px solid gray;
|
||||
border-radius: 3rem;
|
||||
padding: 0 5rem;
|
||||
background: var(--color-second-bg);
|
||||
color: var(--color-font-1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
.main-title {
|
||||
font-size: 18rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.item-title {
|
||||
font-size: 16rem;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
font-size: 14rem;
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.scroll {
|
||||
flex: 1;
|
||||
padding-right: 10rem;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-bottom: 20rem;
|
||||
}
|
||||
|
||||
.desc {
|
||||
margin-bottom: 10rem;
|
||||
font-size: 12rem;
|
||||
}
|
||||
|
||||
.line {
|
||||
border-bottom: 1px solid #c4c3c3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,60 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import Dialog from "@/components/dialog/Dialog.vue";
|
||||
import {$ref} from "vue/macros";
|
||||
import {onMounted, onUnmounted, watch} from "vue";
|
||||
import {emitter, EventKey} from "@/utils/eventBus.ts";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import WordList from "@/components/list/WordList.vue";
|
||||
import Empty from "@/components/Empty.vue";
|
||||
|
||||
let show = $ref(false)
|
||||
let list = $ref([])
|
||||
let title = $ref('')
|
||||
|
||||
onMounted(() => {
|
||||
emitter.on(EventKey.openWordListModal, (val: any) => {
|
||||
show = true
|
||||
list = val.list
|
||||
title = val.title + `(${list.length}词)`
|
||||
})
|
||||
})
|
||||
|
||||
watch(() => show, v => {
|
||||
if (!v) {
|
||||
list = []
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
emitter.off(EventKey.openWordListModal)
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
:title="title"
|
||||
v-model="show">
|
||||
<div class="all-word">
|
||||
<WordList
|
||||
v-if="list.length"
|
||||
class="word-list"
|
||||
:list="list">
|
||||
</WordList>
|
||||
<Empty v-else/>
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/assets/css/style";
|
||||
|
||||
.all-word {
|
||||
padding-bottom: var(--space);
|
||||
padding-top: 0;
|
||||
width: 400rem;
|
||||
height: 75vh;
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import {Icon} from "@iconify/vue";
|
||||
import Tooltip from "@/components/Tooltip.vue";
|
||||
import Tooltip from "@/pages/pc/components/Tooltip.vue";
|
||||
|
||||
defineEmits(['click'])
|
||||
defineProps<{
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import {Icon} from "@iconify/vue";
|
||||
import {$ref} from "vue/macros";
|
||||
import IconWrapper from "@/components/IconWrapper.vue";
|
||||
import Tooltip from "@/components/Tooltip.vue";
|
||||
import IconWrapper from "@/pages/pc/components/IconWrapper.vue";
|
||||
import Tooltip from "@/pages/pc/components/Tooltip.vue";
|
||||
import {ShortcutKey} from "@/types.ts";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import Input from "@/components/Input.vue";
|
||||
import {$computed, $ref} from "vue/macros";
|
||||
import {Article} from "@/types.ts";
|
||||
import BaseList from "@/components/list/BaseList.vue";
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
list: Article[],
|
||||
showTranslate?: boolean
|
||||
}>(), {
|
||||
list: [],
|
||||
showTranslate: true,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
click: [val: { item: Article, index: number }],
|
||||
title: [val: { item: Article, index: number }],
|
||||
}>()
|
||||
|
||||
let searchKey = $ref('')
|
||||
let localList = $computed(() => {
|
||||
if (searchKey) {
|
||||
return props.list.filter((item: Article) => {
|
||||
//把搜索内容,分词之后,判断是否有这个词,比单纯遍历包含体验更好
|
||||
return searchKey.toLowerCase().split(' ').filter(v => v).some(value => {
|
||||
return item.title.toLowerCase().includes(value) || item.titleTranslate.toLowerCase().includes(value)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
return props.list
|
||||
}
|
||||
})
|
||||
|
||||
const listRef: any = $ref(null as any)
|
||||
|
||||
function scrollToBottom() {
|
||||
listRef?.scrollToBottom()
|
||||
}
|
||||
|
||||
function scrollToItem(index: number) {
|
||||
listRef?.scrollToItem(index)
|
||||
}
|
||||
|
||||
defineExpose({scrollToBottom, scrollToItem})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="list">
|
||||
<div class="search">
|
||||
<Input v-model="searchKey"/>
|
||||
</div>
|
||||
<BaseList
|
||||
ref="listRef"
|
||||
@click="(e:any) => emit('click',e)"
|
||||
:list="localList"
|
||||
v-bind="$attrs">
|
||||
<template v-slot:prefix="{ item, index }">
|
||||
<slot name="prefix" :item="item" :index="index"></slot>
|
||||
</template>
|
||||
<template v-slot="{ item, index }">
|
||||
<div class="item-title" @click.stop="emit('title',{item,index})">
|
||||
<div class="name"> {{ `${searchKey ? '' : (index + 1) + '. '}${item.title}` }}</div>
|
||||
</div>
|
||||
<div class="item-sub-title" v-if="item.titleTranslate && showTranslate">
|
||||
<div class="item-translate"> {{ ` ${item.titleTranslate}` }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:suffix="{ item, index }">
|
||||
<slot name="suffix" :item="item" :index="index"></slot>
|
||||
</template>
|
||||
</BaseList>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15rem;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.search {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
padding: 0 var(--space);
|
||||
}
|
||||
|
||||
.translate {
|
||||
font-size: 16rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,183 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {nextTick, watch} from 'vue'
|
||||
import {$computed} from "vue/macros";
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
list?: any[],
|
||||
activeIndex?: number,
|
||||
activeId?: string,
|
||||
isActive?: boolean
|
||||
showBorder?: boolean
|
||||
static?: boolean
|
||||
}>(), {
|
||||
list: [],
|
||||
activeIndex: -1,
|
||||
activeId: '',
|
||||
isActive: false,
|
||||
showBorder: false,
|
||||
static: true
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
click: [val: {
|
||||
item: any,
|
||||
index: number
|
||||
}],
|
||||
}>()
|
||||
|
||||
//虚拟列表长度限制
|
||||
const limit = 101
|
||||
const settingStore = useSettingStore()
|
||||
const listRef: any = $ref()
|
||||
|
||||
const localActiveIndex = $computed(() => {
|
||||
if (props.activeId) {
|
||||
return props.list.findIndex(v => v.id === props.activeId)
|
||||
}
|
||||
return props.activeIndex
|
||||
})
|
||||
|
||||
function scrollViewToCenter(index: number) {
|
||||
if (index === -1) return
|
||||
nextTick(() => {
|
||||
if (props.list.length > limit) {
|
||||
listRef?.scrollToItem(index)
|
||||
} else {
|
||||
listRef?.children[index]?.scrollIntoView({block: 'center', behavior: 'smooth'})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
watch(() => localActiveIndex, (n: any) => {
|
||||
if (props.static) return
|
||||
if (settingStore.showPanel) {
|
||||
scrollViewToCenter(n)
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => props.isActive, (n: boolean) => {
|
||||
if (props.static) return
|
||||
if (n) {
|
||||
setTimeout(() => scrollViewToCenter(localActiveIndex), 300)
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => props.list, () => {
|
||||
if (props.static) return
|
||||
nextTick(() => {
|
||||
if (props.list.length > limit) {
|
||||
listRef?.scrollToItem(0)
|
||||
} else {
|
||||
listRef?.scrollTo(0, 0)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
function scrollToBottom() {
|
||||
nextTick(() => {
|
||||
if (props.list.length > limit) {
|
||||
listRef.scrollToBottom()
|
||||
} else {
|
||||
listRef?.scrollTo(0, listRef.scrollHeight)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function scrollToItem(index: number) {
|
||||
nextTick(() => {
|
||||
if (props.list.length > limit) {
|
||||
listRef?.scrollToItem(index)
|
||||
} else {
|
||||
listRef?.children[index]?.scrollIntoView({block: 'center', behavior: 'smooth'})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function itemIsActive(item: any, index: number) {
|
||||
return props.activeId ?
|
||||
props.activeId === item.id
|
||||
: props.activeIndex === index
|
||||
}
|
||||
|
||||
defineExpose({scrollToBottom, scrollToItem})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DynamicScroller
|
||||
v-if="list.length>limit"
|
||||
:items="list"
|
||||
ref="listRef"
|
||||
:min-item-size="90"
|
||||
class="scroller"
|
||||
>
|
||||
<template v-slot="{ item, index, active }">
|
||||
<DynamicScrollerItem
|
||||
:item="item"
|
||||
:active="active"
|
||||
:size-dependencies="[
|
||||
item.id,
|
||||
]"
|
||||
:data-index="index"
|
||||
>
|
||||
<div class="list-item-wrapper">
|
||||
<div class="common-list-item"
|
||||
:class="{
|
||||
active:itemIsActive(item,index),
|
||||
border:showBorder
|
||||
}"
|
||||
@click="emit('click',{item,index})"
|
||||
>
|
||||
<div class="left">
|
||||
<slot name="prefix" :item="item" :index="index"></slot>
|
||||
<div class="title-wrapper">
|
||||
<slot :item="item" :index="index"></slot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<slot name="suffix" :item="item" :index="index"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DynamicScrollerItem>
|
||||
</template>
|
||||
</DynamicScroller>
|
||||
<div
|
||||
v-else
|
||||
class="scroller"
|
||||
style="overflow: auto;"
|
||||
ref="listRef">
|
||||
<div class="list-item-wrapper"
|
||||
v-for="(item,index) in list"
|
||||
:key="item.id"
|
||||
>
|
||||
<div class="common-list-item"
|
||||
:class="{
|
||||
active:itemIsActive(item,index),
|
||||
border:showBorder
|
||||
}"
|
||||
@click="emit('click',{item,index})"
|
||||
>
|
||||
<div class="left">
|
||||
<slot name="prefix" :item="item" :index="index"></slot>
|
||||
<div class="title-wrapper">
|
||||
<slot :item="item" :index="index"></slot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<slot name="suffix" :item="item" :index="index"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/assets/css/variable";
|
||||
|
||||
.scroller {
|
||||
flex: 1;
|
||||
padding: 0 var(--space);
|
||||
}
|
||||
</style>
|
||||
@@ -1,76 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import {$computed, $ref} from "vue/macros";
|
||||
import {watch} from "vue";
|
||||
import {DictResource} from "@/types.ts";
|
||||
import DictItem from "@/components/list/DictItem.vue";
|
||||
import DictList from "@/components/list/DictList.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
category: string,
|
||||
groupByTag: any,
|
||||
selectId: string
|
||||
}>()
|
||||
const emit = defineEmits<{
|
||||
selectDict: [val: { dict: DictResource, index: number }]
|
||||
detail: [],
|
||||
}>()
|
||||
const tagList = $computed(() => Object.keys(props.groupByTag))
|
||||
let currentTag = $ref(tagList[0])
|
||||
let list = $computed(() => {
|
||||
return props.groupByTag[currentTag]
|
||||
})
|
||||
|
||||
watch(() => props.groupByTag, () => {
|
||||
currentTag = tagList[0]
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dict-group">
|
||||
<div class="category">{{ category }}</div>
|
||||
<div class="tags">
|
||||
<div class="tag" :class="i === currentTag &&'active'"
|
||||
@click="currentTag = i"
|
||||
v-for="i in Object.keys(groupByTag)">{{ i }}
|
||||
</div>
|
||||
</div>
|
||||
<DictList
|
||||
@selectDict="e => emit('selectDict',e)"
|
||||
:list="list"
|
||||
:select-id="selectId"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dict-group {
|
||||
color: var(--color-font-1);
|
||||
margin-bottom: 40rem;
|
||||
//border-bottom: 1px dashed gray;
|
||||
|
||||
.category {
|
||||
font-size: 24rem;
|
||||
padding-bottom: 10rem;
|
||||
border-bottom: 1px dashed gray;
|
||||
}
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 10rem 0;
|
||||
|
||||
.tag {
|
||||
color: var(--color-font-1);
|
||||
cursor: pointer;
|
||||
padding: 5rem 10rem;
|
||||
border-radius: 20rem;
|
||||
|
||||
&.active {
|
||||
color: var(--color-font-active-1);
|
||||
background: gray;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,139 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {Dict, DictType} from "@/types.ts";
|
||||
import {Icon} from "@iconify/vue";
|
||||
import {$computed} from "vue/macros";
|
||||
|
||||
const props = defineProps<{
|
||||
dict?: Dict,
|
||||
active?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
selectDict: [val: { dict: Dict, index: number }]
|
||||
add: []
|
||||
}>()
|
||||
|
||||
let length = $computed(() => {
|
||||
let isWord = [DictType.word,DictType.collect,DictType.simple,DictType.wrong].includes(props.dict.type)
|
||||
let len: any = ''
|
||||
if (props.dict.length) {
|
||||
len = props.dict.length
|
||||
len += (isWord ? '词' : '篇')
|
||||
} else {
|
||||
if (isWord) {
|
||||
len = props.dict.originWords.length + '词'
|
||||
} else {
|
||||
len = props.dict.articles.length + '篇'
|
||||
}
|
||||
}
|
||||
return len
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="dict-item anim"
|
||||
:class="active && 'active'"
|
||||
>
|
||||
<template v-if="dict.id">
|
||||
<div class="top">
|
||||
<div class="name">{{ dict.name }}</div>
|
||||
<div class="desc">{{ dict.description }}</div>
|
||||
</div>
|
||||
<div class="bottom">
|
||||
<div class="num">{{ length }}</div>
|
||||
</div>
|
||||
<div class="pin" v-if="dict.type === DictType.article">文章</div>
|
||||
</template>
|
||||
<div v-else class="add" @click.stop="emit('add')">
|
||||
<Icon icon="fluent:add-20-filled" width="38" color="#929596"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dict-item {
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
padding: 10rem;
|
||||
width: 125rem;
|
||||
height: 165rem;
|
||||
border-radius: 10rem;
|
||||
position: relative;
|
||||
background: var(--color-third-bg);
|
||||
border: 1px solid var(--color-item-border);
|
||||
color: var(--color-font-1);
|
||||
font-size: 14rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
overflow: hidden;
|
||||
|
||||
.name {
|
||||
font-size: 16rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box; //作为弹性伸缩盒子模型显示。
|
||||
-webkit-box-orient: vertical; //设置伸缩盒子的子元素排列方式--从上到下垂直排列
|
||||
-webkit-line-clamp: 2; //显示的行
|
||||
}
|
||||
|
||||
.desc {
|
||||
color: var(--color-font-2);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box; //作为弹性伸缩盒子模型显示。
|
||||
-webkit-box-orient: vertical; //设置伸缩盒子的子元素排列方式--从上到下垂直排列
|
||||
-webkit-line-clamp: 2; //显示的行
|
||||
}
|
||||
|
||||
.num {
|
||||
text-align: right;
|
||||
color: var(--color-font-2);
|
||||
//font-weight: bold;
|
||||
}
|
||||
|
||||
.go {
|
||||
position: absolute;
|
||||
right: 10rem;
|
||||
bottom: 15rem;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--color-item-active);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--color-item-active);
|
||||
}
|
||||
|
||||
.add {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.pin {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 55rem;
|
||||
width: 55rem;
|
||||
color: white;
|
||||
//background-color: skyblue;
|
||||
background-color: var(--color-main-active);
|
||||
clip-path: polygon(0 10%, 0% 100%, 100% 100%);
|
||||
font-size: 12rem;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-end;
|
||||
padding: 4rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,36 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import {Dict} from "@/types.ts";
|
||||
import {Icon} from "@iconify/vue";
|
||||
import DictItem from "@/components/list/DictItem.vue";
|
||||
|
||||
defineProps<{
|
||||
list?: Dict[],
|
||||
selectId?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
selectDict: [val: { dict: any, index: number }]
|
||||
detail: [],
|
||||
add: []
|
||||
}>()
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dict-list">
|
||||
<DictItem v-for="(dict,index) in list"
|
||||
:active="selectId === dict.id"
|
||||
@click="emit('selectDict',{dict,index})"
|
||||
@add="emit('add')"
|
||||
:dict="dict"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dict-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15rem;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,207 +0,0 @@
|
||||
<script setup lang="ts" generic="T extends {id:string}">
|
||||
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import Input from "@/components/Input.vue";
|
||||
import {$computed, $ref} from "vue/macros";
|
||||
import {cloneDeep, throttle} from "lodash-es";
|
||||
import {Article} from "@/types.ts";
|
||||
|
||||
interface IProps {
|
||||
list: T[]
|
||||
selectItem: T,
|
||||
}
|
||||
|
||||
const props = defineProps<IProps>()
|
||||
const emit = defineEmits<{
|
||||
selectItem: [index: T],
|
||||
delSelectItem: [],
|
||||
'update:searchKey': [val: string],
|
||||
'update:list': [list: T[]],
|
||||
}>()
|
||||
|
||||
let dragItem: T = $ref({id: ''} as any)
|
||||
let searchKey = $ref('')
|
||||
let draggable = $ref(false)
|
||||
|
||||
let localList = $computed({
|
||||
get() {
|
||||
if (searchKey) {
|
||||
return props.list.filter((item: Article) => {
|
||||
//把搜索内容,分词之后,判断是否有这个词,比单纯遍历包含体验更好
|
||||
return searchKey.toLowerCase().split(' ').filter(v => v).some(value => {
|
||||
return item.title.toLowerCase().includes(value) || item.titleTranslate.toLowerCase().includes(value)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
return props.list
|
||||
}
|
||||
},
|
||||
set(newValue) {
|
||||
emit('update:list', newValue)
|
||||
}
|
||||
})
|
||||
|
||||
function dragstart(item: T) {
|
||||
dragItem = item;
|
||||
}
|
||||
|
||||
const dragenter = throttle((e, item: T) => {
|
||||
// console.log('dragenter', 'item.id', item.id, 'dragItem.id', dragItem.id)
|
||||
e.preventDefault();
|
||||
// 避免源对象触发自身的dragenter事件
|
||||
if (dragItem.id && dragItem.id !== item.id) {
|
||||
let rIndex = props.list.findIndex(v => v.id === dragItem.id)
|
||||
let rIndex2 = props.list.findIndex(v => v.id === item.id)
|
||||
// console.log('dragenter', 'item-Index', rIndex2, 'dragItem.index', rIndex)
|
||||
//这里不能直接用localList splice。不知道为什么会导致有筛选的情况下,多动无法变换位置
|
||||
let temp = cloneDeep(props.list)
|
||||
temp.splice(rIndex, 1);
|
||||
temp.splice(rIndex2, 0, cloneDeep(dragItem));
|
||||
localList = temp;
|
||||
}
|
||||
}, 300)
|
||||
|
||||
function dragover(e) {
|
||||
// console.log('dragover')
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
function dragend() {
|
||||
// console.log('dragend')
|
||||
draggable = false
|
||||
dragItem = {id: ''} as T
|
||||
}
|
||||
|
||||
function delItem(item: T) {
|
||||
if (item.id === props.selectItem.id) {
|
||||
emit('delSelectItem')
|
||||
}
|
||||
let rIndex = props.list.findIndex(v => v.id === item.id)
|
||||
if (rIndex > -1) {
|
||||
localList.splice(rIndex, 1)
|
||||
}
|
||||
}
|
||||
|
||||
let el: HTMLDivElement = $ref()
|
||||
|
||||
function scrollBottom() {
|
||||
el.scrollTo({
|
||||
top: el.scrollHeight,
|
||||
left: 0,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
|
||||
defineExpose({scrollBottom})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="list-wrapper"
|
||||
ref="el"
|
||||
>
|
||||
<div class="search">
|
||||
<Input v-model="searchKey"/>
|
||||
</div>
|
||||
<transition-group name="drag" class="list" tag="div">
|
||||
<div class="item"
|
||||
:class="[
|
||||
(selectItem.id === item.id) && 'active',
|
||||
draggable && 'draggable',
|
||||
(dragItem.id === item.id) && 'active'
|
||||
]"
|
||||
@click="emit('selectItem',item)"
|
||||
v-for="(item,index) in localList"
|
||||
:key="item.id"
|
||||
:draggable="draggable"
|
||||
@dragstart="dragstart(item)"
|
||||
@dragenter="dragenter($event, item)"
|
||||
@dragover="dragover($event)"
|
||||
@dragend="dragend()"
|
||||
>
|
||||
<div class="left">
|
||||
<slot :item="item" :index="index"></slot>
|
||||
</div>
|
||||
<div class="right">
|
||||
<BaseIcon
|
||||
@click="delItem(item)"
|
||||
title="删除" icon="fluent:delete-24-regular"/>
|
||||
<div
|
||||
@mousedown="draggable = true"
|
||||
@mouseup="draggable = false"
|
||||
>
|
||||
<BaseIcon
|
||||
icon="carbon:move"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.drag-move, /* 对移动中的元素应用的过渡 */
|
||||
.drag-enter-active,
|
||||
.drag-leave-active {
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.drag-enter-from,
|
||||
.drag-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(50rem);
|
||||
}
|
||||
|
||||
/* 确保将离开的元素从布局流中删除
|
||||
以便能够正确地计算移动的动画。 */
|
||||
.drag-leave-active {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.list-wrapper {
|
||||
transition: all .3s;
|
||||
flex: 1;
|
||||
overflow: overlay;
|
||||
padding-right: 5rem;
|
||||
|
||||
.search {
|
||||
margin: 10rem 0;
|
||||
}
|
||||
|
||||
.list {
|
||||
.item {
|
||||
box-sizing: border-box;
|
||||
background: var(--color-item-bg);
|
||||
color: var(--color-font-1);
|
||||
border-radius: 8rem;
|
||||
margin-bottom: 10rem;
|
||||
padding: 10rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
transition: all .3s;
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: all .3s;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.right {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--color-item-active);
|
||||
color: var(--color-font-1);
|
||||
}
|
||||
|
||||
&.draggable {
|
||||
cursor: move;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,67 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {$ref} from "vue/macros";
|
||||
import {Word} from "@/types.ts";
|
||||
import VolumeIcon from "@/components/icon/VolumeIcon.vue";
|
||||
import BaseList from "@/components/list/BaseList.vue";
|
||||
import {usePlayWordAudio} from "@/hooks/sound.ts";
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
list: Word[],
|
||||
showTranslate?: boolean
|
||||
showWord?: boolean
|
||||
}>(), {
|
||||
list: [],
|
||||
showTranslate: true,
|
||||
showWord: true
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
click: [val: { item: Word, index: number }],
|
||||
title: [val: { item: Word, index: number }],
|
||||
}>()
|
||||
|
||||
const listRef: any = $ref(null as any)
|
||||
|
||||
function scrollToBottom() {
|
||||
listRef?.scrollToBottom()
|
||||
}
|
||||
|
||||
function scrollToItem(index: number) {
|
||||
listRef?.scrollToItem(index)
|
||||
}
|
||||
|
||||
const playWordAudio = usePlayWordAudio()
|
||||
|
||||
defineExpose({scrollToBottom, scrollToItem})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseList
|
||||
ref="listRef"
|
||||
@click="(e:any) => emit('click',e)"
|
||||
:list="list"
|
||||
v-bind="$attrs">
|
||||
<template v-slot:prefix="{ item, index }">
|
||||
<slot name="prefix" :item="item" :index="index"></slot>
|
||||
</template>
|
||||
<template v-slot="{ item, index }">
|
||||
<div class="item-title">
|
||||
<span class="word" :class="!showWord && 'text-shadow'">{{ item.word }}</span>
|
||||
<span class="phonetic">{{ item.phonetic0 }}</span>
|
||||
<VolumeIcon class="volume" @click="playWordAudio(item.word)"></VolumeIcon>
|
||||
</div>
|
||||
<div class="item-sub-title" v-if="item.trans.length && showTranslate">
|
||||
<div v-for="v in item.trans">{{ (v.pos ? v.pos + '.' : '') + v.cn }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:suffix="{ item, index }">
|
||||
<slot name="suffix" :item="item" :index="index"></slot>
|
||||
</template>
|
||||
</BaseList>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
@@ -1,39 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {Icon} from "@iconify/vue";
|
||||
import IconWrapper from "@/components/IconWrapper.vue";
|
||||
import Tooltip from "@/components/Tooltip.vue";
|
||||
import {$ref} from "vue/macros";
|
||||
|
||||
let show = $ref(false)
|
||||
|
||||
function toggle() {
|
||||
show = !show
|
||||
// emitter.emit(EventKey.openArticleListModal)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="setting" @click.stop="null">
|
||||
<Tooltip title="添加">
|
||||
<IconWrapper>
|
||||
<Icon icon="ic:outline-cloud-upload"
|
||||
@click="toggle"
|
||||
/>
|
||||
</IconWrapper>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/assets/css/style";
|
||||
|
||||
.wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.setting {
|
||||
position: relative;
|
||||
|
||||
}
|
||||
</style>
|
||||
@@ -1,108 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import MiniDialog from "@/components/dialog/MiniDialog.vue";
|
||||
import {useWindowClick} from "@/hooks/event.ts";
|
||||
import {emitter, EventKey} from "@/utils/eventBus.ts";
|
||||
import {nextTick, watch} from "vue";
|
||||
|
||||
const store = useBaseStore()
|
||||
let timer = 0
|
||||
let show = $ref(false)
|
||||
useWindowClick(() => show = false)
|
||||
|
||||
function toggle(val) {
|
||||
clearTimeout(timer)
|
||||
if (val) {
|
||||
emitter.emit(EventKey.closeOther)
|
||||
show = val
|
||||
} else {
|
||||
timer = setTimeout(() => {
|
||||
show = val
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
|
||||
function clickJumpSpecifiedChapter(index: number) {
|
||||
emitter.emit(EventKey.jumpSpecifiedChapter, index)
|
||||
}
|
||||
|
||||
const listRef: HTMLElement = $ref(null as any)
|
||||
|
||||
watch(() => show, n => {
|
||||
if (n){
|
||||
nextTick(()=>{
|
||||
listRef?.children[store.currentDict.chapterIndex]?.scrollIntoView({block: 'center'})
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ChapterName" @click.stop="null">
|
||||
<div class="info hvr-grow"
|
||||
@mouseenter="toggle(true)"
|
||||
@mouseleave="toggle(false)"
|
||||
>
|
||||
{{ store.chapterName }}
|
||||
</div>
|
||||
<MiniDialog
|
||||
v-model="show"
|
||||
@mouseenter="toggle(true)"
|
||||
@mouseleave="toggle(false)"
|
||||
style="width: 230rem;"
|
||||
>
|
||||
<div class="chapter-list" ref="listRef">
|
||||
<div class="chapter-list-item"
|
||||
:class="store.currentDict.chapterIndex === index && 'active'"
|
||||
v-for="(item,index) in store.currentDict.chapterWords"
|
||||
@click="clickJumpSpecifiedChapter(index)">
|
||||
<input type="radio" :checked="store.currentDict.chapterIndex === index">
|
||||
<div class="title">第{{ index + 1 }}章 {{ item.length }}词</div>
|
||||
</div>
|
||||
</div>
|
||||
</MiniDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.ChapterName {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.chapter-list {
|
||||
max-height: 250rem;
|
||||
overflow: auto;
|
||||
padding-right: 5rem;
|
||||
|
||||
.chapter-list-item {
|
||||
margin-bottom: 7rem;
|
||||
height: 36rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rem;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
background: var(--color-item-bg);
|
||||
color: var(--color-font-1);
|
||||
font-size: 18rem;
|
||||
border-radius: 8rem;
|
||||
transition: all .3s;
|
||||
padding: 10rem;
|
||||
border: 1px solid var(--color-item-border);
|
||||
|
||||
&:hover {
|
||||
background: var(--color-item-hover);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--color-item-active);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-radio-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
</style>
|
||||
@@ -1,75 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import Dialog from "@/components/dialog/Dialog.vue"
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import {GITHUB} from "@/config/ENV.ts";
|
||||
|
||||
const emit = defineEmits([
|
||||
'close',
|
||||
])
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
@close="emit('close')"
|
||||
title="反馈">
|
||||
<div 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>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/assets/css/variable";
|
||||
|
||||
.feedback-modal {
|
||||
width: 500rem;
|
||||
//height: 80vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--color-second-bg);
|
||||
align-items: center;
|
||||
padding: var(--space);
|
||||
//justify-content: center;
|
||||
color: var(--color-font-1);
|
||||
|
||||
p {
|
||||
font-size: 30rem;
|
||||
}
|
||||
|
||||
.github {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space);
|
||||
|
||||
.options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,89 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import MiniDialog from "@/components/dialog/MiniDialog.vue";
|
||||
import {Icon} from "@iconify/vue";
|
||||
import IconWrapper from "@/components/IconWrapper.vue";
|
||||
import Tooltip from "@/components/Tooltip.vue";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import {useWindowClick} from "@/hooks/event.ts";
|
||||
import {emitter, EventKey} from "@/utils/eventBus.ts";
|
||||
import {onMounted} from "vue";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
|
||||
const store = useBaseStore()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
let show = $ref(false)
|
||||
useWindowClick(() => show = false)
|
||||
|
||||
let timer = 0
|
||||
|
||||
function toggle(val) {
|
||||
clearTimeout(timer)
|
||||
if (val) {
|
||||
emitter.emit(EventKey.closeOther)
|
||||
show = val
|
||||
} else {
|
||||
timer = setTimeout(() => {
|
||||
show = val
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="setting" @click.stop="null">
|
||||
<IconWrapper>
|
||||
<Icon icon="tabler:repeat"
|
||||
@mouseenter="toggle(true)"
|
||||
@mouseleave="toggle(false)"
|
||||
/>
|
||||
</IconWrapper>
|
||||
<MiniDialog
|
||||
v-model="show"
|
||||
@mouseenter="toggle(true)"
|
||||
@mouseleave="toggle(false)"
|
||||
style="width: 230rem;"
|
||||
>
|
||||
<div class="mini-row-title">
|
||||
单词循环设置
|
||||
</div>
|
||||
<el-radio-group v-model="settingStore.repeatCount">
|
||||
<el-radio :label="1" size="default">1</el-radio>
|
||||
<el-radio :label="2" size="default">2</el-radio>
|
||||
<el-radio :label="3" size="default">3</el-radio>
|
||||
<el-radio :label="5" size="default">5</el-radio>
|
||||
<el-radio :label="100" size="default">自定义</el-radio>
|
||||
</el-radio-group>
|
||||
<div class="mini-row" v-if="settingStore.repeatCount === 100">
|
||||
<label class="item-title">自定义循环次数</label>
|
||||
<el-input-number v-model="settingStore.repeatCustomCount"
|
||||
:min="6"
|
||||
:max="15"
|
||||
type="number"
|
||||
/>
|
||||
</div>
|
||||
</MiniDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/assets/css/style";
|
||||
|
||||
.setting {
|
||||
position: relative;
|
||||
|
||||
.title {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.el-radio-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,122 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import MiniDialog from "@/components/dialog/MiniDialog.vue";
|
||||
import {Icon} from "@iconify/vue";
|
||||
import IconWrapper from "@/components/IconWrapper.vue";
|
||||
import Tooltip from "@/components/Tooltip.vue";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import {useWindowClick} from "@/hooks/event.ts";
|
||||
import {emitter, EventKey} from "@/utils/eventBus.ts";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import Dialog from "@/components/dialog/Dialog.vue";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {ShortcutKey} from "@/types.ts";
|
||||
|
||||
const store = useBaseStore()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
let show = $ref(false)
|
||||
let showCustomTranslateModal = $ref(false)
|
||||
|
||||
useWindowClick(() => show = false)
|
||||
|
||||
function toggle() {
|
||||
// if (!show) emitter.emit(EventKey.closeOther)
|
||||
// show = !show
|
||||
settingStore.translate = !settingStore.translate
|
||||
}
|
||||
|
||||
let translateType = $ref(0)
|
||||
let networkTranslateEngine = $ref('baidu')
|
||||
const TranslateEngine = [
|
||||
{value: 'baidu', label: '百度'},
|
||||
{value: 'youdao', label: '有道'},
|
||||
]
|
||||
|
||||
function save() {
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="setting" @click.stop="null">
|
||||
<Tooltip
|
||||
:title="`开关释义显示(快捷键:${settingStore.shortcutKeyMap[ShortcutKey.ToggleShowTranslate]})`"
|
||||
>
|
||||
<IconWrapper>
|
||||
<Icon v-if="settingStore.translate" icon="mdi:translate"
|
||||
@click="toggle"
|
||||
/>
|
||||
<Icon v-else icon="mdi:translate-off"
|
||||
@click="toggle"
|
||||
/>
|
||||
</IconWrapper>
|
||||
</Tooltip>
|
||||
<MiniDialog v-model="show"
|
||||
style="width: 230rem;"
|
||||
>
|
||||
<div class="mini-row">
|
||||
<label class="item-title">显示翻译</label>
|
||||
<div class="wrapper">
|
||||
<el-switch v-model="settingStore.translate"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mini-row">
|
||||
<label class="item-title">翻译类型</label>
|
||||
<el-radio-group v-model="translateType" size="small">
|
||||
<el-radio-button :label="1">本地翻译</el-radio-button>
|
||||
<el-radio-button :label="0">网络翻译</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<div class="mini-row" v-if="translateType">
|
||||
<label class="item-title">本地翻译</label>
|
||||
<div class="wrapper">
|
||||
<Tooltip title="开关释义显示">
|
||||
<IconWrapper>
|
||||
<Icon icon="mingcute:edit-line"
|
||||
@click="toggle"
|
||||
/>
|
||||
</IconWrapper>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mini-row" v-else>
|
||||
<label class="item-title">网络翻译</label>
|
||||
<div class="wrapper">
|
||||
<el-select v-model="networkTranslateEngine" class="m-2" placeholder="Select" size="small">
|
||||
<el-option
|
||||
v-for="item in TranslateEngine"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<BaseButton size="small" @click="show = false">取消</BaseButton>
|
||||
<BaseButton size="small" @click="save">确定</BaseButton>
|
||||
</div>
|
||||
</MiniDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/assets/css/style.scss";
|
||||
|
||||
.setting {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 10rem;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10rem;
|
||||
}
|
||||
</style>
|
||||
@@ -1,177 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import MiniDialog from "@/components/dialog/MiniDialog.vue";
|
||||
import {Icon} from "@iconify/vue";
|
||||
import IconWrapper from "@/components/IconWrapper.vue";
|
||||
import Tooltip from "@/components/Tooltip.vue";
|
||||
import {useWindowClick} from "@/hooks/event.ts";
|
||||
import {emitter, EventKey} from "@/utils/eventBus.ts";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import VolumeIcon from "@/components/icon/VolumeIcon.vue";
|
||||
import {getAudioFileUrl, useChangeAllSound, usePlayAudio, useWatchAllSound} from "@/hooks/sound.ts";
|
||||
import {SoundFileOptions} from "@/utils/const.ts";
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
let show = $ref(false)
|
||||
|
||||
useWindowClick(() => show = false)
|
||||
useWatchAllSound()
|
||||
|
||||
let timer = 0
|
||||
|
||||
function toggle(val: boolean) {
|
||||
clearTimeout(timer)
|
||||
if (val) {
|
||||
emitter.emit(EventKey.closeOther)
|
||||
show = val
|
||||
} else {
|
||||
timer = setTimeout(() => {
|
||||
show = val
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function toggle2() {
|
||||
if (!show){
|
||||
emitter.emit(EventKey.closeOther)
|
||||
}
|
||||
show = !show
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="setting"
|
||||
@click.stop="null"
|
||||
>
|
||||
<Tooltip title="音效设置">
|
||||
<IconWrapper>
|
||||
<Icon v-if="settingStore.allSound" icon="icon-park-outline:volume-notice"
|
||||
@click="toggle2()"
|
||||
/>
|
||||
<Icon v-else icon="icon-park-outline:volume-mute"
|
||||
@click="toggle2()"
|
||||
/>
|
||||
</IconWrapper>
|
||||
</Tooltip>
|
||||
<MiniDialog
|
||||
width="250rem"
|
||||
v-model="show">
|
||||
<div class="mini-row-title">
|
||||
音效设置
|
||||
</div>
|
||||
<div class="mini-row">
|
||||
<label class="item-title">所有音效</label>
|
||||
<div class="wrapper">
|
||||
<el-switch v-model="settingStore.allSound"
|
||||
@change="useChangeAllSound"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mini-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="mini-row">
|
||||
<label class="item-title">单词/句子发音口音</label>
|
||||
<div class="wrapper">
|
||||
<el-select v-model="settingStore.wordSoundType"
|
||||
placeholder="请选择"
|
||||
size="small">
|
||||
<el-option label="美音" value="us"/>
|
||||
<el-option label="英音" value="uk"/>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="mini-row">-->
|
||||
<!-- <label class="item-title">释义发音</label>-->
|
||||
<!-- <div class="wrapper">-->
|
||||
<!-- <el-switch v-model="settingStore.translateSound"-->
|
||||
<!-- inline-prompt-->
|
||||
<!-- active-text="开"-->
|
||||
<!-- inactive-text="关"-->
|
||||
<!-- />-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<div class="mini-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="mini-row">
|
||||
<label class="item-title">按键音效</label>
|
||||
<div class="wrapper">
|
||||
<el-select v-model="settingStore.keyboardSoundFile"
|
||||
placeholder="请选择"
|
||||
size="small">
|
||||
<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="mini-row">
|
||||
<label class="item-title">效果音</label>
|
||||
<div class="wrapper">
|
||||
<el-switch v-model="settingStore.effectSound"
|
||||
inline-prompt
|
||||
active-text="开"
|
||||
inactive-text="关"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</MiniDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/assets/css/style";
|
||||
|
||||
.wrapper {
|
||||
width: 100rem;
|
||||
position: relative;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.setting {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.el-option-row {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.icon-wrapper {
|
||||
transform: translateX(10rem);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,258 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import Tooltip from "@/components/Tooltip.vue"
|
||||
import useTheme from "@/hooks/theme.ts"
|
||||
import {useBaseStore} from "@/stores/base.ts"
|
||||
import FeedbackModal from "@/components/toolbar/FeedbackModal.vue"
|
||||
|
||||
import {Icon} from '@iconify/vue';
|
||||
|
||||
import IconWrapper from "@/components/IconWrapper.vue";
|
||||
import {watch} from "vue"
|
||||
import VolumeSetting from "@/components/toolbar/VolumeSetting.vue";
|
||||
import RepeatSetting from "@/components/toolbar/RepeatSetting.vue";
|
||||
import TranslateSetting from "@/components/toolbar/TranslateSetting.vue";
|
||||
import {useSettingStore} from "@/stores/setting.ts";
|
||||
import {usePracticeStore} from "@/stores/practice.ts";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import {$ref} from "vue/macros";
|
||||
import {DictType, ShortcutKey} from "@/types.ts";
|
||||
import ChapterName from "@/components/toolbar/ChapterName.vue";
|
||||
import {emitter, EventKey} from "@/utils/eventBus.ts";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
|
||||
const {toggleTheme} = useTheme()
|
||||
const store = useBaseStore()
|
||||
const settingStore = useSettingStore()
|
||||
const runtimeStore = useRuntimeStore()
|
||||
const practiceStore = usePracticeStore()
|
||||
|
||||
const showFeedbackModal = $ref(false)
|
||||
const headerRef = $ref<HTMLDivElement>(null)
|
||||
const moreOptionsRef = $ref<HTMLDivElement>(null)
|
||||
|
||||
watch([() => settingStore.showToolbar, () => headerRef], n => {
|
||||
if (n[1]) {
|
||||
if (n[0]) {
|
||||
n[1].style.marginTop = '10rem'
|
||||
} else {
|
||||
let rect = n[1].getBoundingClientRect()
|
||||
n[1].style.marginTop = `-${rect.height}px`
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function toggle() {
|
||||
if (settingStore.collapse) {
|
||||
setTimeout(() => {
|
||||
moreOptionsRef.style.overflow = 'unset'
|
||||
}, 300)
|
||||
} else {
|
||||
moreOptionsRef.style.overflow = 'hidden'
|
||||
}
|
||||
settingStore.collapse = !settingStore.collapse
|
||||
}
|
||||
|
||||
watch(() => store.load, n => {
|
||||
if (!settingStore.collapse) {
|
||||
moreOptionsRef.style.overflow = 'unset'
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header ref="headerRef">
|
||||
<div class="content">
|
||||
<div class="dict-name">
|
||||
<Tooltip
|
||||
:title="`词典详情(快捷键:${settingStore.shortcutKeyMap[ShortcutKey.OpenDictDetail]})`">
|
||||
<div class="info hvr-grow" @click="emitter.emit(EventKey.openDictModal,'detail')">
|
||||
{{ store.currentDict.name }} {{ practiceStore.repeatNumber ? ' 复习错词' : '' }}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<ChapterName v-if="store.currentDict.type === DictType.word"/>
|
||||
<div class="info-text" v-if="practiceStore.repeatNumber">
|
||||
复习错词
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="options" ref="moreOptionsRef">
|
||||
<div class="more" :class="settingStore.collapse && 'hide'">
|
||||
<Tooltip title="收起图标">
|
||||
<IconWrapper>
|
||||
<Icon :icon="`system-uicons:window-collapse-${settingStore.collapse?'left':'right'}`"
|
||||
@click="toggle"/>
|
||||
</IconWrapper>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip
|
||||
:title="`开关默写模式(快捷键:${settingStore.shortcutKeyMap[ShortcutKey.ToggleDictation]})`"
|
||||
>
|
||||
<IconWrapper>
|
||||
<Icon icon="majesticons:eye-off-line"
|
||||
v-if="settingStore.dictation"
|
||||
@click="settingStore.dictation = false"/>
|
||||
<Icon icon="mdi:eye-outline"
|
||||
v-else
|
||||
@click="settingStore.dictation = true"/>
|
||||
</IconWrapper>
|
||||
</Tooltip>
|
||||
|
||||
<TranslateSetting/>
|
||||
|
||||
<VolumeSetting/>
|
||||
|
||||
<RepeatSetting/>
|
||||
|
||||
<!-- <Add/>-->
|
||||
|
||||
<BaseIcon
|
||||
@click="emitter.emit(EventKey.openDictModal,'my')"
|
||||
title="添加"
|
||||
icon="ic:outline-cloud-upload"/>
|
||||
|
||||
<BaseIcon
|
||||
@click="showFeedbackModal = true"
|
||||
title="反馈"
|
||||
icon="ph:bug-beetle"/>
|
||||
</div>
|
||||
|
||||
<div class="with-bg anim">
|
||||
<BaseIcon
|
||||
@click="runtimeStore.showSettingModal = true"
|
||||
:title="`设置(快捷键:${settingStore.shortcutKeyMap[ShortcutKey.OpenSetting]})`"
|
||||
icon="uil:setting"/>
|
||||
<BaseIcon
|
||||
@click="settingStore.showPanel = !settingStore.showPanel"
|
||||
:title="`单词本(快捷键:${settingStore.shortcutKeyMap[ShortcutKey.TogglePanel]})`"
|
||||
icon="tdesign:menu-unfold"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Tooltip :title="settingStore.showToolbar?'收起':'展开'">
|
||||
<Icon icon="icon-park-outline:down"
|
||||
@click="settingStore.showToolbar = !settingStore.showToolbar"
|
||||
class="arrow"
|
||||
:class="!settingStore.showToolbar && 'down'"
|
||||
width="24"
|
||||
color="#999"/>
|
||||
</Tooltip>
|
||||
</header>
|
||||
<FeedbackModal v-if="showFeedbackModal" @close="showFeedbackModal = false"/>
|
||||
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.info {
|
||||
border-radius: 6rem;
|
||||
color: var(--color-font-1);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: all .3s;
|
||||
padding: 6rem 8rem;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-main-active);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.info-text {
|
||||
@extend .info;
|
||||
cursor: unset;
|
||||
|
||||
&:hover {
|
||||
background: unset;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style scoped lang="scss">
|
||||
@import "@/assets/css/style";
|
||||
|
||||
.test-enter-active,
|
||||
.test-leave-active {
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.test-enter-from,
|
||||
.test-leave-to {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
header {
|
||||
width: var(--toolbar-width);
|
||||
margin-top: 10rem;
|
||||
background: var(--color-second-bg);
|
||||
border-radius: 8rem;
|
||||
margin-bottom: 30rem;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
padding: 4rem var(--space);
|
||||
box-sizing: border-box;
|
||||
gap: 10rem;
|
||||
border: 1px solid var(--color-item-border);
|
||||
transition: all var(--anim-time);
|
||||
box-shadow: var(--shadow);
|
||||
|
||||
.content {
|
||||
min-height: var(--toolbar-height);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.dict-name {
|
||||
display: flex;
|
||||
max-width: 45%;
|
||||
font-size: 17rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.hide {
|
||||
transform: translateX(calc(100% - 36rem));
|
||||
}
|
||||
|
||||
.options {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
|
||||
.icon-wrapper {
|
||||
margin-left: 10rem;
|
||||
}
|
||||
|
||||
:deep(.icon-wrapper) {
|
||||
margin-left: 10rem;
|
||||
}
|
||||
|
||||
.more {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: all .3s;
|
||||
}
|
||||
|
||||
.with-bg {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
background: var(--color-second-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.arrow {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
cursor: pointer;
|
||||
transition: all .5s;
|
||||
transform: translate3d(-50%, 100%, 0) rotate(180deg);
|
||||
padding: 5rem;
|
||||
|
||||
&.down {
|
||||
transform: translate3d(-50%, 100%, 0) rotate(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user