aamin před 1 rokem
revize
c4887a030e

+ 24 - 0
.gitignore

@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

+ 3 - 0
.vscode/extensions.json

@@ -0,0 +1,3 @@
+{
+  "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
+}

+ 18 - 0
README.md

@@ -0,0 +1,18 @@
+# Vue 3 + TypeScript + Vite
+
+This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
+
+## Recommended IDE Setup
+
+- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
+
+## Type Support For `.vue` Imports in TS
+
+TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
+
+If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
+
+1. Disable the built-in TypeScript Extension
+   1. Run `Extensions: Show Built-in Extensions` from VSCode's command palette
+   2. Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
+2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.

+ 22 - 0
components.d.ts

@@ -0,0 +1,22 @@
+/* eslint-disable */
+/* prettier-ignore */
+// @ts-nocheck
+// Generated by unplugin-vue-components
+// Read more: https://github.com/vuejs/core/pull/3399
+export {}
+
+declare module 'vue' {
+  export interface GlobalComponents {
+    Audio3: typeof import('./src/components/audio3.vue')['default']
+    AudioBox: typeof import('./src/components/AudioBox.vue')['default']
+    AudioPlayer: typeof import('./src/components/audioPlayer.vue')['default']
+    AudioPlayerNew: typeof import('./src/components/audioPlayerNew.vue')['default']
+    AudioPlayerNews: typeof import('./src/components/audioPlayerNews.vue')['default']
+    HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
+    Progress: typeof import('./src/components/progress.vue')['default']
+    RouterLink: typeof import('vue-router')['RouterLink']
+    RouterView: typeof import('vue-router')['RouterView']
+    VanCollapse: typeof import('vant/es')['Collapse']
+    VanCollapseItem: typeof import('vant/es')['CollapseItem']
+  }
+}

+ 14 - 0
index.html

@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>岳阳博物馆</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.ts"></script>
+    <script src="/js/flexible.js"></script>
+  </body>
+</html>

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 3405 - 0
package-lock.json


+ 31 - 0
package.json

@@ -0,0 +1,31 @@
+{
+  "name": "yfyc2",
+  "private": true,
+  "version": "0.0.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "build:prod": "cross-env NODE_ENV=https://houseoss.4dkankan.com vite build"
+  },
+  "dependencies": {
+    "easyscroller": "^1.0.1",
+    "pinia": "^2.1.6",
+    "swiper": "^10.2.0",
+    "vant": "^4.7.1",
+    "vite-plugin-style-import": "^2.0.0",
+    "vue": "^3.3.4",
+    "vue-router": "^4.2.4"
+  },
+  "devDependencies": {
+    "@types/node": "^20.8.4",
+    "@vitejs/plugin-vue": "^4.2.3",
+    "less": "^4.2.0",
+    "sass": "^1.65.1",
+    "typescript": "^5.0.2",
+    "unplugin-auto-import": "^0.16.6",
+    "unplugin-vue-components": "^0.25.1",
+    "vite": "^4.4.5",
+    "vue-tsc": "^1.8.5"
+  }
+}

+ 44 - 0
public/js/flexible.js

@@ -0,0 +1,44 @@
+(function flexible(window, document) {
+  var docEl = document.documentElement;
+  var dpr = window.devicePixelRatio || 1;
+
+  // adjust body font size
+  function setBodyFontSize() {
+    if (document.body) {
+      document.body.style.fontSize = 12 * dpr + "px";
+    } else {
+      document.addEventListener("DOMContentLoaded", setBodyFontSize);
+    }
+  }
+  setBodyFontSize();
+
+  // set 1rem = viewWidth / 10
+  function setRemUnit() {
+    // 将视口划分为24份
+    var rem = docEl.clientWidth / 24;
+    docEl.style.fontSize = rem + "px";
+  }
+
+  setRemUnit();
+
+  // reset rem unit on page resize
+  window.addEventListener("resize", setRemUnit);
+  window.addEventListener("pageshow", function (e) {
+    if (e.persisted) {
+      setRemUnit();
+    }
+  });
+
+  // detect 0.5px supports
+  if (dpr >= 2) {
+    var fakeBody = document.createElement("body");
+    var testElement = document.createElement("div");
+    testElement.style.border = ".5px solid transparent";
+    fakeBody.appendChild(testElement);
+    docEl.appendChild(fakeBody);
+    if (testElement.offsetHeight === 1) {
+      docEl.classList.add("hairlines");
+    }
+    docEl.removeChild(fakeBody);
+  }
+})(window, document);

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 1 - 0
public/vite.svg


+ 42 - 0
src/App.vue

