save
This commit is contained in:
@@ -18,7 +18,9 @@ import {useDisableEventListener} from "@/hooks/event.ts";
|
||||
import {MessageBox} from "@/utils/MessageBox.tsx";
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import {useBaseStore} from "@/stores/base.ts";
|
||||
import {$ref} from "vue/macros";
|
||||
import {$computed, $ref} from "vue/macros";
|
||||
import Input from "@/components/Input.vue";
|
||||
import List from "@/components/List.vue";
|
||||
|
||||
interface IProps {
|
||||
selectIndex?: number
|
||||
@@ -31,7 +33,9 @@ const props = withDefaults(defineProps<IProps>(), {
|
||||
const base = useBaseStore()
|
||||
let article = $ref<Article>(cloneDeep(DefaultArticle))
|
||||
let selectIndex = $ref<number>(props.selectIndex)
|
||||
let selectItem = $ref<number>(props.selectIndex)
|
||||
let networkTranslateEngine = $ref('baidu')
|
||||
let searchKey = $ref('')
|
||||
let progress = $ref(0)
|
||||
const TranslateEngineOptions = [
|
||||
{value: 'baidu', label: '百度'},
|
||||
@@ -264,26 +268,43 @@ watch(() => article.useTranslateType, () => {
|
||||
}
|
||||
})
|
||||
|
||||
const list = $computed(() => {
|
||||
if (searchKey) {
|
||||
return base.currentEditDict.articles.filter((item: Article) => {
|
||||
//把搜索内容,分词之后,判断是否有这个词,比单纯遍历包含体验更好
|
||||
return searchKey.toLowerCase().split(' ').filter(v => v).some(value => {
|
||||
return item.title.toLowerCase().includes(value) || item.titleTranslate.toLowerCase().includes(value)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
return base.currentEditDict.articles
|
||||
}
|
||||
})
|
||||
|
||||
function selectArticle(index: number) {
|
||||
article = cloneDeep(base.currentEditDict.articles[index])
|
||||
selectIndex = index
|
||||
}
|
||||
|
||||
function delArticle(index: number) {
|
||||
if (index < selectIndex) {
|
||||
selectIndex--
|
||||
} else if (index === selectIndex) {
|
||||
if (selectIndex === base.currentEditDict.articles.length - 1) {
|
||||
function delArticle(item: Article) {
|
||||
let rIndex = base.currentEditDict.articles.findIndex((v: Article) => v.id === item.id)
|
||||
if (rIndex > -1) {
|
||||
if (index < selectIndex) {
|
||||
selectIndex--
|
||||
} else if (index === selectIndex) {
|
||||
if (selectIndex === base.currentEditDict.articles.length - 1) {
|
||||
selectIndex--
|
||||
}
|
||||
}
|
||||
base.currentEditDict.articles.splice(index, 1)
|
||||
if (selectIndex < 0) {
|
||||
article = cloneDeep(DefaultArticle)
|
||||
} else {
|
||||
article = cloneDeep(base.currentEditDict.articles[selectIndex])
|
||||
}
|
||||
}
|
||||
base.currentEditDict.articles.splice(index, 1)
|
||||
if (selectIndex < 0) {
|
||||
article = cloneDeep(DefaultArticle)
|
||||
} else {
|
||||
article = cloneDeep(base.currentEditDict.articles[selectIndex])
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -293,27 +314,19 @@ function delArticle(index: number) {
|
||||
<div class="dict-name">{{ base.currentEditDict.name }}</div>
|
||||
<BaseIcon title="选择其他词典/文章" icon="carbon:change-catalog"/>
|
||||
</header>
|
||||
<div class="article-list">
|
||||
<div class="item"
|
||||
:class="[
|
||||
(selectIndex === index) && 'active'
|
||||
]"
|
||||
@click="selectArticle(index)"
|
||||
v-for="(item,index) in base.currentEditDict.articles">
|
||||
<div class="left">
|
||||
<div class="name"> {{ `${index + 1}. ${item.title}` }}</div>
|
||||
<div class="translate-name"> {{ ` ${item.titleTranslate}` }}</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<BaseIcon
|
||||
@click="delArticle(index)"
|
||||
title="删除" icon="fluent:delete-24-regular"/>
|
||||
<BaseIcon
|
||||
@click="delArticle(index)"
|
||||
title="删除" icon="carbon:move"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<List
|
||||
v-model:list="list"
|
||||
v-model:searchKey="searchKey"
|
||||
:select-index="selectIndex"
|
||||
:row-key="(item:Article) => item.title"
|
||||
@del-article="delArticle"
|
||||
@select-article="selectArticle"
|
||||
>
|
||||
<template v-slot="{item,index}">
|
||||
<div class="name"> {{ `${index + 1}. ${item.title}` }}</div>
|
||||
<div class="translate-name"> {{ ` ${item.titleTranslate}` }}</div>
|
||||
</template>
|
||||
</List>
|
||||
<div class="footer">
|
||||
<BaseButton>导入</BaseButton>
|
||||
<BaseButton>导出</BaseButton>
|
||||
@@ -471,6 +484,7 @@ function delArticle(index: number) {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
//opacity: 0;
|
||||
|
||||
.dict-name {
|
||||
font-size: 30rem;
|
||||
@@ -478,46 +492,12 @@ function delArticle(index: number) {
|
||||
}
|
||||
}
|
||||
|
||||
.article-list {
|
||||
flex: 1;
|
||||
width: 300rem;
|
||||
overflow: auto;
|
||||
.name {
|
||||
font-size: 18rem;
|
||||
}
|
||||
|
||||
.item {
|
||||
background: #e1e1e1;
|
||||
border-radius: 8rem;
|
||||
margin-bottom: 10rem;
|
||||
padding: 10rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
transition: all .3s;
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: all .3s;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.right {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--color-item-active);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 18rem;
|
||||
}
|
||||
|
||||
.translate-name {
|
||||
font-size: 16rem;
|
||||
}
|
||||
}
|
||||
.translate-name {
|
||||
font-size: 16rem;
|
||||
}
|
||||
|
||||
.footer {
|
||||
@@ -534,6 +514,7 @@ function delArticle(index: number) {
|
||||
display: flex;
|
||||
gap: $space;
|
||||
padding: $space;
|
||||
//opacity: 0;
|
||||
}
|
||||
|
||||
.row {
|
||||
|
||||
@@ -5,7 +5,7 @@ import IconWrapper from "@/components/IconWrapper.vue";
|
||||
import {Icon} from "@iconify/vue";
|
||||
|
||||
defineProps<{
|
||||
title: string,
|
||||
title?: string,
|
||||
icon: string,
|
||||
}>()
|
||||
|
||||
|
||||
22
src/components/Close.vue
Normal file
22
src/components/Close.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import {Icon} from "@iconify/vue";
|
||||
|
||||
defineEmits(['click'])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="close"
|
||||
@click="$emit('click')"
|
||||
>
|
||||
<Icon icon="ic:round-close"
|
||||
width="20"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.close {
|
||||
cursor: pointer;
|
||||
|
||||
}
|
||||
</style>
|
||||
77
src/components/Input.vue
Normal file
77
src/components/Input.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<script setup lang="ts">
|
||||
import {$ref} from "vue/macros";
|
||||
import {Icon} from "@iconify/vue";
|
||||
import Close from "@/components/Close.vue";
|
||||
import {useWindowClick} from "@/hooks/event.ts";
|
||||
|
||||
defineProps<{
|
||||
modelValue: string
|
||||
}>()
|
||||
|
||||
defineEmits(['update:modelValue'])
|
||||
let focus = $ref(false)
|
||||
let inputEl = $ref<HTMLDivElement>()
|
||||
|
||||
useWindowClick((e: PointerEvent) => {
|
||||
focus = inputEl.contains(e.target as any);
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="base-input"
|
||||
:class="{focus}"
|
||||
ref="inputEl"
|
||||
>
|
||||
<Icon icon="fluent:search-24-regular"
|
||||
width="20"/>
|
||||
<input type="text"
|
||||
:value="modelValue"
|
||||
@input="e=>$emit('update:modelValue',e.target.value)"
|
||||
>
|
||||
<Close @click="$emit('update:modelValue','')"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/assets/css/style.scss";
|
||||
|
||||
.base-input {
|
||||
border: 1px solid var(--color-main-bg);
|
||||
border-radius: 4rem;
|
||||
padding: 3rem 5rem;
|
||||
transition: all .3s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: all .3s;
|
||||
|
||||
:deep(svg) {
|
||||
transition: all .3s;
|
||||
color: var(--color-main-bg);
|
||||
}
|
||||
|
||||
&.focus {
|
||||
border: 1px solid var(--color-main-active);
|
||||
|
||||
:deep(svg) {
|
||||
color: gray;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
font-family: $font-family;
|
||||
font-size: 18rem;
|
||||
outline: none;
|
||||
min-height: 20rem;
|
||||
flex: 1;
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
border: none;
|
||||
|
||||
&[readonly] {
|
||||
cursor: not-allowed;
|
||||
opacity: .7;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
171
src/components/List.vue
Normal file
171
src/components/List.vue
Normal file
@@ -0,0 +1,171 @@
|
||||
<script setup lang="ts" generic="T">
|
||||
|
||||
import BaseIcon from "@/components/BaseIcon.vue";
|
||||
import Input from "@/components/Input.vue";
|
||||
import {$ref} from "vue/macros";
|
||||
import {cloneDeep, throttle} from "lodash-es";
|
||||
|
||||
let dragIndex = $ref(-1)
|
||||
|
||||
interface IProps {
|
||||
list: T[]
|
||||
searchKey: string,
|
||||
selectIndex: number,
|
||||
rowKey: (item: T) => string
|
||||
}
|
||||
|
||||
const props = defineProps<IProps>()
|
||||
const emit = defineEmits<{
|
||||
selectArticle: [index: number],
|
||||
delArticle: [item: T],
|
||||
'update:searchKey': [val: string],
|
||||
'update:list': [list: T[]],
|
||||
}>()
|
||||
let draggable = $ref(false)
|
||||
|
||||
function dragstart(index: number) {
|
||||
dragIndex = index;
|
||||
}
|
||||
|
||||
const dragenter = throttle((e, index) => {
|
||||
// console.log('dragenter', 'dragIndex', dragIndex, 'index', index)
|
||||
e.preventDefault();
|
||||
// 避免源对象触发自身的dragenter事件
|
||||
if (dragIndex !== index && dragIndex !== -1) {
|
||||
const source = props.list[dragIndex];
|
||||
let temp = cloneDeep(props.list)
|
||||
temp.splice(dragIndex, 1);
|
||||
temp.splice(index, 0, source);
|
||||
emit('update:list', temp)
|
||||
// props.list = temp
|
||||
// 排序变化后目标对象的索引变成源对象的索引
|
||||
dragIndex = index;
|
||||
}
|
||||
}, 200)
|
||||
|
||||
function dragover(e, index) {
|
||||
// console.log('dragover')
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
function dragend() {
|
||||
// console.log('dragend')
|
||||
draggable = false
|
||||
dragIndex = -1
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="list-wrapper">
|
||||
<div class="search">
|
||||
<Input :model-value="searchKey"
|
||||
@update:model-value="(e:string) => emit('update:searchKey',e)"
|
||||
/>
|
||||
</div>
|
||||
<transition-group
|
||||
name="drag"
|
||||
class="list"
|
||||
tag="div"
|
||||
>
|
||||
<div class="item"
|
||||
:class="[
|
||||
(props.selectIndex === index) && 'active',
|
||||
draggable && 'draggable',
|
||||
(dragIndex === index) && 'active'
|
||||
]"
|
||||
@click="emit('selectArticle',index)"
|
||||
v-for="(item,index) in props.list"
|
||||
:key="rowKey(item)"
|
||||
:draggable="draggable"
|
||||
@dragstart="dragstart(index)"
|
||||
@dragenter="dragenter($event, index)"
|
||||
@dragover="dragover($event, index)"
|
||||
@dragend="dragend()"
|
||||
>
|
||||
<div class="left">
|
||||
<slot :item="item" :index="index"></slot>
|
||||
</div>
|
||||
<div class="right">
|
||||
<BaseIcon
|
||||
@click="emit('delArticle',item)"
|
||||
title="删除" icon="fluent:delete-24-regular"/>
|
||||
<div
|
||||
@mousedown="draggable = true"
|
||||
@mouseup="draggable = false"
|
||||
>
|
||||
<BaseIcon
|
||||
icon="carbon:move"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.drag-move, /* 对移动中的元素应用的过渡 */
|
||||
.drag-enter-active,
|
||||
.drag-leave-active {
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.drag-enter-from,
|
||||
.drag-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
||||
|
||||
/* 确保将离开的元素从布局流中删除
|
||||
以便能够正确地计算移动的动画。 */
|
||||
.drag-leave-active {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.list-wrapper {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding-right: 5rem;
|
||||
|
||||
.search {
|
||||
margin: 10rem 0;
|
||||
width: 260rem;
|
||||
}
|
||||
|
||||
.list {
|
||||
.item {
|
||||
width: 260rem;
|
||||
box-sizing: border-box;
|
||||
background: #e1e1e1;
|
||||
border-radius: 8rem;
|
||||
margin-bottom: 10rem;
|
||||
padding: 10rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
transition: all .3s;
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: all .3s;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.right {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--color-item-active);
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.draggable {
|
||||
cursor: move;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -25,6 +25,7 @@ export default {
|
||||
methods: {
|
||||
showPop(e) {
|
||||
if (this.disabled) return
|
||||
if (!this.title) return
|
||||
e.stopPropagation()
|
||||
let rect = e.target.getBoundingClientRect()
|
||||
this.show = true
|
||||
@@ -49,7 +50,7 @@ export default {
|
||||
<Teleport to="body">
|
||||
<Transition name="fade">
|
||||
{
|
||||
this.show && (
|
||||
this.show && this.title && (
|
||||
<div ref="tip" class="tip">
|
||||
{this.title}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user