升级到 vue3

This commit is contained in:
lyswhut
2021-12-03 22:11:11 +08:00
parent c6635e554a
commit 8059d9cd73
260 changed files with 14443 additions and 10979 deletions
+84
View File
@@ -0,0 +1,84 @@
<template>
<button
:class="[$style.btn, {[$style.min]: min}, {[$style.outline]: outline}]"
:disabled="disabled"
@click="$emit('click', $event)"
>
<slot />
</button>
</template>
<script>
export default {
props: {
min: {
type: Boolean,
},
outline: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
},
emits: ['click'],
}
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.btn {
display: inline-block;
border: none;
border-radius: @form-radius;
cursor: pointer;
padding: 8px 15px;
color: @color-btn;
outline: none;
transition: background-color 0.2s ease;
background-color: @color-btn-background;
font-size: 14px;
&[disabled] {
opacity: .4;
}
&.outline {
background-color: transparent;
}
&:hover {
background-color: @color-btn-hover;
}
&:active {
background-color: @color-btn-active;
}
}
.min {
padding: 3px 8px;
font-size: 12px;
}
each(@themes, {
:global(#root.@{value}) {
.btn {
color: ~'@{color-@{value}-btn}';
background-color: ~'@{color-@{value}-btn-background}';
&.outline {
background-color: transparent;
}
&:hover {
background-color: ~'@{color-@{value}-btn-hover}';
}
&:active {
background-color: ~'@{color-@{value}-btn-active}';
}
}
}
})
</style>
+213
View File
@@ -0,0 +1,213 @@
<template lang="pug">
div(:class="$style.checkbox")
input(:type="need ? 'radio' : 'checkbox'" :class="$style.input" :id="id" :disabled="disabled" :value="value" :name="name" @change="change" v-model="bool")
label(:for="id" :class="$style.content")
div(v-if="indeterminate" :class="$style.container")
svg(v-show="indeterminate" :class="$style.icon" version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' width="100%" viewBox='0 32 448 448' space='preserve')
use(xlink:href='#icon-check-indeterminate')
svg(v-show="!indeterminate" :class="$style.icon" version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' width="100%" viewBox='0 0 448 512' space='preserve')
use(xlink:href='#icon-check-true')
div(v-else :class="$style.container")
svg(version='1.1' :class="$style.icon" xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' width="100%" viewBox='0 32 448 448' space='preserve')
use(xlink:href='#icon-check-true')
span(v-if="label != null" v-html="label" :class="$style.label")
</template>
<script>
export default {
props: {
modelValue: {},
value: {},
id: {
type: String,
required: true,
},
name: {
type: String,
},
need: {
type: Boolean,
default: false,
},
label: {
type: String,
},
disabled: {
type: Boolean,
default: false,
},
indeterminate: {
type: Boolean,
default: false,
},
},
data() {
return {
bool: false,
}
},
watch: {
modelValue(n) {
this.setValue(n)
},
},
mounted() {
this.setValue(this.modelValue)
},
methods: {
change() {
let checked
if (Array.isArray(this.modelValue)) {
checked = [...this.modelValue]
const index = checked.indexOf(this.value)
if (index < 0) {
checked.push(this.value)
} else {
checked.splice(index, 1)
}
} else {
let bool = this.bool
switch (typeof this.modelValue) {
case 'boolean':
if (this.indeterminate) {
bool = true
this.$nextTick(() => {
this.bool = bool
})
}
checked = bool
break
case 'number':
checked = this.value
break
default:
checked = bool ? this.value : ''
break
}
}
this.$emit('update:modelValue', checked)
this.$emit('change', checked)
},
setValue(value) {
let bool
if (Array.isArray(value)) {
bool = value.includes(this.value)
} else {
switch (typeof value) {
case 'boolean':
bool = value
break
// case 'string':
// case 'number':
default:
bool = value === this.value
break
}
}
this.bool = this.need ? bool && this.value : bool
},
},
}
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.checkbox {
display: inline-block;
// font-size: 56px;
}
.input {
display: none;
&[disabled] {
+ .content {
opacity: .5;
}
}
&:checked {
+ .content {
.container {
&:after {
border-color: @color-theme;
}
}
.icon {
transform: scale(1);
// opacity: 1;
}
}
}
}
.content {
display: flex;
justify-content: center;
}
.container {
flex: none;
position: relative;
display: inline-block;
width: 1em;
height: 1em;
cursor: pointer;
display: flex;
color: @color-theme;
margin-top: .25em;
// border: 1px solid #ccc;
&:after {
position: absolute;
content: ' ';
top: 0;
bottom: 0;
left: 0;
right: 0;
border: 1px solid @color-theme_2-font-label;
transition: border-color 0.2s ease;
border-radius: 2px;
}
}
.icon {
transition: 0.3s ease;
transition-property: transform;
transform: scale(0);
border-radius: 2px;
// opacity: 0;
}
.label {
flex: auto;
line-height: 1;
margin-left: 5px;
line-height: 1.5;
cursor: pointer;
}
each(@themes, {
:global(#root.@{value}) {
.checkbox {
> input {
&:checked {
+ .content {
.container {
&:after {
border-color: ~'@{color-@{value}-theme}';
}
}
}
}
}
}
.content {
.container {
color: ~'@{color-@{value}-theme}';
// border: 1px solid #ccc;
&:after {
border-color: ~'@{color-@{value}-theme_2-font-label}';
}
}
}
}
})
</style>
+102
View File
@@ -0,0 +1,102 @@
<template>
<input
:class="$style.input"
ref="dom_input"
:type="type"
:placeholder="placeholder"
:value="modelValue"
:disabled="disabled"
@input="handleInput"
@change="$emit('change', $event.target.value.trim())"
@keyup.enter="$emit('submit', $event.target.value.trim())"
/>
</template>
<script>
export default {
props: {
placeholder: {
type: String,
default: '',
},
disabled: {
type: Boolean,
default: false,
},
modelValue: {
type: String,
default: '',
},
type: {
type: String,
default: 'text',
},
},
emits: ['update:modelValue', 'keydown', 'blur', 'submit', 'change', 'focus'],
methods: {
handleInput(event) {
let value = event.target.value.trim()
event.target.value = value
this.$emit('update:modelValue', value)
},
focus() {
this.$refs.dom_input.focus()
},
},
}
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.input {
display: inline-block;
border: none;
border-radius: @form-radius;
padding: 7px 8px;
color: @color-btn;
outline: none;
transition: background-color 0.2s ease;
background-color: @color-btn-background;
font-size: 13.3px;
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
&[disabled] {
opacity: .4;
}
&:hover, &:focus {
background-color: @color-theme_2-hover;
}
&:active {
background-color: @color-theme_2-active;
}
}
.min {
padding: 3px 8px;
font-size: 12px;
}
each(@themes, {
:global(#root.@{value}) {
.input {
color: ~'@{color-@{value}-btn}';
background-color: ~'@{color-@{value}-btn-background}';
&:hover, &:focus {
background-color: ~'@{color-@{value}-theme_2-hover}';
}
&:active {
background-color: ~'@{color-@{value}-theme_2-active}';
}
}
}
})
</style>
+193
View File
@@ -0,0 +1,193 @@
<template>
<ul :class="$style.list" :style="listStyles" ref="dom_list">
<li v-for="item in menus"
:key="item.action"
@click="handleClick(item)"
v-show="!item.hide && (item.action == 'download' ? setting.download.enable : true)"
:disabled="item.disabled ? true : null">{{item[itemName]}}</li>
</ul>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
props: {
menus: {
type: Array,
default() {
return []
},
},
itemName: {
type: String,
default: 'name',
},
isShow: {
type: Boolean,
default: false,
},
location: {
type: Object,
default() {
return { x: 0, y: 0 }
},
},
},
emits: ['menu-click'],
computed: {
...mapGetters(['setting']),
},
watch: {
isShow: {
handler(n) {
n ? this.handleShow() : this.handleHide()
},
immediate: true,
},
location: {
handler(n) {
this.listStyles.left = n.x + 2 + 'px'
this.listStyles.top = n.y + 'px'
if (this.show) {
if (this.listStyles.transitionProperty != this.transition2) this.listStyles.transitionProperty = this.transition2
this.listStyles.transform = `scaleY(1) translate(${this.handleGetOffsetXY(n.x, n.y)})`
}
},
deep: true,
},
},
data() {
return {
show: false,
listStyles: {
left: 0,
top: 0,
opacity: 0,
transitionProperty: 'transform, opacity',
transform: 'scale(0) translate(0,0)',
},
transition1: 'transform, opacity',
transition2: 'transform, opacity, top, left',
transitionProperty: 'transform, opacity',
}
},
mounted() {
document.addEventListener('click', this.handleDocumentClick)
},
beforeUnmount() {
document.removeEventListener('click', this.handleDocumentClick)
},
methods: {
handleShow() {
this.show = true
this.listStyles.opacity = 1
this.listStyles.transform = `scaleY(1) translate(${this.handleGetOffsetXY(this.location.x, this.location.y)})`
},
handleHide(e) {
this.listStyles.opacity = 0
this.listStyles.transform = 'scale(0) translate(0, 0)'
this.show = false
},
handleGetOffsetXY(left, top) {
const listWidth = this.$refs.dom_list.clientWidth
const listHeight = this.$refs.dom_list.clientHeight
const dom_container = this.$refs.dom_list.offsetParent
const containerWidth = dom_container.clientWidth
const containerHeight = dom_container.clientHeight
const offsetWidth = containerWidth - left - listWidth
const offsetHeight = containerHeight - top - listHeight
let x = 0
let y = 0
if (containerWidth > listWidth && offsetWidth < 17) {
x = offsetWidth - 17
}
if (containerHeight > listHeight && offsetHeight < 5) {
y = offsetHeight - 5
}
return `${x}px, ${y}px`
},
handleDocumentClick(event) {
if (!this.show) return
if (event.target == this.$refs.dom_list || this.$refs.dom_list.contains(event.target)) return
if (this.show && this.listStyles.transitionProperty != this.transition1) this.listStyles.transitionProperty = this.transition1
this.handleClick(null)
},
handleClick(item) {
if (item && item.disabled) return
this.$emit('menu-click', item)
},
},
}
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.list {
font-size: 12px;
position: absolute;
opacity: 0;
transform: scale(0);
transform-origin: 0 0 0;
transition: .25s ease;
transition-property: transform, opacity;
border-radius: @radius-border;
background-color: @color-theme_2-background_2;
box-shadow: 0 1px 8px 0 rgba(0,0,0,.2);
z-index: 10;
overflow: hidden;
// will-change: transform;
li {
cursor: pointer;
min-width: 96px;
line-height: 34px;
// color: @color-btn;
padding: 0 10px;
text-align: center;
outline: none;
transition: @transition-theme;
transition-property: background-color, opacity;
box-sizing: border-box;
.mixin-ellipsis-1;
&:hover {
background-color: @color-theme_2-hover;
}
&:active {
background-color: @color-theme_2-active;
}
&[disabled] {
cursor: default;
opacity: .4;
&:hover {
background: none !important;
}
}
}
}
each(@themes, {
:global(#root.@{value}) {
.list {
background-color: ~'@{color-@{value}-theme_2-background_2}';
li {
// color: ~'@{color-@{value}-btn}';
&:hover {
background-color: ~'@{color-@{value}-theme_2-hover}';
}
&:active {
background-color: ~'@{color-@{value}-theme_2-active}';
}
}
}
}
})
</style>
+167
View File
@@ -0,0 +1,167 @@
<template>
<div :class="[$style.select, {[$style.active]: show}]">
<div :class="$style.label" ref="dom_btn" @click="handleShow">{{modelValue ? itemName ? list.find(l => l.id == modelValue).name : modelValue : ''}}</div>
<ul :class="$style.list">
<li v-for="(item, index) in list" :key="index" @click="handleClick(itemKey ? item[itemKey] : item)">{{itemName ? item[itemName] : item}}</li>
</ul>
</div>
</template>
<script>
export default {
props: {
list: {
type: Array,
default() {
return []
},
},
modelValue: {
type: [String, Number],
},
itemName: {
type: String,
},
itemKey: {
type: String,
},
},
data() {
return {
show: false,
}
},
mounted() {
document.addEventListener('click', this.handleHide)
},
beforeUnmount() {
document.removeEventListener('click', this.handleHide)
},
methods: {
handleHide(e) {
// if (e && e.target.parentNode != this.$refs.dom_list && this.show) return this.show = false
if (e && e.target == this.$refs.dom_btn) return
setTimeout(() => {
this.show = false
}, 50)
},
handleClick(item) {
if (item === this.modelValue) return
this.$emit('update:modelValue', item)
this.$emit('change', item)
},
handleShow() {
this.show = !this.show
},
},
}
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.select {
font-size: 12px;
position: relative;
&.active {
.list {
transform: scaleY(1);
opacity: 1;
}
}
}
.label {
padding: 8px 15px;
// background-color: @color-btn-background;
transition: background-color @transition-theme;
border-top: 2px solid @color-tab-border-bottom;
border-left: 2px solid @color-tab-border-bottom;
box-sizing: border-box;
text-align: center;
border-top-left-radius: @radius-border;
color: @color-btn;
cursor: pointer;
.mixin-ellipsis-1;
&:hover {
background-color: @color-btn-hover;
}
&:active {
background-color: @color-btn-active;
}
}
.list {
position: absolute;
width: 100%;
top: 100%;
left: 0;
border-bottom: 2px solid @color-tab-border-bottom;
border-left: 2px solid @color-tab-border-bottom;
border-bottom-left-radius: @radius-border;
background-color: @color-theme_2-background_2;
opacity: 0;
transform: scaleY(0);
transform-origin: 0 0 0;
transition: .25s ease;
transition-property: transform, opacity;
z-index: 10;
li {
cursor: pointer;
padding: 8px 15px;
// color: @color-btn;
text-align: center;
outline: none;
transition: background-color @transition-theme;
background-color: @color-btn-background;
box-sizing: border-box;
.mixin-ellipsis-1;
&:hover {
background-color: @color-btn-hover;
}
&:active {
background-color: @color-btn-active;
}
}
}
each(@themes, {
:global(#root.@{value}) {
.label {
border-top-color: ~'@{color-@{value}-tab-border-bottom}';
border-left-color: ~'@{color-@{value}-tab-border-bottom}';
color: ~'@{color-@{value}-btn}';
&:hover {
background-color: ~'@{color-@{value}-btn-hover}';
}
&:active {
background-color: ~'@{color-@{value}-btn-active}';
}
}
.list {
border-bottom-color: ~'@{color-@{value}-tab-border-bottom}';
border-left-color: ~'@{color-@{value}-tab-border-bottom}';
background-color: ~'@{color-@{value}-theme_2-background_2}';
li {
// color: ~'@{color-@{value}-btn}';
background-color: ~'@{color-@{value}-btn-background}';
&:hover {
background-color: ~'@{color-@{value}-btn-hover}';
}
&:active {
background-color: ~'@{color-@{value}-btn-active}';
}
}
}
}
})
</style>
+229
View File
@@ -0,0 +1,229 @@
<template>
<div class="content" :class="[$style.select, show ? $style.active : '']">
<div class="label-content" :class="$style.label" ref="dom_btn" @click="handleShow">
<span class="label">{{label}}</span>
<div class="icon" :class="$style.icon">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="100%" viewBox="0 0 451.847 451.847" space="preserve">
<use xlink:href="#icon-down"></use>
</svg>
</div>
</div>
<ul class="selection-list scroll" :class="$style.list" :style="listStyles" ref="dom_list">
<li v-for="(item, index) in list" :key="index" :class="(itemKey ? item[itemKey] : item) == modelValue ? $style.active : null"
@click="handleClick(item)" :tips="itemName ? item[itemName] : item">{{itemName ? item[itemName] : item}}</li>
</ul>
</div>
</template>
<script>
export default {
props: {
list: {
type: Array,
default() {
return []
},
},
modelValue: {
type: [String, Number],
},
itemName: {
type: String,
},
itemKey: {
type: String,
},
},
data() {
return {
show: false,
listStyles: {
transform: 'scaleY(0) translateY(0)',
},
}
},
mounted() {
document.addEventListener('click', this.handleHide)
},
beforeUnmount() {
document.removeEventListener('click', this.handleHide)
},
computed: {
label() {
if (this.modelValue == null) return ''
if (this.itemName == null) return this.modelValue
const item = this.list.find(l => l[this.itemKey] == this.modelValue)
if (!item) return ''
return item[this.itemName]
},
},
methods: {
handleHide(e) {
// if (e && e.target.parentNode != this.$refs.dom_list && this.show) return this.show = false
if (e && (e.target == this.$refs.dom_btn || this.$refs.dom_btn.contains(e.target))) return
this.listStyles.transform = 'scaleY(0) translateY(0)'
setTimeout(() => {
this.show = false
}, 50)
},
handleClick(item) {
// console.log(this.modelValue)
if (item === this.modelValue) return
this.$emit('update:modelValue', this.itemKey ? item[this.itemKey] : item)
this.$emit('change', item)
},
handleShow() {
this.show = true
this.listStyles.transform = `scaleY(1) translateY(${this.handleGetOffset()}px)`
},
handleGetOffset() {
const listHeight = this.$refs.dom_list.clientHeight
const dom_select = this.$refs.dom_list.offsetParent
const dom_container = dom_select.offsetParent
const containerHeight = dom_container.clientHeight
if (containerHeight < listHeight) return 0
const offsetHeight = (dom_container.scrollTop + containerHeight) - (dom_select.offsetTop + listHeight)
if (offsetHeight > 0) return 0
return offsetHeight - 5
},
},
}
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
@selection-height: 28px;
.select {
display: inline-block;
font-size: 12px;
position: relative;
width: 300px;
&.active {
.label {
background-color: @color-btn-background;
}
.list {
opacity: 1;
}
.icon {
svg{
transform: rotate(180deg);
}
}
}
}
.label {
background-color: @color-btn-background;
padding: 0 10px;
transition: background-color @transition-theme;
height: @selection-height;
line-height: 27px;
box-sizing: border-box;
color: @color-btn;
border-radius: @form-radius;
cursor: pointer;
display: flex;
span {
flex: auto;
.mixin-ellipsis-1;
}
.icon {
flex: none;
margin-left: 7px;
line-height: 0;
svg {
width: 1em;
transition: transform .2s ease;
transform: rotate(0);
}
}
&:hover {
background-color: @color-btn-hover;
}
&:active {
background-color: @color-btn-active;
}
}
.list {
position: absolute;
top: 0;
left: 0;
width: 100%;
background-color: @color-theme_2-background_2;
opacity: 0;
transform: scaleY(0) translateY(0);
transform-origin: 0 (@selection-height / 2) 0;
transition: .25s ease;
transition-property: transform, opacity;
z-index: 10;
border-radius: @form-radius;
box-shadow: 0 0 4px rgba(0, 0, 0, .15);
overflow: auto;
max-height: 200px;
li {
cursor: pointer;
padding: 0 10px;
line-height: @selection-height;
// color: @color-btn;
outline: none;
transition: background-color @transition-theme;
background-color: @color-btn-background;
box-sizing: border-box;
.mixin-ellipsis-1;
&:hover {
background-color: @color-btn-hover;
}
&:active {
background-color: @color-btn-active;
}
&.active {
color: @color-btn;
}
}
}
each(@themes, {
:global(#root.@{value}) {
.label {
color: ~'@{color-@{value}-btn}';
background-color: ~'@{color-@{value}-btn-background}';
&:hover {
background-color: ~'@{color-@{value}-btn-hover}';
}
&:active {
background-color: ~'@{color-@{value}-btn-active}';
}
}
.list {
background-color: ~'@{color-@{value}-theme_2-background_2}';
li {
// color: ~'@{color-@{value}-btn}';
background-color: ~'@{color-@{value}-btn-background}';
&:hover {
background-color: ~'@{color-@{value}-btn-hover}';
}
&:active {
background-color: ~'@{color-@{value}-btn-active}';
}
&.active {
color: ~'@{color-@{value}-btn}';
}
}
}
}
})
</style>
+225
View File
@@ -0,0 +1,225 @@
<template lang="pug">
div.scroll(:class="$style.tab")
//- div(:class="$style.content")
ul
li.ignore-to-rem(v-for="item in list" :key="itemKey ? item[itemKey] : item" :class="modelValue === (itemKey ? item[itemKey] : item) ? $style.active : ''")
button(type="button"
@click="handleClick(itemKey ? item[itemKey] : item)") {{ itemName ? item[itemName] : item }}
//- div(:class="$style.control")
div(:class="$style.left")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' viewBox='0 0 451.846 451.847' space='preserve')
use(xlink:href='#icon-left')
div(:class="$style.right")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' viewBox='0 0 451.846 451.847' space='preserve')
use(xlink:href='#icon-right')
</template>
<script>
export default {
props: {
list: {
type: Array,
default() {
return []
},
},
modelValue: {
type: [String, Number],
},
itemName: {
type: String,
},
itemKey: {
type: String,
},
},
methods: {
handleClick(item) {
if (item === this.modelValue) return
this.$emit('update:modelValue', item)
this.$emit('change', item)
},
},
}
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.tab {
// overflow: hidden;
// display: flex;
// flex-flow: row nowrap;
overflow-x: auto;
ul {
display: flex;
flex: none;
border-bottom: 2px solid @color-tab-border-bottom;
transition: border-bottom-color @transition-theme;
li {
position: relative;
flex: none;
border-top: 2px solid @color-tab-border-top;
border-left: 2px solid @color-tab-btn-background;
border-right: 2px solid @color-tab-btn-background;
// box-sizing: border-box;
transition: border-color @transition-theme;
&:global(.ignore-to-rem) {
margin-left: -2px;
margin-bottom: -2px;
&:after, &:before {
height: 2px;
}
}
&:after {
content: ' ';
display: block;
width: 50%;
position: absolute;
bottom: 0;
left: 0;
background-color: @color-tab-border-bottom;
transition: width @transition-theme;
}
&:before {
content: ' ';
display: block;
width: 50%;
position: absolute;
bottom: 0;
right: 0;
background-color: @color-tab-border-bottom;
transition: width @transition-theme;
}
&:first-child {
border-left: none;
margin-left: 0;
button {
border-top-left-radius: 3px;
// border-bottom-left-radius: @radius-border;
}
}
&:last-child {
border-right: 2px solid @color-tab-border-top;
border-top-right-radius: @radius-border;
button {
border-top-right-radius: 3px;
// border-bottom-right-radius: @radius-border;
}
}
button {
display: inline-block;
border: none;
cursor: pointer;
padding: 0 10px;
font-size: 12px;
min-width: 56px;
// color: @color-btn;
outline: none;
transition: background-color @transition-theme;
background-color: @color-tab-btn-background;
line-height: 28px;
}
&:hover {
// border-left-color: @color-theme_2-hover;
// border-right-color: @color-theme_2-hover;
button {
background-color: @color-tab-btn-background-hover;
}
}
&:active {
// border-left-color: @color-theme_2-active;
// border-right-color: @color-theme_2-active;
button {
background-color: @color-theme_2-active;
}
}
&.active {
border-bottom-color: @color-theme_2-background_1;
border-top-color: @color-tab-border-bottom;
border-left-color: @color-tab-border-bottom;
border-right-color: @color-tab-border-bottom;
&::after {
width: 0;
}
&::before {
width: 0;
}
button {
background-color: @color-theme_2-background_1;
}
}
}
}
}
// .control {
// flex: none;
// border-bottom: 2px solid @color-tab-border-bottom;
// width: 1em;
// svg {
// width: .85em;
// }
// }
// .left, .right {
// line-height: 0;
// }
// .content {
// flex: auto;
// overflow: hidden;
// }
each(@themes, {
:global(#root.@{value}) {
.tab {
ul {
border-bottom-color: ~'@{color-@{value}-tab-border-bottom}';
li {
border-top-color: ~'@{color-@{value}-tab-border-top}';
border-left-color: ~'@{color-@{value}-tab-btn-background}';
border-right-color: ~'@{color-@{value}-tab-btn-background}';
&::after {
background-color: ~'@{color-@{value}-tab-border-bottom}';
}
&::before {
background-color: ~'@{color-@{value}-tab-border-bottom}';
}
&:last-child {
border-right-color: ~'@{color-@{value}-tab-border-top}';
}
button {
// color: ~'@{color-@{value}-btn}';
background-color: ~'@{color-@{value}-tab-btn-background}';
}
&:hover {
// border-left-color: ~'@{color-@{value}-theme_2-hover}';
// border-right-color: ~'@{color-@{value}-theme_2-hover}';
button {
background-color: ~'@{color-@{value}-tab-btn-background-hover}';
}
}
&:active {
// border-left-color: ~'@{color-@{value}-theme_2-active}';
// border-right-color: ~'@{color-@{value}-theme_2-active}';
button {
background-color: ~'@{color-@{value}-theme_2-active}';
}
}
&.active {
border-bottom-color: ~'@{color-@{value}-theme_2}';
border-top-color: ~'@{color-@{value}-tab-border-bottom}';
border-left-color: ~'@{color-@{value}-tab-border-bottom}';
border-right-color: ~'@{color-@{value}-tab-border-bottom}';
button {
background-color: ~'@{color-@{value}-theme_2-background_1}';
}
}
}
}
}
}
})
</style>
@@ -0,0 +1,257 @@
<template>
<component :is="containerEl" :class="containerClass" ref="dom_scrollContainer" style="height: 100%; overflow: auto; position: relative; display: block;">
<component :is="contentEl" :class="contentClass" :style="contentStyle">
<div v-for="item in views" :key="item.key" :style="item.style">
<slot name="default" v-bind="{ item: item.item, index: item.index }" />
</div>
</component>
<slot name="footer" />
</component>
</template>
<script>
import {
computed,
ref,
nextTick,
watch,
onMounted,
onBeforeUnmount,
} from 'vue'
const easeInOutQuad = (t, b, c, d) => {
t /= d / 2
if (t < 1) return (c / 2) * t * t + b
t--
return (-c / 2) * (t * (t - 2) - 1) + b
}
const handleScroll = (element, to, duration = 300, callback = () => {}, onCancel = () => {}) => {
if (!element) return callback()
const start = element.scrollTop || element.scrollY || 0
let cancel = false
if (to > start) {
let maxScrollTop = element.scrollHeight - element.clientHeight
if (to > maxScrollTop) to = maxScrollTop
} else if (to < start) {
if (to < 0) to = 0
} else return callback()
const change = to - start
const increment = 10
if (!change) return callback()
let currentTime = 0
let val
let cancelCallback
const animateScroll = () => {
currentTime += increment
val = parseInt(easeInOutQuad(currentTime, start, change, duration))
if (element.scrollTo) {
element.scrollTo(0, val)
} else {
element.scrollTop = val
}
if (currentTime < duration) {
if (cancel) {
cancelCallback()
onCancel()
return
}
setTimeout(animateScroll, increment)
} else {
callback()
}
}
animateScroll()
return (callback) => {
cancelCallback = callback
cancel = true
}
}
export default {
name: 'VirtualizedList',
props: {
containerEl: {
type: String,
default: 'div',
},
containerClass: {
type: String,
default: 'virtualized-list',
},
contentEl: {
type: String,
default: 'div',
},
contentClass: {
type: String,
default: 'virtualized-list-content',
},
outsideNum: {
type: Number,
default: 1,
},
itemHeight: {
type: Number,
required: true,
},
keyName: {
type: String,
require: true,
},
list: {
type: Array,
require: true,
},
},
emits: ['contextmenu', 'scroll'],
setup(props, { emit }) {
const views = ref([])
const dom_scrollContainer = ref(null)
let startIndex = -1
let endIndex = -1
let scrollTop = -1
let cachedList = []
let cancelScroll = null
let isScrolling = false
let scrollToValue = 0
const createList = (startIndex, endIndex) => {
const cache = cachedList.slice(startIndex, endIndex)
const list = props.list.slice(startIndex, endIndex).map((item, i) => {
if (cache[i]) return cache[i]
const top = (startIndex + i) * props.itemHeight
const index = startIndex + i
return cachedList[index] = {
item,
top,
style: { position: 'absolute', left: 0, right: 0, top: top + 'px', height: props.itemHeight + 'px' },
index,
key: item[props.keyName],
}
})
return list
}
const updateView = (currentScrollTop = dom_scrollContainer.value.scrollTop) => {
// const currentScrollTop = this.$refs.dom_scrollContainer.scrollTop
const itemHeight = props.itemHeight
const currentStartIndex = Math.floor(currentScrollTop / itemHeight)
const scrollContainerHeight = dom_scrollContainer.value.clientHeight
const currentEndIndex = currentStartIndex + Math.ceil(scrollContainerHeight / itemHeight)
const continuous = currentStartIndex <= endIndex && currentEndIndex >= startIndex
const currentStartRenderIndex = Math.max(Math.floor(currentScrollTop / itemHeight) - props.outsideNum, 0)
const currentEndRenderIndex = currentStartIndex + Math.ceil(scrollContainerHeight / itemHeight) + props.outsideNum
// console.log(continuous)
// debugger
if (continuous) {
// if (Math.abs(currentScrollTop - this.scrollTop) < this.itemHeight * this.outsideNum * 0.6) return
// console.log('update')
// if (currentScrollTop > scrollTop) { // scroll down
// // console.log('scroll down')
// views.value = createList(currentStartRenderIndex, currentEndRenderIndex)
// // views.value.push(...list.slice(list.indexOf(views.value[views.value.length - 1]) + 1))
// // // if (this.views.length > 100) {
// // nextTick(() => {
// // views.value.splice(0, views.value.indexOf(list[0]))
// // })
// // }
// } else if (currentScrollTop < scrollTop) { // scroll up
// // console.log('scroll up')
// views.value = createList(currentStartRenderIndex, currentEndRenderIndex)
// } else return
if (currentScrollTop == scrollTop) return
views.value = createList(currentStartRenderIndex, currentEndRenderIndex)
} else {
views.value = createList(currentStartRenderIndex, currentEndRenderIndex)
}
startIndex = currentStartIndex
endIndex = currentEndIndex
scrollTop = currentScrollTop
}
const onScroll = event => {
const currentScrollTop = dom_scrollContainer.value.scrollTop
if (Math.abs(currentScrollTop - scrollTop) > props.itemHeight * props.outsideNum * 0.6) {
updateView(currentScrollTop)
}
emit('scroll', event)
}
const scrollTo = (scrollTop, animate = false) => {
return new Promise(resolve => {
if (cancelScroll) {
cancelScroll(resolve)
} else {
resolve()
}
}).then(() => {
return new Promise((resolve, reject) => {
if (animate) {
isScrolling = true
scrollToValue = scrollTop
cancelScroll = handleScroll(dom_scrollContainer.value, scrollTop, 300, () => {
cancelScroll = null
isScrolling = false
resolve()
}, () => {
cancelScroll = null
isScrolling = false
reject('canceled')
})
} else {
dom_scrollContainer.value.scrollTop = scrollTop
}
})
})
}
const scrollToIndex = (index, offset = 0, animate = false) => {
return scrollTo(Math.max(index * props.itemHeight + offset, 0), animate)
}
const getScrollTop = () => {
return isScrolling ? scrollToValue : dom_scrollContainer.value.scrollTop
}
const contentStyle = computed(() => ({
display: 'block',
height: props.list.length * props.itemHeight + 'px',
}))
watch(() => props.itemHeight, updateView)
watch(() => props.list, (list) => {
cachedList = Array(list.length)
startIndex = -1
endIndex = -1
nextTick(() => {
updateView()
})
}, {
deep: true,
})
onMounted(() => {
dom_scrollContainer.value.addEventListener('scroll', onScroll, false)
cachedList = Array(props.list.length)
startIndex = -1
endIndex = -1
updateView()
})
onBeforeUnmount(() => {
dom_scrollContainer.value.removeEventListener('scroll', onScroll)
if (cancelScroll) cancelScroll()
})
return {
views,
dom_scrollContainer,
contentStyle,
scrollTo,
scrollToIndex,
getScrollTop,
}
},
}
</script>