@@ -0,0 +1,42 @@
+<script setup lang="ts">
+onMounted(() => {
+  //首先我们获得视口高度并将其乘以1%以获得1vh单位的值
+  let vh = window.innerHeight * 0.01;
+  // 然后,我们将——vh自定义属性中的值设置为文档的根
+  document.documentElement.style.setProperty("--vh", `${vh}px`);
+
+  // 我们监听resize事件 视图大小发生变化就重新计算1vh的值
+  window.addEventListener("resize", () => {
+    // 我们执行与前面相同的脚本
+    let vh = window.innerHeight * 0.01;
+    document.documentElement.style.setProperty("--vh", `${vh}px`);
+  });
+});
+</script>
+
+<template>
+  <div class="all">
+    <router-view />
+  </div>
+</template>
+
+<style scoped>
+@font-face {
+  font-family: "Microsoft YaHei-Bold"; /* 字体名称 */
+  src: url("../src/assets/font/MicrosoftYaHei-Bold.ttf"); /* 字体文件相对路径 */
+}
+@font-face {
+  font-family: "Microsoft YaHei"; /* 字体名称 */
+  src: url("../src/assets/font/MicrosoftYaHei.ttf"); /* 字体文件相对路径 */
+}
+.all {
+  width: 100%;
+  height: calc(var(--vh, 1vh) * 100);
+  background: url(/src\assets\img\bg.png);
+  background-size: 100% 100%;
+  overflow: hidden;
+}
+* {
+  font-family: "Microsoft YaHei-Bold, Microsoft YaHei";
+}
+</style>

binární
src/assets/font/MicrosoftYaHei-Bold.ttf


binární
src/assets/font/MicrosoftYaHei.ttf


binární
src/assets/img/bg.png


binární
src/assets/img/contentBg.png


binární
src/assets/img/down.png


binární
src/assets/img/home-icon.png


binární
src/assets/img/home/0.png


binární
src/assets/img/home/1.png


binární
src/assets/img/home/2.png


binární
src/assets/img/left.png


binární
src/assets/img/pause.png


binární
src/assets/img/play.png


binární
src/assets/img/right.png


binární
src/assets/img/up.png


+ 70 - 0
src/auto-import.d.ts

@@ -0,0 +1,70 @@
+/* eslint-disable */
+/* prettier-ignore */
+// @ts-nocheck
+// noinspection JSUnusedGlobalSymbols
+// Generated by unplugin-auto-import
+export {}
+declare global {
+  const EffectScope: typeof import('vue')['EffectScope']
+  const computed: typeof import('vue')['computed']
+  const createApp: typeof import('vue')['createApp']
+  const customRef: typeof import('vue')['customRef']
+  const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
+  const defineComponent: typeof import('vue')['defineComponent']
+  const effectScope: typeof import('vue')['effectScope']
+  const getCurrentInstance: typeof import('vue')['getCurrentInstance']
+  const getCurrentScope: typeof import('vue')['getCurrentScope']
+  const h: typeof import('vue')['h']
+  const inject: typeof import('vue')['inject']
+  const isProxy: typeof import('vue')['isProxy']
+  const isReactive: typeof import('vue')['isReactive']
+  const isReadonly: typeof import('vue')['isReadonly']
+  const isRef: typeof import('vue')['isRef']
+  const markRaw: typeof import('vue')['markRaw']
+  const nextTick: typeof import('vue')['nextTick']
+  const onActivated: typeof import('vue')['onActivated']
+  const onBeforeMount: typeof import('vue')['onBeforeMount']
+  const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
+  const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
+  const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
+  const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
+  const onDeactivated: typeof import('vue')['onDeactivated']
+  const onErrorCaptured: typeof import('vue')['onErrorCaptured']
+  const onMounted: typeof import('vue')['onMounted']
+  const onRenderTracked: typeof import('vue')['onRenderTracked']
+  const onRenderTriggered: typeof import('vue')['onRenderTriggered']
+  const onScopeDispose: typeof import('vue')['onScopeDispose']
+  const onServerPrefetch: typeof import('vue')['onServerPrefetch']
+  const onUnmounted: typeof import('vue')['onUnmounted']
+  const onUpdated: typeof import('vue')['onUpdated']
+  const provide: typeof import('vue')['provide']
+  const reactive: typeof import('vue')['reactive']
+  const readonly: typeof import('vue')['readonly']
+  const ref: typeof import('vue')['ref']
+  const resolveComponent: typeof import('vue')['resolveComponent']
+  const shallowReactive: typeof import('vue')['shallowReactive']
+  const shallowReadonly: typeof import('vue')['shallowReadonly']
+  const shallowRef: typeof import('vue')['shallowRef']
+  const toRaw: typeof import('vue')['toRaw']
+  const toRef: typeof import('vue')['toRef']
+  const toRefs: typeof import('vue')['toRefs']
+  const toValue: typeof import('vue')['toValue']
+  const triggerRef: typeof import('vue')['triggerRef']
+  const unref: typeof import('vue')['unref']
+  const useAttrs: typeof import('vue')['useAttrs']
+  const useCssModule: typeof import('vue')['useCssModule']
+  const useCssVars: typeof import('vue')['useCssVars']
+  const useLink: typeof import('vue-router')['useLink']
+  const useRoute: typeof import('vue-router')['useRoute']
+  const useRouter: typeof import('vue-router')['useRouter']
+  const useSlots: typeof import('vue')['useSlots']
+  const watch: typeof import('vue')['watch']
+  const watchEffect: typeof import('vue')['watchEffect']
+  const watchPostEffect: typeof import('vue')['watchPostEffect']
+  const watchSyncEffect: typeof import('vue')['watchSyncEffect']
+}
+// for type re-export
+declare global {
+  // @ts-ignore
+  export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
+}

