This commit is contained in:
王念超
2024-06-11 16:14:14 +08:00
parent b4e16be6e6
commit e48c097d90
8 changed files with 345 additions and 138 deletions

View File

@@ -19,7 +19,7 @@
--color-header-bg: white;
--color-tooltip-bg: white;
--color-tooltip-shadow: #bbbbbb;
--color-tooltip-shadow: #d9d9d9;
--color-font-1: rgb(91, 91, 91);
--color-font-2: rgb(46, 46, 46);
--color-font-3: rgb(75, 85, 99);

View File

@@ -7,19 +7,55 @@ import {Icon} from "@iconify/vue";
defineProps<{
title?: string,
icon: string,
disabled?: boolean,
}>()
defineEmits(['click'])
const emit = defineEmits(['click'])
</script>
<template>
<Tooltip :title="title">
<IconWrapper v-bind="$attrs" @click.stop="$emit('click')">
<Icon :icon="icon"/>
</IconWrapper>
<div
v-bind="$attrs"
@click="e => (!disabled) && emit('click',e)"
class="icon-wrapper"
:class="{disabled}"
>
<Icon :icon="icon"/>
</div>
</Tooltip>
</template>
<style scoped lang="scss">
$w: 1.4rem;
.icon-wrapper {
cursor: pointer;
//padding: 2rem;
width: 2rem;
height: 2rem;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: .3rem;
background: transparent;
transition: all .3s;
//color: var(--color-main-active);
&:hover:not(.disabled) {
background: var(--color-primary);
color: white;
}
&.disabled {
cursor: not-allowed;
opacity: .3;
}
:deep(svg) {
width: $w;
height: $w;
}
}
</style>

View File

@@ -0,0 +1,153 @@
<script setup lang="ts">
import {Word} from "@/types.ts";
import VolumeIcon from "@/components/icon/VolumeIcon.vue";
import {usePlayWordAudio} from "@/hooks/sound.ts";
const props = withDefaults(defineProps<{
item: Word,
showTranslate?: boolean
showWord?: boolean
border?: boolean
}>(), {
showTranslate: true,
showWord: true,
border: true
})
const playWordAudio = usePlayWordAudio()
</script>
<template>
<div class="word-item"
:class="{
border,
}"
>
<div class="left">
<slot name="prefix" :item="item"></slot>
<div class="title-wrapper">
<div class="item-title">
<span class="word" :class="!showWord && 'word-shadow'">{{ item.word }}</span>
<span class="phonetic">{{ item.phonetic0 }}</span>
<VolumeIcon class="volume" @click="playWordAudio(item.word)"></VolumeIcon>
</div>
<div class="item-sub-title" v-if="item.trans.length && showTranslate">
<div v-for="v in item.trans">{{ (v.pos ? v.pos + '.' : '') + (v.cn || v.en) }}</div>
</div>
</div>
</div>
<div class="right">
<slot name="suffix" :item="item"></slot>
</div>
</div>
</template>
<style scoped lang="scss">
.word-item {
cursor: pointer;
width: 100%;
box-sizing: border-box;
background: var(--color-item-bg);
color: var(--color-font-1);
font-size: 1.1rem;
border-radius: .5rem;
display: flex;
justify-content: space-between;
transition: all .3s;
padding: .6rem;
gap: .6rem;
border: 1px solid var(--color-item-border);
.left {
display: flex;
gap: .6rem;
.title-wrapper {
display: flex;
flex-direction: column;
gap: .2rem;
word-break: break-word;
}
}
.right {
display: flex;
flex-direction: column;
gap: .3rem;
transition: all .3s;
}
.volume, .collect, .easy {
opacity: 0;
}
&:hover {
background: var(--color-item-hover);
.volume, .collect, .easy {
opacity: 1;
}
}
&.active {
background: var(--color-item-active);
$c: #E6A23C;
.phonetic, .item-sub-title {
color: var(--color-gray) !important;
}
.volume, .collect, .easy, .fill {
color: $c;
}
}
&.border {
&.active {
.item-title {
border-bottom: 2px solid gray !important;
}
}
.item-title {
transition: all .3s;
cursor: pointer;
border-bottom: 2px solid transparent;
}
&:hover {
.item-title {
border-bottom: 2px solid gray !important;
}
}
}
.item-title {
display: flex;
align-items: center;
gap: .5rem;
color: var(--color-font-1);
.word {
font-size: 1.2rem;
display: flex;
}
.phonetic {
font-size: .9rem;
color: gray;
}
}
.item-sub-title {
font-size: 1rem;
color: gray;
}
}
</style>

