张延森 2 anos atrás
pai
commit
2656b397d5

+ 1
- 0
package.json Ver arquivo

9
     "preview": "vite preview"
9
     "preview": "vite preview"
10
   },
10
   },
11
   "dependencies": {
11
   "dependencies": {
12
+    "@zjxpcyc/vue-tiny-store": "^1.0.1",
12
     "vue": "^3.2.37"
13
     "vue": "^3.2.37"
13
   },
14
   },
14
   "devDependencies": {
15
   "devDependencies": {

+ 8
- 0
pnpm-lock.yaml Ver arquivo

2
 
2
 
3
 specifiers:
3
 specifiers:
4
   '@vitejs/plugin-vue': ^3.0.3
4
   '@vitejs/plugin-vue': ^3.0.3
5
+  '@zjxpcyc/vue-tiny-store': ^1.0.1
5
   less: ^4.1.3
6
   less: ^4.1.3
6
   vite: ^3.0.7
7
   vite: ^3.0.7
7
   vue: ^3.2.37
8
   vue: ^3.2.37
8
 
9
 
9
 dependencies:
10
 dependencies:
11
+  '@zjxpcyc/vue-tiny-store': registry.npmmirror.com/@zjxpcyc/vue-tiny-store/1.0.1
10
   vue: registry.npmmirror.com/vue/3.2.37
12
   vue: registry.npmmirror.com/vue/3.2.37
11
 
13
 
12
 devDependencies:
14
 devDependencies:
166
     name: '@vue/shared'
168
     name: '@vue/shared'
167
     version: 3.2.37
169
     version: 3.2.37
168
 
170
 
171
+  registry.npmmirror.com/@zjxpcyc/vue-tiny-store/1.0.1:
172
+    resolution: {integrity: sha512-2r/E2bxGygn859ntWLHPDI+pLUR+MuIEPGSN82Y6xPdbQqcsmX1/+L+Pivh/LwFeEY51YcEDhPH8wkhzmtuD8g==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@zjxpcyc/vue-tiny-store/-/vue-tiny-store-1.0.1.tgz}
173
+    name: '@zjxpcyc/vue-tiny-store'
174
+    version: 1.0.1
175
+    dev: false
176
+
169
   registry.npmmirror.com/copy-anything/2.0.6:
177
   registry.npmmirror.com/copy-anything/2.0.6:
170
     resolution: {integrity: sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/copy-anything/-/copy-anything-2.0.6.tgz}
178
     resolution: {integrity: sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/copy-anything/-/copy-anything-2.0.6.tgz}
171
     name: copy-anything
179
     name: copy-anything

+ 2
- 0
src/App.vue Ver arquivo

1
 <script setup>
1
 <script setup>
2
 import { computed, onMounted, ref } from 'vue';
2
 import { computed, onMounted, ref } from 'vue';
3
 import Loader from '@/components/Loader.vue';
3
 import Loader from '@/components/Loader.vue';
4
+import ScrollDown from '@/components/ScrollDown.vue';
4
 import Image from '@/components/Image.vue';
5
 import Image from '@/components/Image.vue';
5
 import { preload } from '@/utils/preload';
6
 import { preload } from '@/utils/preload';
6
 
7
 
22
 
23
 
23
 <template>
24
 <template>
24
   <Loader :loading="loading" :percent="percent" />
25
   <Loader :loading="loading" :percent="percent" />
26
+  <ScrollDown />
25
   <Image v-for="res in resources" :key="res.image" :resource="res"></Image>
27
   <Image v-for="res in resources" :key="res.image" :resource="res"></Image>
26
 </template>
28
 </template>
27
 
29
 

BIN
src/assets/audios/10台城.mp3 Ver arquivo


BIN
src/assets/audios/11十里堤.mp3 Ver arquivo


BIN
src/assets/audios/12石头城.mp3 Ver arquivo


BIN
src/assets/audios/13莫愁湖公园.mp3 Ver arquivo


BIN
src/assets/audios/14赏心亭.mp3 Ver arquivo


BIN
src/assets/audios/15长干里.mp3 Ver arquivo


BIN
src/assets/audios/16大报恩寺.mp3 Ver arquivo


BIN
src/assets/audios/17凤凰台.mp3 Ver arquivo


BIN
src/assets/audios/18桃叶渡.mp3 Ver arquivo


BIN
src/assets/audios/19雨花台.mp3 Ver arquivo


BIN
src/assets/audios/1南京长江大桥.mp3 Ver arquivo


BIN
src/assets/audios/20牛首山.mp3 Ver arquivo


BIN
src/assets/audios/21石臼湖.mp3 Ver arquivo


BIN
src/assets/audios/22东山.mp3 Ver arquivo


BIN
src/assets/audios/23瞻园.mp3 Ver arquivo


BIN
src/assets/audios/24白鹭洲.mp3 Ver arquivo


BIN
src/assets/audios/25随园.mp3 Ver arquivo


BIN
src/assets/audios/26世界文学客厅.mp3 Ver arquivo


BIN
src/assets/audios/2阅江楼.mp3 Ver arquivo


BIN
src/assets/audios/3秦淮河.mp3 Ver arquivo


BIN
src/assets/audios/4乌衣巷.mp3 Ver arquivo


BIN
src/assets/audios/5燕子矶.mp3 Ver arquivo


BIN
src/assets/audios/6栖霞山.mp3 Ver arquivo


BIN
src/assets/audios/7朝天宫.mp3 Ver arquivo


BIN
src/assets/audios/8梁洲.mp3 Ver arquivo


BIN
src/assets/audios/9后湖映月.mp3 Ver arquivo


+ 82
- 6
src/components/Image.vue Ver arquivo

1
 <template>
1
 <template>
2
-  <img :src="resource.img.src" alt="">
2
+  <div class="img-wrapper">
3
+    <img :src="resource.img.src" @load="onLoad" alt="">
4
+    <div v-if="resource.playBtn" class="paybtn" :style="btnStyle" @click="onPlayBtn">
5
+      <audio :src="resource.audio" preload="auto" ref="audioRef" @ended="onEnded"></audio>
6
+    </div>
7
+  </div>
3
 </template>
8
 </template>
4
 
9
 
5
 <script setup>
10
 <script setup>
11
+import { ref, watch } from 'vue';
12
+import { useModel } from '@zjxpcyc/vue-tiny-store';
13
+
14
+const [current, updateCurrent] = useModel('audio');
15
+
6
 const props = defineProps({
16
 const props = defineProps({
7
   resource: Object,
17
   resource: Object,
8
   default: () => ({})
18
   default: () => ({})
19
+});
20
+
21
+const btnStyle = ref();
22
+const audioRef = ref();
23
+const playState = ref(0);
24
+
25
+const playAudio = () => {
26
+    audioRef.value.play();
27
+    playState.value = 1;
28
+    updateCurrent(props.resource.audio);
29
+}
30
+
31
+const stopAudio = () => {
32
+    audioRef.value.pause();
33
+    playState.value = 0;
34
+}
35
+
36
+// 监控全局, 如果非当前播放器, 则停止播放
37
+watch(current, (nw) => {
38
+  if (nw !== props.resource.audio && playState.value === 1) {
39
+    stopAudio();
40
+  }
9
 })
41
 })
10
 
42
 
43
+// 点击播放
44
+const onPlayBtn = () => {
45
+  if (playState.value) {
46
+    stopAudio();
47
+  } else {
48
+    playAudio();
49
+  }
50
+}
51
+
52
+// 播放完成
53
+const onEnded = () => {
54
+  stopAudio();
55
+}
56
+
57
+// 图片加载完成, 设置播放器
58
+const onLoad = (e) => {
59
+  if (!props.resource.playBtn) return;
60
+
61
+  const { pos } = props.resource.playBtn;
62
+  const { width, height, naturalHeight, naturalWidth } = e.target;
63
+  const d = 32; // 宽度一半
64
+  const x = width * pos[0] / naturalWidth - d;
65
+  const y = height * pos[1] / naturalHeight - d;
66
+
67
+  btnStyle.value = { left: `${x}px`, top: `${y}px` }
68
+}
69
+
11
 </script>
70
 </script>
12
 
71
 
13
 <style lang="less" scoped>
72
 <style lang="less" scoped>
14
-img {
15
-  display: block;
16
-  width: 100vw;
17
-  height: auto;
18
-  object-fit: contain;
73
+.img-wrapper {
74
+  position: relative;
75
+
76
+  .paybtn {
77
+    position: absolute;
78
+    width: 64px;
79
+    height: 64px;
80
+    // background: rgba(255, 0, 0, .5);
81
+    z-index: 10;
82
+
83
+    & > audio {
84
+      max-width: 1px;
85
+      max-height: 1px;
86
+    }
87
+  }
88
+
89
+  img {
90
+    display: block;
91
+    width: 100vw;
92
+    height: auto;
93
+    object-fit: contain;
94
+  }
19
 }
95
 }
20
 </style>
96
 </style>
21
 
97
 

+ 1
- 1
src/components/Loader.vue Ver arquivo

1
 <template>
1
 <template>
2
   <div class="loader-wrapper" :class="{ loaded: !loading }">
2
   <div class="loader-wrapper" :class="{ loaded: !loading }">
3
     <div class="loader">
3
     <div class="loader">
4
-      <P class="tip">{{tip}}</P>
4
+      <p class="tip">{{tip}}</p>
5
       <p class="percent">{{percent}}%</p>
5
       <p class="percent">{{percent}}%</p>
6
       <p class="progress">
6
       <p class="progress">
7
         <div :style="progressStyle"></div>
7
         <div :style="progressStyle"></div>

+ 85
- 0
src/components/ScrollDown.vue Ver arquivo

1
+<template>
2
+  <div class="scroll-down" :style="style">
3
+    <span></span><span></span><span></span>
4
+  </div>
5
+</template>
6
+
7
+<script setup>
8
+import { onBeforeUnmount, onMounted, ref } from "vue"
9
+
10
+const style = ref({ opacity: 1 })
11
+
12
+const onScroll = (e) => {
13
+  const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
14
+  const clientHeight = document.documentElement.clientHeight || document.body.clientHeight;
15
+  const scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight;
16
+	if (clientHeight + scrollTop === scrollHeight) {
17
+		style.value = { opacity: 0 }
18
+    console.log('已到底部');
19
+	} else {
20
+    if (style.value.opacity === 0) {
21
+      style.value = { opacity: 1 }
22
+    }
23
+  }
24
+}
25
+
26
+onMounted(() => {
27
+  document.addEventListener('scroll', onScroll);
28
+})
29
+
30
+onBeforeUnmount(() => {
31
+  document.removeEventListener('scroll', onScroll);
32
+})
33
+
34
+</script>
35
+
36
+<style lang="less" scoped>
37
+@arrow-color: #5C9495;
38
+
39
+.scroll-down {
40
+  position: fixed;
41
+  bottom: 100px;
42
+  right: 60px;
43
+  z-index: 2;
44
+
45
+  & > span {
46
+    position: absolute;
47
+    display: block;
48
+    top: 0;
49
+    left: 50%;
50
+    width: 24px;
51
+    height: 24px;
52
+    margin-left: -12px;
53
+    border-left: 2px solid @arrow-color;
54
+    border-bottom: 2px solid @arrow-color;
55
+    transform: rotate(-45deg);
56
+    animation: sdb07 2s infinite;
57
+    opacity: 0;
58
+    box-sizing: border-box;
59
+
60
+    &:nth-of-type(1) {
61
+      animation-delay: 0s;
62
+    }
63
+    &:nth-of-type(2) {
64
+      animation-delay: .15s;
65
+      top: 16px;
66
+    }
67
+    &:nth-of-type(3) {
68
+      animation-delay: .3s;
69
+      top: 32px;
70
+    }
71
+  }
72
+
73
+  @keyframes sdb07 {
74
+    0% {
75
+      opacity: 0;
76
+    }
77
+    50% {
78
+      opacity: 1;
79
+    }
80
+    100% {
81
+      opacity: 0;
82
+    }
83
+  }
84
+}
85
+</style>

+ 4
- 1
src/main.js Ver arquivo

1
 import { createApp } from 'vue'
1
 import { createApp } from 'vue'
2
 import './style.css'
2
 import './style.css'
3
 import App from './App.vue'
3
 import App from './App.vue'
4
+import store from './store'
4
 
5
 
5
-createApp(App).mount('#app')
6
+const app = createApp(App);
7
+app.use(store);
8
+app.mount('#app');

+ 19
- 0
src/store/index.js Ver arquivo

1
+import { ref } from 'vue';
2
+import createStore from '@zjxpcyc/vue-tiny-store';
3
+
4
+const useAudio = () => {
5
+  const current = ref()
6
+
7
+  const changeAudio = (aduRef) => {
8
+    current.value = aduRef;
9
+  }
10
+
11
+  return [
12
+    current,
13
+    changeAudio,
14
+  ]
15
+};
16
+
17
+const store = createStore({ audio: useAudio });
18
+
19
+export default store;

+ 3
- 3
src/utils/preload.js Ver arquivo

5
     const total = resources.length;
5
     const total = resources.length;
6
     let cursor = 0;
6
     let cursor = 0;
7
 
7
 
8
-    const callback = () => {
8
+    const callback = (e, isError) => {
9
       cursor += 1;
9
       cursor += 1;
10
       onProgress(cursor / total);
10
       onProgress(cursor / total);
11
       if (cursor >= total) {
11
       if (cursor >= total) {
15
 
15
 
16
     for (let res of resources) {
16
     for (let res of resources) {
17
       const img = document.createElement('img');
17
       const img = document.createElement('img');
18
-      img.onload = callback;
19
-      img.onerror = callback;
18
+      img.onload = e => callback(e, false);
19
+      img.onerror = e => callback(e, true);
20
       img.src = res.image;
20
       img.src = res.image;
21
       res.img = img;
21
       res.img = img;
22
     }
22
     }

+ 146
- 15
src/utils/resources.js Ver arquivo

33
 import img22 from '@/assets/images/22.jpg';
33
 import img22 from '@/assets/images/22.jpg';
34
 import img23 from '@/assets/images/23.jpg';
34
 import img23 from '@/assets/images/23.jpg';
35
 
35
 
36
+import audio1 from '@/assets/audios/1南京长江大桥.mp3';
37
+import audio2 from '@/assets/audios/2阅江楼.mp3';
38
+import audio3 from '@/assets/audios/3秦淮河.mp3';
39
+import audio4 from '@/assets/audios/4乌衣巷.mp3';
40
+import audio5 from '@/assets/audios/5燕子矶.mp3';
41
+import audio6 from '@/assets/audios/6栖霞山.mp3';
42
+import audio7 from '@/assets/audios/7朝天宫.mp3';
43
+import audio8 from '@/assets/audios/8梁洲.mp3';
44
+import audio9 from '@/assets/audios/9后湖映月.mp3';
45
+import audio10 from '@/assets/audios/10台城.mp3';
46
+import audio11 from '@/assets/audios/11十里堤.mp3';
47
+import audio12 from '@/assets/audios/12石头城.mp3';
48
+import audio13 from '@/assets/audios/13莫愁湖公园.mp3';
49
+import audio14 from '@/assets/audios/14赏心亭.mp3';
50
+import audio15 from '@/assets/audios/15长干里.mp3';
51
+import audio16 from '@/assets/audios/16大报恩寺.mp3';
52
+import audio17 from '@/assets/audios/17凤凰台.mp3';
53
+import audio18 from '@/assets/audios/18桃叶渡.mp3';
54
+import audio19 from '@/assets/audios/19雨花台.mp3';
55
+import audio20 from '@/assets/audios/20牛首山.mp3';
56
+import audio21 from '@/assets/audios/21石臼湖.mp3';
57
+import audio22 from '@/assets/audios/22东山.mp3';
58
+import audio23 from '@/assets/audios/23瞻园.mp3';
59
+import audio24 from '@/assets/audios/24白鹭洲.mp3';
60
+import audio25 from '@/assets/audios/25随园.mp3';
61
+import audio26 from '@/assets/audios/26世界文学客厅.mp3';
62
+
36
 // 预加载资源
63
 // 预加载资源
37
 export const resources = [
64
 export const resources = [
38
   { 
65
   { 
43
   },
70
   },
44
   { 
71
   { 
45
     image: img3,
72
     image: img3,
73
+    playBtn: {
74
+      pos: [616, 932],
75
+    },
76
+    audio: audio1,
46
   },
77
   },
47
   { 
78
   { 
48
     image: img4,
79
     image: img4,
80
+    playBtn: {
81
+      pos: [100, 406],
82
+    },
83
+    audio: audio2,
49
   },
84
   },
50
   { 
85
   { 
51
     image: img5_1,
86
     image: img5_1,
87
+    playBtn: {
88
+      pos: [650, 172],
89
+    },
90
+    audio: audio3,
52
   },
91
   },
53
   { 
92
   { 
54
     image: img5_2,
93
     image: img5_2,
94
+    playBtn: {
95
+      pos: [652, 773],
96
+    },
97
+    audio: audio4,
55
   },
98
   },
56
-  { 
57
-    image: img5,
58
-  },
99
+  // { 
100
+  //   image: img5,
101
+  // },
59
   { 
102
   { 
60
     image: img6,
103
     image: img6,
104
+    playBtn: {
105
+      pos: [624, 890],
106
+    },
107
+    audio: audio5,
61
   },
108
   },
62
   { 
109
   { 
63
     image: img7,
110
     image: img7,
111
+    playBtn: {
112
+      pos: [612, 486],
113
+    },
114
+    audio: audio6,
64
   },
115
   },
65
   { 
116
   { 
66
     image: img8_1,
117
     image: img8_1,
118
+    playBtn: {
119
+      pos: [93, 557],
120
+    },
121
+    audio: audio7,
67
   },
122
   },
68
   { 
123
   { 
69
     image: img8_2,
124
     image: img8_2,
125
+    playBtn: {
126
+      pos: [163, 224],
127
+    },
128
+    audio: audio8,
70
   },
129
   },
71
-  { 
72
-    image: img8,
73
-  },
130
+  // { 
131
+  //   image: img8,
132
+  // },
74
   { 
133
   { 
75
     image: img9,
134
     image: img9,
135
+    playBtn: {
136
+      pos: [152, 782],
137
+    },
138
+    audio: audio9,
76
   },
139
   },
77
   { 
140
   { 
78
     image: img10_1,
141
     image: img10_1,
142
+    playBtn: {
143
+      pos: [659, 368],
144
+    },
145
+    audio: audio10,
79
   },
146
   },
80
   { 
147
   { 
81
     image: img10_2,
148
     image: img10_2,
149
+    playBtn: {
150
+      pos: [646, 217],
151
+    },
152
+    audio: audio11,
82
   },
153
   },
83
-  { 
84
-    image: img10,
85
-  },
154
+  // { 
155
+  //   image: img10,
156
+  // },
86
   { 
157
   { 
87
     image: img11,
158
     image: img11,
159
+    playBtn: {
160
+      pos: [574, 1074],
161
+    },
162
+    audio: audio12,
88
   },
163
   },
89
   { 
164
   { 
90
     image: img12,
165
     image: img12,
166
+    playBtn: {
167
+      pos: [132, 550],
168
+    },
169
+    audio: audio13,
91
   },
170
   },
92
   { 
171
   { 
93
     image: img13,
172
     image: img13,
173
+    playBtn: {
174
+      pos: [642, 310],
175
+    },
176
+    audio: audio14,
94
   },
177
   },
95
   { 
178
   { 
96
     image: img14,
179
     image: img14,
180
+    playBtn: {
181
+      pos: [92, 80],
182
+    },
183
+    audio: audio15,
97
   },
184
   },
98
   { 
185
   { 
99
     image: img15_1,
186
     image: img15_1,
187
+    playBtn: {
188
+      pos: [652, 31],
189
+    },
190
+    audio: audio16,
100
   },
191
   },
101
   { 
192
   { 
102
     image: img15_2,
193
     image: img15_2,
194
+    playBtn: {
195
+      pos: [652, 353],
196
+    },
197
+    audio: audio17,
103
   },
198
   },
104
-  { 
105
-    image: img15,
106
-  },
199
+  // { 
200
+  //   image: img15,
201
+  // },
107
   { 
202
   { 
108
     image: img16,
203
     image: img16,
204
+    playBtn: {
205
+      pos: [166, 600],
206
+    },
207
+    audio: audio18,
109
   },
208
   },
110
   { 
209
   { 
111
     image: img17,
210
     image: img17,
211
+    playBtn: {
212
+      pos: [364, 606],
213
+    },
214
+    audio: audio19,
112
   },
215
   },
113
   { 
216
   { 
114
     image: img18,
217
     image: img18,
218
+    playBtn: {
219
+      pos: [386, 472],
220
+    },
221
+    audio: audio20,
115
   },
222
   },
116
   { 
223
   { 
117
     image: img19_1,
224
     image: img19_1,
225
+    playBtn: {
226
+      pos: [132, 367],
227
+    },
228
+    audio: audio21,
118
   },
229
   },
119
   { 
230
   { 
120
     image: img19_2,
231
     image: img19_2,
232
+    playBtn: {
233
+      pos: [250, 699],
234
+    },
235
+    audio: audio22,
121
   },
236
   },
122
-  { 
123
-    image: img19,
124
-  },
237
+  // { 
238
+  //   image: img19,
239
+  // },
125
   { 
240
   { 
126
     image: img20,
241
     image: img20,
242
+    playBtn: {
243
+      pos: [152, 1040],
244
+    },
245
+    audio: audio23,
127
   },
246
   },
128
   { 
247
   { 
129
     image: img21,
248
     image: img21,
249
+    playBtn: {
250
+      pos: [654, 886],
251
+    },
252
+    audio: audio24,
130
   },
253
   },
131
   { 
254
   { 
132
     image: img22,
255
     image: img22,
256
+    playBtn: {
257
+      pos: [98, 196],
258
+    },
259
+    audio: audio25,
133
   },
260
   },
134
   { 
261
   { 
135
     image: img23,
262
     image: img23,
263
+    playBtn: {
264
+      pos: [160, 64],
265
+    },
266
+    audio: audio26,
136
   },
267
   },
137
 ]
268
 ]