+ 208 - 0
src/components/AudioBox.vue

@@ -0,0 +1,208 @@
+<script setup lang="ts">
+// 播放状态
+const state = ref(false);
+const stateChange = (stateNew: boolean) => {
+  console.log("ooo");
+  var audio = document.getElementById("music1");
+  if (stateNew) {
+    audio!.play();
+  } else {
+    audio!.pause();
+  }
+  state.value = stateNew;
+};
+const getAssetURL = (image: string) => {
+  return new URL(`../assets/img/${image}`, import.meta.url).href;
+};
+
+const props = defineProps({
+  audioUrl: {
+    default: "",
+    type: String,
+  },
+});
+
+const allTime = ref("");
+
+// 拖拽(鼠标按下)
+const handleMousedown = (e: any) => {
+  const progressBg = document.getElementById("progressBg");
+
+  // 改变播放状态
+  state.value = false;
+  // 计算进度条距离
+  let moveMin = progressBg?.offsetParent?.offsetLeft + progressBg?.offsetLeft;
+  let moveMax =
+    progressBg?.offsetParent?.offsetLeft +
+    progressBg?.offsetLeft +
+    progressBg?.clientWidth;
+
+  console.log(document, e);
+  if (e.targetTouches[0].pageX >= moveMax) {
+    return;
+  } else if (e.targetTouches[0].pageX <= moveMin) {
+    return;
+  }
+  // circle!.style.left = e.targetTouches[0].pageX - moveMin - circleWidth + "px";
+  updateProgress2(e.targetTouches[0].pageX - moveMin);
+};
+
+// 更新进度条(拖动进度条)
+const updateProgress2 = (moveX: number) => {
+  console.log(moveX);
+  const progressBg = document.getElementById("progressBg");
+  const progressBar = document.getElementById("progressBar");
+
+  var audio = document.getElementById("music1");
+  //当前移动的位置 = 当前移动的位置 / 当前进度条的可视长度    //this.$refs.progress.clientWidth 注意一定要拿总长度 否则会拿进度条已经走过的长度
+  let clickProgress = moveX / progressBg!.clientWidth;
+  //设置播放的时间 = 总时长 * 当前点击的长度
+  audio!.currentTime = audio!.duration * clickProgress;
+  //设置移动的位置
+  progressBar!.style.width = moveX + "px";
+
+  var audio = document.getElementById("music1");
+  setTimeout(() => {
+    audio!.play();
+    state.value = true;
+  }, 200);
+};
+
+// 更新进度条(播放中)
+const updateProgress = (e: any) => {
+  const progressBar = document.getElementById("progressBar");
+  var value = e.target.currentTime / e.target.duration;
+  if (progressBar) {
+    progressBar!.style.width = value * 100 + "%";
+    if (e.target.currentTime === e.target.duration) {
+      state.value = false;
+    }
+  } else {
+    state.value = true;
+  }
+};
+
+const durationRef = ref(null);
+
+const getDuration = () => {
+  var audio = document.getElementById("music1");
+  durationRef.value = timeToMinute(audio!.duration);
+};
+const timeToMinute = (times: any) => {
+  var t = "";
+  if (times > -1) {
+    var min = Math.floor(times / 60) % 60;
+    var sec = times % 60;
+    if (min < 10) {
+      t += "0";
+    }
+    t += min + ":";
+    if (sec < 10) {
+      t += "0";
+    }
+    t += sec.toFixed(2);
+  }
+  t = t?.substring(0, t.length - 3);
+  return t;
+};
+
+const clear = () => {
+  const progressBar = document.getElementById("progressBar");
+  progressBar!.style.width = 0 + "px";
+};
+
+//暴露state和play方法
+defineExpose({
+  clear,
+});
+
+onMounted(() => {
+  clear();
+});
+</script>
+
+<template>
+  <audio
+    id="music1"
+    :src="props.audioUrl"
+    @timeupdate="updateProgress"
+    controls
+    ref="audioRef"
+    style="display: none"
+    @canplay="getDuration"
+  ></audio>
+  <div class="audio-box">
+    <img
+      @click="stateChange(!state)"
+      :src="state ? getAssetURL('pause.png') : getAssetURL('play.png')"
+    />
+    <div class="progress-box">
+      <div class="progress-bg" id="progressBg">
+        <div class="progress-active" id="progressBar">
+          <div
+            class="active-o"
+            id="progressBtn"
+            @touchmove="handleMousedown"
+          ></div>
+        </div>
+      </div>
+    </div>
+    <div class="time">{{ durationRef }}</div>
+  </div>
+</template>
+
+<style lang="less" scoped>
+.audio-box {
+  width: 100%;
+  height: 100%;
+  padding: 0;
+  border-radius: 50px;
+  background: rgba(0, 0, 0, 0.3);
+  box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.6);
+  border: 1px solid rgba(0, 0, 0, 0.4);
+  display: flex;
+  justify-content: space-between;
+
+  .progress-box {
+    width: 85%;
+    height: 100%;
+    display: flex;
+    position: relative;
+    align-items: center;
+    justify-content: right;
+    .progress-bg {
+      width: 90%;
+      height: 2px;
+      background: rgba(255, 255, 255, 0.6);
+      position: absolute;
+    }
+    .progress-active {
+      width: 0;
+      height: 2px;
+      background: #3c7e70;
+      position: relative;
+
+      .active-o {
+        width: 13px;
+        height: 13px;
+        border-radius: 100%;
+        background: #3c7e70;
+        position: absolute;
+        right: 0;
+        transform: translate(50%, -45%);
+        z-index: 10000;
+      }
+    }
+  }
+  .time {
+    width: 20%;
+    height: 100%;
+    font-size: 14px;
+    color: rgba(255, 255, 255, 0.493);
+    text-align: center;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+}
+</style>