View File

@@ -2,29 +2,27 @@
import {useSettingStore} from "@/stores/setting.ts";
import {nextTick, useSlots} from "vue";
import VolumeIcon from "@/components/icon/VolumeIcon.vue";
import {Sort} from "@/types.ts";
import MiniDialog from "@/pages/pc/components/dialog/MiniDialog.vue";
import BaseIcon from "@/components/BaseIcon.vue";
import BaseButton from "@/components/BaseButton.vue";
import {cloneDeep, reverse, shuffle} from "lodash-es";
import Input from "@/pages/pc/components/Input.vue";
import PopConfirm from "@/pages/pc/components/PopConfirm.vue";
import Empty from "@/components/Empty.vue";
import {Icon} from "@iconify/vue";
let list = defineModel('list')
const props = withDefaults(defineProps<{
activeIndex?: number,
activeId?: string,
isActive?: boolean
showBorder?: boolean
loading?: boolean
del?: Function
batchDel?: Function
add?: Function
}>(), {
activeIndex: -1,
activeId: '',
isActive: false,
showBorder: false,
loading: true,
del: () => void 0,
add: () => void 0,
batchDel: () => void 0
@@ -52,26 +50,16 @@ function scrollToItem(index: number) {
})
}
function itemIsActive(item: any, index: number) {
return props.activeId ?
props.activeId === item.id
: props.activeIndex === index
}
defineExpose({scrollToBottom, scrollToItem})
let pageNo = $ref(1)
let pageSize = $ref(50)
let currentList = $computed({
get() {
if (searchKey) {
return list.value.filter(v => v.word.includes(searchKey))
}
return list.value.slice((pageNo - 1) * pageSize, (pageNo - 1) * pageSize + pageSize)
},
set(v) {
list.value = v
let currentList = $computed(() => {
if (searchKey) {
return list.value.filter(v => v.word.includes(searchKey))
}
return list.value.slice((pageNo - 1) * pageSize, (pageNo - 1) * pageSize + pageSize)
})
let selectIds = $ref([])
@@ -116,6 +104,11 @@ function handleBatchDel() {
selectIds = []
}
function handlePageNo(e) {
pageNo = e
console.log('listRef', listRef)
}
const s = useSlots()
defineRender(
@@ -127,43 +120,55 @@ defineRender(
return (
<div class="flex flex-col gap-3">
<div class="">
<div>
{
showSearchInput ? (
<div
class="flex gap-2"
class="flex gap-4"
>
<Input
modelValue={searchKey}
onUpdate:model-value={e => searchKey = e}
class="flex-1"/>
<BaseButton onClick={() => showSearchInput = false}>取消</BaseButton>
<BaseButton onClick={() => (showSearchInput = false, searchKey = '')}>取消</BaseButton>
</div>
) : (
<div class="flex justify-between " v-else>
<el-checkbox
onClick={() => toggleSelectAll()}
modelValue={selectAll}
size="large"/>
<div class="flex gap-2 items-center">
<el-checkbox
disabled={!currentList.length}
onClick={() => toggleSelectAll()}
modelValue={selectAll}
size="large"/>
<span>{selectIds.length} / {list.value.length}</span>
</div>
<div class="flex gap-2 relative">
{
selectIds.length ? <BaseIcon
onClick={handleBatchDel}
class="del"
title="删除"
icon="solar:trash-bin-minimalistic-linear"/> : null
selectIds.length ?
<PopConfirm title="确认删除所有选中数据?"
onConfirm={handleBatchDel}
>
<BaseIcon
class="del"
title="删除"
icon="solar:trash-bin-minimalistic-linear"/>
</PopConfirm>
: null
}
<BaseIcon
onClick={props.add}
icon="fluent:add-20-filled"
title="添加单词"/>
<BaseIcon
disabled={!currentList.length}
title="改变顺序"
icon="icon-park-outline:sort-two"
onClick={() => showSortDialog = !showSortDialog}
/>
<BaseIcon
disabled={!currentList.length}
onClick={() => showSearchInput = !showSearchInput}
title="搜索"
icon="fluent:search-24-regular"/>
@@ -186,29 +191,41 @@ defineRender(
)
}
</div>
<div class="flex-1 overflow-auto"
ref="listRef">
{
currentList.map((item, index) => {
return (
<div class="list-item-wrapper"
key={item.id}
>
{s.default({checkbox: d, item})}
</div>
)
})
}
</div>
<div class="flex justify-end">
<el-pagination background
currentPage={pageNo}
onUpdate:current-page={(e) => pageNo = e}
pageSize={pageSize}
onUpdate:page-size={(e) => pageSize = e}
layout="prev, pager, next"
total={list.value.length}/>
</div>
{
props.loading ?
<div class="h-full w-full center text-4xl">
<Icon
icon="eos-icons:loading"
color="gray"
/>
</div>
: currentList.length ? (
<>
<div class="flex-1 overflow-auto"
ref='listRef'>
{currentList.map((item) => {
return (
<div class="list-item-wrapper"
key={item.id}
>
{s.default({checkbox: d, item})}
</div>
)
})}
</div>
<div class="flex justify-end">
<el-pagination background
currentPage={pageNo}
onUpdate:current-page={handlePageNo}
pageSize={pageSize}
onUpdate:page-size={(e) => pageSize = e}
pageSizes={[20, 50, 100, 200]}
layout="sizes, prev, pager, next"
total={list.value.length}/>
</div>
</>
) : <Empty/>
}
</div>
)
}

View File

@@ -42,7 +42,7 @@ export default {
this.show = true
this.$nextTick(() => {
let tip = this.$refs?.tip?.getBoundingClientRect()
console.log('rect', rect, tip)
// console.log('rect', rect, tip)
if (!tip) return
if (rect.top < 150) {
this.$refs.tip.style.top = rect.top + rect.height + tip.height + 30 + 'px'
@@ -65,13 +65,13 @@ export default {
<Transition>
{
this.show && (
<div ref="tip" className="pop-confirm-content">
<div className="text">
<div ref="tip" class="pop-confirm-content">
<div class="text">
{this.title}
</div>
<div className="options">
<div class="options">
<div onClick={() => this.show = false}>取消</div>
<div className="main" onClick={() => this.confirm()}>确认</div>
<div class="main" onClick={() => this.confirm()}>确认</div>
</div>
</div>
)
@@ -91,7 +91,7 @@ $bg-color: rgb(226, 226, 226);
position: fixed;
background: var(--color-tooltip-bg);
padding: 1rem;
border-radius: .24rem;
border-radius: .3rem;
transform: translate(-50%, calc(-100% - .6rem));
box-shadow: 0 0 6px 1px var(--color-tooltip-shadow);
z-index: 999;

View File

@@ -49,19 +49,20 @@ export default {
},
},
render() {
if (!this.title) return this.$slots.default()
let Vnode = this.$slots.default()[0]
return <>
{
this.show && this.title && (
<Teleport to="body">
<Transition name="fade">
<div ref="tip" className="tip">
<Teleport to="body">
<Transition name="fade">
{
this.show && (
<div ref="tip" class="tip">
{this.title}
</div>
</Transition>
</Teleport>
)
}
)
}
</Transition>
</Teleport>
<Vnode
onClick={() => this.show = false}
onmouseenter={(e) => this.showPop(e)}
@@ -72,14 +73,12 @@ export default {
}
</script>
<style lang="scss" scoped>
@import "@/assets/css/style";
.tip {
position: fixed;
font-size: 0.8rem;
font-size: 0.9rem;
z-index: 9999;
border-radius: .2rem;
padding: .8rem;
border-radius: .3rem;
padding: 0.4rem .8rem;
color: var(--color-font-1);
background: var(--color-tooltip-bg);
//box-shadow: 1px 1px 6px #bbbbbb;

View File

@@ -9,9 +9,11 @@ const props = withDefaults(defineProps<{
item: Word,
showTranslate?: boolean
showWord?: boolean
hiddenOptionIcon?: boolean
}>(), {
showTranslate: true,
showWord: true
showWord: true,
hiddenOptionIcon: false,
})
const playWordAudio = usePlayWordAudio()
@@ -20,9 +22,7 @@ const playWordAudio = usePlayWordAudio()
<template>
<div class="word-item"
:class="{
border:true
}"
:class="{hiddenOptionIcon}"
>
<div class="left">
<slot name="prefix" :item="item"></slot>
@@ -79,51 +79,10 @@ const playWordAudio = usePlayWordAudio()
transition: all .3s;
}
.volume, .collect, .easy {
.volume {
opacity: 0;
}
&:hover {
background: var(--color-item-hover);
.volume, .collect, .easy {
opacity: 1;
}
}
&.active {
background: var(--color-item-active);
$c: #E6A23C;
.phonetic, .item-sub-title {
color: var(--color-gray) !important;
}
.volume, .collect, .easy, .fill {
color: $c;
}
}
&.border {
&.active {
.item-title {
border-bottom: 2px solid gray !important;
}
}
.item-title {
transition: all .3s;
cursor: pointer;
border-bottom: 2px solid transparent;
}
&:hover {
.item-title {
border-bottom: 2px solid gray !important;
}
}
}
.item-title {
display: flex;
align-items: center;
@@ -146,6 +105,32 @@ const playWordAudio = usePlayWordAudio()
color: gray;
}
&:hover {
background: var(--color-item-hover);
.volume, :deep(.option-icon) {
opacity: 1;
}
}
&.hiddenOptionIcon {
:deep(.option-icon) {
opacity: 0;
}
}
&.active {
background: var(--color-item-active);
$c: #E6A23C;
.phonetic, .item-sub-title {
color: var(--color-gray) !important;
}
.volume, .collect, .easy, .fill {
color: $c;
}
}
}
</style>

View File

@@ -13,10 +13,12 @@ import BaseTable from "@/pages/pc/components/BaseTable.vue";
import WordItem from "@/pages/pc/components/WordItem.vue";
import type {Word} from "@/types.ts";
import type {FormInstance, FormRules} from "element-plus";
import PopConfirm from "@/pages/pc/components/PopConfirm.vue";
const runtimeStore = useRuntimeStore()
const store = useBaseStore()
const route = useRoute()
let loading = $ref(false)
let list = $computed({
get() {
@@ -27,12 +29,16 @@ let list = $computed({
}
})
onMounted(() => {
onMounted(async () => {
switch (Number(route.query.type)) {
case -1:
if (runtimeStore.routeData) {
loading = true
runtimeStore.editDict = cloneDeep(runtimeStore.routeData)
_checkDictWords(runtimeStore.editDict)
await _checkDictWords(runtimeStore.editDict)
setTimeout(() => {
loading = false
}, 300)
}
break
case 0:
@@ -148,6 +154,10 @@ function closeWordForm() {
wordForm = cloneDeep(DefaultFormWord)
}
function s(ss) {
console.log('s', ss)
}
defineRender(() => {
return (
<BasePage>
@@ -165,32 +175,39 @@ defineRender(() => {
}
</div>
</header>
<div class="flex h-120">
<div class="flex" style="height:calc(100vh - 8rem)">
<div class="w-1/2">
<BaseTable
class="h-full"
list={list}
loading={loading}
onUpdate:list={e => list = e}
del={delWord}
batchDel={batchDel}
add={addWord}
>
{
(val) =>
<WordItem item={val.item}>
<WordItem
item={val.item}>
{{
prefix: () => val.checkbox(val.item),
suffix: () => (
<div class='flex flex-col'>
<BaseIcon
class="del"
class="option-icon"
onClick={() => editWord(val.item)}
title="编辑"
icon="tabler:edit"/>
<BaseIcon
class="del"
onClick={() => delWord(val.item.id)}
title="删除"
icon="solar:trash-bin-minimalistic-linear"/>
<PopConfirm title="确认删除?"
onConfirm={() => delWord(val.item.id)}
>
<BaseIcon
class="option-icon"
title="删除"
icon="solar:trash-bin-minimalistic-linear"/>
</PopConfirm>
</div>
)
}}
@@ -205,7 +222,7 @@ defineRender(() => {
{wordFormData.type === FormMode.Add ? '添加' : '修改'}单词
</div>
<el-form
class="form"
className="form"
ref="wordFormRef"
rules={wordRules}
model={wordForm}