fix:color
This commit is contained in:
2
components.d.ts
vendored
2
components.d.ts
vendored
@@ -10,6 +10,7 @@ declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
About: typeof import('./src/components/About.vue')['default']
|
||||
ArticleList: typeof import('./src/components/list/ArticleList.vue')['default']
|
||||
ArticleSetting: typeof import('./src/components/setting/ArticleSetting.vue')['default']
|
||||
ArticleSettting: typeof import('./src/components/setting/ArticleSettting.vue')['default']
|
||||
Audio: typeof import('./src/components/base/Audio.vue')['default']
|
||||
BackIcon: typeof import('./src/components/BackIcon.vue')['default']
|
||||
@@ -50,6 +51,7 @@ declare module 'vue' {
|
||||
IconFluentArrowClockwise20Regular: typeof import('~icons/fluent/arrow-clockwise20-regular')['default']
|
||||
IconFluentArrowDownload20Regular: typeof import('~icons/fluent/arrow-download20-regular')['default']
|
||||
IconFluentArrowLeft16Regular: typeof import('~icons/fluent/arrow-left16-regular')['default']
|
||||
IconFluentArrowMove20Regular: typeof import('~icons/fluent/arrow-move20-regular')['default']
|
||||
IconFluentArrowRepeatAll20Regular: typeof import('~icons/fluent/arrow-repeat-all20-regular')['default']
|
||||
IconFluentArrowRight16Regular: typeof import('~icons/fluent/arrow-right16-regular')['default']
|
||||
IconFluentArrowShuffle16Regular: typeof import('~icons/fluent/arrow-shuffle16-regular')['default']
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
@use "anim" as *;
|
||||
@use 'anim' as *;
|
||||
@use 'shepherd.css';
|
||||
|
||||
:root {
|
||||
--color-reverse-white: white;
|
||||
--color-reverse-black: black;
|
||||
--bg-history: white;
|
||||
--color-item-bg: rgb(228, 230, 232);
|
||||
--color-item-hover: white;
|
||||
//--color-item-active: rgb(75, 110, 175);
|
||||
--color-item-active: rgb(253, 246, 236);
|
||||
--color-item-border: rgb(226, 226, 226);
|
||||
|
||||
--color-tooltip-bg: white;
|
||||
@@ -31,7 +27,6 @@
|
||||
--modal-padding: 1.3rem;
|
||||
--space: 0.9rem;
|
||||
--stat-gap: 1rem;
|
||||
--shadow: rgba(0, 0, 0, 0.08) 0px 4px 12px;
|
||||
--word-panel-margin-left: calc(50% + var(--toolbar-width) / 2 + 1rem);
|
||||
--anim-time: 0.5s;
|
||||
|
||||
@@ -48,66 +43,53 @@
|
||||
--font-family: -apple-system, sans-serif;
|
||||
--word-font-family: ui-monospace, sans-serif;
|
||||
--en-article-family: Georgia, sans-serif;
|
||||
--zh-article-family: "Songti SC", "SimSun", "Noto Serif CJK SC", serif;
|
||||
--zh-article-family: 'Songti SC', 'SimSun', 'Noto Serif CJK SC', serif;
|
||||
|
||||
|
||||
--color-primary: #E6E8EB;
|
||||
--color-primary: #e6e8eb;
|
||||
--color-second: rgb(247, 247, 247);
|
||||
--color-third: rgb(226 232 240 / 1);
|
||||
--color-fourth: rgb(193, 193, 193);
|
||||
--color-third: rgb(228, 230, 232);
|
||||
--color-fourth: rgb(218, 220, 222);
|
||||
--color-fifth: rgb(253, 246, 236);
|
||||
|
||||
//--color-card-active: #FED7AA;
|
||||
--color-card-active: rgb(253, 246, 236);
|
||||
--color-list-item-active: rgb(253, 246, 236);
|
||||
--color-icon-hightlight: rgb(12, 140, 233);
|
||||
//--color-icon-hightlight: rgb(12, 140, 233);
|
||||
--color-sub-text: gray;
|
||||
--color-main-text: rgb(91, 91, 91);
|
||||
--color-select-bg: rgb(12, 140, 233);
|
||||
--color-select-text: white;
|
||||
|
||||
--color-notice-bg: rgb(247, 247, 247);
|
||||
|
||||
|
||||
//修改的进度条底色
|
||||
--color-progress-bar: #d1d5df !important;
|
||||
|
||||
--color-label-bg: whitesmoke;
|
||||
--color-link: #2563EB;
|
||||
--color-link: #2563eb;
|
||||
|
||||
--color-card-bg: white;
|
||||
|
||||
|
||||
--bg-card-primary: white;
|
||||
--bg-card-secend: rgb(247, 247, 247);
|
||||
}
|
||||
|
||||
.footer {
|
||||
&.hide {
|
||||
--color-progress-bar: #dbdbdb !important;
|
||||
}
|
||||
--bg-book: rgb(226 232 240);
|
||||
}
|
||||
|
||||
html.dark {
|
||||
--color-reverse-white: black;
|
||||
--color-reverse-black: white;
|
||||
|
||||
--color-primary: #0E1217;
|
||||
--color-second: rgb(30, 31, 34);
|
||||
--color-third: rgb(43, 45, 48);
|
||||
--color-primary: #202124;
|
||||
--color-second: #292a2d;
|
||||
--color-third: #35373a;
|
||||
--color-fourth: rgb(70, 70, 70);
|
||||
--color-fifth: rgb(84, 84, 84);
|
||||
|
||||
--color-card-active: rgb(84, 84, 84);
|
||||
--color-list-item-active: rgb(84, 84, 84);
|
||||
--color-icon-hightlight: rgb(147, 173, 227);
|
||||
--color-sub-text: #b8b8b8;
|
||||
--color-main-text: rgba(249, 250, 251, 0.8);
|
||||
--color-select-bg: rgb(147, 173, 227);
|
||||
--color-select-text: black;
|
||||
--color-notice-bg: rgb(43, 45, 48);
|
||||
|
||||
--bg-history: rgb(43, 45, 48);
|
||||
--color-item-bg: rgb(43, 45, 48);
|
||||
--color-item-hover: rgb(67, 69, 74);
|
||||
--color-item-active: rgb(84, 84, 84);
|
||||
--color-item-border: rgb(41, 41, 41);
|
||||
|
||||
--color-tooltip-bg: #252525;
|
||||
@@ -119,8 +101,7 @@ html.dark {
|
||||
--color-scrollbar: rgb(92, 93, 94);
|
||||
|
||||
--color-input-color: white;
|
||||
--color-input-bg: rgba(14, 18, 23, 1);
|
||||
--color-input-icon: #383737;
|
||||
--color-input-bg: var(--color-third);
|
||||
|
||||
--color-textarea-bg: rgb(43, 45, 48);
|
||||
--color-article: white;
|
||||
@@ -134,11 +115,8 @@ html.dark {
|
||||
--bg-card-primary: rgb(30, 31, 34);
|
||||
--bg-card-secend: rgb(43, 45, 48);
|
||||
|
||||
.footer {
|
||||
&.hide {
|
||||
--color-progress-bar: var(--color-third) !important;
|
||||
}
|
||||
}
|
||||
--bg-book: #35373a;
|
||||
|
||||
}
|
||||
|
||||
@media (max-width: 1720px) {
|
||||
@@ -186,7 +164,10 @@ html.dark {
|
||||
}
|
||||
|
||||
.anim {
|
||||
transition: background var(--anim-time), color var(--anim-time), border var(--anim-time), opacity var(--anim-time);
|
||||
transition: background var(--anim-time),
|
||||
color var(--anim-time),
|
||||
border var(--anim-time),
|
||||
opacity var(--anim-time);
|
||||
}
|
||||
|
||||
.en-article-family {
|
||||
@@ -200,9 +181,7 @@ html.dark {
|
||||
html,
|
||||
body {
|
||||
//font-size: 1px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow-x: hidden;
|
||||
@apply p-0 m-0 overflow-x-hidden;
|
||||
color: var(--color-main-text);
|
||||
font-family: var(--font-family);
|
||||
background: var(--color-primary);
|
||||
@@ -211,42 +190,14 @@ body {
|
||||
}
|
||||
|
||||
.page {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
font-size: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
transition: all .3s;
|
||||
@apply relative z-1 h-full w-full flex flex-col;
|
||||
}
|
||||
|
||||
.mobile-page {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
overflow: auto;
|
||||
font-size: 18rem;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@apply fixed left-0 right-0 bottom-0 top-0 overflow-auto font-size-18 w-full h-full flex flex-col;
|
||||
|
||||
& > .page-content {
|
||||
padding: 10rem;
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
@apply p-10 box-border overflow-auto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,38 +222,33 @@ a {
|
||||
|
||||
@supports selector(::-webkit-scrollbar) {
|
||||
::-webkit-scrollbar {
|
||||
width: .5rem;
|
||||
height: .6rem;
|
||||
width: 0.5rem;
|
||||
height: 0.6rem;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
border-radius: .1rem;
|
||||
border-radius: 0.1rem;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--color-scrollbar);
|
||||
border-radius: .6rem;
|
||||
border-radius: 0.6rem;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-page-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
@apply flex flex-col h-full box-border;
|
||||
padding-bottom: var(--space);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.scroll {
|
||||
padding: 0 var(--space);
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
@apply flex-1 overflow-auto;
|
||||
}
|
||||
|
||||
.virtual-list {
|
||||
overflow: overlay;
|
||||
height: 100%;
|
||||
@apply overflow-overlay h-full;
|
||||
padding: 0 var(--space);
|
||||
}
|
||||
|
||||
@@ -311,45 +257,27 @@ a {
|
||||
}
|
||||
|
||||
.common-list-item {
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
background: var(--color-item-bg);
|
||||
@apply cursor-pointer w-full box-border bg-third rounded-lg flex justify-between gap-1 transition-all duration-300 p-2;
|
||||
color: var(--color-main-text);
|
||||
font-size: 1.1rem;
|
||||
border-radius: .5rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
transition: all .3s;
|
||||
padding: .6rem;
|
||||
gap: .3rem;
|
||||
border: 1px solid var(--color-item-border);
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
gap: .6rem;
|
||||
|
||||
@apply flex gap-1;
|
||||
.title-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .2rem;
|
||||
word-break: break-word;
|
||||
@apply flex flex-col gap-0.5 word-break-break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .1rem;
|
||||
transition: all .3s;
|
||||
@apply flex flex-col gap-0.5 transition-all duration-300;
|
||||
}
|
||||
|
||||
svg {
|
||||
opacity: 0;
|
||||
@apply opacity-0;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--color-list-item-active);
|
||||
@apply bg-fifth;
|
||||
|
||||
.item-sub-title {
|
||||
color: var(--color-sub-text);
|
||||
@@ -364,6 +292,7 @@ a {
|
||||
|
||||
&:hover {
|
||||
@extend .active;
|
||||
background: var(--color-fourth);
|
||||
|
||||
svg {
|
||||
opacity: 1;
|
||||
@@ -373,7 +302,7 @@ a {
|
||||
.item-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
gap: 0.5rem;
|
||||
color: var(--color-main-text);
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -386,7 +315,7 @@ a {
|
||||
}
|
||||
|
||||
.phonetic {
|
||||
font-size: .9rem;
|
||||
font-size: 0.9rem;
|
||||
color: gray;
|
||||
}
|
||||
}
|
||||
@@ -399,7 +328,7 @@ a {
|
||||
|
||||
.word-shadow {
|
||||
color: transparent !important;
|
||||
text-shadow: #b0b0b0 0 0 .5rem;
|
||||
text-shadow: #b0b0b0 0 0 0.5rem;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@@ -415,14 +344,14 @@ a {
|
||||
.slide {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
transition: height .3s;
|
||||
transition: height 0.3s;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.slide-infinite {
|
||||
z-index: 1;
|
||||
margin-top: 0;
|
||||
transition: all .3s;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.slide-list {
|
||||
@@ -466,7 +395,8 @@ a {
|
||||
|
||||
.book {
|
||||
@extend .anim;
|
||||
@apply p-3 rounded-md relative cursor-pointer bg-third hover:bg-card-active flex flex-col justify-between shrink-0;
|
||||
@apply p-3 rounded-md relative cursor-pointer hover:bg-fifth flex flex-col justify-between shrink-0;
|
||||
background: var(--bg-book);
|
||||
$w: 7rem;
|
||||
width: $w;
|
||||
height: calc($w * 1.4);
|
||||
@@ -476,7 +406,6 @@ a {
|
||||
width: 100%;
|
||||
border-bottom: 1px solid var(--color-item-border);
|
||||
@apply hover:text-blue-700;
|
||||
|
||||
}
|
||||
|
||||
.line-white {
|
||||
@@ -501,31 +430,19 @@ a {
|
||||
}
|
||||
|
||||
@keyframes underline {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
border-left: .1rem solid var(--color-article);
|
||||
border-left: 0.1rem solid var(--color-article);
|
||||
}
|
||||
|
||||
50% {
|
||||
border-left: .1rem solid transparent;
|
||||
border-left: 0.1rem solid transparent;
|
||||
}
|
||||
}
|
||||
|
||||
#typing-listener {
|
||||
position: fixed;
|
||||
left: -9999px;
|
||||
top: -9999px;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
opacity: 0.01;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
@apply fixed left-[-9999px] top-[-9999px] w-1 h-1 opacity-0.01 z-[-1] pointer-events-none border-none outline-none bg-transparent text-transparent;
|
||||
font-size: 16px; // 防止iOS缩放
|
||||
color: transparent; // 文字透明
|
||||
}
|
||||
|
||||
.btn-no-margin {
|
||||
@@ -536,5 +453,5 @@ a {
|
||||
|
||||
.target-number {
|
||||
@apply text-3xl!;
|
||||
color: rgb(176, 116, 211)!important;
|
||||
color: rgb(176, 116, 211) !important;
|
||||
}
|
||||
|
||||
@@ -33,14 +33,9 @@ provide('tabIndex', computed(() => tabIndex))
|
||||
<style scoped lang="scss">
|
||||
|
||||
.panel {
|
||||
border-radius: .5rem;
|
||||
width: var(--panel-width);
|
||||
background: var(--color-second);
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid var(--color-item-border);
|
||||
box-shadow: var(--shadow);
|
||||
@apply shadow-lg flex flex-col h-full rounded-xl;
|
||||
}
|
||||
|
||||
// 移动端适配
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
<template>
|
||||
<div class="inline-flex w-full relative"
|
||||
:class="[disabled && 'disabled']"
|
||||
>
|
||||
<div class="inline-flex w-full relative" :class="[disabled && 'disabled']">
|
||||
<textarea
|
||||
ref="textareaRef"
|
||||
v-model="innerValue"
|
||||
:placeholder="placeholder"
|
||||
:maxlength="maxlength"
|
||||
:rows="rows"
|
||||
:disabled="disabled"
|
||||
:style="textareaStyle"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md outline-none resize-none transition-colors duration-200 box-border"
|
||||
@input="handleInput"
|
||||
ref="textareaRef"
|
||||
v-model="innerValue"
|
||||
:placeholder="placeholder"
|
||||
:maxlength="maxlength"
|
||||
:rows="rows"
|
||||
:disabled="disabled"
|
||||
:style="textareaStyle"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md outline-none resize-none transition-colors duration-200 box-border"
|
||||
@input="handleInput"
|
||||
/>
|
||||
<!-- 字数统计 -->
|
||||
<span
|
||||
v-if="showWordLimit && maxlength"
|
||||
class="absolute bottom-1 right-2 text-xs text-gray-400 select-none"
|
||||
v-if="showWordLimit && maxlength"
|
||||
class="absolute bottom-1 right-2 text-xs text-gray-400 select-none"
|
||||
>
|
||||
{{ innerValue.length }} / {{ maxlength }}
|
||||
</span>
|
||||
@@ -24,36 +22,38 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, watch, computed, nextTick} from "vue"
|
||||
|
||||
import { ref, watch, computed, nextTick } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string,
|
||||
placeholder?: string,
|
||||
maxlength?: number,
|
||||
rows?: number,
|
||||
modelValue: string
|
||||
placeholder?: string
|
||||
maxlength?: number
|
||||
rows?: number
|
||||
autosize: boolean | { minRows?: number; maxRows?: number }
|
||||
showWordLimit?: boolean
|
||||
disabled?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(["update:modelValue"])
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const innerValue = ref(props.modelValue ?? "")
|
||||
watch(() => props.modelValue, v => (innerValue.value = v ?? ""))
|
||||
const innerValue = ref(props.modelValue ?? '')
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
v => (innerValue.value = v ?? '')
|
||||
)
|
||||
|
||||
const textareaRef = ref<HTMLTextAreaElement>()
|
||||
|
||||
// 样式(用于控制高度)
|
||||
const textareaStyle = computed(() => {
|
||||
return props.autosize ? {height: "auto"} : {}
|
||||
return props.autosize ? { height: 'auto' } : {}
|
||||
})
|
||||
|
||||
// 输入处理
|
||||
const handleInput = (e: Event) => {
|
||||
const val = (e.target as HTMLTextAreaElement).value
|
||||
innerValue.value = val
|
||||
emit("update:modelValue", val)
|
||||
emit('update:modelValue', val)
|
||||
if (props.autosize) nextTick(resizeTextarea)
|
||||
}
|
||||
|
||||
@@ -61,36 +61,38 @@ const handleInput = (e: Event) => {
|
||||
const resizeTextarea = () => {
|
||||
if (!textareaRef.value) return
|
||||
const el = textareaRef.value
|
||||
el.style.height = "auto"
|
||||
el.style.height = 'auto'
|
||||
let height = el.scrollHeight
|
||||
let overflow = "hidden"
|
||||
let overflow = 'hidden'
|
||||
|
||||
if (typeof props.autosize === "object") {
|
||||
const {minRows, maxRows} = props.autosize
|
||||
if (typeof props.autosize === 'object') {
|
||||
const { minRows, maxRows } = props.autosize
|
||||
const lineHeight = 24 // 行高约等于 24px
|
||||
if (minRows) height = Math.max(height, minRows * lineHeight)
|
||||
if (maxRows) {
|
||||
const maxHeight = maxRows * lineHeight
|
||||
if (height > maxHeight) {
|
||||
height = maxHeight
|
||||
overflow = "auto" // 超出时允许滚动
|
||||
overflow = 'auto' // 超出时允许滚动
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
el.style.height = height + "px"
|
||||
el.style.height = height + 'px'
|
||||
el.style.overflowY = overflow
|
||||
}
|
||||
|
||||
watch(innerValue, () => {
|
||||
if (props.autosize) nextTick(resizeTextarea)
|
||||
}, {immediate: true})
|
||||
|
||||
watch(
|
||||
innerValue,
|
||||
() => {
|
||||
if (props.autosize) nextTick(resizeTextarea)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.disabled {
|
||||
opacity: 0.5;
|
||||
|
||||
textarea {
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ const selectHandler = inject('selectHandler', null);
|
||||
|
||||
// 计算当前选项是否被选中
|
||||
const isSelected = computed(() => {
|
||||
return selectValue === props.value;
|
||||
return selectValue.value === props.value;
|
||||
});
|
||||
|
||||
// 点击选项时调用ElSelect提供的方法
|
||||
@@ -45,20 +45,16 @@ watch(() => props.value, () => {}, { immediate: true });
|
||||
|
||||
<style scoped lang="scss">
|
||||
.option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.2rem 1rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
@apply flex items-center px-2 py-1 cursor-pointer transition-all duration-300;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-third);
|
||||
background-color: var(--color-fourth);
|
||||
}
|
||||
|
||||
&.is-selected {
|
||||
color: var(--color-select-bg);
|
||||
font-weight: bold;
|
||||
background-color: var(--color-third);
|
||||
background-color: var(--color-fifth);
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
|
||||
@@ -1,197 +1,185 @@
|
||||
<script setup lang="ts">
|
||||
import {computed, nextTick, onBeforeUnmount, onMounted, provide, ref, useAttrs, useSlots, VNode, watch} from 'vue';
|
||||
import {useWindowClick} from "@/hooks/event.ts";
|
||||
import {
|
||||
computed,
|
||||
nextTick,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
provide,
|
||||
ref,
|
||||
useAttrs,
|
||||
useSlots,
|
||||
VNode,
|
||||
watch,
|
||||
} from 'vue'
|
||||
import { useWindowClick } from '@/hooks/event.ts'
|
||||
|
||||
interface Option {
|
||||
label: string;
|
||||
value: any;
|
||||
disabled?: boolean;
|
||||
label: string
|
||||
value: any
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: any;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
options?: Option[];
|
||||
}>();
|
||||
modelValue: any
|
||||
placeholder?: string
|
||||
disabled?: boolean
|
||||
options?: Option[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
const attrs = useAttrs();
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const attrs = useAttrs()
|
||||
|
||||
const isOpen = ref(false);
|
||||
const isReverse = ref(false);
|
||||
const dropdownStyle = ref({}); // Teleport 用的样式
|
||||
const selectedOption = ref<Option | null>(null);
|
||||
const selectRef = ref<HTMLDivElement | null>(null);
|
||||
const dropdownRef = ref<HTMLDivElement | null>(null);
|
||||
const slots = useSlots();
|
||||
const isOpen = ref(false)
|
||||
const isReverse = ref(false)
|
||||
const dropdownStyle = ref({}) // Teleport 用的样式
|
||||
const selectedOption = ref<Option | null>(null)
|
||||
const selectRef = ref<HTMLDivElement | null>(null)
|
||||
const dropdownRef = ref<HTMLDivElement | null>(null)
|
||||
const slots = useSlots()
|
||||
|
||||
const displayValue = computed(() => {
|
||||
return selectedOption.value
|
||||
? selectedOption.value.label
|
||||
: props.placeholder || '请选择';
|
||||
});
|
||||
return selectedOption.value ? selectedOption.value.label : props.placeholder || '请选择'
|
||||
})
|
||||
|
||||
const updateDropdownPosition = async () => {
|
||||
if (!selectRef.value || !dropdownRef.value) return;
|
||||
if (!selectRef.value || !dropdownRef.value) return
|
||||
|
||||
// 等待 DOM 完全渲染(尤其是下拉框高度)
|
||||
await nextTick();
|
||||
await new Promise(requestAnimationFrame);
|
||||
await nextTick()
|
||||
await new Promise(requestAnimationFrame)
|
||||
|
||||
const rect = selectRef.value.getBoundingClientRect();
|
||||
const dropdownHeight = dropdownRef.value.offsetHeight;
|
||||
const spaceBelow = window.innerHeight - rect.bottom;
|
||||
const spaceAbove = rect.top;
|
||||
const rect = selectRef.value.getBoundingClientRect()
|
||||
const dropdownHeight = dropdownRef.value.offsetHeight
|
||||
const spaceBelow = window.innerHeight - rect.bottom
|
||||
const spaceAbove = rect.top
|
||||
|
||||
isReverse.value = spaceBelow < dropdownHeight && spaceAbove > spaceBelow;
|
||||
isReverse.value = spaceBelow < dropdownHeight && spaceAbove > spaceBelow
|
||||
|
||||
dropdownStyle.value = {
|
||||
position: 'fixed',
|
||||
left: rect.left + 'px',
|
||||
width: rect.width + 'px',
|
||||
top: !isReverse.value
|
||||
? rect.bottom + 5 + 'px'
|
||||
: 'auto',
|
||||
bottom: isReverse.value
|
||||
? window.innerHeight - rect.top + 5 + 'px'
|
||||
: 'auto',
|
||||
zIndex: 9999
|
||||
};
|
||||
};
|
||||
top: !isReverse.value ? rect.bottom + 5 + 'px' : 'auto',
|
||||
bottom: isReverse.value ? window.innerHeight - rect.top + 5 + 'px' : 'auto',
|
||||
zIndex: 9999,
|
||||
}
|
||||
}
|
||||
|
||||
const toggleDropdown = async () => {
|
||||
if (props.disabled) return;
|
||||
if (props.disabled) return
|
||||
|
||||
isOpen.value = !isOpen.value;
|
||||
isOpen.value = !isOpen.value
|
||||
|
||||
if (isOpen.value) {
|
||||
await nextTick();
|
||||
await new Promise(requestAnimationFrame);
|
||||
await updateDropdownPosition();
|
||||
await nextTick()
|
||||
await new Promise(requestAnimationFrame)
|
||||
await updateDropdownPosition()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const selectOption = (value: any, label: string) => {
|
||||
selectedOption.value = {value, label};
|
||||
emit('update:modelValue', value);
|
||||
isOpen.value = false;
|
||||
};
|
||||
selectedOption.value = { value, label }
|
||||
emit('update:modelValue', value)
|
||||
isOpen.value = false
|
||||
}
|
||||
|
||||
let selectValue = $ref(props.modelValue);
|
||||
let selectValue = ref(props.modelValue)
|
||||
|
||||
provide('selectValue', selectValue);
|
||||
provide('selectHandler', selectOption);
|
||||
provide('selectValue', selectValue)
|
||||
provide('selectHandler', selectOption)
|
||||
|
||||
useWindowClick((e: PointerEvent) => {
|
||||
if (!e) return;
|
||||
if (!e) return
|
||||
if (
|
||||
selectRef.value &&
|
||||
!selectRef.value.contains(e.target as Node) &&
|
||||
dropdownRef.value &&
|
||||
!dropdownRef.value.contains(e.target as Node)
|
||||
selectRef.value &&
|
||||
!selectRef.value.contains(e.target as Node) &&
|
||||
dropdownRef.value &&
|
||||
!dropdownRef.value.contains(e.target as Node)
|
||||
) {
|
||||
isOpen.value = false;
|
||||
isOpen.value = false
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
selectValue = newValue;
|
||||
if (slots.default) {
|
||||
let slot = slots.default();
|
||||
let list = [];
|
||||
if (slot.length === 1) {
|
||||
list = Array.from(slot[0].children as Array<VNode>);
|
||||
} else {
|
||||
list = slot;
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
newValue => {
|
||||
selectValue.value = newValue
|
||||
if (slots.default) {
|
||||
let slot = slots.default()
|
||||
let list = []
|
||||
if (slot.length === 1) {
|
||||
list = Array.from(slot[0].children as Array<VNode>)
|
||||
} else {
|
||||
list = slot
|
||||
}
|
||||
const option = list.find(opt => opt.props.value === newValue)
|
||||
if (option) {
|
||||
selectedOption.value = option.props
|
||||
}
|
||||
return
|
||||
}
|
||||
const option = list.find(opt => opt.props.value === newValue);
|
||||
if (option) {
|
||||
selectedOption.value = option.props;
|
||||
if (props.options) {
|
||||
const option = props.options.find(opt => opt.value === newValue)
|
||||
if (option) {
|
||||
selectedOption.value = option
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (props.options) {
|
||||
const option = props.options.find(opt => opt.value === newValue);
|
||||
if (option) {
|
||||
selectedOption.value = option;
|
||||
}
|
||||
}
|
||||
}, {immediate: true});
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(() => props.options, (newOptions) => {
|
||||
if (newOptions && props.modelValue) {
|
||||
const option = newOptions.find(opt => opt.value === props.modelValue);
|
||||
if (option) {
|
||||
selectedOption.value = option;
|
||||
watch(
|
||||
() => props.options,
|
||||
newOptions => {
|
||||
if (newOptions && props.modelValue) {
|
||||
const option = newOptions.find(opt => opt.value === props.modelValue)
|
||||
if (option) {
|
||||
selectedOption.value = option
|
||||
}
|
||||
}
|
||||
}
|
||||
}, {immediate: true});
|
||||
|
||||
const handleOptionClick = (option: Option) => {
|
||||
if (option.disabled) return;
|
||||
selectOption(option.value, option.label);
|
||||
};
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const onScrollOrResize = () => {
|
||||
if (isOpen.value) updateDropdownPosition();
|
||||
};
|
||||
if (isOpen.value) updateDropdownPosition()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('scroll', onScrollOrResize, true);
|
||||
window.addEventListener('resize', onScrollOrResize);
|
||||
});
|
||||
window.addEventListener('scroll', onScrollOrResize, true)
|
||||
window.addEventListener('resize', onScrollOrResize)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('scroll', onScrollOrResize, true);
|
||||
window.removeEventListener('resize', onScrollOrResize);
|
||||
});
|
||||
window.removeEventListener('scroll', onScrollOrResize, true)
|
||||
window.removeEventListener('resize', onScrollOrResize)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="select"
|
||||
v-bind="attrs"
|
||||
:class="{ 'is-disabled': disabled, 'is-active': isOpen, 'is-reverse': isReverse }"
|
||||
ref="selectRef"
|
||||
>
|
||||
<div class="select__wrapper" @click="toggleDropdown">
|
||||
<div class="select" ref="selectRef">
|
||||
<div
|
||||
class="select__wrapper"
|
||||
:class="{ disabled: disabled, active: isOpen }"
|
||||
@click="toggleDropdown"
|
||||
>
|
||||
<div class="select__label" :class="{ 'is-placeholder': !selectedOption }">
|
||||
{{ displayValue }}
|
||||
</div>
|
||||
<div class="select__suffix">
|
||||
<IconFluentChevronLeft20Filled
|
||||
class="arrow"
|
||||
:class="{ 'is-reverse': isOpen }"
|
||||
width="16"
|
||||
/>
|
||||
</div>
|
||||
<IconFluentChevronLeft20Filled
|
||||
class="select__arrow"
|
||||
:class="{ 'is-reverse': isOpen }"
|
||||
width="16"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<teleport to="body">
|
||||
<transition :name="isReverse ? 'zoom-in-bottom' : 'zoom-in-top'" :key="isReverse ? 'bottom' : 'top'">
|
||||
<div
|
||||
class="select__dropdown"
|
||||
v-if="isOpen"
|
||||
ref="dropdownRef"
|
||||
:style="dropdownStyle"
|
||||
>
|
||||
<ul class="select__options">
|
||||
<li
|
||||
v-if="options"
|
||||
v-for="(option, index) in options"
|
||||
:key="index"
|
||||
class="select__option"
|
||||
:class="{
|
||||
'is-selected': option.value === modelValue,
|
||||
'is-disabled': option.disabled
|
||||
}"
|
||||
@click="handleOptionClick(option)"
|
||||
>
|
||||
{{ option.label }}
|
||||
</li>
|
||||
<slot v-else></slot>
|
||||
</ul>
|
||||
<transition
|
||||
:name="isReverse ? 'zoom-in-bottom' : 'zoom-in-top'"
|
||||
:key="isReverse ? 'bottom' : 'top'"
|
||||
>
|
||||
<div class="select__dropdown" v-if="isOpen" ref="dropdownRef" :style="dropdownStyle">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</transition>
|
||||
</teleport>
|
||||
@@ -200,24 +188,27 @@ onBeforeUnmount(() => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.select {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
font-size: 1rem;
|
||||
@apply relative w-full;
|
||||
|
||||
&__wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
@apply flex items-center justify-between rounded-md cursor-pointer transition-all duration-300;
|
||||
height: 2rem;
|
||||
padding: 0 0.5rem;
|
||||
border: 1px solid var(--color-input-border);
|
||||
border-radius: 0.25rem;
|
||||
background-color: var(--color-input-bg, #fff);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-select-bg);
|
||||
&:not(.disabled):hover {
|
||||
border-color: #3c89e8;
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-color: #1668dc !important;
|
||||
box-shadow: 0 0 2px 1px #166ce4;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background: var(--color-fourth);
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,17 +223,12 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
}
|
||||
|
||||
&__suffix {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
&__arrow {
|
||||
color: #999;
|
||||
transform: rotate(-90deg);
|
||||
transition: transform 0.3s;
|
||||
|
||||
.arrow {
|
||||
transform: rotate(-90deg);
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.is-reverse {
|
||||
&.is-reverse {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
@@ -251,44 +237,17 @@ onBeforeUnmount(() => {
|
||||
.select__dropdown {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
background-color: var(--color-input-bg);
|
||||
border: 1px solid var(--color-input-border);
|
||||
background-color: var(--color-third);
|
||||
border-radius: 0.25rem;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.select__options {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.select__option {
|
||||
padding: 0.5rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-item-hover);
|
||||
}
|
||||
|
||||
&.is-selected {
|
||||
color: var(--color-select-bg);
|
||||
font-weight: bold;
|
||||
background-color: var(--color-item-active);
|
||||
}
|
||||
}
|
||||
|
||||
.is-disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
@apply shadow-lg;
|
||||
}
|
||||
|
||||
/* 往下展开的动画 */
|
||||
.zoom-in-top-enter-active,
|
||||
.zoom-in-top-leave-active {
|
||||
transition: transform 0.3s cubic-bezier(0.23, 1, 0.32, 1),
|
||||
opacity 0.3s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
transition:
|
||||
transform 0.3s cubic-bezier(0.23, 1, 0.32, 1),
|
||||
opacity 0.3s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
transform-origin: center top;
|
||||
}
|
||||
|
||||
@@ -301,8 +260,9 @@ onBeforeUnmount(() => {
|
||||
/* 往上展开的动画 */
|
||||
.zoom-in-bottom-enter-active,
|
||||
.zoom-in-bottom-leave-active {
|
||||
transition: transform 0.3s cubic-bezier(0.23, 1, 0.32, 1),
|
||||
opacity 0.3s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
transition:
|
||||
transform 0.3s cubic-bezier(0.23, 1, 0.32, 1),
|
||||
opacity 0.3s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
transform-origin: center bottom;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, watch } from "vue";
|
||||
import Tooltip from "@/components/base/Tooltip.vue";
|
||||
import { useEventListener } from "@/hooks/event.ts";
|
||||
import { onMounted, onUnmounted, watch } from 'vue'
|
||||
import Tooltip from '@/components/base/Tooltip.vue'
|
||||
import { useEventListener } from '@/hooks/event.ts'
|
||||
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import { useRuntimeStore } from "@/stores/runtime.ts";
|
||||
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;
|
||||
modelValue?: boolean
|
||||
showClose?: boolean
|
||||
title?: string
|
||||
content?: string
|
||||
fullScreen?: boolean
|
||||
padding?: boolean
|
||||
footer?: boolean
|
||||
header?: boolean
|
||||
confirmButtonText?: string
|
||||
cancelButtonText?: string,
|
||||
keyboard?: boolean,
|
||||
closeOnClickBg?: boolean,
|
||||
cancelButtonText?: string
|
||||
keyboard?: boolean
|
||||
closeOnClickBg?: boolean
|
||||
confirm?: any
|
||||
beforeClose?: any
|
||||
}
|
||||
@@ -32,15 +32,10 @@ const props = withDefaults(defineProps<ModalProps>(), {
|
||||
header: true,
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消',
|
||||
keyboard: true
|
||||
keyboard: true,
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'update:modelValue',
|
||||
'close',
|
||||
'ok',
|
||||
'cancel',
|
||||
])
|
||||
const emit = defineEmits(['update:modelValue', 'close', 'ok', 'cancel'])
|
||||
|
||||
let confirmButtonLoading = $ref(false)
|
||||
let zIndex = $ref(999)
|
||||
@@ -56,21 +51,21 @@ async function close() {
|
||||
return
|
||||
}
|
||||
if (props.beforeClose) {
|
||||
if (!await props.beforeClose()) {
|
||||
if (!(await props.beforeClose())) {
|
||||
return
|
||||
}
|
||||
}
|
||||
//记录停留时间,避免时间太短,弹框闪烁
|
||||
let stayTime = Date.now() - openTime;
|
||||
let closeTime = 300;
|
||||
let stayTime = Date.now() - openTime
|
||||
let closeTime = 300
|
||||
if (stayTime < 500) {
|
||||
closeTime += 500 - stayTime;
|
||||
closeTime += 500 - stayTime
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
maskRef?.classList.toggle('bounce-out');
|
||||
modalRef?.classList.toggle('bounce-out');
|
||||
}, 500 - stayTime);
|
||||
maskRef?.classList.toggle('bounce-out')
|
||||
modalRef?.classList.toggle('bounce-out')
|
||||
}, 500 - stayTime)
|
||||
|
||||
setTimeout(() => {
|
||||
emit('update:modelValue', false)
|
||||
@@ -82,20 +77,23 @@ async function close() {
|
||||
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()
|
||||
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(() => {
|
||||
if (props.modelValue === undefined) {
|
||||
@@ -139,32 +137,30 @@ 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="closeOnClickBg && close()"></div>
|
||||
<div class="modal"
|
||||
ref="modalRef"
|
||||
:class="[
|
||||
fullScreen?'full':'window'
|
||||
]"
|
||||
>
|
||||
<div class="modal-root" :style="{ 'z-index': zIndex }" v-if="visible">
|
||||
<div
|
||||
class="modal-mask"
|
||||
ref="maskRef"
|
||||
v-if="!fullScreen"
|
||||
@click.stop="closeOnClickBg && close()"
|
||||
></div>
|
||||
<div class="modal" ref="modalRef" :class="[fullScreen ? 'full' : 'window']">
|
||||
<Tooltip title="关闭">
|
||||
<IconFluentDismiss20Regular @click="close"
|
||||
v-if="showClose"
|
||||
class="close cursor-pointer"
|
||||
width="24"/>
|
||||
<IconFluentDismiss20Regular
|
||||
@click="close"
|
||||
v-if="showClose"
|
||||
class="close cursor-pointer"
|
||||
width="24"
|
||||
/>
|
||||
</Tooltip>
|
||||
<div class="modal-header" v-if="header">
|
||||
<div class="title">{{ props.title }}</div>
|
||||
</div>
|
||||
<div class="modal-body" :class="{padding}">
|
||||
<div class="modal-body" :class="{ padding }">
|
||||
<slot></slot>
|
||||
<div v-if="content" class="content max-h-60vh">{{ content }}</div>
|
||||
</div>
|
||||
@@ -174,10 +170,8 @@ async function cancel() {
|
||||
</div>
|
||||
<div class="right">
|
||||
<BaseButton type="info" @click="cancel">{{ cancelButtonText }}</BaseButton>
|
||||
<BaseButton
|
||||
id="dialog-ok"
|
||||
:loading="confirmButtonLoading"
|
||||
@click="ok">{{ confirmButtonText }}
|
||||
<BaseButton id="dialog-ok" :loading="confirmButtonLoading" @click="ok"
|
||||
>{{ confirmButtonText }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
@@ -187,12 +181,7 @@ async function cancel() {
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
|
||||
$modal-mask-bg: rgba(#000, .6);
|
||||
$radius: .5rem;
|
||||
$time: 0.3s;
|
||||
$header-height: 4rem;
|
||||
|
||||
@keyframes bounce-in {
|
||||
0% {
|
||||
@@ -224,38 +213,21 @@ $header-height: 4rem;
|
||||
}
|
||||
|
||||
.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;
|
||||
@apply fixed top-0 left-0 z-999 flex items-center justify-center w-full h-full overflow-hidden;
|
||||
|
||||
.modal-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: $modal-mask-bg;
|
||||
transition: background 0.3s;
|
||||
@apply fixed top-0 left-0 w-full h-full transition-all duration-300;
|
||||
background: rgba(#000, 0.6);
|
||||
animation: fade-in $time;
|
||||
|
||||
&.bounce-out {
|
||||
background: transparent;
|
||||
@apply bg-transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.window {
|
||||
//width: 75vw;
|
||||
//height: 70vh;
|
||||
box-shadow: var(--shadow);
|
||||
border-radius: $radius;
|
||||
animation: bounce-in $time ease-out;
|
||||
@apply shadow-lg rounded-lg;
|
||||
|
||||
&.bounce-out {
|
||||
opacity: 0;
|
||||
@@ -263,8 +235,7 @@ $header-height: 4rem;
|
||||
}
|
||||
|
||||
.full {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
@apply w-full h-full;
|
||||
animation: bounce-in-full $time ease-out;
|
||||
|
||||
&.bounce-out {
|
||||
@@ -274,61 +245,34 @@ $header-height: 4rem;
|
||||
}
|
||||
|
||||
.modal {
|
||||
position: relative;
|
||||
background: var(--color-second);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: transform $time, opacity $time;
|
||||
@apply relative bg-second overflow-hidden flex flex-col transition-all duration-300;
|
||||
|
||||
.close {
|
||||
position: absolute;
|
||||
right: 1.2rem;
|
||||
top: 1.2rem;
|
||||
z-index: 999;
|
||||
@apply absolute right-1.2rem top-1.2rem z-999;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--modal-padding);
|
||||
padding-bottom: 0;
|
||||
border-radius: $radius $radius 0 0;
|
||||
@apply flex justify-between items-center p-5 pb-0 rounded-t-lg;
|
||||
|
||||
.title {
|
||||
color: var(--color-font-1);
|
||||
font-weight: bold;
|
||||
font-size: 1.3rem;
|
||||
line-height: 1.8rem;
|
||||
@apply font-bold text-xl;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
box-sizing: border-box;
|
||||
color: var(--color-main-text);
|
||||
font-weight: 400;
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.7rem;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
@apply box-border text-main-text font-normal text-base leading-6 w-full flex-1 overflow-hidden flex;
|
||||
|
||||
&.padding {
|
||||
padding: .2rem var(--modal-padding);
|
||||
@apply p-1 px-5;
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 25rem;
|
||||
padding: .2rem 1.6rem 1.6rem;
|
||||
@apply w-64 p-2 px-4 pb-4;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: var(--modal-padding);
|
||||
@apply flex justify-between p-5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,7 +182,7 @@ defineExpose({scrollBottom})
|
||||
.list {
|
||||
.item {
|
||||
box-sizing: border-box;
|
||||
background: var(--color-item-bg);
|
||||
background: var(--color-second);
|
||||
color: var(--color-font-1);
|
||||
border-radius: .5rem;
|
||||
margin-bottom: .6rem;
|
||||
@@ -199,13 +199,14 @@ defineExpose({scrollBottom})
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--color-third);
|
||||
.right {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--color-item-active);
|
||||
background: var(--color-fourth);
|
||||
color: var(--color-font-1);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import { ShortcutKey } from "@/types/types.ts";
|
||||
import { SoundFileOptions } from "@/config/env.ts";
|
||||
import { getAudioFileUrl, usePlayAudio } from "@/hooks/sound.ts";
|
||||
import Switch from "@/components/base/Switch.vue";
|
||||
import { Option, Select } from "@/components/base/select";
|
||||
import Textarea from "@/components/base/Textarea.vue";
|
||||
import VolumeIcon from "@/components/icon/VolumeIcon.vue";
|
||||
import Slider from "@/components/base/Slider.vue";
|
||||
import SettingItem from "@/pages/setting/SettingItem.vue";
|
||||
import { useSettingStore } from "@/stores/setting.ts";
|
||||
import { useBaseStore } from "@/stores/base.ts";
|
||||
import { ShortcutKey } from '@/types/types.ts'
|
||||
import { SoundFileOptions } from '@/config/env.ts'
|
||||
import { getAudioFileUrl, usePlayAudio } from '@/hooks/sound.ts'
|
||||
import Switch from '@/components/base/Switch.vue'
|
||||
import { Option, Select } from '@/components/base/select'
|
||||
import Textarea from '@/components/base/Textarea.vue'
|
||||
import VolumeIcon from '@/components/icon/VolumeIcon.vue'
|
||||
import Slider from '@/components/base/Slider.vue'
|
||||
import SettingItem from '@/pages/setting/SettingItem.vue'
|
||||
import { useSettingStore } from '@/stores/setting.ts'
|
||||
import { useBaseStore } from '@/stores/base.ts'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const store = useBaseStore()
|
||||
@@ -19,11 +18,9 @@ const simpleWords = $computed({
|
||||
get: () => store.simpleWords.join(','),
|
||||
set: v => {
|
||||
try {
|
||||
store.simpleWords = v.split(',');
|
||||
} catch (e) {
|
||||
|
||||
}
|
||||
}
|
||||
store.simpleWords = v.split(',')
|
||||
} catch (e) {}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -32,81 +29,71 @@ const simpleWords = $computed({
|
||||
<!-- 通用练习设置-->
|
||||
<!-- 通用练习设置-->
|
||||
<div>
|
||||
<SettingItem title="忽略大小写"
|
||||
desc="开启后,输入时不区分大小写,如输入“hello”和“Hello”都会被认为是正确的"
|
||||
<SettingItem
|
||||
title="忽略大小写"
|
||||
desc="开启后,输入时不区分大小写,如输入“hello”和“Hello”都会被认为是正确的"
|
||||
>
|
||||
<Switch v-model="settingStore.ignoreCase"/>
|
||||
<Switch v-model="settingStore.ignoreCase" />
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem title="允许默写模式下显示提示"
|
||||
:desc="`开启后,可以通过将鼠标移动到单词上或者按快捷键 ${settingStore.shortcutKeyMap[ShortcutKey.ShowWord]} 显示正确答案`"
|
||||
<SettingItem
|
||||
title="允许默写模式下显示提示"
|
||||
:desc="`开启后,可以通过将鼠标移动到单词上或者按快捷键 ${settingStore.shortcutKeyMap[ShortcutKey.ShowWord]} 显示正确答案`"
|
||||
>
|
||||
<Switch v-model="settingStore.allowWordTip"/>
|
||||
<Switch v-model="settingStore.allowWordTip" />
|
||||
</SettingItem>
|
||||
|
||||
<div class="line"></div>
|
||||
<SettingItem title="简单词过滤"
|
||||
desc="开启后,练习的单词中不会包含简单词;文章统计的总词数中不会包含简单词"
|
||||
<SettingItem
|
||||
title="简单词过滤"
|
||||
desc="开启后,练习的单词中不会包含简单词;文章统计的总词数中不会包含简单词"
|
||||
>
|
||||
<Switch v-model="settingStore.ignoreSimpleWord"/>
|
||||
<Switch v-model="settingStore.ignoreSimpleWord" />
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem title="简单词列表"
|
||||
class="items-start!"
|
||||
v-if="settingStore.ignoreSimpleWord"
|
||||
>
|
||||
<Textarea
|
||||
placeholder="多个单词用英文逗号隔号"
|
||||
v-model="simpleWords" :autosize="{minRows: 6, maxRows: 10}"/>
|
||||
<SettingItem title="简单词列表" class="items-start!" v-if="settingStore.ignoreSimpleWord">
|
||||
<Textarea
|
||||
placeholder="多个单词用英文逗号隔号"
|
||||
v-model="simpleWords"
|
||||
:autosize="{ minRows: 6, maxRows: 10 }"
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<!-- 音效-->
|
||||
<!-- 音效-->
|
||||
<!-- 音效-->
|
||||
<div class="line"></div>
|
||||
<SettingItem main-title="音效"/>
|
||||
<SettingItem title="单词/句子发音口音"
|
||||
desc="仅单词生效,文章固定美音"
|
||||
>
|
||||
<Select v-model="settingStore.soundType"
|
||||
placeholder="请选择"
|
||||
class="w-50!"
|
||||
>
|
||||
<Option label="美音" value="us"/>
|
||||
<Option label="英音" value="uk"/>
|
||||
<SettingItem main-title="音效" />
|
||||
<SettingItem title="单词/句子发音口音" desc="仅单词生效,文章固定美音">
|
||||
<Select v-model="settingStore.soundType" placeholder="请选择" class="w-50!">
|
||||
<Option label="美音" value="us" />
|
||||
<Option label="英音" value="uk" />
|
||||
</Select>
|
||||
</SettingItem>
|
||||
|
||||
<div class="line"></div>
|
||||
<SettingItem title="按键音">
|
||||
<Switch v-model="settingStore.keyboardSound"/>
|
||||
<Switch v-model="settingStore.keyboardSound" />
|
||||
</SettingItem>
|
||||
<SettingItem title="按键音效">
|
||||
<Select v-model="settingStore.keyboardSoundFile"
|
||||
placeholder="请选择"
|
||||
class="w-50!"
|
||||
>
|
||||
<Select v-model="settingStore.keyboardSoundFile" placeholder="请选择" class="w-50!">
|
||||
<Option
|
||||
v-for="item in SoundFileOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
v-for="item in SoundFileOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
>
|
||||
<div class="flex justify-between items-center w-full">
|
||||
<span>{{ item.label }}</span>
|
||||
<VolumeIcon
|
||||
:time="100"
|
||||
@click="usePlayAudio(getAudioFileUrl(item.value)[0])"/>
|
||||
<VolumeIcon :time="100" @click="usePlayAudio(getAudioFileUrl(item.value)[0])" />
|
||||
</div>
|
||||
</Option>
|
||||
</Select>
|
||||
</SettingItem>
|
||||
<SettingItem title="音量">
|
||||
<Slider v-model="settingStore.keyboardSoundVolume" showText showValue unit="%"/>
|
||||
<Slider v-model="settingStore.keyboardSoundVolume" showText showValue unit="%" />
|
||||
</SettingItem>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { defineAsyncComponent } from "vue";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import CommonSetting from "@/components/setting/CommonSetting.vue";
|
||||
import WordSetting from "@/components/setting/WordSetting.vue";
|
||||
import ArticleSettting from "@/components/setting/ArticleSettting.vue";
|
||||
import ArticleSetting from "@/components/setting/ArticleSetting.vue";
|
||||
|
||||
const Dialog = defineAsyncComponent(() => import('@/components/dialog/Dialog.vue'))
|
||||
|
||||
@@ -39,7 +39,7 @@ let show = $ref(false)
|
||||
<div class="content">
|
||||
<CommonSetting v-if="tabIndex === 0"/>
|
||||
<WordSetting v-if="tabIndex === 1"/>
|
||||
<ArticleSettting v-if="tabIndex === 2"/>
|
||||
<ArticleSetting v-if="tabIndex === 2"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -309,7 +309,6 @@ function updateList(e) {
|
||||
height: 100vh;
|
||||
box-sizing: border-box;
|
||||
color: var(--color-font-1);
|
||||
background: var(--color-second);
|
||||
display: flex;
|
||||
|
||||
.close {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import { computed, onMounted, onUnmounted, provide, watch } from "vue";
|
||||
import { useBaseStore } from "@/stores/base.ts";
|
||||
import { emitter, EventKey, useEvents } from "@/utils/eventBus.ts";
|
||||
import { useSettingStore } from "@/stores/setting.ts";
|
||||
import { computed, onMounted, onUnmounted, provide, watch } from 'vue'
|
||||
import { useBaseStore } from '@/stores/base.ts'
|
||||
import { emitter, EventKey, useEvents } from '@/utils/eventBus.ts'
|
||||
import { useSettingStore } from '@/stores/setting.ts'
|
||||
import {
|
||||
Article,
|
||||
ArticleItem,
|
||||
@@ -13,37 +12,50 @@ import {
|
||||
PracticeArticleWordType,
|
||||
ShortcutKey,
|
||||
Statistics,
|
||||
Word
|
||||
} from "@/types/types.ts";
|
||||
import { useDisableEventListener, useOnKeyboardEventListener, useStartKeyboardEventListener } from "@/hooks/event.ts";
|
||||
import useTheme from "@/hooks/theme.ts";
|
||||
Word,
|
||||
} from '@/types/types.ts'
|
||||
import {
|
||||
useDisableEventListener,
|
||||
useOnKeyboardEventListener,
|
||||
useStartKeyboardEventListener,
|
||||
} from '@/hooks/event.ts'
|
||||
import useTheme from '@/hooks/theme.ts'
|
||||
import Toast from '@/components/base/toast/Toast.ts'
|
||||
import { _getDictDataByUrl, _nextTick, cloneDeep, isMobile, loadJsLib, msToMinute, resourceWrap, total } from "@/utils";
|
||||
import { usePracticeStore } from "@/stores/practice.ts";
|
||||
import { useArticleOptions } from "@/hooks/dict.ts";
|
||||
import { genArticleSectionData, usePlaySentenceAudio } from "@/hooks/article.ts";
|
||||
import { getDefaultArticle, getDefaultDict, getDefaultWord } from "@/types/func.ts";
|
||||
import TypingArticle from "@/pages/article/components/TypingArticle.vue";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import Panel from "@/components/Panel.vue";
|
||||
import ArticleList from "@/components/list/ArticleList.vue";
|
||||
import EditSingleArticleModal from "@/pages/article/components/EditSingleArticleModal.vue";
|
||||
import Tooltip from "@/components/base/Tooltip.vue";
|
||||
import ConflictNotice from "@/components/ConflictNotice.vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import PracticeLayout from "@/components/PracticeLayout.vue";
|
||||
import ArticleAudio from "@/pages/article/components/ArticleAudio.vue";
|
||||
import { AppEnv, DICT_LIST, LIB_JS_URL, TourConfig } from "@/config/env.ts";
|
||||
import { addStat, setUserDictProp } from "@/apis";
|
||||
import { useRuntimeStore } from "@/stores/runtime.ts";
|
||||
import SettingDialog from "@/components/setting/SettingDialog.vue";
|
||||
import {PRACTICE_ARTICLE_CACHE} from "@/utils/cache.ts";
|
||||
import {
|
||||
_getDictDataByUrl,
|
||||
_nextTick,
|
||||
cloneDeep,
|
||||
isMobile,
|
||||
loadJsLib,
|
||||
msToMinute,
|
||||
resourceWrap,
|
||||
total,
|
||||
} from '@/utils'
|
||||
import { usePracticeStore } from '@/stores/practice.ts'
|
||||
import { useArticleOptions } from '@/hooks/dict.ts'
|
||||
import { genArticleSectionData, usePlaySentenceAudio } from '@/hooks/article.ts'
|
||||
import { getDefaultArticle, getDefaultDict, getDefaultWord } from '@/types/func.ts'
|
||||
import TypingArticle from '@/pages/article/components/TypingArticle.vue'
|
||||
import BaseIcon from '@/components/BaseIcon.vue'
|
||||
import Panel from '@/components/Panel.vue'
|
||||
import ArticleList from '@/components/list/ArticleList.vue'
|
||||
import EditSingleArticleModal from '@/pages/article/components/EditSingleArticleModal.vue'
|
||||
import Tooltip from '@/components/base/Tooltip.vue'
|
||||
import ConflictNotice from '@/components/ConflictNotice.vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import PracticeLayout from '@/components/PracticeLayout.vue'
|
||||
import ArticleAudio from '@/pages/article/components/ArticleAudio.vue'
|
||||
import { AppEnv, DICT_LIST, LIB_JS_URL, TourConfig } from '@/config/env.ts'
|
||||
import { addStat, setUserDictProp } from '@/apis'
|
||||
import { useRuntimeStore } from '@/stores/runtime.ts'
|
||||
import SettingDialog from '@/components/setting/SettingDialog.vue'
|
||||
import { PRACTICE_ARTICLE_CACHE } from '@/utils/cache.ts'
|
||||
|
||||
const store = useBaseStore()
|
||||
const runtimeStore = useRuntimeStore()
|
||||
const settingStore = useSettingStore()
|
||||
const statStore = usePracticeStore()
|
||||
const {toggleTheme} = useTheme()
|
||||
const { toggleTheme } = useTheme()
|
||||
|
||||
let articleData = $ref({
|
||||
list: [],
|
||||
@@ -82,9 +94,9 @@ function prev() {
|
||||
}
|
||||
}
|
||||
|
||||
const toggleShowTranslate = () => settingStore.translate = !settingStore.translate
|
||||
const toggleDictation = () => settingStore.dictation = !settingStore.dictation
|
||||
const togglePanel = () => settingStore.showPanel = !settingStore.showPanel
|
||||
const toggleShowTranslate = () => (settingStore.translate = !settingStore.translate)
|
||||
const toggleDictation = () => (settingStore.dictation = !settingStore.dictation)
|
||||
const togglePanel = () => (settingStore.showPanel = !settingStore.showPanel)
|
||||
const skip = () => typingArticleRef?.nextSentence()
|
||||
const collect = () => toggleArticleCollect(articleData.article)
|
||||
const shortcutKeyEdit = () => edit()
|
||||
@@ -144,59 +156,70 @@ const initAudio = () => {
|
||||
|
||||
const handleVolumeUpdate = (volume: number) => {
|
||||
settingStore.setState({
|
||||
articleSoundVolume: volume
|
||||
articleSoundVolume: volume,
|
||||
})
|
||||
}
|
||||
|
||||
const handleSpeedUpdate = (speed: number) => {
|
||||
settingStore.setState({
|
||||
articleSoundSpeed: speed
|
||||
articleSoundSpeed: speed,
|
||||
})
|
||||
}
|
||||
|
||||
watch([() => store.load, () => loading], ([a, b]) => {
|
||||
if (a && b) init()
|
||||
}, {immediate: true})
|
||||
watch(
|
||||
[() => store.load, () => loading],
|
||||
([a, b]) => {
|
||||
if (a && b) init()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(() => articleData?.article?.id, id => {
|
||||
if (id) {
|
||||
_nextTick(async () => {
|
||||
const Shepherd = await loadJsLib('Shepherd', LIB_JS_URL.SHEPHERD);
|
||||
const tour = new Shepherd.Tour(TourConfig);
|
||||
tour.on('cancel', () => {
|
||||
localStorage.setItem('tour-guide', '1');
|
||||
});
|
||||
tour.addStep({
|
||||
id: 'step8',
|
||||
text: '这里可以练习文章,只需要按下键盘上对应的按键即可,没有输入框!',
|
||||
attachTo: {
|
||||
element: '#article-content',
|
||||
on: 'auto'
|
||||
},
|
||||
buttons: [
|
||||
{
|
||||
text: `关闭`,
|
||||
action() {
|
||||
settingStore.first = false
|
||||
tour.next()
|
||||
setTimeout(() => {
|
||||
showConflictNotice = true
|
||||
}, 1500)
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
const r = localStorage.getItem('tour-guide');
|
||||
if (settingStore.first && !r && !isMobile()) {
|
||||
tour.start();
|
||||
}
|
||||
}, 500)
|
||||
watch(
|
||||
() => articleData?.article?.id,
|
||||
id => {
|
||||
if (id) {
|
||||
_nextTick(async () => {
|
||||
const Shepherd = await loadJsLib('Shepherd', LIB_JS_URL.SHEPHERD)
|
||||
const tour = new Shepherd.Tour(TourConfig)
|
||||
tour.on('cancel', () => {
|
||||
localStorage.setItem('tour-guide', '1')
|
||||
})
|
||||
tour.addStep({
|
||||
id: 'step8',
|
||||
text: '这里可以练习文章,只需要按下键盘上对应的按键即可,没有输入框!',
|
||||
attachTo: {
|
||||
element: '#article-content',
|
||||
on: 'auto',
|
||||
},
|
||||
buttons: [
|
||||
{
|
||||
text: `关闭`,
|
||||
action() {
|
||||
settingStore.first = false
|
||||
tour.next()
|
||||
setTimeout(() => {
|
||||
showConflictNotice = true
|
||||
}, 1500)
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
const r = localStorage.getItem('tour-guide')
|
||||
if (settingStore.first && !r && !isMobile()) {
|
||||
tour.start()
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
watch(() => settingStore.$state, (n) => {
|
||||
initAudio()
|
||||
}, {immediate: true, deep: true})
|
||||
watch(
|
||||
() => settingStore.$state,
|
||||
n => {
|
||||
initAudio()
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
if (store.sbook?.articles?.length) {
|
||||
@@ -251,19 +274,22 @@ function savePracticeData(init = true, regenerate = true) {
|
||||
regenerate && savePracticeData()
|
||||
}
|
||||
} else {
|
||||
localStorage.setItem(PRACTICE_ARTICLE_CACHE.key, JSON.stringify({
|
||||
version: PRACTICE_ARTICLE_CACHE.version,
|
||||
val: {
|
||||
practiceData: {
|
||||
sectionIndex: 0,
|
||||
sentenceIndex: 0,
|
||||
wordIndex: 0,
|
||||
stringIndex: 0,
|
||||
id: articleData.article.id
|
||||
localStorage.setItem(
|
||||
PRACTICE_ARTICLE_CACHE.key,
|
||||
JSON.stringify({
|
||||
version: PRACTICE_ARTICLE_CACHE.version,
|
||||
val: {
|
||||
practiceData: {
|
||||
sectionIndex: 0,
|
||||
sentenceIndex: 0,
|
||||
wordIndex: 0,
|
||||
stringIndex: 0,
|
||||
id: articleData.article.id,
|
||||
},
|
||||
statStoreData: statStore.$state,
|
||||
},
|
||||
statStoreData: statStore.$state,
|
||||
}
|
||||
}))
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,7 +303,7 @@ function setArticle(val: Article) {
|
||||
articleData.article = val
|
||||
let ignoreList = [store.allIgnoreWords, store.knownWords][settingStore.ignoreSimpleWord ? 0 : 1]
|
||||
articleData.article.sections.map((v, i) => {
|
||||
v.map((w) => {
|
||||
v.map(w => {
|
||||
w.words.map(s => {
|
||||
if (!ignoreList.includes(s.word.toLowerCase()) && s.type === PracticeArticleWordType.Word) {
|
||||
statStore.total++
|
||||
@@ -305,7 +331,7 @@ async function complete() {
|
||||
}, 1500)
|
||||
|
||||
//todo 有空了改成实时保存
|
||||
let data: Partial<Statistics> & { title: string, articleId: number } = {
|
||||
let data: Partial<Statistics> & { title: string; articleId: number } = {
|
||||
articleId: articleData.article.id,
|
||||
title: articleData.article.title,
|
||||
spend: statStore.spend,
|
||||
@@ -321,7 +347,7 @@ async function complete() {
|
||||
complete: store.sbook.complete,
|
||||
title: articleData.article.title,
|
||||
spend: Number(statStore.spend / 1000 / 60).toFixed(1),
|
||||
s: ''
|
||||
s: '',
|
||||
}
|
||||
reportData.s = `name:${store.sbook.name},title:${store.sbook.lastLearnIndex}.${data.title},spend:${Number(statStore.spend / 1000 / 60).toFixed(1)}`
|
||||
window.umami?.track('endStudyArticle', reportData)
|
||||
@@ -331,7 +357,8 @@ async function complete() {
|
||||
}
|
||||
if (AppEnv.CAN_REQUEST) {
|
||||
let res = await addStat({
|
||||
...data, type: 'article',
|
||||
...data,
|
||||
type: 'article',
|
||||
complete: store.sdict.complete,
|
||||
})
|
||||
if (!res.success) {
|
||||
@@ -379,7 +406,7 @@ function edit(val: Article = articleData.article) {
|
||||
}
|
||||
|
||||
function wrong(word: Word) {
|
||||
let temp = word.word.toLowerCase();
|
||||
let temp = word.word.toLowerCase()
|
||||
//过滤简单词
|
||||
if (settingStore.ignoreSimpleWord) {
|
||||
if (store.simpleWords.includes(temp)) return
|
||||
@@ -396,7 +423,10 @@ function wrong(word: Word) {
|
||||
}
|
||||
|
||||
function nextWord(word: ArticleWord) {
|
||||
if (!store.allIgnoreWords.includes(word.word.toLowerCase()) && word.type === PracticeArticleWordType.Word) {
|
||||
if (
|
||||
!store.allIgnoreWords.includes(word.word.toLowerCase()) &&
|
||||
word.type === PracticeArticleWordType.Word
|
||||
) {
|
||||
statStore.inputWordNumber++
|
||||
}
|
||||
}
|
||||
@@ -425,10 +455,7 @@ const handlePlayNext = (nextArticle: Article) => {
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
isArticleCollect,
|
||||
toggleArticleCollect
|
||||
} = useArticleOptions()
|
||||
const { isArticleCollect, toggleArticleCollect } = useArticleOptions()
|
||||
|
||||
function play() {
|
||||
typingArticleRef?.play()
|
||||
@@ -473,7 +500,6 @@ useEvents([
|
||||
[ShortcutKey.EditArticle, shortcutKeyEdit],
|
||||
])
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
isFocus = !document.hidden
|
||||
@@ -484,7 +510,7 @@ onUnmounted(() => {
|
||||
timer && clearInterval(timer)
|
||||
})
|
||||
|
||||
const {playSentenceAudio} = usePlaySentenceAudio()
|
||||
const { playSentenceAudio } = usePlaySentenceAudio()
|
||||
|
||||
function play2(e) {
|
||||
_nextTick(() => {
|
||||
@@ -504,9 +530,7 @@ const currentPractice = computed(() => {
|
||||
provide('currentPractice', currentPractice)
|
||||
</script>
|
||||
<template>
|
||||
<PracticeLayout
|
||||
v-loading="loading"
|
||||
panelLeft="var(--article-panel-margin-left)">
|
||||
<PracticeLayout v-loading="loading" panelLeft="var(--article-panel-margin-left)">
|
||||
<template v-slot:practice>
|
||||
<TypingArticle
|
||||
ref="typingArticleRef"
|
||||
@@ -520,11 +544,12 @@ provide('currentPractice', currentPractice)
|
||||
/>
|
||||
</template>
|
||||
<template v-slot:panel>
|
||||
<Panel :style="{width:'var(--article-panel-width)'}">
|
||||
<Panel :style="{ width: 'var(--article-panel-width)' }">
|
||||
<template v-slot:title>
|
||||
<span>{{
|
||||
store.sbook.name
|
||||
}} ({{ store.sbook.lastLearnIndex + 1 }} / {{ articleData.list.length }})</span>
|
||||
<span
|
||||
>{{ store.sbook.name }} ({{ store.sbook.lastLearnIndex + 1 }} /
|
||||
{{ articleData.list.length }})</span
|
||||
>
|
||||
</template>
|
||||
<div class="panel-page-item pl-4">
|
||||
<ArticleList
|
||||
@@ -532,26 +557,30 @@ provide('currentPractice', currentPractice)
|
||||
:static="false"
|
||||
:show-translate="settingStore.translate"
|
||||
@click="changeArticle"
|
||||
:active-id="articleData.article.id??''"
|
||||
:list="articleData.list ">
|
||||
:active-id="articleData.article.id ?? ''"
|
||||
:list="articleData.list"
|
||||
>
|
||||
</ArticleList>
|
||||
</div>
|
||||
</Panel>
|
||||
</template>
|
||||
<template v-slot:footer>
|
||||
<div class="footer">
|
||||
<Tooltip :title="settingStore.showToolbar?'收起':'展开'">
|
||||
<Tooltip :title="settingStore.showToolbar ? '收起' : '展开'">
|
||||
<IconFluentChevronLeft20Filled
|
||||
@click="settingStore.showToolbar = !settingStore.showToolbar"
|
||||
class="arrow"
|
||||
:class="!settingStore.showToolbar && 'down'"
|
||||
color="#999"/>
|
||||
color="#999"
|
||||
/>
|
||||
</Tooltip>
|
||||
<div class="bottom">
|
||||
<div class="flex justify-between items-center gap-2">
|
||||
<div class="stat">
|
||||
<div class="row">
|
||||
<div class="num">{{ currentPractice.length }}次/{{ msToMinute(total(currentPractice, 'spend')) }}</div>
|
||||
<div class="num">
|
||||
{{ currentPractice.length }}次/{{ msToMinute(total(currentPractice, 'spend')) }}
|
||||
</div>
|
||||
<div class="line"></div>
|
||||
<div class="name">记录</div>
|
||||
</div>
|
||||
@@ -564,10 +593,12 @@ provide('currentPractice', currentPractice)
|
||||
<div class="num center gap-1">
|
||||
{{ statStore.total }}
|
||||
<Tooltip>
|
||||
<IconFluentQuestionCircle20Regular width="18"/>
|
||||
<IconFluentQuestionCircle20Regular width="18" />
|
||||
<template #reference>
|
||||
<div>
|
||||
统计词数{{ settingStore.ignoreSimpleWord ? '不包含' : '包含' }}简单词,不包含已掌握
|
||||
统计词数{{
|
||||
settingStore.ignoreSimpleWord ? '不包含' : '包含'
|
||||
}}简单词,不包含已掌握
|
||||
<div>简单词可在设置 -> 练习设置 -> 简单词过滤中修改</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -587,31 +618,34 @@ provide('currentPractice', currentPractice)
|
||||
></ArticleAudio>
|
||||
<div class="flex flex-col items-center justify-center gap-1">
|
||||
<div class="flex gap-2 center">
|
||||
<SettingDialog type="article"/>
|
||||
<SettingDialog type="article" />
|
||||
|
||||
<BaseIcon
|
||||
:title="`下一句(${settingStore.shortcutKeyMap[ShortcutKey.Next]})`"
|
||||
@click="skip">
|
||||
<IconFluentArrowBounce20Regular class="transform-rotate-180"/>
|
||||
@click="skip"
|
||||
>
|
||||
<IconFluentArrowBounce20Regular class="transform-rotate-180" />
|
||||
</BaseIcon>
|
||||
<BaseIcon
|
||||
:title="`播放当前句子(${settingStore.shortcutKeyMap[ShortcutKey.PlayWordPronunciation]})`"
|
||||
@click="play">
|
||||
<IconFluentReplay20Regular/>
|
||||
@click="play"
|
||||
>
|
||||
<IconFluentReplay20Regular />
|
||||
</BaseIcon>
|
||||
<BaseIcon
|
||||
@click="settingStore.dictation = !settingStore.dictation"
|
||||
:title="`开关默写模式(${settingStore.shortcutKeyMap[ShortcutKey.ToggleDictation]})`"
|
||||
>
|
||||
<IconFluentEyeOff16Regular v-if="settingStore.dictation"/>
|
||||
<IconFluentEye16Regular v-else/>
|
||||
<IconFluentEyeOff16Regular v-if="settingStore.dictation" />
|
||||
<IconFluentEye16Regular v-else />
|
||||
</BaseIcon>
|
||||
|
||||
<BaseIcon
|
||||
:title="`开关释义显示(${settingStore.shortcutKeyMap[ShortcutKey.ToggleShowTranslate]})`"
|
||||
@click="settingStore.translate = !settingStore.translate">
|
||||
<IconFluentTranslate16Regular v-if="settingStore.translate"/>
|
||||
<IconFluentTranslateOff16Regular v-else/>
|
||||
@click="settingStore.translate = !settingStore.translate"
|
||||
>
|
||||
<IconFluentTranslate16Regular v-if="settingStore.translate" />
|
||||
<IconFluentTranslateOff16Regular v-else />
|
||||
</BaseIcon>
|
||||
|
||||
<!-- <BaseIcon-->
|
||||
@@ -621,8 +655,9 @@ provide('currentPractice', currentPractice)
|
||||
<!-- />-->
|
||||
<BaseIcon
|
||||
@click="settingStore.showPanel = !settingStore.showPanel"
|
||||
:title="`面板(${settingStore.shortcutKeyMap[ShortcutKey.TogglePanel]})`">
|
||||
<IconFluentTextListAbcUppercaseLtr20Regular/>
|
||||
:title="`面板(${settingStore.shortcutKeyMap[ShortcutKey.TogglePanel]})`"
|
||||
>
|
||||
<IconFluentTextListAbcUppercaseLtr20Regular />
|
||||
</BaseIcon>
|
||||
</div>
|
||||
</div>
|
||||
@@ -632,47 +667,33 @@ provide('currentPractice', currentPractice)
|
||||
</template>
|
||||
</PracticeLayout>
|
||||
|
||||
<EditSingleArticleModal
|
||||
v-model="showEditArticle"
|
||||
:article="editArticle"
|
||||
@save="saveArticle"
|
||||
/>
|
||||
<EditSingleArticleModal v-model="showEditArticle" :article="editArticle" @save="saveArticle" />
|
||||
|
||||
<ConflictNotice v-if="showConflictNotice"/>
|
||||
<ConflictNotice v-if="showConflictNotice" />
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.footer {
|
||||
width: var(--article-toolbar-width);
|
||||
|
||||
.bottom {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border-radius: .6rem;
|
||||
background: var(--color-second);
|
||||
padding: .5rem var(--space);
|
||||
z-index: 2;
|
||||
@apply relative w-full box-border rounded-lg bg-second shadow-lg z-2;
|
||||
padding: 0.5rem var(--space);
|
||||
border: 1px solid var(--color-item-border);
|
||||
box-shadow: var(--shadow);
|
||||
|
||||
.stat {
|
||||
margin-top: .5rem;
|
||||
margin-top: 0.5rem;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
gap: var(--stat-gap);
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: .3rem;
|
||||
color: gray;
|
||||
@apply flex flex-col items-center gap-1 text-gray-500;
|
||||
|
||||
.num, .name {
|
||||
.num,
|
||||
.name {
|
||||
word-break: keep-all;
|
||||
padding: 0 .4rem;
|
||||
padding: 0 0.4rem;
|
||||
}
|
||||
|
||||
.line {
|
||||
@@ -689,9 +710,9 @@ provide('currentPractice', currentPractice)
|
||||
top: -40%;
|
||||
left: 50%;
|
||||
cursor: pointer;
|
||||
transition: all .5s;
|
||||
transition: all 0.5s;
|
||||
transform: rotate(-90deg);
|
||||
padding: .5rem;
|
||||
padding: 0.5rem;
|
||||
font-size: 1.2rem;
|
||||
|
||||
&.down {
|
||||
|
||||
@@ -1,43 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import {Article, Sentence, TranslateEngine} from "@/types/types.ts";
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import EditAbleText from "@/components/EditAbleText.vue";
|
||||
import {getNetworkTranslate, getSentenceAllText, getSentenceAllTranslateText} from "@/hooks/translate.ts";
|
||||
import {genArticleSectionData, splitCNArticle2, splitEnArticle2, usePlaySentenceAudio} from "@/hooks/article.ts";
|
||||
import {_nextTick, _parseLRC, cloneDeep, last} from "@/utils";
|
||||
import {defineAsyncComponent, watch} from "vue";
|
||||
import Empty from "@/components/Empty.vue";
|
||||
import { Article, Sentence, TranslateEngine } from '@/types/types.ts'
|
||||
import BaseButton from '@/components/BaseButton.vue'
|
||||
import EditAbleText from '@/components/EditAbleText.vue'
|
||||
import {
|
||||
getNetworkTranslate,
|
||||
getSentenceAllText,
|
||||
getSentenceAllTranslateText,
|
||||
} from '@/hooks/translate.ts'
|
||||
import {
|
||||
genArticleSectionData,
|
||||
splitCNArticle2,
|
||||
splitEnArticle2,
|
||||
usePlaySentenceAudio,
|
||||
} from '@/hooks/article.ts'
|
||||
import { _nextTick, _parseLRC, cloneDeep, last } from '@/utils'
|
||||
import { defineAsyncComponent, watch } from 'vue'
|
||||
import Empty from '@/components/Empty.vue'
|
||||
import Toast from '@/components/base/toast/Toast.ts'
|
||||
import * as Comparison from "string-comparison"
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import {getDefaultArticle} from "@/types/func.ts";
|
||||
import copy from "copy-to-clipboard";
|
||||
import {Option, Select} from "@/components/base/select";
|
||||
import Tooltip from "@/components/base/Tooltip.vue";
|
||||
import InputNumber from "@/components/base/InputNumber.vue";
|
||||
import {nanoid} from "nanoid";
|
||||
import {update} from "idb-keyval";
|
||||
import ArticleAudio from "@/pages/article/components/ArticleAudio.vue";
|
||||
import BaseInput from "@/components/base/BaseInput.vue";
|
||||
import Textarea from "@/components/base/Textarea.vue";
|
||||
import {LOCAL_FILE_KEY} from "@/config/env.ts";
|
||||
import PopConfirm from "@/components/PopConfirm.vue";
|
||||
import * as Comparison from 'string-comparison'
|
||||
import BaseIcon from '@/components/BaseIcon.vue'
|
||||
import { getDefaultArticle } from '@/types/func.ts'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import { Option, Select } from '@/components/base/select'
|
||||
import Tooltip from '@/components/base/Tooltip.vue'
|
||||
import InputNumber from '@/components/base/InputNumber.vue'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { update } from 'idb-keyval'
|
||||
import ArticleAudio from '@/pages/article/components/ArticleAudio.vue'
|
||||
import BaseInput from '@/components/base/BaseInput.vue'
|
||||
import Textarea from '@/components/base/Textarea.vue'
|
||||
import { LOCAL_FILE_KEY } from '@/config/env.ts'
|
||||
import PopConfirm from '@/components/PopConfirm.vue'
|
||||
|
||||
const Dialog = defineAsyncComponent(() => import('@/components/dialog/Dialog.vue'))
|
||||
|
||||
interface IProps {
|
||||
article?: Article,
|
||||
article?: Article
|
||||
type?: 'single' | 'batch'
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<IProps>(), {
|
||||
article: () => getDefaultArticle(),
|
||||
type: 'single'
|
||||
type: 'single',
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
save: [val: Article],
|
||||
save: [val: Article]
|
||||
saveAndNext: [val: Article]
|
||||
}>()
|
||||
|
||||
@@ -47,23 +55,30 @@ let failCount = $ref(0)
|
||||
let textareaRef = $ref<HTMLTextAreaElement>()
|
||||
const TranslateEngineOptions = [
|
||||
// {value: 'youdao', label: '有道'},
|
||||
{value: 'baidu', label: '百度'},
|
||||
{ value: 'baidu', label: '百度' },
|
||||
]
|
||||
|
||||
let editArticle = $ref<Article>(getDefaultArticle())
|
||||
|
||||
watch(() => props.article, val => {
|
||||
editArticle = getDefaultArticle(val)
|
||||
progress = 0
|
||||
failCount = 0
|
||||
apply(false)
|
||||
}, {immediate: true})
|
||||
watch(
|
||||
() => props.article,
|
||||
val => {
|
||||
editArticle = getDefaultArticle(val)
|
||||
progress = 0
|
||||
failCount = 0
|
||||
apply(false)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(() => editArticle.text, (s) => {
|
||||
if (!s.trim()) {
|
||||
editArticle.sections = []
|
||||
watch(
|
||||
() => editArticle.text,
|
||||
s => {
|
||||
if (!s.trim()) {
|
||||
editArticle.sections = []
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
function apply(isHandle: boolean = true) {
|
||||
let text = editArticle.text.trim()
|
||||
@@ -141,11 +156,13 @@ function save(option: 'save' | 'saveAndNext') {
|
||||
return resolve(false)
|
||||
}
|
||||
|
||||
editArticle.lrcPosition = editArticle.sections.map(v => {
|
||||
return v.map((w, j) => {
|
||||
return w.audioPosition ?? []
|
||||
editArticle.lrcPosition = editArticle.sections
|
||||
.map(v => {
|
||||
return v.map((w, j) => {
|
||||
return w.audioPosition ?? []
|
||||
})
|
||||
})
|
||||
}).flat()
|
||||
.flat()
|
||||
|
||||
console.log(editArticle)
|
||||
|
||||
@@ -161,7 +178,7 @@ function save(option: 'save' | 'saveAndNext') {
|
||||
}
|
||||
|
||||
//不知道为什么直接用editArticle,取到是空的默认值
|
||||
defineExpose({save, getEditArticle: () => cloneDeep(editArticle)})
|
||||
defineExpose({ save, getEditArticle: () => cloneDeep(editArticle) })
|
||||
|
||||
// 处理音频文件上传
|
||||
async function handleAudioChange(e: any) {
|
||||
@@ -172,7 +189,7 @@ async function handleAudioChange(e: any) {
|
||||
file: uploadFile,
|
||||
}
|
||||
//把文件存到indexDB
|
||||
await update(LOCAL_FILE_KEY, (val) => {
|
||||
await update(LOCAL_FILE_KEY, val => {
|
||||
if (val) val.push(data)
|
||||
else val = [data]
|
||||
return val
|
||||
@@ -192,32 +209,34 @@ function handleChange(e: any) {
|
||||
if (!uploadFile) return
|
||||
|
||||
// 读取文件内容
|
||||
let reader = new FileReader();
|
||||
reader.readAsText(uploadFile, 'UTF-8');
|
||||
let reader = new FileReader()
|
||||
reader.readAsText(uploadFile, 'UTF-8')
|
||||
reader.onload = function (e) {
|
||||
let lrc: string = e.target.result as string;
|
||||
let lrc: string = e.target.result as string
|
||||
console.log(lrc)
|
||||
if (lrc.trim()) {
|
||||
let lrcList = _parseLRC(lrc)
|
||||
console.log('lrcList', lrcList)
|
||||
if (lrcList.length) {
|
||||
editArticle.lrcPosition = editArticle.sections.map((v, i) => {
|
||||
return v.map((w, j) => {
|
||||
for (let k = 0; k < lrcList.length; k++) {
|
||||
let s = lrcList[k]
|
||||
let d = Comparison.default.cosine.similarity(w.text, s.text)
|
||||
d = Comparison.default.levenshtein.similarity(w.text, s.text)
|
||||
d = Comparison.default.longestCommonSubsequence.similarity(w.text, s.text)
|
||||
// d = Comparison.default.metricLcs.similarity(w.text, s.text)
|
||||
// console.log(w.text, s.text, d)
|
||||
if (d >= 0.8) {
|
||||
w.audioPosition = [s.start, s.end ?? -1]
|
||||
break
|
||||
editArticle.lrcPosition = editArticle.sections
|
||||
.map((v, i) => {
|
||||
return v.map((w, j) => {
|
||||
for (let k = 0; k < lrcList.length; k++) {
|
||||
let s = lrcList[k]
|
||||
let d = Comparison.default.cosine.similarity(w.text, s.text)
|
||||
d = Comparison.default.levenshtein.similarity(w.text, s.text)
|
||||
d = Comparison.default.longestCommonSubsequence.similarity(w.text, s.text)
|
||||
// d = Comparison.default.metricLcs.similarity(w.text, s.text)
|
||||
// console.log(w.text, s.text, d)
|
||||
if (d >= 0.8) {
|
||||
w.audioPosition = [s.start, s.end ?? -1]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return w.audioPosition ?? []
|
||||
return w.audioPosition ?? []
|
||||
})
|
||||
})
|
||||
}).flat()
|
||||
.flat()
|
||||
|
||||
Toast.success('LRC文件解析成功')
|
||||
}
|
||||
@@ -238,12 +257,15 @@ let sentenceAudioRef = $ref<HTMLAudioElement>()
|
||||
let audioRef = $ref<HTMLAudioElement>()
|
||||
|
||||
let nameListRef = $ref<string[]>([])
|
||||
watch(() => showNameDialog, (v) => {
|
||||
if (v) {
|
||||
nameListRef = cloneDeep(Array.isArray(editArticle.nameList) ? editArticle.nameList : [])
|
||||
nameListRef.push('')
|
||||
watch(
|
||||
() => showNameDialog,
|
||||
v => {
|
||||
if (v) {
|
||||
nameListRef = cloneDeep(Array.isArray(editArticle.nameList) ? editArticle.nameList : [])
|
||||
nameListRef.push('')
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
function addName() {
|
||||
nameListRef.push('')
|
||||
@@ -287,7 +309,10 @@ function recordStart() {
|
||||
sentenceAudioRef.play()
|
||||
}
|
||||
editSentence.audioPosition[0] = Number(sentenceAudioRef.currentTime.toFixed(2))
|
||||
if (editSentence.audioPosition[0] > editSentence.audioPosition[1] && editSentence.audioPosition[1] !== 0) {
|
||||
if (
|
||||
editSentence.audioPosition[0] > editSentence.audioPosition[1] &&
|
||||
editSentence.audioPosition[1] !== 0
|
||||
) {
|
||||
editSentence.audioPosition[1] = editSentence.audioPosition[0]
|
||||
}
|
||||
}
|
||||
@@ -299,12 +324,14 @@ function recordEnd() {
|
||||
editSentence.audioPosition[1] = Number(sentenceAudioRef.currentTime.toFixed(2))
|
||||
}
|
||||
|
||||
const {playSentenceAudio} = usePlaySentenceAudio()
|
||||
const { playSentenceAudio } = usePlaySentenceAudio()
|
||||
|
||||
function saveLrcPosition() {
|
||||
// showEditAudioDialog = false
|
||||
currentSentence.audioPosition = cloneDeep(editSentence.audioPosition)
|
||||
editArticle.lrcPosition = editArticle.sections.map((v, i) => v.map((w, j) => (w.audioPosition ?? []))).flat()
|
||||
editArticle.lrcPosition = editArticle.sections
|
||||
.map((v, i) => v.map((w, j) => w.audioPosition ?? []))
|
||||
.flat()
|
||||
}
|
||||
|
||||
function jumpAudio(time: number) {
|
||||
@@ -347,7 +374,6 @@ function minusStartTime(val: Sentence) {
|
||||
if (val.audioPosition[0] <= 0) return
|
||||
val.audioPosition[0] = Number((val.audioPosition[0] - 0.3).toFixed(2))
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -358,7 +384,7 @@ function minusStartTime(val: Sentence) {
|
||||
<div class="shrink-0">标题:</div>
|
||||
<BaseInput
|
||||
v-model="editArticle.title"
|
||||
:disabled="![100,0].includes(progress)"
|
||||
:disabled="![100, 0].includes(progress)"
|
||||
type="text"
|
||||
placeholder="请填写原文标题"
|
||||
/>
|
||||
@@ -368,28 +394,36 @@ function minusStartTime(val: Sentence) {
|
||||
<Tooltip title="配置人名之后,在练习时自动忽略(可选,默认开启)">
|
||||
<div @click="showNameDialog = true" class="center gap-1 cp">
|
||||
<span>人名配置</span>
|
||||
<IconFluentSettings20Regular/>
|
||||
<IconFluentSettings20Regular />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Textarea v-model="editArticle.text"
|
||||
class="h-full"
|
||||
:disabled="![100,0].includes(progress)"
|
||||
placeholder="请复制原文"
|
||||
:autosize="false"/>
|
||||
<Textarea
|
||||
v-model="editArticle.text"
|
||||
class="h-full"
|
||||
:disabled="![100, 0].includes(progress)"
|
||||
placeholder="请复制原文"
|
||||
:autosize="false"
|
||||
/>
|
||||
<div class="justify-end items-center flex">
|
||||
<Tooltip>
|
||||
<IconFluentQuestionCircle20Regular class="mr-3" width="20"/>
|
||||
<IconFluentQuestionCircle20Regular class="mr-3" width="20" />
|
||||
<template #reference>
|
||||
<div>
|
||||
<div class="mb-2">使用方法</div>
|
||||
<ol class="py-0 pl-5 my-0 text-base color-main">
|
||||
<li>复制原文,然后分句</li>
|
||||
<li>点击 <span class="color-red font-bold">分句</span> 按钮进行自动分句<span
|
||||
class="color-red font-bold"> 或</span> 手动编辑分句
|
||||
<li>
|
||||
点击 <span class="color-red font-bold">分句</span> 按钮进行自动分句<span
|
||||
class="color-red font-bold"
|
||||
>
|
||||
或</span
|
||||
>
|
||||
手动编辑分句
|
||||
</li>
|
||||
<li>分句规则:一行一句,段落间空一行</li>
|
||||
<li>修改完成后点击 <span class="color-red font-bold">应用</span> 按钮同步到左侧结果栏
|
||||
<li>
|
||||
修改完成后点击 <span class="color-red font-bold">应用</span> 按钮同步到左侧结果栏
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
@@ -405,24 +439,25 @@ function minusStartTime(val: Sentence) {
|
||||
<div class="shrink-0">标题:</div>
|
||||
<BaseInput
|
||||
v-model="editArticle.titleTranslate"
|
||||
:disabled="![100,0].includes(progress)"
|
||||
:disabled="![100, 0].includes(progress)"
|
||||
type="text"
|
||||
placeholder="请填写翻译标题"
|
||||
/>
|
||||
</div>
|
||||
<div class="">正文:<span class="text-sm color-gray">一行一句,段落间空一行</span></div>
|
||||
<Textarea v-model="editArticle.textTranslate"
|
||||
class="h-full"
|
||||
:disabled="![100,0].includes(progress)"
|
||||
placeholder="请填写翻译"
|
||||
:autosize="false"/>
|
||||
<Textarea
|
||||
v-model="editArticle.textTranslate"
|
||||
class="h-full"
|
||||
:disabled="![100, 0].includes(progress)"
|
||||
placeholder="请填写翻译"
|
||||
:autosize="false"
|
||||
/>
|
||||
<div class="justify-between items-center flex">
|
||||
<div class="flex gap-space items-center w-50">
|
||||
<BaseButton @click="startNetworkTranslate"
|
||||
:loading="progress!==0 && progress !== 100">翻译
|
||||
<BaseButton @click="startNetworkTranslate" :loading="progress !== 0 && progress !== 100"
|
||||
>翻译
|
||||
</BaseButton>
|
||||
<Select v-model="networkTranslateEngine"
|
||||
>
|
||||
<Select v-model="networkTranslateEngine">
|
||||
<Option
|
||||
v-for="item in TranslateEngineOptions"
|
||||
:key="item.value"
|
||||
@@ -434,18 +469,26 @@ function minusStartTime(val: Sentence) {
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<Tooltip>
|
||||
<IconFluentQuestionCircle20Regular class="mr-3" width="20"/>
|
||||
<IconFluentQuestionCircle20Regular class="mr-3" width="20" />
|
||||
<template #reference>
|
||||
<div>
|
||||
<div class="mb-2">使用方法</div>
|
||||
<ol class="py-0 pl-5 my-0 text-base color-black/60">
|
||||
<li>复制译文,如果没有请点击 <span class="color-red font-bold">翻译</span> 按钮</li>
|
||||
<li>点击 <span class="color-red font-bold">分句</span> 按钮进行自动分句<span
|
||||
class="color-red font-bold"> 或</span>
|
||||
<li>
|
||||
复制译文,如果没有请点击 <span class="color-red font-bold">翻译</span> 按钮
|
||||
</li>
|
||||
<li>
|
||||
点击 <span class="color-red font-bold">分句</span> 按钮进行自动分句<span
|
||||
class="color-red font-bold"
|
||||
>
|
||||
或</span
|
||||
>
|
||||
手动编辑分句
|
||||
</li>
|
||||
<li>分句规则:一行一句,段落间空一行</li>
|
||||
<li>修改完成后点击 <span class="color-red font-bold">应用</span> 按钮同步到左侧结果栏
|
||||
<li>
|
||||
修改完成后点击
|
||||
<span class="color-red font-bold">应用</span> 按钮同步到左侧结果栏
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
@@ -460,38 +503,40 @@ function minusStartTime(val: Sentence) {
|
||||
<div class="flex gap-2">
|
||||
<div class="title">结果</div>
|
||||
<div class="flex gap-2 flex-1 justify-end">
|
||||
<ArticleAudio ref="audioRef" :article="editArticle" :autoplay="false"/>
|
||||
<ArticleAudio ref="audioRef" :article="editArticle" :autoplay="false" />
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="editArticle?.sections?.length">
|
||||
<div class="flex-1 overflow-auto flex flex-col">
|
||||
<div class="flex justify-between bg-black/10 py-2 rounded-lt-md rounded-rt-md">
|
||||
<div class="center flex-[7]">内容:
|
||||
<span class="text-sm color-black/70">均可编辑,编辑后点击应用按钮会自动同步</span></div>
|
||||
<div class="center flex-[7]">
|
||||
内容(
|
||||
<span class="text-sm color-gray-500">均可编辑,编辑后点击应用按钮会自动同步</span>)
|
||||
</div>
|
||||
<div>|</div>
|
||||
<div class="center flex-[3] gap-2">
|
||||
<span>音频</span>
|
||||
<BaseIcon title="音频管理" @click="showAudioDialog = true">
|
||||
<IconIconParkOutlineAddMusic/>
|
||||
<IconIconParkOutlineAddMusic />
|
||||
</BaseIcon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="article-translate">
|
||||
<div class="section rounded-md " v-for="(item,indexI) in editArticle.sections">
|
||||
<div class="section rounded-md" v-for="(item, indexI) in editArticle.sections">
|
||||
<div class="section-title text-lg font-bold">第{{ indexI + 1 }}段</div>
|
||||
<div class="sentence" v-for="(sentence,indexJ) in item">
|
||||
<div class="sentence" v-for="(sentence, indexJ) in item">
|
||||
<div class="flex-[7]">
|
||||
<EditAbleText
|
||||
:disabled="![100,0].includes(progress)"
|
||||
:disabled="![100, 0].includes(progress)"
|
||||
:value="sentence.text"
|
||||
@save="(e:string) => saveSentenceText(sentence,e)"
|
||||
@save="(e: string) => saveSentenceText(sentence, e)"
|
||||
/>
|
||||
<EditAbleText
|
||||
class="text-lg!"
|
||||
v-if="sentence.translate"
|
||||
:disabled="![100,0].includes(progress)"
|
||||
:disabled="![100, 0].includes(progress)"
|
||||
:value="sentence.translate"
|
||||
@save="(e:string) => saveSentenceTranslate(sentence,e)"
|
||||
@save="(e: string) => saveSentenceTranslate(sentence, e)"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-[2] flex justify-end gap-1 items-center">
|
||||
@@ -500,45 +545,49 @@ function minusStartTime(val: Sentence) {
|
||||
<div>{{ sentence.audioPosition?.[0] ?? 0 }}s</div>
|
||||
<div class="flex gap-1">
|
||||
<BaseIcon
|
||||
@click="setStartTime(sentence,indexI,indexJ)"
|
||||
:title="indexI === 0 && indexJ === 0 ?'设置开始时间':'使用前一句的结束时间'"
|
||||
@click="setStartTime(sentence, indexI, indexJ)"
|
||||
:title="
|
||||
indexI === 0 && indexJ === 0 ? '设置开始时间' : '使用前一句的结束时间'
|
||||
"
|
||||
>
|
||||
<IconFluentMyLocation20Regular v-if="indexI === 0 && indexJ === 0"/>
|
||||
<IconFluentPaddingLeft20Regular v-else/>
|
||||
<IconFluentMyLocation20Regular v-if="indexI === 0 && indexJ === 0" />
|
||||
<IconFluentPaddingLeft20Regular v-else />
|
||||
</BaseIcon>
|
||||
<BaseIcon
|
||||
@click="minusStartTime(sentence)"
|
||||
title="减 0.3 秒"
|
||||
>
|
||||
<BaseIcon @click="minusStartTime(sentence)" title="减 0.3 秒">
|
||||
-.3s
|
||||
</BaseIcon>
|
||||
</div>
|
||||
</div>
|
||||
<div>-</div>
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<div v-if="sentence.audioPosition?.[1] !== -1">{{ sentence.audioPosition?.[1] ?? 0 }}s</div>
|
||||
<div v-else> 结束</div>
|
||||
<BaseIcon
|
||||
@click="setEndTime(sentence,indexI,indexJ)"
|
||||
title="设置结束时间"
|
||||
>
|
||||
<IconFluentMyLocation20Regular/>
|
||||
<div v-if="sentence.audioPosition?.[1] !== -1">
|
||||
{{ sentence.audioPosition?.[1] ?? 0 }}s
|
||||
</div>
|
||||
<div v-else>结束</div>
|
||||
<BaseIcon @click="setEndTime(sentence, indexI, indexJ)" title="设置结束时间">
|
||||
<IconFluentMyLocation20Regular />
|
||||
</BaseIcon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<BaseIcon :icon="sentence.audioPosition?.length ? 'basil:edit-outline' : 'basil:add-outline'"
|
||||
title="编辑音频对齐"
|
||||
@click="handleShowEditAudioDialog(sentence,indexI,indexJ)">
|
||||
<BaseIcon
|
||||
:icon="
|
||||
sentence.audioPosition?.length ? 'basil:edit-outline' : 'basil:add-outline'
|
||||
"
|
||||
title="编辑音频对齐"
|
||||
@click="handleShowEditAudioDialog(sentence, indexI, indexJ)"
|
||||
>
|
||||
<IconFluentSpeakerEdit20Regular
|
||||
v-if="sentence.audioPosition?.length && sentence.audioPosition[1]"/>
|
||||
<IconFluentAddSquare20Regular v-else/>
|
||||
v-if="sentence.audioPosition?.length && sentence.audioPosition[1]"
|
||||
/>
|
||||
<IconFluentAddSquare20Regular v-else />
|
||||
</BaseIcon>
|
||||
<BaseIcon
|
||||
title="播放"
|
||||
v-if="sentence.audioPosition?.length"
|
||||
@click="playSentenceAudio(sentence,audioRef)">
|
||||
<IconFluentPlay20Regular/>
|
||||
@click="playSentenceAudio(sentence, audioRef)"
|
||||
>
|
||||
<IconFluentPlay20Regular />
|
||||
</BaseIcon>
|
||||
</div>
|
||||
</div>
|
||||
@@ -550,49 +599,58 @@ function minusStartTime(val: Sentence) {
|
||||
<div class="status">
|
||||
<span>状态:</span>
|
||||
<div class="warning" v-if="failCount">
|
||||
<IconFluentShieldQuestion20Regular/>
|
||||
<IconFluentShieldQuestion20Regular />
|
||||
共有{{ failCount }}句没有翻译!
|
||||
</div>
|
||||
<div class="success" v-else>
|
||||
<IconFluentCheckmarkCircle16Regular/>
|
||||
<IconFluentCheckmarkCircle16Regular />
|
||||
翻译完成!
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<BaseButton @click="save('save')">保存</BaseButton>
|
||||
<BaseButton v-if="type === 'batch'" @click="save('saveAndNext')">保存并添加下一篇</BaseButton>
|
||||
<BaseButton v-if="type === 'batch'" @click="save('saveAndNext')"
|
||||
>保存并添加下一篇</BaseButton
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<Empty v-else text="没有译文对照~"/>
|
||||
<Empty v-else text="没有译文对照~" />
|
||||
</div>
|
||||
<Dialog title="调整音频时间轴"
|
||||
v-model="showEditAudioDialog"
|
||||
:footer="true"
|
||||
@close="showEditAudioDialog = false"
|
||||
@ok="saveLrcPosition"
|
||||
<Dialog
|
||||
title="调整音频时间轴"
|
||||
v-model="showEditAudioDialog"
|
||||
:footer="true"
|
||||
@close="showEditAudioDialog = false"
|
||||
@ok="saveLrcPosition"
|
||||
>
|
||||
<div class="p-4 pt-0 color-main w-150 flex flex-col gap-2">
|
||||
<div class="">
|
||||
教程:点击音频播放按钮,当播放到句子开始时,点击开始时间的 <span class="color-red">记录</span>
|
||||
按钮;当播放到句子结束时,点击结束时间的 <span class="color-red">记录</span> 按钮,最后再试听是否正确
|
||||
教程:点击音频播放按钮,当播放到句子开始时,点击开始时间的
|
||||
<span class="color-red">记录</span> 按钮;当播放到句子结束时,点击结束时间的
|
||||
<span class="color-red">记录</span> 按钮,最后再试听是否正确
|
||||
</div>
|
||||
<ArticleAudio ref="sentenceAudioRef"
|
||||
:article="editArticle"
|
||||
:autoplay="false"
|
||||
class="w-full"/>
|
||||
<div class="flex items-center gap-2 justify-between mb-2" v-if="editSentence.audioPosition?.length">
|
||||
<ArticleAudio
|
||||
ref="sentenceAudioRef"
|
||||
:article="editArticle"
|
||||
:autoplay="false"
|
||||
class="w-full"
|
||||
/>
|
||||
<div
|
||||
class="flex items-center gap-2 justify-between mb-2"
|
||||
v-if="editSentence.audioPosition?.length"
|
||||
>
|
||||
<div>{{ editSentence.text }}</div>
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
<div>
|
||||
<span>{{ editSentence.audioPosition?.[0] }}s</span>
|
||||
<span v-if="editSentence.audioPosition?.[1] !== -1"> - {{ editSentence.audioPosition?.[1] }}s</span>
|
||||
<span v-if="editSentence.audioPosition?.[1] !== -1">
|
||||
- {{ editSentence.audioPosition?.[1] }}s</span
|
||||
>
|
||||
<span v-else> - 结束</span>
|
||||
</div>
|
||||
<BaseIcon
|
||||
title="播放"
|
||||
@click="playSentenceAudio(editSentence,sentenceAudioRef)">
|
||||
<IconFluentPlay20Regular/>
|
||||
<BaseIcon title="播放" @click="playSentenceAudio(editSentence, sentenceAudioRef)">
|
||||
<IconFluentPlay20Regular />
|
||||
</BaseIcon>
|
||||
</div>
|
||||
</div>
|
||||
@@ -601,28 +659,36 @@ function minusStartTime(val: Sentence) {
|
||||
<div>开始时间:</div>
|
||||
<div class="flex justify-between flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<InputNumber v-model="editSentence.audioPosition[0]" :precision="2" :step="0.1"/>
|
||||
<InputNumber v-model="editSentence.audioPosition[0]" :precision="2" :step="0.1" />
|
||||
<BaseIcon
|
||||
@click="jumpAudio(editSentence.audioPosition[0])"
|
||||
:title='`跳转至${editSentence.audioPosition[0]}秒`'
|
||||
:title="`跳转至${editSentence.audioPosition[0]}秒`"
|
||||
>
|
||||
<IconFluentMyLocation20Regular/>
|
||||
<IconFluentMyLocation20Regular />
|
||||
</BaseIcon>
|
||||
<BaseIcon
|
||||
v-if="preSentence"
|
||||
@click="setPreEndTimeToCurrentStartTime"
|
||||
:title="`使用前一句的结束时间:${preSentence?.audioPosition?.[1]||0}秒`"
|
||||
:title="`使用前一句的结束时间:${preSentence?.audioPosition?.[1] || 0}秒`"
|
||||
>
|
||||
<IconFluentPaddingLeft20Regular/>
|
||||
<IconFluentPaddingLeft20Regular />
|
||||
</BaseIcon>
|
||||
<BaseIcon
|
||||
@click="editSentence.audioPosition[0] = Number((editSentence.audioPosition[0] - 0.3).toFixed(2))"
|
||||
@click="
|
||||
editSentence.audioPosition[0] = Number(
|
||||
(editSentence.audioPosition[0] - 0.3).toFixed(2)
|
||||
)
|
||||
"
|
||||
title="减少 0.3 秒"
|
||||
>
|
||||
-.3s
|
||||
</BaseIcon>
|
||||
<BaseIcon
|
||||
@click="editSentence.audioPosition[0] = Number((editSentence.audioPosition[0] + 0.3).toFixed(2))"
|
||||
@click="
|
||||
editSentence.audioPosition[0] = Number(
|
||||
(editSentence.audioPosition[0] + 0.3).toFixed(2)
|
||||
)
|
||||
"
|
||||
title="增加 0.3 秒"
|
||||
>
|
||||
+.3s
|
||||
@@ -635,9 +701,11 @@ function minusStartTime(val: Sentence) {
|
||||
<div>结束时间:</div>
|
||||
<div class="flex justify-between flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<InputNumber v-model="editSentence.audioPosition[1]" :precision="2" :step="0.1"/>
|
||||
<InputNumber v-model="editSentence.audioPosition[1]" :precision="2" :step="0.1" />
|
||||
<span>或</span>
|
||||
<BaseButton size="small" @click="editSentence.audioPosition[1] = -1">结束</BaseButton>
|
||||
<BaseButton size="small" @click="editSentence.audioPosition[1] = -1"
|
||||
>结束</BaseButton
|
||||
>
|
||||
</div>
|
||||
<BaseButton @click="recordEnd">记录</BaseButton>
|
||||
</div>
|
||||
@@ -646,40 +714,46 @@ function minusStartTime(val: Sentence) {
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
<Dialog title="音频管理"
|
||||
v-model="showAudioDialog"
|
||||
:footer="false"
|
||||
@close="showAudioDialog = false"
|
||||
<Dialog
|
||||
title="音频管理"
|
||||
v-model="showAudioDialog"
|
||||
:footer="false"
|
||||
@close="showAudioDialog = false"
|
||||
>
|
||||
<div class="p-4 pt-0 color-main w-150 flex flex-col gap-2">
|
||||
<div class="">
|
||||
1、上传的文件保存在本地电脑上,更换电脑数据将丢失,请及时备份数据
|
||||
<br>
|
||||
<br />
|
||||
2、LRC 文件用于解析句子对应音频的位置,不一定准确,后续可自行修改
|
||||
</div>
|
||||
<!-- <ArticleAudio ref="sentenceAudioRef" :article="editArticle" class="w-full"/>-->
|
||||
<div class="upload relative">
|
||||
<BaseButton>上传音频</BaseButton>
|
||||
<input type="file"
|
||||
accept="audio/*"
|
||||
@change="handleAudioChange"
|
||||
class="w-full h-full absolute left-0 top-0 opacity-0"/>
|
||||
<input
|
||||
type="file"
|
||||
accept="audio/*"
|
||||
@change="handleAudioChange"
|
||||
class="w-full h-full absolute left-0 top-0 opacity-0"
|
||||
/>
|
||||
</div>
|
||||
<div class="upload relative">
|
||||
<BaseButton>上传 LRC 文件</BaseButton>
|
||||
<input type="file"
|
||||
accept=".lrc"
|
||||
@change="handleChange"
|
||||
class="w-full h-full absolute left-0 top-0 opacity-0"/>
|
||||
<input
|
||||
type="file"
|
||||
accept=".lrc"
|
||||
@change="handleChange"
|
||||
class="w-full h-full absolute left-0 top-0 opacity-0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
<Dialog title="人名管理"
|
||||
v-model="showNameDialog"
|
||||
:footer="true"
|
||||
@close="showNameDialog = false"
|
||||
@ok="saveNameList"
|
||||
<Dialog
|
||||
title="人名管理"
|
||||
v-model="showNameDialog"
|
||||
:footer="true"
|
||||
@close="showNameDialog = false"
|
||||
@ok="saveNameList"
|
||||
>
|
||||
<div class="p-4 pt-0 color-main w-150 flex flex-col gap-3">
|
||||
<div class="flex justify-between items-center">
|
||||
@@ -688,17 +762,18 @@ function minusStartTime(val: Sentence) {
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center gap-2" v-for="(name,i) in nameListRef" :key="i">
|
||||
<BaseInput v-model="nameListRef[i]"
|
||||
placeholder="输入名称"
|
||||
size="large"
|
||||
:autofocus="i===nameListRef.length-1"/>
|
||||
<div class="flex items-center gap-2" v-for="(name, i) in nameListRef" :key="i">
|
||||
<BaseInput
|
||||
v-model="nameListRef[i]"
|
||||
placeholder="输入名称"
|
||||
size="large"
|
||||
:autofocus="i === nameListRef.length - 1"
|
||||
/>
|
||||
<BaseButton type="info" @click="removeName(i)">删除</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -782,7 +857,7 @@ function minusStartTime(val: Sentence) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 1.2rem;
|
||||
color: #67C23A;
|
||||
color: #67c23a;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -807,7 +882,8 @@ function minusStartTime(val: Sentence) {
|
||||
}
|
||||
|
||||
// 表单元素优化
|
||||
.base-input, .base-textarea {
|
||||
.base-input,
|
||||
.base-textarea {
|
||||
width: 100%;
|
||||
font-size: 16px; // 防止iOS自动缩放
|
||||
}
|
||||
@@ -872,7 +948,8 @@ function minusStartTime(val: Sentence) {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.warning, .success {
|
||||
.warning,
|
||||
.success {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import MigrateDialog from '@/components/MigrateDialog.vue'
|
||||
import Log from '@/pages/setting/Log.vue'
|
||||
import About from '@/components/About.vue'
|
||||
import CommonSetting from '@/components/setting/CommonSetting.vue'
|
||||
import ArticleSettting from '@/components/setting/ArticleSettting.vue'
|
||||
import ArticleSetting from '@/components/setting/ArticleSetting.vue'
|
||||
import WordSetting from '@/components/setting/WordSetting.vue'
|
||||
import { PRACTICE_ARTICLE_CACHE, PRACTICE_WORD_CACHE } from '@/utils/cache.ts'
|
||||
|
||||
@@ -357,7 +357,7 @@ function transferOk() {
|
||||
<div class="flex-1 overflow-y-auto overflow-x-hidden pr-4 content">
|
||||
<CommonSetting v-if="tabIndex === 0" />
|
||||
<WordSetting v-if="tabIndex === 1" />
|
||||
<ArticleSettting v-if="tabIndex === 2" />
|
||||
<ArticleSetting v-if="tabIndex === 2" />
|
||||
|
||||
<div class="body" v-if="tabIndex === 3">
|
||||
<div class="row">
|
||||
|
||||
@@ -188,28 +188,15 @@ const progress = $computed(() => {
|
||||
}
|
||||
|
||||
.bottom {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border-radius: 0.6rem;
|
||||
background: var(--color-second);
|
||||
@apply relative w-full box-border rounded-xl bg-second shadow-lg z-10;
|
||||
padding: 0.2rem var(--space) calc(0.4rem + env(safe-area-inset-bottom, 0px)) var(--space);
|
||||
border: 1px solid var(--color-item-border);
|
||||
box-shadow: var(--shadow);
|
||||
z-index: 10;
|
||||
|
||||
.stat {
|
||||
margin-top: 0.5rem;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
gap: var(--stat-gap);
|
||||
@apply flex justify-around gap-[var(--stat-gap)] mt-2;
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
color: gray;
|
||||
@apply flex flex-col items-center gap-1 text-gray;
|
||||
|
||||
.line {
|
||||
height: 1px;
|
||||
|
||||
@@ -723,7 +723,7 @@ useEvents([
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dictation {
|
||||
border-bottom: 2px solid black;
|
||||
border-bottom: 2px solid gray;
|
||||
}
|
||||
|
||||
.typing-word {
|
||||
|
||||
@@ -7,6 +7,7 @@ export default defineConfig({
|
||||
'bg-second': 'bg-[var(--color-second)]',
|
||||
'bg-third': 'bg-[var(--color-third)]',
|
||||
'bg-fourth': 'bg-[var(--color-fourth)]',
|
||||
'bg-fifth': 'bg-[var(--color-fifth)]',
|
||||
'bg-card-active': 'bg-[var(--color-card-active)]',
|
||||
'bg-item': 'bg-[var(--color-item-bg)]',
|
||||
'bg-reverse-white': 'bg-[var(--color-reverse-white)]',
|
||||
|
||||
Reference in New Issue
Block a user