+ 110 - 0
src/components/progress.vue

@@ -0,0 +1,110 @@
+<template>
+  <div class="progress-bar" @click="clickProgress" ref="progressBar" :style="{background:props.bgColor}">
+    <div class="progress" :style="{ width: progressWidth,background:props.activeColor }"></div>
+    <div class="progress-handle" @mousedown="startDrag" :style="{ background:props.activeColor }">{{ progress }}</div>
+  </div>
+</template>
+
+<script setup>
+import {ref, computed, watch, getCurrentInstance} from 'vue';
+
+const {proxy} = getCurrentInstance()
+const props = defineProps({
+  // 背景色
+  bgColor: {
+    type: String,
+    default: '#f0f0f0'
+  },
+  // 前景色
+  activeColor: {
+    type: String,
+    default: '#4caf50'
+  },
+  //当前进度
+  percentage: {
+    type: Number,
+    default: 0
+  }
+})
+
+const progress = ref(0);
+const progressBar = ref(null);
+let dragging = false;
+let dragStartX = 0;
+
+
+watch(
+    () => [progress, props.percentage],
+    ([progress, percentage]) => {
+      if (percentage) {
+        // progress.value = percentage.toFixed(2)
+      }
+    },
+    {deep: true}
+)
+
+const updateProgress = (clientX) => {
+  const progressBarRect = progressBar.value.getBoundingClientRect();
+  const progressWidthPercentage = ((clientX - progressBarRect.left) / progressBarRect.width) * 100;
+  progress.value = Math.max(0, Math.min(progressWidthPercentage, 100)).toFixed(2);
+  proxy.$emit('valueChange', progress.value)
+};
+
+const startDrag = (event) => {
+  dragging = true;
+  dragStartX = event.clientX;
+  document.addEventListener('mousemove', handleDrag);
+  document.addEventListener('mouseup', endDrag);
+};
+
+const handleDrag = (event) => {
+  if (dragging) {
+    updateProgress(event.clientX);
+  }
+};
+
+const endDrag = () => {
+  dragging = false;
+  document.removeEventListener('mousemove', handleDrag);
+  document.removeEventListener('mouseup', endDrag);
+};
+
+const clickProgress = (event) => {
+  updateProgress(event.clientX);
+};
+
+const progressWidth = computed(() => {
+  return progress.value + '%';
+});
+
+</script>
+
+<style scoped>
+.progress-bar {
+  width: 99%;
+  height: 20px;
+  border-radius: 10px;
+  display: flex;
+  justify-content: start;
+  position: relative;
+  cursor: pointer;
+}
+
+.progress {
+  height: 100%;
+  border-radius: 10px;
+  transition: width 0.2s;
+}
+
+.progress-handle {
+  width: 40px;
+  height: 40px;
+  border-radius: 50%;
+  margin-top: -10px;
+  margin-left: -10px;
+  line-height: 40px;
+  color: #fff;
+  text-align: center;
+}
+</style>
+

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 342 - 0
src/data/data.ts


+ 19 - 0
src/main.ts

