Compare commits

...

10 Commits

Author SHA1 Message Date
bunny 67c49a26a4 同步 2024-07-23 08:26:43 +08:00
bunny e9e0dab2f8 feat: 🚀 complex Tweening 2024-07-17 09:46:25 +08:00
bunny fbf929cc81 page: 📄 common Easings 2024-07-17 08:58:34 +08:00
bunny 1fd9d75928 page: 📄 元素改变时动画 2024-07-17 08:44:54 +08:00
bunny 5ff3d64e23 docs: 📚 修改文档内容 2024-07-16 16:52:40 +08:00
bunny f86dfe348a page: 📄 设置元素的zIndex相关API 2024-07-16 16:52:01 +08:00
bunny 4a19611284 page: 📄 将圆形zIndex设置为1 2024-07-16 16:40:50 +08:00
bunny 07762aec41 feat: 🚀 移动到不同组中 2024-07-16 16:33:32 +08:00
bunny 106f52f71e feat: 🚀 添加组 2024-07-16 16:16:01 +08:00
bunny a42bc7d82d feat: 🚀 字体最小宽度 2024-07-16 15:44:19 +08:00
15 changed files with 751 additions and 188 deletions

View File

@ -1,55 +1,37 @@
```sh
force update
text resizing
ignore stroke
clipping
simple clip
complex clip
groups, layers and ordering
groups
layering
change containers
zindex
filters
blur
brighten
contrast
emboss
enhance
grayscale
hsl
hsv
rgb
invert
kaleidoscope
mask
noise
pixelate
custom filter
multiple filters
tweens
linear easing
common easings
all easings
finish event
all controls
tween filter
complex tweening
animations
create an animation
moving
rotation
scaling
stop animation
selectors
select by id
select by type
select by name
data & serialization & export
serialize a stage
simple load
complex load
json best practices
stage data url
export to hd image
```
https://konvajs.org/docs/animations/Create_an_Animation.html
https://konvajs.org/docs/animations/Moving.html
https://konvajs.org/docs/animations/Rotation.html
https://konvajs.org/docs/animations/Scaling.html
https://konvajs.org/docs/animations/Stop_Animation.html
https://konvajs.org/docs/filters/Blur.html
https://konvajs.org/docs/filters/Brighten.html
https://konvajs.org/docs/filters/Contrast.html
https://konvajs.org/docs/filters/Emboss.html
https://konvajs.org/docs/filters/Enhance.html
https://konvajs.org/docs/filters/Grayscale.html
https://konvajs.org/docs/filters/HSL.html
https://konvajs.org/docs/filters/HSV.html
https://konvajs.org/docs/filters/RGB.html
https://konvajs.org/docs/filters/Invert.html
https://konvajs.org/docs/filters/Kaleidoscope.html
https://konvajs.org/docs/filters/Mask.html
https://konvajs.org/docs/filters/Noise.html
https://konvajs.org/docs/filters/Pixelate.html
https://konvajs.org/docs/filters/Custom_Filter.html
https://konvajs.org/docs/filters/Multiple_Filters.html
https://konvajs.org/docs/data_and_serialization/Serialize_a_Stage.html
https://konvajs.org/docs/data_and_serialization/Simple_Load.html
https://konvajs.org/docs/data_and_serialization/Complex_Load.html
https://konvajs.org/docs/data_and_serialization/Best_Practices.html
https://konvajs.org/docs/data_and_serialization/Stage_Data_URL.html
https://konvajs.org/docs/data_and_serialization/High-Quality-Export.html
https://konvajs.org/docs/performance/Shape_Redraw.html
https://konvajs.org/docs/performance/Optimize_Strokes.html
https://konvajs.org/docs/performance/Optimize_Animation.html
https://konvajs.org/docs/performance/Batch_Draw.html
https://konvajs.org/docs/performance/Layer_Management.html
```

View File

@ -27,7 +27,7 @@ const include = [
// 'sortablejs',
// 'swiper/vue',
// 'mint-filter',
// '@vueuse/core',
'@vueuse/core',
// 'vue3-danmaku',
// 'v-contextmenu',
// 'vue-pdf-embed',

View File

@ -1 +1 @@
{"version":1721089915004}
{"version":1721355798048}

View File

@ -0,0 +1,90 @@
<script setup lang="ts">
import { useWindowSize } from '@vueuse/core';
import { onMounted, ref } from 'vue';
import Konva from 'konva/lib';
import { Group } from 'konva/lib/Group';
import { Rect } from 'konva/lib/shapes/Rect';
const { width, height } = useWindowSize();
const yellowGroup = ref<Group>();
const blueGroup = ref<Group>();
const box = ref<Rect>();
const initial = () => {
const stage = new Konva.Stage({ container: 'container', width: width.value, height: height.value });
const layer = new Konva.Layer();
stage.add(layer);
yellowGroup.value = new Konva.Group({
x: 100,
y: 100,
draggable: true,
});
blueGroup.value = new Konva.Group({
x: 300,
y: 80,
draggable: true,
});
box.value = new Konva.Rect({
x: 10,
y: 10,
width: 100,
height: 50,
fill: 'red',
stroke: 'black',
});
const yellowCircle = new Konva.Circle({
x: 0,
y: 0,
radius: 50,
fill: 'yellow',
stroke: 'black',
});
const blueCircle = new Konva.Circle({
x: 0,
y: 0,
radius: 50,
fill: 'blue',
stroke: 'black',
});
yellowGroup.value.add(yellowCircle);
yellowGroup.value.add(box.value);
blueGroup.value.add(blueCircle);
layer.add(yellowGroup.value);
layer.add(blueGroup.value);
stage.add(layer);
};
/**
* * 移动到蓝色组
*/
const toBlue = () => {
box.value?.moveTo(blueGroup.value);
};
/**
* * 移动到黄色
*/
const toYellow = () => {
box.value?.moveTo(yellowGroup.value);
};
onMounted(() => {
initial();
});
</script>
<template>
<div class="container-fluid">
<button id="toBlue" class="btn btn-primary" @click="toBlue">Move red box to blue group</button>
<button id="toYellow" class="btn btn-warning" @click="toYellow">Move red box to yellow group</button>
<div id="container"></div>
</div>
</template>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,41 @@
<script setup lang="ts">
import { useWindowSize } from '@vueuse/core';
import { onMounted } from 'vue';
import Konva from 'konva/lib';
const { width, height } = useWindowSize();
const initial = () => {
const stage = new Konva.Stage({ container: 'container', width: width.value, height: height.value });
const layer = new Konva.Layer();
stage.add(layer);
const group = new Konva.Group({ x: 120, y: 40, rotation: 20, draggable: true });
const colors = ['red', 'orange', 'yellow'];
for (let i = 0; i < 3; i++) {
const rect = new Konva.Rect({
x: i * 30,
y: i * 30,
width: 100,
height: 20,
name: `color${colors[i]}`,
fill: colors[i],
stroke: 'black',
strokeWidth: 4,
});
group.add(rect);
}
layer.add(group);
};
onMounted(() => {
initial();
});
</script>
<template>
<div id="container"></div>
</template>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,66 @@
<script setup lang="ts">
import { useWindowSize } from '@vueuse/core';
import { onMounted, ref } from 'vue';
import Konva from 'konva/lib';
import { Rect } from 'konva/lib/shapes/Rect';
const { width, height } = useWindowSize();
const yellowBox = ref<Rect>();
const initial = () => {
const stage = new Konva.Stage({ container: 'container', width: width.value, height: height.value });
const layer = new Konva.Layer();
stage.add(layer);
const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'purple'];
for (let n = 0; n < 6; n++) {
(function () {
const i = n;
const box = new Konva.Rect({
x: i * 30 + 210,
y: i * 18 + 40,
width: 100,
height: 50,
fill: colors[i],
stroke: 'black',
strokeWidth: 4,
draggable: true,
name: colors[i],
});
box.on('mouseover', function () {
document.body.style.cursor = 'pointer';
});
box.on('mouseout', function () {
document.body.style.cursor = 'default';
});
if (colors[i] === 'yellow') {
yellowBox.value = box;
}
layer.add(box);
})();
}
stage.add(layer);
};
onMounted(() => {
initial();
});
</script>
<template>
<div class="container-fluid">
<div id="buttons">
<button id="toTop" class="btn btn-warning" @click="yellowBox?.moveToTop()">Move yellow box to top</button>
<button id="toBottom" class="btn btn-outline-info" @click="yellowBox?.moveToBottom()">Move yellow box to bottom</button>
<button id="up" class="btn btn-outline-warning" @click="yellowBox?.moveUp()">Move yellow box up</button>
<button id="down " class="btn btn-info" @click="yellowBox?.moveDown()">Move yellow box down</button>
<button id="zIndex" class="btn btn-primary" @click="yellowBox?.setZIndex(3)">Set yellow box zIndex to 3</button>
</div>
<div id="container"></div>
</div>
</template>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,55 @@
<script setup lang="ts">
import { useWindowSize } from '@vueuse/core';
import Konva from 'konva/lib';
import { onMounted, ref } from 'vue';
import { Circle } from 'konva/lib/shapes/Circle';
const { width, height } = useWindowSize();
const circle = ref<Circle>();
const initial = () => {
const stage = new Konva.Stage({ container: 'container', width: width.value, height: height.value });
const layer = new Konva.Layer();
stage.add(layer);
const group = new Konva.Group();
layer.add(group);
circle.value = new Konva.Circle({
x: 70,
y: 70,
fill: 'red',
radius: 30,
});
group.add(circle.value);
const blackRect = new Konva.Rect({
x: 20,
y: 20,
fill: 'black',
width: 100,
height: 100,
});
group.add(blackRect);
};
/**
* * 将圆形zIndex设置为1
*/
const updateCircle = () => {
circle.value!.zIndex(1);
};
onMounted(() => {
initial();
});
</script>
<template>
<div class="container-fluid">
<button class="btn btn-primary" @click="updateCircle">将圆形zIndex设置为1</button>
<div id="container"></div>
</div>
</template>
<style scoped lang="scss"></style>

View File

@ -1,153 +1,48 @@
<script setup lang="ts">
import { onMounted } from 'vue';
import { useWindowSize } from '@vueuse/core';
import { onMounted, ref } from 'vue';
import { useEventListener, useWindowSize } from '@vueuse/core';
import Konva from 'konva/lib';
import { rect1, rect2, selectionRectangle } from '@/views/select/basic/rect.ts';
import { stageEvent } from '@/views/select/basic/stageEvent.ts';
import { drawLine } from '@/views/select/basic/line.ts';
import { Stage } from 'konva/lib/Stage';
import { Layer } from 'konva/lib/Layer';
const { width, height } = useWindowSize();
// let stage, layer;
const stage = ref<Stage>();
const layer = ref<Layer>();
const tr = ref();
const initial = () => {
const stage = new Konva.Stage({ container: 'container', width: width.value, height: height.value });
const layer = new Konva.Layer();
stage.value = new Konva.Stage({ container: 'container', width: width.value, height: height.value });
layer.value = new Konva.Layer();
stage.value.add(layer.value);
const rect1 = new Konva.Rect({
x: 60,
y: 60,
width: 100,
height: 90,
fill: 'red',
name: 'rect',
draggable: true,
});
layer.add(rect1);
drawLine(stage.value, layer.value);
const rect2 = new Konva.Rect({
x: 180,
y: 200,
width: 100,
height: 200,
fill: 'green',
name: 'rect',
draggable: true,
});
layer.add(rect2);
rect1(layer.value);
rect2(layer.value);
//
const tr = new Konva.Transformer();
layer.add(tr);
tr.nodes([rect1, rect2]);
tr.value = new Konva.Transformer();
layer.value.add(tr.value);
const selectionRectangle = new Konva.Rect({
fill: 'rgba(0,0,255,0.5)',
visible: false,
// disable events to not interrupt with events
listening: false,
});
layer.add(selectionRectangle);
let x1 = 0,
y1 = 0,
x2 = 0,
y2 = 0;
let selecting = false;
stage.on('mousedown touchstart', e => {
// do nothing if we mousedown on any shape
if (e.target !== stage) {
return;
}
e.evt.preventDefault();
x1 = stage.getPointerPosition()!.x;
y1 = stage.getPointerPosition()!.y;
x2 = stage.getPointerPosition()!.x;
y2 = stage.getPointerPosition()!.y;
selectionRectangle.width(0);
selectionRectangle.height(0);
selecting = true;
});
stage.on('mousemove touchmove', e => {
// do nothing if we didn't start selection
if (!selecting) {
return;
}
e.evt.preventDefault();
x2 = stage.getPointerPosition()!.x;
y2 = stage.getPointerPosition()!.y;
selectionRectangle.setAttrs({
visible: true,
x: Math.min(x1, x2),
y: Math.min(y1, y2),
width: Math.abs(x2 - x1),
height: Math.abs(y2 - y1),
});
});
stage.on('mouseup touchend', e => {
// do nothing if we didn't start selection
selecting = false;
if (!selectionRectangle.visible()) {
return;
}
e.evt.preventDefault();
// update visibility in timeout, so we can check it in click event
selectionRectangle.visible(false);
let shapes = stage.find('.rect');
let box = selectionRectangle.getClientRect();
let selected = shapes.filter(shape => Konva.Util.haveIntersection(box, shape.getClientRect()));
tr.nodes(selected);
});
// clicks should select/deselect shapes
stage.on('click tap', function (e) {
// if we are selecting with rect, do nothing
if (selectionRectangle.visible()) {
return;
}
// if click on empty area - remove all selections
if (e.target === stage) {
tr.nodes([]);
return;
}
// do nothing if clicked NOT on our rectangles
if (!e.target.hasName('rect')) {
return;
}
// do we pressed shift or ctrl?
const metaPressed = e.evt.shiftKey || e.evt.ctrlKey || e.evt.metaKey;
const isSelected = tr.nodes().indexOf(e.target) >= 0;
if (!metaPressed && !isSelected) {
// if no key pressed and the node is not selected
// select just one
tr.nodes([e.target]);
} else if (metaPressed && isSelected) {
// if we pressed keys and node was selected
// we need to remove it from selection:
const nodes = tr.nodes().slice(); // use slice to have new copy of array
// remove node from array
nodes.splice(nodes.indexOf(e.target), 1);
tr.nodes(nodes);
} else if (metaPressed && !isSelected) {
// add the node into selection
const nodes = tr.nodes().concat([e.target]);
tr.nodes(nodes);
}
});
stage.add(layer);
stageEvent(stage.value, selectionRectangle(layer.value), tr.value);
};
onMounted(() => {
initial();
useEventListener(window, 'resize', function () {
stage.value?.width(width.value);
stage.value?.height(height.value);
// tr.value.forceUpdate();
});
});
</script>
<template>
<div id="container"></div>
</template>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,33 @@
import { Stage } from 'konva/lib/Stage';
import { Layer } from 'konva/lib/Layer';
import Konva from 'konva/lib';
export const drawLine = (stage: Stage, layer: Layer) => {
const xSnaps = Math.round(stage.width() / 50);
const ySnaps = Math.round(stage.height() / 50);
const cellWidth = stage.width() / xSnaps;
const cellHeight = stage.height() / ySnaps;
for (let i = 0; i < xSnaps; i++) {
const xLine = new Konva.Line({
x: i * cellWidth,
points: [0, 0, 0, stage.height()],
stroke: '#96e04d',
strokeWidth: 1,
id: 'line',
});
layer.add(xLine);
}
for (let i = 0; i < ySnaps; i++) {
layer.add(
new Konva.Line({
y: i * cellHeight,
points: [0, 0, stage.width(), 0],
stroke: '#96e04d',
strokeWidth: 1,
id: 'line',
}),
);
}
};

View File

@ -0,0 +1,41 @@
import { Layer } from 'konva/lib/Layer';
import Konva from 'konva/lib';
export const rect1 = (parent: Layer) => {
const rect = new Konva.Rect({
x: 60,
y: 60,
width: 100,
height: 90,
fill: 'red',
name: 'rect',
draggable: true,
});
parent.add(rect);
return rect;
};
export const rect2 = (parent: Layer) => {
const rect = new Konva.Rect({
x: 180,
y: 200,
width: 100,
height: 200,
fill: 'green',
name: 'rect',
draggable: true,
});
parent.add(rect);
return rect;
};
export const selectionRectangle = (layer: Layer) => {
const selectionRectangle = new Konva.Rect({
fill: 'rgba(0,0,255,0.5)',
visible: false,
listening: false,
});
layer.add(selectionRectangle);
return selectionRectangle;
};

View File

@ -0,0 +1,93 @@
import Konva from 'konva/lib';
import { Stage } from 'konva/lib/Stage';
import { Rect } from 'konva/lib/shapes/Rect';
import { Transformer } from 'konva/lib/shapes/Transformer';
export const stageEvent = (stage: Stage, selectionRectangle: Rect, tr: Transformer) => {
let x1 = 0,
y1 = 0,
x2 = 0,
y2 = 0;
let selecting = false;
stage.on('mousedown touchstart', e => {
e.evt.preventDefault();
x1 = stage.getPointerPosition()!.x;
y1 = stage.getPointerPosition()!.y;
x2 = stage.getPointerPosition()!.x;
y2 = stage.getPointerPosition()!.y;
selectionRectangle.width(0);
selectionRectangle.height(0);
selecting = true;
});
stage.on('mousemove touchmove', e => {
if (!selecting) return;
e.evt.preventDefault();
x2 = stage.getPointerPosition()!.x;
y2 = stage.getPointerPosition()!.y;
selectionRectangle.setAttrs({
visible: true,
x: Math.min(x1, x2),
y: Math.min(y1, y2),
width: Math.abs(x2 - x1),
height: Math.abs(y2 - y1),
});
});
stage.on('mouseup touchend', e => {
selecting = false;
if (!selectionRectangle.visible()) return;
e.evt.preventDefault();
// update visibility in timeout, so we can check it in click event
selectionRectangle.visible(false);
let shapes = stage.find('.rect');
let box = selectionRectangle.getClientRect();
let selected = shapes.filter(shape => Konva.Util.haveIntersection(box, shape.getClientRect()));
tr.nodes(selected);
});
// clicks should select/deselect shapes
stage.on('click tap', function (e) {
// if we are selecting with rect, do nothing
if (selectionRectangle.visible()) {
return;
}
// if click on empty area - remove all selections
if (e.target === stage) {
tr.nodes([]);
return;
}
// do nothing if clicked NOT on our rectangles
if (!e.target.hasName('rect')) {
return;
}
// do we pressed shift or ctrl?
const metaPressed = e.evt.shiftKey || e.evt.ctrlKey || e.evt.metaKey;
const isSelected = tr.nodes().indexOf(e.target) >= 0;
if (!metaPressed && !isSelected) {
// if no key pressed and the node is not selected
// select just one
tr.nodes([e.target]);
} else if (metaPressed && isSelected) {
// if we pressed keys and node was selected
// we need to remove it from selection:
const nodes = tr.nodes().slice(); // use slice to have new copy of array
// remove node from array
nodes.splice(nodes.indexOf(e.target), 1);
tr.nodes(nodes);
} else if (metaPressed && !isSelected) {
// add the node into selection
const nodes = tr.nodes().concat([e.target]);
tr.nodes(nodes);
}
});
};

View File

@ -0,0 +1,54 @@
<script setup lang="ts">
import { onMounted } from 'vue';
import Konva from 'konva/lib';
import { useWindowSize } from '@vueuse/core';
const { width, height } = useWindowSize();
const MIN_WIDTH = 100;
const initial = () => {
const stage = new Konva.Stage({ container: 'container', width: width.value, height: height.value });
const layer = new Konva.Layer();
stage.add(layer);
const text = new Konva.Text({
x: 50,
y: 60,
fontSize: 20,
text: 'Hello from the Konva framework. Try to resize me.',
draggable: true,
});
layer.add(text);
const tr = new Konva.Transformer({
nodes: [text],
padding: 5,
flipEnabled: false,
enabledAnchors: ['middle-left', 'middle-right'],
boundBoxFunc(oldBox, newBox) {
if (Math.abs(newBox.width) < MIN_WIDTH) {
return oldBox;
}
return newBox;
},
});
layer.add(tr);
text.on('transform', () => {
text.setAttrs({
width: Math.max(text.width() * text.scaleX(), MIN_WIDTH),
scaleX: 1,
scaleY: 1,
});
});
};
onMounted(() => {
initial();
});
</script>
<template>
<div id="container"></div>
</template>

View File

@ -0,0 +1,103 @@
<script setup lang="ts">
import { useWindowSize } from '@vueuse/core';
import { onMounted } from 'vue';
import Konva from 'konva/lib';
const { width, height } = useWindowSize();
const initial = () => {
const stage = new Konva.Stage({ container: 'container', width: width.value, height: height.value });
stage.draggable(true);
const layer = new Konva.Layer();
stage.add(layer);
const greenBox = new Konva.Rect({
x: 70,
y: stage.height() / 2,
width: 100,
height: 50,
fill: 'green',
stroke: 'black',
strokeWidth: 4,
offset: {
x: 50,
y: 25,
},
});
const blueBox = new Konva.Rect({
x: 190,
y: stage.height() / 2,
width: 100,
height: 50,
fill: 'blue',
stroke: 'black',
strokeWidth: 4,
offset: {
x: 50,
y: 25,
},
});
const redBox = new Konva.Rect({
x: 310,
y: stage.height() / 2,
width: 100,
height: 50,
fill: 'red',
stroke: 'black',
strokeWidth: 4,
offset: {
x: 50,
y: 25,
},
});
layer.add(greenBox);
layer.add(blueBox);
layer.add(redBox);
// the tween has to be created after the node has been added to the layer
greenBox.tween = new Konva.Tween({
node: greenBox,
scaleX: 2,
scaleY: 1.5,
easing: Konva.Easings.EaseIn,
duration: 1,
});
blueBox.tween = new Konva.Tween({
node: blueBox,
scaleX: 2,
scaleY: 1.5,
easing: Konva.Easings.EaseInOut,
duration: 1,
});
redBox.tween = new Konva.Tween({
node: redBox,
scaleX: 2,
scaleY: 1.5,
easing: Konva.Easings.EaseOut,
duration: 1,
});
// use event delegation
layer.on('mouseover touchstart', function (evt) {
evt.target.tween.play();
});
layer.on('mouseout touchend', function (evt) {
evt.target.tween.reverse();
});
};
onMounted(() => {
initial();
});
</script>
<template>
<div id="container"></div>
</template>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,51 @@
<script setup lang="ts">
import { useWindowSize } from '@vueuse/core';
import { onMounted } from 'vue';
import Konva from 'konva/lib';
const { width, height } = useWindowSize();
const initial = () => {
const stage = new Konva.Stage({ container: 'container', width: width.value, height: height.value });
const layer = new Konva.Layer();
stage.add(layer);
const rect = new Konva.Rect({
x: 50,
y: 20,
width: 120,
height: 50,
fill: 'green',
stroke: 'black',
strokeWidth: 2,
opacity: 0.2,
});
layer.add(rect);
const tween = new Konva.Tween({
node: rect,
duration: 1,
x: 140,
y: 90,
fill: 'red',
rotation: Math.PI * 2,
opacity: 1,
strokeWidth: 6,
scaleX: 1.5,
});
setTimeout(() => {
tween.play();
}, 2000);
};
onMounted(() => {
initial();
});
</script>
<template>
<div id="container"></div>
</template>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,59 @@
<script setup lang="ts">
import { useWindowSize } from '@vueuse/core';
import { onMounted } from 'vue';
import Konva from 'konva/lib';
const { width, height } = useWindowSize();
const initial = () => {
const stage = new Konva.Stage({ container: 'container', width: width.value, height: height.value });
const layer = new Konva.Layer();
stage.add(layer);
const image = new Image();
const lion = new Konva.Image({
x: 80,
y: 30,
width: 50,
height: 50,
draggable: true,
image,
});
layer.add(lion);
image.crossOrigin = 'Anonymous';
image.src = 'https://konvajs.org/assets/darth-vader.jpg';
image.onload = function () {
lion.image(image);
lion.cache();
lion.filters([Konva.Filters.Blur]);
lion.blurRadius(100);
const tween = new Konva.Tween({
node: lion,
duration: 0.6,
blurRadius: 0,
easing: Konva.Easings.EaseInOut,
});
lion.on('mouseover', function () {
tween.play();
});
lion.on('mouseout', function () {
tween.reverse();
});
};
};
onMounted(() => {
initial();
});
</script>
<template>
<div id="container"></div>
</template>
<style scoped lang="scss"></style>