@shijiu/jsview-vue-samples 2.1.200 → 2.1.339-test.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AnimPicture/App.vue +224 -120
- package/AnimPicture/Item.vue +44 -0
- package/ConnectLine/App.vue +173 -0
- package/CoupletsTest/App.vue +212 -0
- package/CoupletsTest/Common/SpriteDeal.js +30 -0
- package/CoupletsTest/LeadWire.vue +221 -0
- package/CoupletsTest/Maroon.vue +112 -0
- package/CoupletsTest/Salvo.vue +251 -0
- package/CoupletsTest/Sprite/firecracker.json +212 -0
- package/CoupletsTest/Sprite/firecracker.png +0 -0
- package/CoupletsTest/Sprite/fireworks.json +220 -0
- package/CoupletsTest/Sprite/fireworks.png +0 -0
- package/CoupletsTest/Sprite/scroll.json +76 -0
- package/CoupletsTest/Sprite/scroll.png +0 -0
- package/CoupletsTest/Sprite/spark.json +268 -0
- package/CoupletsTest/Sprite/spark.png +0 -0
- package/CoupletsTest/images/Couplets.png +0 -0
- package/CoupletsTest/images/goldencoin1.png +0 -0
- package/CoupletsTest/images/goldencoin2.png +0 -0
- package/CoupletsTest/images/leadWire.png +0 -0
- package/CoupletsTest/images/line.png +0 -0
- package/CoupletsTest/images/purpleStar.png +0 -0
- package/CoupletsTest/images/redStar.png +0 -0
- package/CoupletsTest/images/scroll1.png +0 -0
- package/CoupletsTest/images/star1.png +0 -0
- package/CoupletsTest/images/star2.png +0 -0
- package/CoupletsTest/images/star3.png +0 -0
- package/CoupletsTest/images/star4.png +0 -0
- package/CoupletsTest/images/yellowStar.png +0 -0
- package/DemoHomepage/components/BodyFrame.vue +27 -11
- package/DemoHomepage/router.js +35 -5
- package/DemoHomepage/views/Homepage.vue +1 -1
- package/DispersedItemSample/DispersedItemSample.vue +138 -0
- package/DispersedItemSample/DispersedItemWidget/DispersedItemWidget.vue +358 -0
- package/DispersedItemSample/DispersedItemWidget/MyRenderItem.ts +115 -0
- package/DispersedItemSample/Item.vue +55 -0
- package/FilterDemo/AnimatePic.vue +5 -6
- package/FocusMoveStyle/App.vue +126 -110
- package/FocusMoveStyle/CreepFocus.vue +128 -0
- package/FocusMoveStyle/FoldableItem.vue +279 -0
- package/FocusMoveStyle/Item.vue +32 -31
- package/FreeMove/App.vue +2 -2
- package/ImpactStop/App.vue +343 -384
- package/LatexDemo/App.vue +11 -0
- package/MetroWidgetDemos/RefreshDemo/App.vue +101 -0
- package/MetroWidgetDemos/RefreshDemo/Item.vue +116 -0
- package/MetroWidgetDemos/RefreshDemo/assets/imageList.json +237 -0
- package/MetroWidgetDemos/RefreshDemo/data.js +16 -0
- package/MetroWidgetDemos/TripleWidget/App.vue +81 -0
- package/MetroWidgetDemos/TripleWidget/Item.vue +64 -0
- package/MetroWidgetDemos/TripleWidget/SWidgetItem.vue +93 -0
- package/MetroWidgetDemos/TripleWidget/WidgetItem.vue +111 -0
- package/MetroWidgetDemos/routeList.js +12 -0
- package/ProgressBar/App.vue +128 -0
- package/QrcodeDemo/App.vue +2 -2
- package/SpriteImage/App.vue +113 -68
- package/SwiperTest/App.vue +105 -0
- package/ViewOpacity/App.vue +98 -0
- package/package.json +1 -1
package/FocusMoveStyle/App.vue
CHANGED
|
@@ -1,60 +1,15 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import {
|
|
3
|
-
MetroWidget,
|
|
4
3
|
HORIZONTAL,
|
|
5
|
-
JsvNinePatch,
|
|
6
4
|
useFocusHub,
|
|
7
5
|
getTextWidth,
|
|
8
|
-
|
|
6
|
+
FixPositionSlide,
|
|
7
|
+
ListWidget,
|
|
9
8
|
} from "jsview";
|
|
10
|
-
import
|
|
9
|
+
import FoldableItem from "./FoldableItem.vue";
|
|
11
10
|
import redCircle from "./assets/redCircle.png";
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
const focusHub = useFocusHub();
|
|
15
|
-
const styleShell = getKeyFramesGroup();
|
|
16
|
-
|
|
17
|
-
const animationCache = {};
|
|
18
|
-
const addAnimation = (fromRect, toRect, name) => {
|
|
19
|
-
if (animationCache.hasOwnProperty(name)) {
|
|
20
|
-
styleShell.removeRule(name);
|
|
21
|
-
delete animationCache[name];
|
|
22
|
-
}
|
|
23
|
-
console.log("LudlDebug anim");
|
|
24
|
-
if (!(name in animationCache)) {
|
|
25
|
-
const animStr = `@keyframes ${name} {
|
|
26
|
-
0% {
|
|
27
|
-
left: ${fromRect.left};
|
|
28
|
-
top: ${fromRect.top};
|
|
29
|
-
width: ${fromRect.width};
|
|
30
|
-
height: ${fromRect.height};
|
|
31
|
-
}
|
|
32
|
-
50% {
|
|
33
|
-
left: ${Math.min(fromRect.left, toRect.left)};
|
|
34
|
-
top: ${Math.min(fromRect.top, toRect.top)};
|
|
35
|
-
width: ${
|
|
36
|
-
Math.max(fromRect.left + fromRect.width, toRect.left + toRect.width) -
|
|
37
|
-
Math.min(fromRect.left, toRect.left)
|
|
38
|
-
};
|
|
39
|
-
height: ${
|
|
40
|
-
Math.max(fromRect.top + fromRect.height, toRect.top + toRect.height) -
|
|
41
|
-
Math.min(fromRect.top, toRect.top)
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
100% {
|
|
45
|
-
left: ${toRect.left};
|
|
46
|
-
top: ${toRect.top};
|
|
47
|
-
width: ${toRect.width};
|
|
48
|
-
height: ${toRect.height};
|
|
49
|
-
}
|
|
50
|
-
}`;
|
|
51
|
-
animationCache[name] = animStr;
|
|
52
|
-
styleShell.insertRule(animStr);
|
|
53
|
-
}
|
|
54
|
-
};
|
|
55
|
-
const removeAnimation = (name) => {
|
|
56
|
-
styleShell.removeRule(name);
|
|
57
|
-
};
|
|
11
|
+
import { ref, provide, onMounted } from "vue";
|
|
12
|
+
import CreepFocus from "./CreepFocus.vue";
|
|
58
13
|
|
|
59
14
|
const randomColor = () => {
|
|
60
15
|
let randomColor = Math.round(Math.random() * 2 ** 24).toString(16);
|
|
@@ -62,40 +17,24 @@ const randomColor = () => {
|
|
|
62
17
|
"#" + new Array(6 - randomColor.length).fill("0").join("") + randomColor
|
|
63
18
|
);
|
|
64
19
|
};
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
height: 0,
|
|
68
|
-
left: 0,
|
|
69
|
-
top: 0,
|
|
70
|
-
animation: null,
|
|
71
|
-
onTransitionEnd: null,
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
let animateChange = 0; // A B版动画切换,解决animation属性不变化引起不刷新问题
|
|
20
|
+
const slideType = new FixPositionSlide();
|
|
21
|
+
const focusHub = useFocusHub();
|
|
75
22
|
|
|
23
|
+
const creepFocus = ref(null);
|
|
24
|
+
const showFocus = ref(true);
|
|
25
|
+
let currentFocusRect = {};
|
|
76
26
|
const focusFrameController = {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
this.curRect = rect;
|
|
84
|
-
if (this.focusIndex >= 0) {
|
|
85
|
-
const animName = "test_focusMove_" + animateChange;
|
|
86
|
-
animateChange = animateChange == 0 ? 1 : 0; // A B 版切换
|
|
87
|
-
addAnimation(this.preRect, this.curRect, animName);
|
|
88
|
-
focusSize.animation = `${animName} 0.3s linear`;
|
|
89
|
-
}
|
|
90
|
-
focusSize.left = this.curRect.left;
|
|
91
|
-
focusSize.top = this.curRect.top;
|
|
92
|
-
focusSize.width = this.curRect.width;
|
|
93
|
-
focusSize.height = this.curRect.height;
|
|
94
|
-
this.focusIndex = focusIndex;
|
|
27
|
+
focusVisible: (visible) => {
|
|
28
|
+
showFocus.value = visible ? true : false;
|
|
29
|
+
},
|
|
30
|
+
onFocusChange: (rect, focusIndex, doAnim) => {
|
|
31
|
+
creepFocus.value?.changeRect(rect, doAnim);
|
|
32
|
+
currentFocusRect = rect;
|
|
95
33
|
},
|
|
96
34
|
};
|
|
97
35
|
provide("focusFrameController", focusFrameController);
|
|
98
36
|
|
|
37
|
+
//<<<<<<< 创建数据
|
|
99
38
|
const contentData = [
|
|
100
39
|
"推荐",
|
|
101
40
|
"电视剧",
|
|
@@ -113,37 +52,85 @@ const contentData = [
|
|
|
113
52
|
const SizeObj = {
|
|
114
53
|
fontSize: 40,
|
|
115
54
|
};
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
55
|
+
const data = [];
|
|
56
|
+
const getChildTabList = (textSize, childTextSize, contentText) => {
|
|
57
|
+
const list = [];
|
|
58
|
+
// 3-5个
|
|
59
|
+
const number = Math.round(Math.random() * 2 + 3);
|
|
60
|
+
for (let i = 0; i < number; ++i) {
|
|
61
|
+
list.push({
|
|
62
|
+
width: (i == number - 1 ? textSize : childTextSize) + 60,
|
|
121
63
|
height: 65,
|
|
64
|
+
marginRight: i == number - 1 ? 0 : 10,
|
|
122
65
|
color: randomColor(),
|
|
123
|
-
content:
|
|
66
|
+
content: contentText + (i == number - 1 ? "" : "_" + i),
|
|
124
67
|
});
|
|
125
68
|
}
|
|
69
|
+
return list;
|
|
70
|
+
};
|
|
71
|
+
for (let i = 0; i < 12; ++i) {
|
|
72
|
+
const textSize = getTextWidth(contentData[i], SizeObj);
|
|
73
|
+
const childTextSize = getTextWidth(contentData[i] + "_0", SizeObj);
|
|
74
|
+
const childList = getChildTabList(textSize, childTextSize, contentData[i]);
|
|
75
|
+
let totalWidth = 0;
|
|
76
|
+
childList.forEach((item) => {
|
|
77
|
+
totalWidth += item.width + item.marginRight;
|
|
78
|
+
});
|
|
79
|
+
const initFocus = Math.round(Math.random() * (childList.length - 1));
|
|
80
|
+
|
|
81
|
+
data.push({
|
|
82
|
+
width: textSize + 60,
|
|
83
|
+
height: 65,
|
|
84
|
+
color: randomColor(),
|
|
85
|
+
content: contentData[i],
|
|
86
|
+
marginRight: 10,
|
|
87
|
+
name: "item_" + i, // focus name
|
|
88
|
+
childTab: {
|
|
89
|
+
width: totalWidth, //4个item + 3个gap
|
|
90
|
+
list: childList,
|
|
91
|
+
},
|
|
92
|
+
initFocus,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
const provideData = () => {
|
|
126
96
|
return data;
|
|
127
97
|
};
|
|
98
|
+
// >>>>>>
|
|
99
|
+
|
|
100
|
+
const getDetaileInfo = () => {
|
|
101
|
+
let str = "";
|
|
102
|
+
for (let item of data) {
|
|
103
|
+
str += `${item.content}共有${item.childTab.list.length}个子tab, 展开后的默认焦点为${item.initFocus}\n`;
|
|
104
|
+
}
|
|
105
|
+
return str;
|
|
106
|
+
};
|
|
128
107
|
|
|
129
108
|
const measures = (item) => {
|
|
130
109
|
return {
|
|
131
110
|
width: item.width,
|
|
132
111
|
height: item.height,
|
|
133
|
-
marginRight:
|
|
134
|
-
marginBottom: 10,
|
|
112
|
+
marginRight: item.marginRight,
|
|
135
113
|
};
|
|
136
114
|
};
|
|
137
115
|
|
|
116
|
+
const widgetRef = ref();
|
|
138
117
|
onMounted(() => {
|
|
118
|
+
provide("parentWidget", widgetRef);
|
|
139
119
|
focusHub.setFocus("myWidget");
|
|
140
120
|
});
|
|
141
121
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
122
|
+
const onFocusRectChange = () => {
|
|
123
|
+
//展开时的滑动不需要更新焦点大小
|
|
124
|
+
if (showFocus.value) {
|
|
125
|
+
const curFocusIndex = widgetRef.value.getCurrentFocusIndex();
|
|
126
|
+
const rect = widgetRef.value.getTemplatePosition(curFocusIndex);
|
|
127
|
+
focusFrameController.onFocusChange(rect, curFocusIndex);
|
|
145
128
|
}
|
|
146
|
-
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const onAllResizeDone = () => {
|
|
132
|
+
console.log("onAllResizeDone");
|
|
133
|
+
};
|
|
147
134
|
</script>
|
|
148
135
|
|
|
149
136
|
<template>
|
|
@@ -163,38 +150,67 @@ onBeforeUnmount(() => {
|
|
|
163
150
|
>
|
|
164
151
|
爬行样式的焦点示例
|
|
165
152
|
</div>
|
|
166
|
-
<
|
|
153
|
+
<list-widget
|
|
154
|
+
ref="widgetRef"
|
|
167
155
|
name="myWidget"
|
|
156
|
+
:onFocusRectChange="onFocusRectChange"
|
|
157
|
+
:onAllItemResizeDone="onAllResizeDone"
|
|
168
158
|
:top="100"
|
|
169
159
|
:left="40"
|
|
170
160
|
:width="1200"
|
|
171
161
|
:height="110"
|
|
162
|
+
:slideSetting="slideType"
|
|
172
163
|
:direction="HORIZONTAL"
|
|
173
164
|
:provideData="provideData"
|
|
174
165
|
:measures="measures"
|
|
175
166
|
:padding="{ left: 20, right: 20, top: 20, height: 20 }"
|
|
176
167
|
>
|
|
177
|
-
<template #renderItem="{ data, onAction, query }">
|
|
178
|
-
<item
|
|
168
|
+
<template #renderItem="{ data, onAction, query, onItemEdge }">
|
|
169
|
+
<foldable-item
|
|
170
|
+
:data="data"
|
|
171
|
+
:onAction="onAction"
|
|
172
|
+
:query="query"
|
|
173
|
+
:onItemEdge="onItemEdge"
|
|
174
|
+
/>
|
|
179
175
|
</template>
|
|
180
176
|
<template #background>
|
|
181
|
-
<
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
:
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
:centerWidth="0"
|
|
192
|
-
:borderOutset="0"
|
|
193
|
-
:imageDspWidth="(focusSize.height / 17) * 2"
|
|
194
|
-
:animTime="0.2"
|
|
195
|
-
:waitForInit="true"
|
|
196
|
-
></jsv-nine-patch>
|
|
177
|
+
<creep-focus
|
|
178
|
+
v-if="showFocus"
|
|
179
|
+
ref="creepFocus"
|
|
180
|
+
:imgUrl="redCircle"
|
|
181
|
+
:initLeft="currentFocusRect?.left"
|
|
182
|
+
:initTop="currentFocusRect?.top"
|
|
183
|
+
:initWidth="currentFocusRect?.width"
|
|
184
|
+
:initHeight="currentFocusRect?.height"
|
|
185
|
+
>
|
|
186
|
+
</creep-focus>
|
|
197
187
|
</template>
|
|
198
|
-
</
|
|
188
|
+
</list-widget>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<div
|
|
192
|
+
:style="{
|
|
193
|
+
left: 100,
|
|
194
|
+
top: 250,
|
|
195
|
+
width: 1280,
|
|
196
|
+
height: 400,
|
|
197
|
+
fontSize: 25,
|
|
198
|
+
color: '#FFFFFF',
|
|
199
|
+
}"
|
|
200
|
+
>
|
|
201
|
+
OK键展开, 返回键折叠.
|
|
202
|
+
展开后的默认焦点的位置和展开前相同(第一个和最后一个除外)
|
|
203
|
+
</div>
|
|
204
|
+
<div
|
|
205
|
+
:style="{
|
|
206
|
+
left: 100,
|
|
207
|
+
top: 300,
|
|
208
|
+
width: 1280,
|
|
209
|
+
height: 400,
|
|
210
|
+
fontSize: 20,
|
|
211
|
+
color: '#FFFFFF',
|
|
212
|
+
}"
|
|
213
|
+
>
|
|
214
|
+
{{ getDetaileInfo() }}
|
|
199
215
|
</div>
|
|
200
216
|
</template>
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { JsvNinePatch, getKeyFramesGroup } from "jsview";
|
|
3
|
+
import { reactive, onBeforeUnmount } from "vue";
|
|
4
|
+
|
|
5
|
+
const props = defineProps({
|
|
6
|
+
imgUrl: {
|
|
7
|
+
type: String,
|
|
8
|
+
},
|
|
9
|
+
initWidth: {
|
|
10
|
+
type: Number,
|
|
11
|
+
default: 0,
|
|
12
|
+
},
|
|
13
|
+
initHeight: {
|
|
14
|
+
type: Number,
|
|
15
|
+
default: 0,
|
|
16
|
+
},
|
|
17
|
+
initLeft: {
|
|
18
|
+
type: Number,
|
|
19
|
+
default: 0,
|
|
20
|
+
},
|
|
21
|
+
initTop: {
|
|
22
|
+
type: Number,
|
|
23
|
+
default: 0,
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const styleShell = getKeyFramesGroup();
|
|
28
|
+
|
|
29
|
+
const animationCache = {};
|
|
30
|
+
const addAnimation = (fromRect, toRect, name) => {
|
|
31
|
+
if (animationCache.hasOwnProperty(name)) {
|
|
32
|
+
styleShell.removeRule(name);
|
|
33
|
+
delete animationCache[name];
|
|
34
|
+
}
|
|
35
|
+
if (!(name in animationCache)) {
|
|
36
|
+
const animStr = `@keyframes ${name} {
|
|
37
|
+
0% {
|
|
38
|
+
left: ${fromRect.left};
|
|
39
|
+
top: ${fromRect.top};
|
|
40
|
+
width: ${fromRect.width};
|
|
41
|
+
height: ${fromRect.height};
|
|
42
|
+
}
|
|
43
|
+
50% {
|
|
44
|
+
left: ${Math.min(fromRect.left, toRect.left)};
|
|
45
|
+
top: ${Math.min(fromRect.top, toRect.top)};
|
|
46
|
+
width: ${
|
|
47
|
+
Math.max(fromRect.left + fromRect.width, toRect.left + toRect.width) -
|
|
48
|
+
Math.min(fromRect.left, toRect.left)
|
|
49
|
+
};
|
|
50
|
+
height: ${
|
|
51
|
+
Math.max(fromRect.top + fromRect.height, toRect.top + toRect.height) -
|
|
52
|
+
Math.min(fromRect.top, toRect.top)
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
100% {
|
|
56
|
+
left: ${toRect.left};
|
|
57
|
+
top: ${toRect.top};
|
|
58
|
+
width: ${toRect.width};
|
|
59
|
+
height: ${toRect.height};
|
|
60
|
+
}
|
|
61
|
+
}`;
|
|
62
|
+
animationCache[name] = animStr;
|
|
63
|
+
styleShell.insertRule(animStr);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
const removeAnimation = (name) => {
|
|
67
|
+
styleShell.removeRule(name);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const focusSize = reactive({
|
|
71
|
+
width: props.initWidth,
|
|
72
|
+
height: props.initHeight,
|
|
73
|
+
left: props.initLeft,
|
|
74
|
+
top: props.initTop,
|
|
75
|
+
animation: null,
|
|
76
|
+
onTransitionEnd: null,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
let animateChange = 0; // A B版动画切换,解决animation属性不变化引起不刷新问题
|
|
80
|
+
let preRect = null;
|
|
81
|
+
let curRect = null;
|
|
82
|
+
let first = true;
|
|
83
|
+
|
|
84
|
+
const changeRect = (rect, doAnim = true) => {
|
|
85
|
+
preRect = curRect;
|
|
86
|
+
curRect = rect;
|
|
87
|
+
if (first) {
|
|
88
|
+
first = false;
|
|
89
|
+
} else if (doAnim) {
|
|
90
|
+
const animName = "test_focusMove_" + animateChange;
|
|
91
|
+
animateChange = animateChange == 0 ? 1 : 0; // A B 版切换
|
|
92
|
+
addAnimation(preRect, curRect, animName);
|
|
93
|
+
focusSize.animation = `${animName} 0.3s linear`;
|
|
94
|
+
}
|
|
95
|
+
focusSize.left = curRect.left;
|
|
96
|
+
focusSize.top = curRect.top;
|
|
97
|
+
focusSize.width = curRect.width;
|
|
98
|
+
focusSize.height = curRect.height;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
onBeforeUnmount(() => {
|
|
102
|
+
for (let name in animationCache) {
|
|
103
|
+
removeAnimation(name);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
defineExpose({
|
|
108
|
+
changeRect,
|
|
109
|
+
});
|
|
110
|
+
</script>
|
|
111
|
+
|
|
112
|
+
<template>
|
|
113
|
+
<jsv-nine-patch
|
|
114
|
+
:style="{
|
|
115
|
+
width: focusSize.width,
|
|
116
|
+
height: focusSize.height,
|
|
117
|
+
top: focusSize.top,
|
|
118
|
+
left: focusSize.left,
|
|
119
|
+
}"
|
|
120
|
+
:animation="focusSize.animation"
|
|
121
|
+
:imageUrl="imgUrl"
|
|
122
|
+
:imageWidth="35"
|
|
123
|
+
:centerWidth="0"
|
|
124
|
+
:borderOutset="0"
|
|
125
|
+
:animTime="0.2"
|
|
126
|
+
:waitForInit="true"
|
|
127
|
+
></jsv-nine-patch>
|
|
128
|
+
</template>
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
* @Author: ChenChanghua
|
|
3
|
+
* @Date: 2023-03-08 14:52:17
|
|
4
|
+
* @Description: file content
|
|
5
|
+
-->
|
|
6
|
+
<script setup>
|
|
7
|
+
import { ref, inject, shallowRef, provide } from "vue";
|
|
8
|
+
import { ListWidget, HORIZONTAL, useFocusHub, getKeyFramesGroup } from "jsview";
|
|
9
|
+
import Item from "./Item.vue";
|
|
10
|
+
import CreepFocus from "./CreepFocus.vue";
|
|
11
|
+
import redCircle from "./assets/redCircle.png";
|
|
12
|
+
|
|
13
|
+
const DURATION = 200;
|
|
14
|
+
const props = defineProps({
|
|
15
|
+
data: Object,
|
|
16
|
+
query: Object,
|
|
17
|
+
onAction: Object,
|
|
18
|
+
onItemEdge: Function,
|
|
19
|
+
});
|
|
20
|
+
const focusHub = useFocusHub();
|
|
21
|
+
const folded = ref(true);
|
|
22
|
+
const creepFocus = ref(null);
|
|
23
|
+
const showFocus = ref(true);
|
|
24
|
+
const styleShell = getKeyFramesGroup();
|
|
25
|
+
const itemKeepScale = ref(false);
|
|
26
|
+
provide("itemKeepScale", itemKeepScale);
|
|
27
|
+
|
|
28
|
+
const focused = ref(false);
|
|
29
|
+
const width = ref(props.data.width);
|
|
30
|
+
const widgetRef = shallowRef(null);
|
|
31
|
+
const widgetLeft = ref(0);
|
|
32
|
+
|
|
33
|
+
const focusRectController = {
|
|
34
|
+
onFocusChange: (rect) => {
|
|
35
|
+
creepFocus.value?.changeRect(rect);
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
const focusFrameController = inject("focusFrameController");
|
|
39
|
+
|
|
40
|
+
const provideData = () => {
|
|
41
|
+
return props.data.childTab.list;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const measures = (data) => {
|
|
45
|
+
return {
|
|
46
|
+
width: data.width,
|
|
47
|
+
height: data.height,
|
|
48
|
+
marginRight: data.marginRight,
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// 注册回调
|
|
53
|
+
const onFocus = () => {
|
|
54
|
+
focusFrameController.focusVisible(true);
|
|
55
|
+
focused.value = true;
|
|
56
|
+
folded.value = true;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const foldSubTab = () => {
|
|
60
|
+
if (!folded.value) {
|
|
61
|
+
if (width.value == props.data.childTab.width) {
|
|
62
|
+
// showFocus.value = false;
|
|
63
|
+
width.value = props.data.width;
|
|
64
|
+
let index =
|
|
65
|
+
widgetRef.value?.getCurrentFocusIndex() ?? props.data.initFocus;
|
|
66
|
+
|
|
67
|
+
const anchor = getAnchor(index);
|
|
68
|
+
widgetLeft.value = -Math.round(
|
|
69
|
+
(props.data.childTab.width - props.data.width) * anchor
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
//创建动画
|
|
73
|
+
const anim = getClipAnimation(
|
|
74
|
+
props.data.childTab.width,
|
|
75
|
+
width.value,
|
|
76
|
+
anchor
|
|
77
|
+
);
|
|
78
|
+
clipAnimStr.value = anim.clip;
|
|
79
|
+
slideAnimStr.value = anim.slide;
|
|
80
|
+
|
|
81
|
+
props.query.updateItemSize(
|
|
82
|
+
props.query.index,
|
|
83
|
+
{
|
|
84
|
+
width: width.value,
|
|
85
|
+
height: props.data.height,
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
duration: DURATION,
|
|
89
|
+
onEnd: () => {
|
|
90
|
+
focusFrameController.focusVisible(true);
|
|
91
|
+
folded.value = true;
|
|
92
|
+
console.log("fold anim end.");
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
focusHub.returnFocusToParent();
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const onBlur = () => {
|
|
102
|
+
if (!itemKeepScale.value) {
|
|
103
|
+
showFocus.value = false;
|
|
104
|
+
}
|
|
105
|
+
focused.value = false;
|
|
106
|
+
foldSubTab();
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
let animToken = 1;
|
|
110
|
+
const clipAnimStr = ref("");
|
|
111
|
+
const slideAnimStr = ref("");
|
|
112
|
+
const animationCache = {};
|
|
113
|
+
const getClipAnimation = (from, to, anchor) => {
|
|
114
|
+
animToken++;
|
|
115
|
+
const name = "foldableItemClipAnim" + animToken;
|
|
116
|
+
if (animationCache.hasOwnProperty(name)) {
|
|
117
|
+
styleShell.removeRule(name);
|
|
118
|
+
delete animationCache[name];
|
|
119
|
+
}
|
|
120
|
+
const clipAnimStr = `@keyframes ${name} { from { width: ${from}; } to { width: ${to}; } }`;
|
|
121
|
+
animationCache[name] = clipAnimStr;
|
|
122
|
+
styleShell.insertRule(clipAnimStr);
|
|
123
|
+
|
|
124
|
+
//平移动画
|
|
125
|
+
const anchorPosition = Math.round((to - from) * anchor);
|
|
126
|
+
const slideName = "foldableItemSlideAnim" + animToken;
|
|
127
|
+
if (animationCache.hasOwnProperty(slideName)) {
|
|
128
|
+
styleShell.removeRule(slideName);
|
|
129
|
+
delete animationCache[slideName];
|
|
130
|
+
}
|
|
131
|
+
const slideAnimStr = `@keyframes ${slideName} { from { transform: translate3d(${-anchorPosition},0,0); } to { transform: translate3d(0,0,0); } }`;
|
|
132
|
+
|
|
133
|
+
animationCache[slideName] = slideAnimStr;
|
|
134
|
+
styleShell.insertRule(slideAnimStr);
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
clip: `${name} ${DURATION / 1000}s`,
|
|
138
|
+
slide: `${slideName} ${DURATION / 1000}s`,
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const getAnchor = (index) => {
|
|
143
|
+
let anchor = 0;
|
|
144
|
+
const l = props.data.childTab.list;
|
|
145
|
+
if (index <= 0) {
|
|
146
|
+
anchor = 0;
|
|
147
|
+
} else if (index > l.length - 1) {
|
|
148
|
+
anchor = 1;
|
|
149
|
+
} else {
|
|
150
|
+
let focusMiddle = 0;
|
|
151
|
+
for (let i = 0; i <= index - 1; ++i) {
|
|
152
|
+
focusMiddle += l[i].width + l[i].marginRight;
|
|
153
|
+
}
|
|
154
|
+
focusMiddle += l[index].width / 2;
|
|
155
|
+
const scaleRate = props.data.childTab.width / props.data.width;
|
|
156
|
+
const targetPercent = focusMiddle / props.data.childTab.width; // 居中目标位置的百分比
|
|
157
|
+
/**
|
|
158
|
+
* anchor计算说明
|
|
159
|
+
* 考虑[0,1]的线段, 以a(0 <= a <= 1)为锚点, 放大n倍(n > 1)
|
|
160
|
+
* 坐标的映射关系为 x -> n(x - a) + a
|
|
161
|
+
* 此处我们想要的是目标焦点的中线对齐0.5, 即targetPercent放大后的位置是0.5
|
|
162
|
+
* 所以方程为 scaleRate * (targetPercent - anchor) + anchor = 0.5
|
|
163
|
+
*/
|
|
164
|
+
anchor = (scaleRate * targetPercent - 0.5) / (scaleRate - 1);
|
|
165
|
+
}
|
|
166
|
+
return anchor;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const onClick = () => {
|
|
170
|
+
if (folded.value) {
|
|
171
|
+
itemKeepScale.value = false;
|
|
172
|
+
showFocus.value = true;
|
|
173
|
+
widgetLeft.value = 0;
|
|
174
|
+
folded.value = false;
|
|
175
|
+
width.value = props.data.childTab.width;
|
|
176
|
+
//为了居中而计算锚点
|
|
177
|
+
const anchor = getAnchor(props.data.initFocus);
|
|
178
|
+
//创建动画
|
|
179
|
+
const anim = getClipAnimation(props.data.width, width.value, anchor);
|
|
180
|
+
clipAnimStr.value = anim.clip;
|
|
181
|
+
slideAnimStr.value = anim.slide;
|
|
182
|
+
|
|
183
|
+
props.query.updateItemSize(
|
|
184
|
+
props.query.index,
|
|
185
|
+
{
|
|
186
|
+
width: width.value,
|
|
187
|
+
height: props.data.height,
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
anchor,
|
|
191
|
+
duration: DURATION,
|
|
192
|
+
onEnd: () => {
|
|
193
|
+
console.log("unfold anim end.");
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
focusHub.setFocus(props.data.name);
|
|
199
|
+
focusFrameController.focusVisible(false);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
props.onAction.register("onFocus", onFocus);
|
|
203
|
+
props.onAction.register("onBlur", onBlur);
|
|
204
|
+
props.onAction.register("onClick", onClick);
|
|
205
|
+
|
|
206
|
+
const onKeyDown = (ev) => {
|
|
207
|
+
if (ev.keyCode === 10000 || ev.keyCode === 8 || ev.keyCode === 27) {
|
|
208
|
+
itemKeepScale.value = true;
|
|
209
|
+
foldSubTab();
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
return false;
|
|
213
|
+
};
|
|
214
|
+
</script>
|
|
215
|
+
|
|
216
|
+
<template>
|
|
217
|
+
<div
|
|
218
|
+
v-if="folded"
|
|
219
|
+
:style="{
|
|
220
|
+
fontSize: 40,
|
|
221
|
+
width: data.width,
|
|
222
|
+
height: data.height,
|
|
223
|
+
color: '#FFFFFF',
|
|
224
|
+
textAlign: 'center',
|
|
225
|
+
lineHeight: data.height,
|
|
226
|
+
transform: focused ? 'scale3d(1.1,1.1,1)' : 'scale3d(1,1,1)',
|
|
227
|
+
}"
|
|
228
|
+
>
|
|
229
|
+
{{ data.content }}
|
|
230
|
+
</div>
|
|
231
|
+
<div
|
|
232
|
+
v-else
|
|
233
|
+
:style="{
|
|
234
|
+
width: width,
|
|
235
|
+
height: data.height,
|
|
236
|
+
overflow: 'hidden',
|
|
237
|
+
animation: clipAnimStr,
|
|
238
|
+
}"
|
|
239
|
+
>
|
|
240
|
+
<jsv-focus-block
|
|
241
|
+
:onAction="{
|
|
242
|
+
onKeyDown,
|
|
243
|
+
}"
|
|
244
|
+
:style="{
|
|
245
|
+
left: widgetLeft,
|
|
246
|
+
width: data.childTab.width,
|
|
247
|
+
height: data.height,
|
|
248
|
+
backgroundColor: '#00990099',
|
|
249
|
+
borderRadius: 10,
|
|
250
|
+
animation: slideAnimStr,
|
|
251
|
+
}"
|
|
252
|
+
>
|
|
253
|
+
<list-widget
|
|
254
|
+
ref="widgetRef"
|
|
255
|
+
:name="data.name"
|
|
256
|
+
:width="data.childTab.width"
|
|
257
|
+
:height="data.height"
|
|
258
|
+
:provideData="provideData"
|
|
259
|
+
:direction="HORIZONTAL"
|
|
260
|
+
:measures="measures"
|
|
261
|
+
:onEdge="onItemEdge"
|
|
262
|
+
:initFocusId="data.initFocus"
|
|
263
|
+
>
|
|
264
|
+
<template #renderItem="{ data, onAction, query }">
|
|
265
|
+
<item
|
|
266
|
+
:focusRectController="focusRectController"
|
|
267
|
+
:data="data"
|
|
268
|
+
:onAction="onAction"
|
|
269
|
+
:query="query"
|
|
270
|
+
/>
|
|
271
|
+
</template>
|
|
272
|
+
<template #background>
|
|
273
|
+
<creep-focus v-if="showFocus" ref="creepFocus" :imgUrl="redCircle">
|
|
274
|
+
</creep-focus>
|
|
275
|
+
</template>
|
|
276
|
+
</list-widget>
|
|
277
|
+
</jsv-focus-block>
|
|
278
|
+
</div>
|
|
279
|
+
</template>
|