@@ -0,0 +1,19 @@
+import { createApp } from "vue";
+import "./style.css";
+import App from "./App.vue";
+import { Toast } from 'vant';
+import 'vant/es/toast/style'
+
+// Routes
+import router from "./routes/index";
+
+// Pinia
+import { createPinia } from "pinia";
+const pinia = createPinia();
+
+const app = createApp(App);
+
+app.use(Toast);
+app.use(router);
+app.use(pinia);
+app.mount("#app");

+ 29 - 0
src/routes/index.ts

@@ -0,0 +1,29 @@
+import { createRouter, createWebHashHistory } from "vue-router";
+
+let routes = [
+  // 首页 -> 展馆列表
+  {
+    path: "/",
+    name: "home",
+    component: () => import("@/view/home/index.vue"),
+  },
+  // 展馆 -> 音频列表
+  {
+    path: "/exhibitionList",
+    name: "exhibitionList",
+    component: () => import("@/view/exhibitionList/index.vue"),
+  },
+  // 展馆 -> 音频详情
+  {
+    path: "/audioDetail",
+    name: "audioDetail",
+    component: () => import("@/view/audioDetail/index.vue"),
+  },
+];
+// 路由
+const router = createRouter({
+  history: createWebHashHistory(),
+  routes,
+});
+// 导出
+export default router;

+ 43 - 0
src/store/index.ts

@@ -0,0 +1,43 @@
+
+import { defineStore } from "pinia";
+
+type Paragraph = {
+  paragraphName: string;
+  contentText: string;
+};
+
+type Part = {
+  partName: string;
+  paragraphs: Paragraph[];
+};
+
+type Exhibition = {
+  id: number;
+  name: string;
+  parts: Part[];
+};
+
+export const useStore = defineStore("home", {
+  // 相当于data
+  state: () => {
+    return {
+      currentScene: {} as Exhibition,
+      currentParagraph: {} as Paragraph,
+      currentPart:{} as Part
+    };
+  },
+  // 相当于计算属性
+  getters: {},
+  // 相当于vuex的 mutation + action,可以同时写同步和异步的代码
+  actions: {
+    setCurrentScene(currentScene: Exhibition) {
+      this.currentScene = currentScene;
+    },
+    setCurrentParagraph(currentP: Paragraph) {
+      this.currentParagraph = currentP;
+    },
+    setCurrentPart(currentPart: Part) {
+      this.currentPart = currentPart;
+    },
+  },
+});

+ 15 - 0
src/style.css

@@ -0,0 +1,15 @@
+
+#app {
+  margin: 0;
+  width: 100vw;
+  height: 100%;
+  /* overflow: hidden; */
+}
+body {
+  margin: 0;
+  padding: 0;
+  display: block;
+}
+div {
+  box-sizing: border-box;
+}

+ 4 - 0
src/utils/index.ts

@@ -0,0 +1,4 @@
+const getAssetURL = (image: string) => {
+  return new URL(`${image}`, import.meta.url).href;
+};
+export default getAssetURL

+ 229 - 0
src/view/audioDetail/index.vue

@@ -0,0 +1,229 @@
+<script setup lang="ts">
+import dataList from "@/data/data";
+import { useStore } from "@/store/index";
+import { showToast } from "vant";
+import AudioBoxVue from "@/components/AudioBox.vue";
+
+const route = useRoute();
+const router = useRouter();
+const store = useStore();
+
+const currentP = ref();
+const getAssetURL = (image: string) => {
+  return new URL(`../../assets/img/${image}`, import.meta.url).href;
+};
+
+// 查看全部
+const goBack = () => {
+  router.push({
+    path: "/exhibitionList",
+  });
+};
+
+// 上一个
+const prev = () => {
+  // 当前段落下标
+  let indexPP = store.currentPart.paragraphs.findIndex((item: any) => {
+    return item.paragraphName == currentP.value.paragraphName;
+  });
+  // 当前Part的下标
+  let indexP = store.currentScene.parts.findIndex((item: any) => {
+    return item.partName == store.currentPart.partName;
+  });
+  //console.log(indexPP, indexP);
+  if (indexPP === 0 && indexP == 0) {
+    showToast("暂无更多");
+  } else if (indexPP === 0 && indexP != 0) {
+    //console.log("切换");
+    // 当前段落为part第一章,则part - 1, 段落为part - 1的最后一个段落
+    indexP -= 1;
+    indexPP = store.currentScene.parts[indexP].paragraphs.length - 1;
+    //console.log(indexP, indexPP);
+  } else {
+    // 当前章节的非第一段落
+    indexPP -= 1;
+  }
+  // 更新仓库章节
+  store.currentPart = store.currentScene.parts[indexP];
+  // 更新仓库段落
+  store.currentParagraph = store.currentPart.paragraphs[indexPP];
+  currentP.value = store.currentParagraph;
+};
+
+// 下一个
+const next = () => {
+  // 当前段落下标
+  let indexPP = store.currentPart.paragraphs.findIndex((item: any) => {
+    return item.paragraphName == currentP.value.paragraphName;
+  });
+  // 当前Part的下标
+  let indexP = store.currentScene.parts.findIndex((item: any) => {
+    return item.partName == store.currentPart.partName;
+  });
+  //console.log(indexPP, indexP);
+  if (
+    indexPP === store.currentPart.paragraphs.length - 1 &&
+    indexP == store.currentScene.parts.length - 1
+  ) {
+    showToast("暂无更多");
+  } else if (
+    indexPP === store.currentPart.paragraphs.length - 1 &&
+    indexP != store.currentScene.parts.length - 1
+  ) {
+    //console.log("切换");
+    // 当前段落为part最后一章,则part + 1, 段落为part + 1的第一个段落
+    indexP += 1;
+    indexPP = 0;
+    //console.log(indexP, indexPP);
+  } else {
+    // 当前章节的非第一段落
+    indexPP += 1;
+  }
+  // 更新仓库章节
+  store.currentPart = store.currentScene.parts[indexP];
+  // 更新仓库段落
+  store.currentParagraph = store.currentPart.paragraphs[indexPP];
+  currentP.value = store.currentParagraph;
+};
+
+const audiobox = ref(null);
+watch(currentP, (newValue: any) => {
+  console.log(audiobox.value);
+  if (audiobox.value == null) {
+    return;
+    // audiobox.value.clear();
+  } else {
+    audiobox!.value.clear();
+  }
+});
+
+onMounted(() => {
+  currentP.value = store.currentParagraph;
+  currentP.value.contentText.replace(/(\n|\r|\r\n|↵)/g, "<br /><p> </ p>");
+  //console.log(currentP.value);
+});
+</script>
+
+<template>
+  <div class="all" v-if="currentP">
+    <div class="content">
+      <div class="top-img">
+        <img :src="getAssetURL(`home/${store.currentScene.coverImg}`)" alt="" />
+      </div>
+      <div class="content-box">
+        <div class="content-title">{{ currentP.paragraphName }}</div>
+        <div class="p" v-html="currentP.contentText"></div>
+      </div>
+      <div class="control-box">
+        <!-- <audio :src="currentP.audioUrl"></audio> -->
+        <div class="control-top">
+          <div class="top-audio">
+            <AudioBox :audioUrl="currentP.audioUrl" ref="audiobox" />
+            <!-- <audioPlayer :audioUrl="currentP.audioUrl" /> -->
+          </div>
+        </div>
+        <div class="control-bottom">
+          <div class="bottom-left" @click.stop="prev()">
+            <img :src="getAssetURL('left.png')" alt="" />
+          </div>
+          <div class="bottom-center" @click.stop="goBack()">查看全部</div>
+          <div class="bottom-right" @click.stop="next()">
+            <img :src="getAssetURL('right.png')" alt="" />
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style lang="less" scoped>
+.all {
+  width: 100%;
+  height: 100%;
+  padding: 5% 5% 0 5%;
+  overflow: hidden;
+
+  .content {
+    width: 100%;
+    height: 100%;
+    background: url(/src\assets\img\contentBg.png);
+    background-size: 100% 100%;
+    border-radius: 8px 8px 0 0;
+    position: relative;
+    .top-img {
+      width: 100%;
+      // height: 30vh;
+      img {
+        width: 100%;
+        height: 100%;
+        object-fit: cover;
+        box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25);
+      }
+    }
+    .content-box {
+      width: 100%;
+      height: 55vh;
+      overflow: auto;
+      padding: 5%;
+      .content-title {
+        text-align: center;
+        font-size: 1.1rem;
+        font-weight: bold;
+        text-shadow: 0px 3px 5px rgba(0, 0, 0, 0.25);
+      }
+      .p {
+        font-size: 1rem;
+        line-height: 1.5rem;
+        letter-spacing: 6px;
+        font-weight: 500;
+        white-space: pre-wrap;
+      }
+    }
+
+    .control-box {
+      width: 100%;
+      position: absolute;
+      bottom: 0;
+
+      .control-top {
+        height: 7vh;
+        background: rgba(0, 0, 0, 0.8);
+        display: flex;
+        justify-content: center;
+        align-content: center;
+        position: relative;
+        z-index: 10;
+
+        .top-audio {
+          width: 90%;
+          height: 60%;
+          margin: auto;
+        }
+      }
+      .control-bottom {
+        width: 100%;
+        display: flex;
+        justify-content: space-between;
+        box-shadow: 0px 0px 3px 0px rgba(0, 0, 0, 0.6);
+
+        div {
+          width: 33%;
+          height: 60px;
+          background: rgba(0, 0, 0, 0.8);
+          display: flex;
+          justify-content: center;
+          align-items: center;
+          font-size: 1rem;
+          color: white;
+          font-weight: bold;
+          letter-spacing: 2px;
+
+          img {
+            width: 7vw;
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 190 - 0
src/view/exhibitionList/index.vue

@@ -0,0 +1,190 @@
+<script setup lang="ts">
+import homeIcon from "@/assets/img/home-icon.png";
+import dataList from "@/data/data";
+import { useStore } from "@/store/index";
+
+const route = useRoute();
+const router = useRouter();
+const store = useStore();
+
+const goBack = (id: string) => {
+  router.push({
+    path: "/",
+  });
+};
+
+const goDetail = (item: string, index: number) => {
+  store.currentPart = currentScene.value.parts[index];
+  store.currentParagraph = item;
+  router.push({
+    path: "/audioDetail",
+  });
+};
+
+const open = (partIndex: number) => {
+  if (currentPart.value == partIndex) {
+    currentPart.value = 0;
+  } else {
+    currentPart.value = partIndex;
+  }
+};
+
+const currentPart = ref(-1);
+
+const getAssetURL = (image: string) => {
+  return new URL(`../../assets/img/${image}`, import.meta.url).href;
+};
+
+const currentScene = ref();
+
+
+onMounted(() => {
+  if (route.query.id) {
+    currentScene.value = dataList.find((item) => {
+      return item.id == Number(route.query.id);
+    });
+    store.currentScene = currentScene.value;
+  } else {
+    currentScene.value = store.currentScene;
+  }
+  currentPart.value = 1;
+});
+</script>
+
+<template>
+  <div class="all" v-if="currentScene">
+    <div class="content">
+      <div class="top-img">
+        <img :src="getAssetURL(`home/${currentScene.coverImg}`)" alt="" />
+      </div>
+      <div class="content-box">
+        <div
+          class="content-box-item"
+          v-for="(item, index) in currentScene.parts"
+          :key="item.partName"
+        >
+          <div class="item-title" @click="open(index)">
+            {{ item.partName }}
+            <img
+              v-show="index != 0"
+              :src="
+                currentPart == index
+                  ? getAssetURL('up.png')
+                  : getAssetURL('down.png')
+              "
+              alt=""
+            />
+          </div>
+          <div class="item-list" v-if="currentPart == index || index == 0">
+            <div
+              class="item-list-item"
+              v-for="(p, i) in item.paragraphs"
+              @click="goDetail(p, index)"
+            >
+              {{ p.paragraphName }}
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+    <!-- 回到主页按钮 -->
+    <img :src="homeIcon" alt="" class="home-icon" @click="goBack" />
+  </div>
+</template>
+
+<style lang="less" scoped>
+.all {
+  width: 100%;
+  height: 100%;
+  padding: 5%;
+  overflow: hidden;
+
+  .content {
+    width: 100%;
+    height: 110%;
+    background: url(/src\assets\img\contentBg.png);
+    background-size: 100% 100%;
+    border-radius: 8px 8px 0 0;
+    .top-img {
+      width: 100%;
+      // height: 30vh;
+      img {
+        width: 100%;
+        height: 100%;
+        object-fit: cover;
+        box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25);
+      }
+    }
+    .content-box {
+      width: 100%;
+      height: 70vh;
+      overflow: auto;
+      padding: 5%;
+
+      &-item {
+        width: 100%;
+        margin-bottom: 15px;
+
+        .item-title {
+          width: 100%;
+          height: 8vh;
+          background: rgba(44, 44, 44, 0.8);
+          border-radius: 12px 12px 0px 0px;
+          color: white;
+          font-size: 16px;
+          font-weight: bold;
+          text-align: center;
+          line-height: 8vh;
+          position: relative;
+          img {
+            width: 6vw;
+            height: 6vw;
+            opacity: 0.6;
+            position: absolute;
+            top: 50%;
+            right: 5%;
+            transform: translateY(-50%);
+          }
+        }
+
+        .item-list {
+          transition: height 2s;
+          &-item {
+            width: 100%;
+            height: 8vh;
+            // height: 10vh;
+            color: white;
+            background: rgba(60, 126, 112, 0.6);
+            box-shadow: 0px 2px 2px 0px rgba(0, 0, 0, 0.2);
+            margin-top: 2px;
+            font-size: 15px;
+            text-align: center;
+            line-height: 8vh;
+            font-weight: bold;
+            // white-space: nowrap;
+            // overflow: hidden;
+            // text-overflow: ellipsis;
+          }
+        }
+      }
+
+      // background: url(/src\assets\img\contentBg.png);
+      // background-size: 100% 100%;
+    }
+    .content-box::-webkit-scrollbar-track {
+      /*滚动条里面轨道*/
+      -webkit-box-shadow: inset 0 0 5px transparent;
+      border-radius: 10px;
+      background: transparent;
+    }
+  }
+  .home-icon {
+    width: 15vw;
+    height: 15vw;
+    position: fixed;
+    right: 20px;
+    bottom: 30px;
+    opacity: 0.7;
+  }
+}
+</style>

+ 74 - 0
src/view/home/index.vue

@@ -0,0 +1,74 @@
+<script setup lang="ts">
+import dataList from "@/data/data";
+
+const getAssetURL = (image: string) => {
+  return new URL(`../../assets/img/home/${image}`, import.meta.url).href;
+};
+
+const router = useRouter();
+const goDetail = (id: string) => {
+  router.push({
+    path: "/exhibitionList",
+    query: {
+      id: id,
+    },
+  });
+};
+onMounted(() => {
+  //console.log(dataList);
+});
+</script>
+
+<template>
+  <div class="all">
+    <div
+      v-for="(item, index) in dataList"
+      :key="item.id"
+      class="box"
+      :style="{
+        marginBottom: index == dataList.length - 1 ? '' : '20px',
+      }"
+      @click="goDetail(item.id)"
+    >
+      <img :src="getAssetURL(item.coverImg)" alt="" />
+      <div class="title-box">{{ item.name }}</div>
+    </div>
+  </div>
+</template>
+
+<style lang="less" scoped>
+.all {
+  width: 100%;
+  height: 100%;
+  padding: 8%;
+  overflow: auto;
+  .box {
+    width: 100%;
+    height: 31%;
+    position: relative;
+    border-radius: 8px;
+    box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25);
+
+    img {
+      width: 100%;
+      height: 100%;
+      border-radius: 8px;
+    }
+    .title-box {
+      width: 100%;
+      height: 2.5rem;
+      background: #2c2c2c;
+      position: absolute;
+      bottom: 0px;
+      border-radius: 0 0 8px 8px;
+      font-family: "Microsoft YaHei-Bold, Microsoft YaHei";
+      font-size: 14px;
+      color: #ffffff;
+      line-height: 2.5rem;
+      letter-spacing: 2px;
+      font-weight: bold;
+      padding-left: 5%;
+    }
+  }
+}
+</style>

+ 1 - 0
src/vite-env.d.ts

@@ -0,0 +1 @@
+/// <reference types="vite/client" />

+ 30 - 0
tsconfig.json

@@ -0,0 +1,30 @@
+{
+  "compilerOptions": {
+    "target": "ES2020",
+    "useDefineForClassFields": true,
+    "module": "ESNext",
+    "lib": ["ES2020", "DOM", "DOM.Iterable"],
+    "skipLibCheck": true,
+
+    /* Bundler mode */
+    "moduleResolution": "bundler",
+    "allowImportingTsExtensions": true,
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "noEmit": true,
+    "jsx": "preserve",
+
+    /* Linting */
+    "strict": true,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "noFallthroughCasesInSwitch": true,
+    // 配置@别名
+    "baseUrl": ".",
+    "paths": {
+      "@/*": ["./src/*"]
+    }
+  },
+  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
+  "references": [{ "path": "./tsconfig.node.json" }]
+}

+ 10 - 0
tsconfig.node.json

@@ -0,0 +1,10 @@
+{
+  "compilerOptions": {
+    "composite": true,
+    "skipLibCheck": true,
+    "module": "ESNext",
+    "moduleResolution": "bundler",
+    "allowSyntheticDefaultImports": true
+  },
+  "include": ["vite.config.ts"]
+}

+ 43 - 0
vite.config.ts

@@ -0,0 +1,43 @@
+import { defineConfig } from "vite";
+import vue from "@vitejs/plugin-vue";
+import Components from "unplugin-vue-components/vite";
+import { VantResolver } from "unplugin-vue-components/resolvers";
+// 自动按需导入
+import AutoImport from "unplugin-auto-import/vite";
+import styleImport, { VantResolve } from "vite-plugin-style-import";
+
+import { resolve } from "path";
+
+// https://vitejs.dev/config/
+export default defineConfig({
+  base: "./",
+  plugins: [
+    vue(),
+    Components({
+      resolvers: [VantResolver()],
+    }),
+    AutoImport({
+      //安装两行后你会发现在组件中不用再导入ref,reactive等
+      imports: ["vue", "vue-router"],
+      //存放的位置
+      dts: "src/auto-import.d.ts",
+    }),
+  ],
+  resolve: {
+    // ↓路径别名
+    alias: {
+      "@": resolve(__dirname, "./src"),
+    },
+  },
+  build: {
+    outDir: "dist",
+    assetsDir: "assets",
+    sourcemap: false,
+    terserOptions: {
+      compress: {
+        drop_console: true,
+        drop_debugger: true,
+      },
+    },
+  },
+});