shaogen1995 9 ماه پیش
کامیت
dbfbb669a1
84فایلهای تغییر یافته به همراه15500 افزوده شده و 0 حذف شده
  1. 0 0
      .eslintignore
  2. 15 0
      .eslintrc.cjs
  3. 30 0
      .gitignore
  4. 8 0
      .prettierrc.json
  5. 8 0
      .vscode/extensions.json
  6. 46 0
      README.md
  7. 21 0
      components.d.ts
  8. 1 0
      env.d.ts
  9. 22 0
      eslintrc.js
  10. 26 0
      index.html
  11. 7949 0
      package-lock.json
  12. 52 0
      package.json
  13. BIN
      public/favicon.ico
  14. 13 0
      public/staticData/data.js
  15. 92 0
      src/App.vue
  16. 8 0
      src/api/api/activeBooking/index.ts
  17. 5 0
      src/api/api/artwork/index.ts
  18. 7 0
      src/api/api/dynamic/index.ts
  19. 6 0
      src/api/api/exhibition/index.ts
  20. 9 0
      src/api/api/sceneBooking/index.ts
  21. 6 0
      src/api/api/summarize/index.ts
  22. 8 0
      src/api/api/treasure/index.ts
  23. 6 0
      src/api/api/volunteerHome/index.ts
  24. 1 0
      src/api/record.ts
  25. 87 0
      src/api/request.ts
  26. 16 0
      src/assets/base.css
  27. BIN
      src/assets/fonts/SourceHanSansCN-Bold.ttf
  28. BIN
      src/assets/fonts/SourceHanSansCN-Medium.otf
  29. BIN
      src/assets/fonts/SourceHanSansCN-Regular.otf
  30. BIN
      src/assets/images/back.png
  31. BIN
      src/assets/images/booked-card.png
  32. BIN
      src/assets/images/booking-card.png
  33. BIN
      src/assets/images/cicle.png
  34. BIN
      src/assets/images/code.png
  35. BIN
      src/assets/images/delect.png
  36. BIN
      src/assets/images/income.png
  37. BIN
      src/assets/images/left.png
  38. BIN
      src/assets/images/no-data.png
  39. BIN
      src/assets/images/onlineBg.png
  40. BIN
      src/assets/images/pause.png
  41. BIN
      src/assets/images/play.png
  42. BIN
      src/assets/images/right.png
  43. BIN
      src/assets/images/sceneBack.png
  44. BIN
      src/assets/images/search_icon.png
  45. 1 0
      src/assets/logo.svg
  46. 1 0
      src/assets/main.css
  47. 71 0
      src/auto-import.d.ts
  48. 38 0
      src/base.css
  49. 20 0
      src/components/IframePage/index.vue
  50. 61 0
      src/components/SearchInput/index.vue
  51. 7 0
      src/images.d.ts
  52. 27 0
      src/main.ts
  53. 130 0
      src/router/index.ts
  54. 18 0
      src/stores/index.ts
  55. 9 0
      src/types/api/dynamic.d.ts
  56. 7 0
      src/types/declaration.d.ts
  57. 4 0
      src/types/index.d.ts
  58. 36 0
      src/utils/browser.ts
  59. 10 0
      src/utils/https.ts
  60. 100 0
      src/utils/pass.ts
  61. 44 0
      src/utils/weixin.ts
  62. 214 0
      src/views/EnterNingguo/artwork.vue
  63. 167 0
      src/views/EnterNingguo/dynamic-detail.vue
  64. 263 0
      src/views/EnterNingguo/dynamic.vue
  65. 137 0
      src/views/EnterNingguo/summarize.vue
  66. 262 0
      src/views/ExhibitionService/BookingTime.vue
  67. 191 0
      src/views/ExhibitionService/active-detail.vue
  68. 226 0
      src/views/ExhibitionService/active-info.vue
  69. 240 0
      src/views/ExhibitionService/activeBooking.vue
  70. 234 0
      src/views/ExhibitionService/bookInputInfo.vue
  71. 125 0
      src/views/ExhibitionService/booked-detail.vue
  72. 250 0
      src/views/ExhibitionService/bookedList.vue
  73. 203 0
      src/views/SmartTour/exhibition-detail.vue
  74. 250 0
      src/views/SmartTour/exhibition.vue
  75. 327 0
      src/views/SmartTour/treasure-detail.vue
  76. 233 0
      src/views/SmartTour/treasure.vue
  77. 223 0
      src/views/VolunteerHome/apply.vue
  78. 145 0
      src/views/VolunteerHome/detail.vue
  79. 235 0
      src/views/VolunteerHome/home.vue
  80. 13 0
      tsconfig.app.json
  81. 30 0
      tsconfig.json
  82. 17 0
      tsconfig.node.json
  83. 44 0
      vite.config.ts
  84. 2445 0
      yarn.lock

+ 0 - 0
.eslintignore


+ 15 - 0
.eslintrc.cjs

@@ -0,0 +1,15 @@
+/* eslint-env node */
+require('@rushstack/eslint-patch/modern-module-resolution')
+
+module.exports = {
+  root: true,
+  'extends': [
+    'plugin:vue/vue3-essential',
+    'eslint:recommended',
+    '@vue/eslint-config-typescript',
+    '@vue/eslint-config-prettier/skip-formatting'
+  ],
+  parserOptions: {
+    ecmaVersion: 'latest'
+  }
+}

+ 30 - 0
.gitignore

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

+ 8 - 0
.prettierrc.json

@@ -0,0 +1,8 @@
+{
+  "$schema": "https://json.schemastore.org/prettierrc",
+  "semi": false,
+  "tabWidth": 2,
+  "singleQuote": true,
+  "printWidth": 100,
+  "trailingComma": "none"
+}

+ 8 - 0
.vscode/extensions.json

@@ -0,0 +1,8 @@
+{
+  "recommendations": [
+    "Vue.volar",
+    "Vue.vscode-typescript-vue-plugin",
+    "dbaeumer.vscode-eslint",
+    "esbenp.prettier-vscode"
+  ]
+}

+ 46 - 0
README.md

@@ -0,0 +1,46 @@
+# zhengquan
+
+This template should help get you started developing with Vue 3 in Vite.
+
+## Recommended IDE Setup
+
+[VSCode](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.
+
+## Customize configuration
+
+See [Vite Configuration Reference](https://vitejs.dev/config/).
+
+## Project Setup
+
+```sh
+npm install
+```
+
+### Compile and Hot-Reload for Development
+
+```sh
+npm run dev
+```
+
+### Type-Check, Compile and Minify for Production
+
+```sh
+npm run build
+```
+
+### Lint with [ESLint](https://eslint.org/)
+
+```sh
+npm run lint
+```

+ 21 - 0
components.d.ts

@@ -0,0 +1,21 @@
+/* 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 {
+    IframePage: typeof import('./src/components/IframePage/index.vue')['default']
+    RouterLink: typeof import('vue-router')['RouterLink']
+    RouterView: typeof import('vue-router')['RouterView']
+    SearchInput: typeof import('./src/components/SearchInput/index.vue')['default']
+    VanRadio: typeof import('vant/es')['Radio']
+    VanRadioGroup: typeof import('vant/es')['RadioGroup']
+    VanSwipe: typeof import('vant/es')['Swipe']
+    VanSwipeItem: typeof import('vant/es')['SwipeItem']
+    VanTab: typeof import('vant/es')['Tab']
+    VanTabs: typeof import('vant/es')['Tabs']
+  }
+}

+ 1 - 0
env.d.ts

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

+ 22 - 0
eslintrc.js

@@ -0,0 +1,22 @@
+module.exports = {
+  root: true, // 此项是用来告诉eslint找当前配置文件不能往父级查找
+  env: { // 预定义的全局变量,这里是浏览器环境
+    amd: true,
+    es6: true,
+    commonjs: true,
+    node: true,
+    jquery: true,
+    browser: true
+  },
+  extends: [
+    'plugin:vue/essential',
+    '@vue/standard'
+  ],
+  parserOptions: {
+    sourceType: 'module',
+    parser: 'babel-eslint' // 解析器,默认使用 Espree
+  },
+  rules: {
+    'vue/multi-word-component-names': 'off'
+  }
+}

+ 26 - 0
index.html

@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+  <meta charset="UTF-8">
+  <link rel="icon" href="/favicon.ico">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <meta name="apple-mobile-web-app-capable" content="yes">
+  <meta name="viewport"
+    content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
+
+  <!-- 本地开发 -->
+  <script src="/staticData/data.js"></script>
+  <!-- 打包配置 -->
+  <!-- <script src="./staticData/data.js"></script> -->
+
+  <title>宁国市博物馆</title>
+</head>
+
+<body>
+  <div id="app"></div>
+  <script type="module" src="/src/main.ts"></script>
+  <script src="http://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
+</body>
+
+</html>

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 7949 - 0
package-lock.json


+ 52 - 0
package.json

@@ -0,0 +1,52 @@
+{
+  "name": "zhengquan",
+  "version": "0.0.0",
+  "private": true,
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "run-p type-check \"build-only {@}\" --",
+    "preview": "vite preview",
+    "build-only": "vite build",
+    "type-check": "vue-tsc --build --force",
+    "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
+    "format": "prettier --write src/"
+  },
+  "dependencies": {
+    "@types/node": "^20.5.0",
+    "axios": "^1.6.2",
+    "easyscroller": "^1.0.1",
+    "element-plus": "^2.5.5",
+    "js-base64": "^3.7.5",
+    "pinia": "^2.1.7",
+    "swiper": "^10.3.1",
+    "v-viewer": "^3.0.11",
+    "vant": "^4.8.10",
+    "vue": "^3.3.11",
+    "vue-router": "^4.2.5"
+  },
+  "devDependencies": {
+    "@rushstack/eslint-patch": "^1.3.3",
+    "@tsconfig/node18": "^18.2.2",
+    "@types/node": "^18.19.3",
+    "@vant/auto-import-resolver": "^1.1.0",
+    "@vitejs/plugin-vue": "^4.5.2",
+    "@vue/eslint-config-prettier": "^8.0.0",
+    "@vue/eslint-config-typescript": "^12.0.0",
+    "@vue/tsconfig": "^0.5.0",
+    "eslint": "^8.49.0",
+    "eslint-plugin-vue": "^9.17.0",
+    "less": "^4.2.0",
+    "npm-run-all2": "^6.1.1",
+    "prettier": "^3.0.3",
+    "typescript": "~5.3.0",
+    "unplugin-auto-import": "^0.17.5",
+    "unplugin-vue-components": "^0.26.0",
+    "vite": "^5.0.10",
+    "vue-tsc": "^1.8.25"
+  },
+  "rules": {
+    "no-unused-vars": 0,
+    "vue/multi-word-component-names": "off"
+  }
+}

BIN
public/favicon.ico


+ 13 - 0
public/staticData/data.js

@@ -0,0 +1,13 @@
+let baseUrlRes = ''
+
+if (window.location.href.includes('http://')) {
+  // 本地环境
+  baseUrlRes = 'http://127.0.0.1:8081/staticData/'
+} else {
+  // 正式环境
+  baseUrlRes = '../staticData'
+}
+
+const dataAll = {
+
+}

+ 92 - 0
src/App.vue

@@ -0,0 +1,92 @@
+<script setup lang="ts">
+import { useStore } from "./stores/index";
+const store = useStore();
+
+const time = ref();
+
+
+onBeforeMount(() => {
+  // @ts-ignore
+  store.dataAll = dataAll;
+  // 移动端
+  window.addEventListener(
+    "resize",
+    () => {
+      //@ts-ignore
+      clearTimeout(time.current);
+      //@ts-ignore
+      time.current = window.setTimeout(() => {
+        // 根元素
+        const dom: HTMLDivElement | null = document.querySelector("#app");
+        if (dom && document.documentElement.clientWidth < 480) {
+          dom.style.height = document.documentElement.clientHeight + "px";
+        }
+      }, 100);
+    },
+    true
+  );
+
+  // const baseAPIUrl = ref('')
+  // // const baseAPIUrl = ref('http://192.168.20.61:8063')
+  // // const baseAPIUrl = ref('https://sit-cnzhengquan.4dage.com')
+
+  // // 统计访问量
+  // if (localStorage.getItem('refreshTime')) {
+  //   var nowDate = Math.floor(Date.now() / 1000);
+  //   // @ts-ignore
+  //   var storedTimestamp = parseInt(localStorage.getItem('refreshTime'), 10);
+  //   console.log('时间差',Math.floor((nowDate - storedTimestamp)))
+  //   if (Math.floor((nowDate - storedTimestamp) / 60) > 60) {
+  //     // 发送请求并且更新本地缓存
+
+  //     axios.get(`${baseAPIUrl.value}/api/show/addVisit?id=10&type=visit`).then(() => {
+  //       // @ts-ignore
+  //       localStorage.setItem('refreshTime', nowDate);
+  //     })
+  //   }
+  // } else {
+  //   axios.get(`${baseAPIUrl.value}/api/show/addVisit?id=10&type=visit`).then(() => {
+  //     // @ts-ignore
+  //     localStorage.setItem('refreshTime', Math.floor(Date.now() / 1000));
+  //   })
+
+  // }
+})
+</script>
+
+<template>
+  <router-view />
+</template>
+
+<style scoped>
+@font-face {
+  font-family: 'SourceHanSansCN-Bold';
+  src: url('./assets/fonts/SourceHanSansCN-Bold.ttf');
+}
+
+@font-face {
+  font-family: 'SourceHanSansCN-Regular';
+  src: url('./assets/fonts/SourceHanSansCN-Regular.otf');
+}
+
+@font-face {
+  font-family: 'SourceHanSansCN-Medium';
+  src: url('./assets/fonts/SourceHanSansCN-Medium.otf');
+}
+
+
+
+#app {
+  width: 100%;
+  height: 100%;
+  max-width: 500px;
+  /* height: ; */
+  background: #F7F3E8;
+}
+
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+}
+</style>

+ 8 - 0
src/api/api/activeBooking/index.ts

@@ -0,0 +1,8 @@
+import axiosInstance from '@/api/request'
+export const ActiveBookingApi = {
+  getTypeList: () => axiosInstance.get('show/activity/getDict'),
+  getActiveList: (data: any) => axiosInstance.post('show/activity/pageList', data),
+  getDetailById: (id: number) => axiosInstance.get(`show/activity/detail/${id}`)
+}
+
+

+ 5 - 0
src/api/api/artwork/index.ts

@@ -0,0 +1,5 @@
+import axiosInstance from '@/api/request'
+export const ArtworkApi = {
+  getSearchByKeyword: (data: any) => axiosInstance.post('show/cultural/pageList', data),
+  getConfig:()=> axiosInstance.get('show/cultural/getConfig')
+}

+ 7 - 0
src/api/api/dynamic/index.ts

@@ -0,0 +1,7 @@
+import axiosInstance from '@/api/request'
+export const DynamicApi = {
+  getTypeList: () => axiosInstance.get('show/news/getDict'),
+  getDynamicList: (data: any) => axiosInstance.post('show/news/pageList', data),
+  getDetailById: (id: number) => axiosInstance.get(`show/news/detail/${id}`)
+  
+}

+ 6 - 0
src/api/api/exhibition/index.ts

@@ -0,0 +1,6 @@
+import axiosInstance from '@/api/request'
+export const ExhibitionApi = {
+  getExhibitionList: (data: any) => axiosInstance.post('show/exhibition/pageList', data),
+  getDetailById: (id: number) => axiosInstance.get(`show/exhibition/detail/${id}`)
+}
+

+ 9 - 0
src/api/api/sceneBooking/index.ts

@@ -0,0 +1,9 @@
+import axiosInstance from '@/api/request'
+export const sceneBookingApi = {
+  getBookingListAPi: (data:any) => axiosInstance.get('cms/rule/pageList',data),
+  getDetailByIdAPI: (id:any) => axiosInstance.get(`cms/rule/detail/${id}`),
+  getCodeAPI:(code:any) => axiosInstance.get(`wx/wxLogin/${code}`),
+  // submitBookingAPI: (data:any) => axiosInstance.post('cms/webUser/bookExhibition/save',data),
+  submitBookingAPI: (data:any) => axiosInstance.post('show/bookExhibition/save',data),
+  getBookingTimeAPI: (date:string) => axiosInstance.get(`show/book/config?date=${date}`),
+}

+ 6 - 0
src/api/api/summarize/index.ts

@@ -0,0 +1,6 @@
+import axiosInstance from '@/api/request'
+export const SummarizeApi = {
+  // 获得概述内容
+  // getSummerize: () => request('/wxShow/config/getAppreciate', 'GET', {})
+  getSummerize: () => axiosInstance.get('show/overview/detail'),
+}

+ 8 - 0
src/api/api/treasure/index.ts

@@ -0,0 +1,8 @@
+import axiosInstance from '@/api/request'
+export const TreasureApi = {
+  getTypeList: () => axiosInstance.get('show/goods/getDict'),
+  getTreasureList: (data: any) => axiosInstance.post('show/goods/pageList', data),
+  getDetailById: (id: number) => axiosInstance.get(`show/goods/detail/${id}`)
+}
+
+

+ 6 - 0
src/api/api/volunteerHome/index.ts

@@ -0,0 +1,6 @@
+import axiosInstance from '@/api/request'
+export const VolunteerApi = {
+  getVolunteerList: (data: any) => axiosInstance.post('show/volunteer/pageList', data),
+  getDetailById: (id: number) => axiosInstance.get(`show/volunteer/detail/${id}`),
+  saveApplyAPI: (data: any) => axiosInstance.post('show/bookInfo/save', data)
+}

+ 1 - 0
src/api/record.ts

@@ -0,0 +1 @@
+export const baseIMGUrl ='https://houseoss.4dkankan.com/project/bjfljtl/img'

+ 87 - 0
src/api/request.ts

@@ -0,0 +1,87 @@
+import axios from 'axios';
+
+
+//设置基地址
+// export const baseUrl = "https://wxfalangchang.4dage.com/api";
+// export const baseIMGUrl = "https://wxfalangchang.4dage.com";
+// export const baseUrl = "https://sit-ningguobwg.4dage.com/api/";
+export const baseIMGUrl = "";
+// export const baseIMGUrl = "https://sit-ningguobwg.4dage.com/";
+
+
+export const baseUrl =
+  // 线下的图片地址需要加上/api/
+  // process.env.NODE_ENV === "development"
+  //   ? "http://192.168.20.61:8067/api/"
+  //   : "";
+  process.env.NODE_ENV === "development"
+    ? "https://sit-ningguobwg.4dage.com"
+    : "";
+
+
+const instance = axios.create({
+  // 线下环境
+  // baseURL: baseUrl, // 使用环境变量设置API的基础URL
+  // 线上环境
+  baseURL: baseUrl + "/api/",
+  timeout: 5000, // 可选,设置请求超时时间
+  headers: {
+    'Content-Type': 'application/json',
+  },
+});
+
+// 请求拦截器
+instance.interceptors.request.use(
+  async (config) => {
+    // 在这里执行请求发送前的预处理操作
+
+    // 示例:添加全局请求头(如API版本、认证令牌等)(需要登录)
+    const accessToken = localStorage.getItem('token')
+
+    if (accessToken) {
+      config.headers.Authorization = `Bearer ${accessToken}`;
+    }
+
+    // 可以在此处添加其他通用请求逻辑,如:
+    // - 统一处理请求参数序列化
+    // - 显示全局加载指示器
+    // - ...
+
+    return config;
+  },
+  (error) => {
+    // 对请求错误进行处理,如:
+    // - 清除过期的认证信息
+    // - 提示用户网络问题
+    // - ...
+
+    return Promise.reject(error);
+  },
+);
+
+instance.interceptors.response.use(
+  (response) => {
+    // 对正常响应进行处理,如:
+    // - 解析并标准化返回数据结构
+    // - 隐藏全局加载指示器
+    // - 更新全局状态(如刷新用户信息、更新接口调用次数等)
+
+    return response.data; // 返回响应数据
+  },
+  (error) => {
+   // 对HTTP错误进行处理,如:
+   // - 根据状态码显示相应的错误提示
+   // - 处理token过期,触发重新登录流程
+   // - 将错误信息标准化以便在组件中统一处理
+
+   if (error.response.status === 401) {
+     // 处理未授权情况,如清除token并跳转到登录页面
+   }
+
+   return Promise.reject(error); // 将错误对象传递给后续的catch语句
+  },
+);
+
+
+
+export default instance;

+ 16 - 0
src/assets/base.css

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

BIN
src/assets/fonts/SourceHanSansCN-Bold.ttf


BIN
src/assets/fonts/SourceHanSansCN-Medium.otf


BIN
src/assets/fonts/SourceHanSansCN-Regular.otf


BIN
src/assets/images/back.png


BIN
src/assets/images/booked-card.png


BIN
src/assets/images/booking-card.png


BIN
src/assets/images/cicle.png


BIN
src/assets/images/code.png


BIN
src/assets/images/delect.png


BIN
src/assets/images/income.png


BIN
src/assets/images/left.png


BIN
src/assets/images/no-data.png


BIN
src/assets/images/onlineBg.png


BIN
src/assets/images/pause.png


BIN
src/assets/images/play.png


BIN
src/assets/images/right.png


BIN
src/assets/images/sceneBack.png


BIN
src/assets/images/search_icon.png


+ 1 - 0
src/assets/logo.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

+ 1 - 0
src/assets/main.css

@@ -0,0 +1 @@
+@import './base.css';

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

@@ -0,0 +1,71 @@
+/* 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'
+  import('vue')
+}

+ 38 - 0
src/base.css

@@ -0,0 +1,38 @@
+#app {
+  margin: 0;
+  width: 100%;
+  height: 100vh;
+  max-width: 500px;
+  /* overflow: hidden; */
+}
+body {
+  margin: 0;
+  padding: 0;
+  display: block;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  /* max-width: 500px; */
+}
+::-webkit-scrollbar {
+  width: 3px;
+  /* 横向滚动条的宽度 */
+  height: 10px;
+  /* 纵向滚动条的高度 */
+  background-color: #E4DCC5;
+  /* 轨道背景色 */
+}
+::-webkit-scrollbar-thumb {
+  background-color: #88866F;
+  /* 滑块背景色 */
+  border-radius: 5px;
+  /* 滚动条滑块圆角 */
+  box-shadow: inset 0 0 6px #F1E9D4;
+  /* 滚动条滑块阴影效果 */
+}
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+}

+ 20 - 0
src/components/IframePage/index.vue

@@ -0,0 +1,20 @@
+<!-- eslint-disable vue/multi-word-component-names -->
+<script setup lang='ts'>
+const props = defineProps(['iframeLink'])
+const emit = defineEmits(['isShowIframeChange'])
+
+const isShowIframeChangeFu = () => {
+  emit('isShowIframeChange', false)
+}
+
+
+</script>
+
+<template>
+  <div class="iframe-box" >
+    <img src="@/assets/images/sceneBack.png" alt="" @click="isShowIframeChangeFu">
+    <iframe :src="props.iframeLink" frameborder="0"></iframe>
+  </div>
+</template>
+
+<style lang='less' scoped></style>

+ 61 - 0
src/components/SearchInput/index.vue

@@ -0,0 +1,61 @@
+<!-- eslint-disable vue/multi-word-component-names -->
+<script setup lang='ts'>
+
+const emit = defineEmits(['onSearch']);
+const isSearch = ref(false)
+const inputField = ref(null as any);
+const searchValue = ref('')
+const focusInput = () => {
+  setTimeout(() => {
+    if (inputField.value) {
+      console.log(inputField.value)
+      inputField.value.focus();
+    }
+  }, 200)
+}
+
+watch(searchValue, () => {
+  emit('onSearch', searchValue.value);
+})
+
+</script>
+
+<template>
+  <div class="search-box">
+    <div class="search-input">
+      <input ref="inputField" v-model="searchValue" v-show="isSearch" type="text"
+        @blur="searchValue == '' ? isSearch = false : ''">
+      <div v-show="!isSearch" @click="() => { isSearch = true, focusInput() }"
+        style="display: flex;justify-content: center; align-items: center;color: #88866F;"><img
+          style="width: 20px;margin-right: 10px;" src="@/assets/images/search_icon.png" alt="">请输入关键词</div>
+    </div>
+  </div>
+</template>
+
+<style lang='less' scoped>
+.search-box {
+  width: 100%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  height: 7vh;
+
+  .search-input {
+    width: 80%;
+    height: 60%;
+    border-radius: 50px;
+    background: #F1E9D4;
+    margin-top: 1vh;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+
+    input {
+      border: none;
+      background: transparent;
+      width: 90%;
+      height: 80%;
+    }
+  }
+}
+</style>

+ 7 - 0
src/images.d.ts

@@ -0,0 +1,7 @@
+declare module '*.svg'
+declare module '*.png'
+declare module '*.jpg'
+declare module '*.jpeg'
+declare module '*.gif'
+declare module '*.bmp'
+declare module '*.tiff'

+ 27 - 0
src/main.ts

@@ -0,0 +1,27 @@
+import './base.css'
+
+import { createApp } from 'vue'
+import { createPinia } from 'pinia'
+
+import App from './App.vue'
+import router from './router'
+import { Field, CellGroup, Toast, Swipe, SwipeItem, Tab, Tabs, Search } from 'vant'
+import 'vant/lib/index.css'
+// import "viewerjs/dist/viewer.css";
+// import Viewer from "v-viewer";
+
+const app = createApp(App)
+
+app.use(createPinia())
+app.use(router)
+app.use(Toast)
+app.use(Swipe)
+app.use(SwipeItem)
+app.use(Tab)
+app.use(Tabs)
+app.use(Search)
+app.use(Field)
+app.use(CellGroup)
+// app.use(Viewer)
+
+app.mount('#app')

+ 130 - 0
src/router/index.ts

@@ -0,0 +1,130 @@
+import { createRouter, createWebHashHistory } from 'vue-router'
+
+const routes = [
+  {
+    path: '/',
+    name: 'overview',
+    //使用import可以路由懒加载,如果不使用,太多组件一起加载会造成白屏
+    component: () => import('@/views/EnterNingguo/summarize.vue')
+  },
+  {
+    path: '/dynamic',
+    name: 'dynamic',
+    //使用import可以路由懒加载,如果不使用,太多组件一起加载会造成白屏
+    component: () => import('@/views/EnterNingguo/dynamic.vue')
+  },
+  {
+    path: '/dynamic/detail/:id',
+    name: 'dynamicDetail',
+    component: () => import('@/views/EnterNingguo/dynamic-detail.vue'),
+    props: true
+  },
+  {
+    path: '/artwork',
+    name: 'artwork',
+    //使用import可以路由懒加载,如果不使用,太多组件一起加载会造成白屏
+    component: () => import('@/views/EnterNingguo/artwork.vue')
+  },
+  {
+    path: '/treasure',
+    name: 'treasure',
+    //使用import可以路由懒加载,如果不使用,太多组件一起加载会造成白屏
+    component: () => import('@/views/SmartTour/treasure.vue')
+  },
+  {
+    path: '/treasure/detail/:id',
+    name: 'treasureDetail',
+    //使用import可以路由懒加载,如果不使用,太多组件一起加载会造成白屏
+    component: () => import('@/views/SmartTour/treasure-detail.vue')
+  },
+  {
+    path: '/exhibition',
+    name: 'exhibition',
+    //使用import可以路由懒加载,如果不使用,太多组件一起加载会造成白屏
+    component: () => import('@/views/SmartTour/exhibition.vue')
+  },
+  {
+    path: '/exhibition/detail/:id',
+    name: 'exhibitionDetail',
+    //使用import可以路由懒加载,如果不使用,太多组件一起加载会造成白屏
+    component: () => import('@/views/SmartTour/exhibition-detail.vue')
+  },
+
+  {
+    path: '/booking',
+    name: 'booking',
+    //使用import可以路由懒加载,如果不使用,太多组件一起加载会造成白屏
+    component: () => import('@/views/ExhibitionService/bookedList.vue')
+  },
+
+  {
+    path: '/booking/selectTime',
+    name: 'selectTime',
+    //使用import可以路由懒加载,如果不使用,太多组件一起加载会造成白屏
+    component: () => import('@/views/ExhibitionService/BookingTime.vue')
+  },
+  {
+    path: '/booking/detail/:id',
+    name: 'bookedDetail',
+    //使用import可以路由懒加载,如果不使用,太多组件一起加载会造成白屏
+    component: () => import('@/views/ExhibitionService/booked-detail.vue')
+  },
+
+  {
+    path: '/booking/selectTime/bookInputInfo',
+    name: 'bookInputInfo',
+    //使用import可以路由懒加载,如果不使用,太多组件一起加载会造成白屏
+    component: () => import('@/views/ExhibitionService/bookInputInfo.vue')
+  },
+
+  {
+    path: '/activeBooking',
+    name: 'activeBooking',
+    //使用import可以路由懒加载,如果不使用,太多组件一起加载会造成白屏
+    component: () => import('@/views/ExhibitionService/activeBooking.vue')
+  },
+
+  {
+    path: '/activeBooking/detail/:id',
+    name: 'activeDetail',
+    //使用import可以路由懒加载,如果不使用,太多组件一起加载会造成白屏
+    component: () => import('@/views/ExhibitionService/active-detail.vue')
+  },
+  // 填写预约信息
+  {
+    path: '/activeBooking/booking/:id/:title',
+    name: 'activeBookInfo',
+    //使用import可以路由懒加载,如果不使用,太多组件一起加载会造成白屏
+    component: () => import('@/views/ExhibitionService/active-info.vue')
+  },
+
+  {
+    path: '/volunteerHome',
+    name: 'volunteerHome',
+    //使用import可以路由懒加载,如果不使用,太多组件一起加载会造成白屏
+    component: () => import('@/views/VolunteerHome/home.vue')
+  },
+
+  {
+    path: '/volunteerHome/detail/:id',
+    name: 'volunteerDetail',
+    //使用import可以路由懒加载,如果不使用,太多组件一起加载会造成白屏
+    component: () => import('@/views/VolunteerHome/detail.vue')
+  },
+
+  {
+    path: '/volunteerHome/apply',
+    name: 'activeBookApply',
+    //使用import可以路由懒加载,如果不使用,太多组件一起加载会造成白屏
+    component: () => import('@/views/VolunteerHome/apply.vue')
+  },
+
+  
+]
+// 路由
+const router = createRouter({
+  history: createWebHashHistory(),
+  routes
+})
+// 导出
+export default router

+ 18 - 0
src/stores/index.ts

@@ -0,0 +1,18 @@
+import { defineStore } from 'pinia'
+
+export const useStore = defineStore('home', {
+  // 相当于data
+  state: () => {
+    return {
+      // 原始数据
+      dataAll: {} as any,
+      selectDate: '' as any,
+      selectTime: '' as any,
+      selectId: '' as any,
+    }
+  },
+  // 相当于计算属性
+  getters: {},
+  // 相当于vuex的 mutation + action,可以同时写同步和异步的代码
+  actions: {}
+})

+ 9 - 0
src/types/api/dynamic.d.ts

@@ -0,0 +1,9 @@
+export type dynamicDetailType = {
+  id: number
+  title: String
+  thebm: String
+  abstract: String
+  mainBody: String
+  videos: String[]
+  releaseTime: String
+}

+ 7 - 0
src/types/declaration.d.ts

@@ -0,0 +1,7 @@
+declare module "history";
+declare module "*.scss";
+declare module "*.png";
+declare module "*.jpg";
+declare module "*.gif";
+declare module "js-export-excel";
+declare module 'braft-utils';

+ 4 - 0
src/types/index.d.ts

@@ -0,0 +1,4 @@
+export * from './api/dynamic'
+
+
+

+ 36 - 0
src/utils/browser.ts

@@ -0,0 +1,36 @@
+function versions () {
+  const u = window.navigator.userAgent
+  return {
+    // IE内核
+    trident: u.indexOf('Trident') > -1,
+    // Firefox
+    firefox: u.indexOf('Firefox') > -1,
+    // edge
+    edge: u.indexOf('Edge') > -1,
+    // opera内核
+    presto: u.indexOf('Presto') > -1,
+    // 苹果、谷歌内核
+    webKit: u.indexOf('AppleWebKit') > -1,
+    // 火狐内核
+    gecko: u.indexOf('Gecko') > -1 && u.indexOf('KHTML') === -1,
+    // 是否为移动终端
+    mobile: /Android|webOS|iPhone|iPod|BlackBerry|SM-T|GT-P|Note|Tab|Nexus|Pixel|Tablet|Mi-Tab|Mi-Box|Huawei|Redmi|Mobile|XiaoMi/i.test(navigator.userAgent),
+    // ios终端
+    ios: !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/),
+    // android终端或者uc浏览器
+    android: u.indexOf('Android') > -1 || u.indexOf('Linux') > -1,
+    // 是否为iPhone或者安卓QQ浏览器
+    iPhone: u.indexOf('iPhone') > -1 || u.indexOf('Mac') > -1,
+    // 是否为iPad
+    iPad: u.indexOf('iPad') > -1,
+    // 是否为web应用程序,没有头部与底部
+    webApp: u.indexOf('Safari') === -1,
+    // 是否为微信浏览器
+    weixin: ~u.indexOf('MicroMessenger'),
+    // 获取浏览器语言
+    // @ts-ignore
+    language: (navigator.browserLanguage || navigator.language).toLowerCase()
+  }
+}
+
+export default versions()

+ 10 - 0
src/utils/https.ts

@@ -0,0 +1,10 @@
+// const baseURl = ''
+// 本地
+// export const baseURl = 'http://127.0.0.1:5174/staticData'
+export const baseURl = './staticData'
+
+// 线上开发
+const baseResourceUrl = './staticData'
+
+export default baseResourceUrl
+ 

+ 100 - 0
src/utils/pass.ts

@@ -0,0 +1,100 @@
+function randomWord(randomFlag: boolean, min: number, max: number = 15) {
+  let str = "";
+  let range = min;
+  const arr = [
+    "0",
+    "1",
+    "2",
+    "3",
+    "4",
+    "5",
+    "6",
+    "7",
+    "8",
+    "9",
+    "a",
+    "b",
+    "c",
+    "d",
+    "e",
+    "f",
+    "g",
+    "h",
+    "i",
+    "j",
+    "k",
+    "l",
+    "m",
+    "n",
+    "o",
+    "p",
+    "q",
+    "r",
+    "s",
+    "t",
+    "u",
+    "v",
+    "w",
+    "x",
+    "y",
+    "z",
+    "A",
+    "B",
+    "C",
+    "D",
+    "E",
+    "F",
+    "G",
+    "H",
+    "I",
+    "J",
+    "K",
+    "L",
+    "M",
+    "N",
+    "O",
+    "P",
+    "Q",
+    "R",
+    "S",
+    "T",
+    "U",
+    "V",
+    "W",
+    "X",
+    "Y",
+    "Z",
+  ];
+  // 随机产生
+  if (randomFlag) {
+    range = Math.round(Math.random() * (max - min)) + min;
+  }
+  for (let i = 0; i < range; i++) {
+    const pos = Math.round(Math.random() * (arr.length - 1));
+    str += arr[pos];
+  }
+  return str;
+}
+
+const encodeStr = (str: string, strv = "") => {
+  const NUM = 2;
+  const front = randomWord(false, 8);
+  const middle = randomWord(false, 8);
+  const end = randomWord(false, 8);
+
+  const str1 = str.substring(0, NUM);
+  const str2 = str.substring(NUM);
+
+  if (strv) {
+    const strv1 = strv.substring(0, NUM);
+    const strv2 = strv.substring(NUM);
+    return [
+      front + str2 + middle + str1 + end,
+      front + strv2 + middle + strv1 + end,
+    ];
+  }
+
+  return front + str2 + middle + str1 + end;
+};
+
+export default encodeStr;

+ 44 - 0
src/utils/weixin.ts

@@ -0,0 +1,44 @@
+// export function getUserCode(backUrl: string) {
+//   //此处的ID是在文档的开发-基本配置里面
+//   const appid = 'wx3255043d1b21a4f7'
+//   window.location.href =
+//     'https://open.weixin.qq.com/connect/oauth2/authorize?appid=' +
+//     appid +
+//     '&redirect_uri=' +
+//     encodeURIComponent(backUrl) +
+//     '&response_type=code&scope=snsapi_userinfo&state=bc17befd6d5060f16de95e38f6eaf69c&connect_redirect=1#wechat_redirect'
+// }
+
+export function checkLoginStatus() {
+  const userInfo = localStorage.getItem('userInfo')
+  if (userInfo) {
+    const nowTime = Date.now()
+    const data = JSON.parse(userInfo)
+    // 超过了santia
+    if (nowTime - data.time >= 1000 * 60 * 60 * 72) {
+      return false
+    } else {
+      return true
+    }
+  } else {
+    return false
+  }
+}
+
+export function getQueryCode(name: string) {
+  // 未传参,返回空
+  if (!name) return null
+  // 查询参数:先通过search取值,如果取不到就通过hash来取
+  let after = window.location.search
+  after = after.substr(1) || window.location.hash.split('?')[1]
+  // 地址栏URL没有查询参数,返回空
+  if (!after) return null
+  // 如果查询参数中没有"name",返回空
+  if (after.indexOf(name) === -1) return null
+
+  const reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)')
+  // 当地址栏参数存在中文时,需要解码,不然会乱码
+  const r = decodeURI(after).match(reg)
+  // 如果url中"name"没有值,返回空
+  if (!r) return null
+}

+ 214 - 0
src/views/EnterNingguo/artwork.vue

@@ -0,0 +1,214 @@
+<!-- eslint-disable vue/multi-word-component-names -->
+<script setup lang='ts'>
+import { ArtworkApi } from '@/api/api/artwork'
+import { showToast } from 'vant'
+import SearchInput from '@/components/SearchInput/index.vue'
+import { baseIMGUrl } from '@/api/request.ts'
+
+
+export type artworkType = {
+  createTime: string,
+  creatorId: null,
+  creatorName: string,
+  id: number,
+  link: string,
+  name: string,
+  // 发布日期
+  publishDate: string,
+  thumb: string,
+  updateTime: string
+}
+
+const searchParames = ref({
+  pageNum: 0,
+  pageSize: 0,
+  searchKey: ''
+} as {
+  pageNum: number,
+  pageSize: number,
+  searchKey: string
+})
+
+const searchList = ref([] as artworkType[])
+
+
+
+const getList = async () => {
+  const res: any = await ArtworkApi.getSearchByKeyword(searchParames.value)
+  if (res.code == 0) {
+    searchList.value = res.data.records
+  } else {
+    showToast(res.msg)
+  }
+}
+
+const onSearch = (eventData: any) => {
+  searchParames.value.searchKey = eventData
+  getList()
+}
+
+const isShowIframe = ref(false)
+const iframeLink = ref('')
+
+const isOpen = ref(false)
+
+const getState = async () => {
+  const res: any = await ArtworkApi.getConfig()
+  if (res.code == 0) {
+    isOpen.value = res.data == 1 ? true : false
+  }
+
+  if (isOpen.value) {
+    getList()
+  }
+
+}
+
+
+onBeforeMount(() => {
+  getState()
+})
+
+</script>
+
+<template>
+  <div class="noopen" v-if="!isOpen">
+    宁博文创尚未开放,敬请期待
+  </div>
+  <div class='artwork-box' v-else>
+    <SearchInput @onSearch="onSearch" />
+    <div class="search-list" v-if="searchList.length > 0">
+      <div class="list-item" v-for="(item, index) in searchList" :key="index"
+        @click="() => { isShowIframe = true, iframeLink = item.link }">
+        <img :src="baseIMGUrl + item.thumb" alt="">
+        <div class="name">{{ item.name }}</div>
+      </div>
+    </div>
+    <div class="no-data" v-else>
+      <img src="@/assets/images/no-data.png" alt="">
+      <div class="tips">暂时没有数据 ,请试一下其他关键字</div>
+    </div>
+  </div>
+  <div class="iframe-box" v-show="isShowIframe">
+    <img src="@/assets/images/sceneBack.png" alt="" @click="isShowIframe = false">
+    <iframe :src="iframeLink" frameborder="0"></iframe>
+  </div>
+
+</template>
+
+<style lang='less' scoped>
+.noopen{
+  width: 100%;
+  min-height: 100%;
+  background: #F7F3E8;
+  overflow: auto;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  color: #333333;
+}
+.artwork-box {
+  width: 100%;
+  min-height: 100%;
+  background: #F7F3E8;
+  overflow: auto;
+
+  .search-list {
+    width: 100%;
+    padding: 10px;
+    display: grid;
+    grid-template-columns: 1fr 1fr;
+    grid-auto-rows: auto;
+    gap: 10px;
+    box-sizing: border-box;
+
+
+    .list-item {
+      width: 100%;
+      height: auto;
+      background: #F7F3E8;
+      // padding: 10px;
+      box-sizing: border-box;
+      box-shadow: 0px 0px 20px 0px rgba(222, 216, 199, 0.6);
+      border-radius: 10px 10px 10px 10px;
+      padding-bottom: 10px;
+
+      img {
+        width: 100%;
+        height: 21vh;
+        object-fit: cover;
+        border-radius: 10px 10px 0 0;
+      }
+
+      .name {
+        font-size: 1em;
+        display: -webkit-box;
+        /* 必须添加,以启用WebKit的弹性盒模型布局 */
+        -webkit-line-clamp: 2;
+        /* 最多显示两行 */
+        -webkit-box-orient: vertical;
+        /* 必须添加,使文本垂直显示 */
+
+        overflow: hidden;
+        /* 隐藏超出部分 */
+        text-overflow: ellipsis;
+        /* 末尾显示省略号 */
+        word-break: break-all;
+        /* 可选,根据需要断开长单词以适应行宽 */
+        padding: 5px 10px 0 10px;
+        font-family: 'SourceHanSansCN-Regular';
+        color: #333333;
+        text-align: justified;
+      }
+    }
+  }
+
+  .no-data {
+    width: 100%;
+    height: calc(100% - 7vh);
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    margin-top: 10%;
+
+
+    img {
+      width: 40%;
+    }
+
+    .tips {
+      color: #88866F;
+      margin-top: 10px;
+      font-family: 'SourceHanSansCN-Regular';
+    }
+
+  }
+}
+
+.iframe-box {
+  width: 100%;
+  height: 100%;
+  position: absolute;
+  top: 0;
+  left: 0;
+  z-index: 10;
+
+  iframe {
+    width: 100%;
+    height: 100%;
+    position: absolute;
+    top: 0;
+    left: 0;
+  }
+
+  img {
+    width: 30px;
+    height: 30px;
+    position: absolute;
+    top: 3%;
+    left: 3%;
+    z-index: 11;
+  }
+}
+</style>

+ 167 - 0
src/views/EnterNingguo/dynamic-detail.vue

@@ -0,0 +1,167 @@
+<script setup lang='ts'>
+import { DynamicApi } from '@/api/api/dynamic';
+import { showToast } from 'vant';
+import { baseIMGUrl } from '@/api/request.ts'
+
+
+// import { DynamicApi } from "@/api/api/dynamic/index";
+// import { showToast } from "vant";
+
+// const router = useRouter()
+const route = useRoute()
+
+
+
+
+export type dynamicDetailType = {
+  createTime: string,
+  creatorId: number,
+  creatorName: string,
+  description: string,
+  dirCode: string,
+  fileIds: string,
+  files: any,
+  id: number,
+  name: string,
+  publishDate: string,
+  rtf: string,
+  thumb: string,
+  type: string,
+  updateTime: string
+}
+
+const dynamicDetail = ref({} as dynamicDetailType)
+
+const videos = ref([] as any)
+
+const getDetailById = async () => {
+  // 获取活动详情
+  const res: any = await DynamicApi.getDetailById(Number(route.params.id))
+  if (res.code == 0) {
+    dynamicDetail.value = res.data
+    videos.value = res.data.files.filter((item: any) => {
+      return item.filePath.includes('.mp4')
+    })
+  } else {
+    showToast(res.msg)
+  }
+}
+
+const formatrtf = computed(() => {
+  return dynamicDetail.value.rtf.replace(
+    /<img /g,
+    '<img style="width: 100%;" '
+  ).replace(
+    /<p\b[^>]*>/g,
+    '<p style="width: 100%; word-wrap: break-word;">'
+  );
+})
+
+
+
+onBeforeMount(async () => {
+  getDetailById()
+})
+</script>
+
+<template>
+  <div class='detail-box'>
+    <img class="themb-box" :src="baseIMGUrl + dynamicDetail.thumb" alt="">
+    <div class="content-box">
+      <div class="title-box">{{ dynamicDetail.name }}</div>
+      <div class="time-box">发布时间:{{ dynamicDetail.publishDate }}</div>
+      <div class="abstract-box">{{ dynamicDetail.description }}</div>
+      <div class="mainbody-box" v-html="formatrtf"></div>
+      <div class="video-box">
+        <video :src="baseIMGUrl + item.filePath" controls v-for="(item, index) in videos" :key="index"></video>
+      </div>
+    </div>
+    <!-- <img class="back-icon" @click="() => { router.back() }" src="@/assets/images/back.png" alt=""> -->
+  </div>
+</template>
+
+<style lang='less' scoped>
+.detail-box {
+  width: 100%;
+  min-height: 100%;
+  overflow: auto;
+
+
+  .themb-box {
+    width: 100%;
+    height: 30vh;
+    object-fit: cover;
+  }
+
+  .content-box {
+    width: 100%;
+    height: calc(70vh + 25px);
+    background: #F7F3E8;
+    border-radius: 20px 20px 0 0;
+    margin-top: -30px;
+    position: relative;
+    z-index: 2;
+    padding: 8% 15px 8% 15px 20% 15px;
+    box-sizing: border-box;
+    overflow: auto;
+
+    .title-box {
+      font-size: 1.4em;
+      font-weight: bold;
+      line-height: 1.5em;
+      color: #333333;
+      margin-bottom: 10px;
+      padding: 0 10px;
+      font-family: 'SourceHanSansCN-Medium';
+      margin-bottom: 5px;
+
+    }
+
+    .time-box {
+      color: #9D4F0B;
+      font-size: 0.9em;
+      margin-bottom: 15px;
+      padding: 0 10px;
+      font-family: 'SourceHanSansCN-Regular';
+
+    }
+
+    .abstract-box {
+      color: #88866F;
+      font-size: 1em;
+      margin-bottom: 10px;
+      padding: 0 10px;
+      font-family: 'SourceHanSansCN-Medium';
+      overflow-wrap: break-word;
+    }
+
+    .mainbody-box {
+      width: 100%;
+      // white-space: pre;
+      margin-bottom: 10px;
+      padding: 0 10px;
+
+    }
+
+    .video-box {
+      width: 100%;
+
+      video {
+        width: 100%;
+        height: 30vh;
+        margin-bottom: 10px;
+      }
+
+    }
+
+  }
+
+
+  // .back-icon {
+  //   width: 40px;
+  //   position: fixed;
+  //   right: 10px;
+  //   bottom: 10vh;
+  // }
+}
+</style>

+ 263 - 0
src/views/EnterNingguo/dynamic.vue

@@ -0,0 +1,263 @@
+<!-- eslint-disable vue/multi-word-component-names -->
+<script setup lang='ts'>
+import { DynamicApi } from '@/api/api/dynamic';
+import { showToast } from 'vant';
+
+
+const active = ref(0)
+
+const router = useRouter()
+
+const data = ref([] as DynamicType[])
+
+const searchParames = ref({
+  pageNum: 0,
+  pageSize: 0,
+  searchKey: '',
+  dictId: null
+} as {
+  dictId: number | null,
+  pageNum: number,
+  pageSize: number,
+  searchKey: string
+})
+
+export type DynamicType = {
+  createTime: string,
+  creatorId: number,
+  creatorName: string,
+  description: string,
+  dirCode: string,
+  fileIds: string,
+  files: any,
+  id: number,
+  name: string,
+  publishDate: string,
+  rtf: string,
+  thumb: string,
+  type: string,
+  updateTime: string
+}
+
+const typeList = ref([] as any)
+const allData = ref([] as DynamicType[])
+
+const goDetai = (id: number) => {
+  router.push({ name: 'dynamicDetail', params: { id } });
+}
+
+const getTypeList = async () => {
+  // 获取珍藏类型
+  const resType: any = await DynamicApi.getTypeList()
+  if (resType.code == 0) {
+    typeList.value = resType.data
+    typeList.value.unshift({
+      createTime: null,
+      creatorId: null,
+      creatorName: '',
+      display: null,
+      id: null,
+      name: '全部',
+      parentId: null,
+      rtf: '',
+      sort: null,
+      type: '',
+      typeKey: '',
+      updateTime: null
+    })
+    console.log(typeList.value)
+  } else {
+    showToast(resType.msg)
+  }
+
+  getList()
+}
+
+
+const getList = async () => {
+  searchParames.value.dictId = active.value == 0 ? null : typeList.value[active.value].id!
+  const res: any = await DynamicApi.getDynamicList(searchParames.value)
+  if (res.code == 0) {
+    allData.value = res.data.records
+    data.value = allData.value
+  } else {
+    showToast(res.msg)
+  }
+}
+
+watch(active, () => {
+  getList()
+})
+
+onMounted(() => {
+  getTypeList()
+})
+
+</script>
+
+<template>
+  <div class='dynamic'>
+
+    <div class="tabs">
+      <div class="tab-item" :class="{ active: index === active }" v-for="(item, index) in typeList" :key="item.id"
+        @click="active = index">{{ item.name }}</div>
+    </div>
+
+    <div class="tab-content" v-if="data.length > 0">
+      <div class="activity-card" v-for="(item, index) in data" :key="index" @click="goDetai(item.id)">
+        <img v-show="item.thumb != ''" class="thumb" :src="item.thumb" alt="">
+        <div class="title">{{ item.name }}</div>
+        <div v-show="item.description != ''" class="abstract">{{ item.description }}</div>
+        <div class="time">
+          发布时间:{{ item.publishDate }}
+        </div>
+      </div>
+    </div>
+    <div class="no-data" v-else>
+      <img src="@/assets/images/no-data.png" alt="">
+      <div class="tips">暂时没有数据 ,请试一下其他关键字</div>
+    </div>
+  </div>
+
+</template>
+
+<style lang='less' scoped>
+* {
+  box-sizing: border-box;
+}
+
+
+
+.dynamic {
+  width: 100%;
+  background: #F7F3E8;
+  min-height: 100%;
+  overflow: auto;
+
+
+
+  .tabs {
+    // height: 40px;
+    white-space: nowrap;
+    overflow-x: auto;
+    padding: 15px 10px 5px 0px;
+    // padding-top: 10px;
+  
+    margin: 0 10px;
+    
+    font-family: 'SourceHanSansCN-Regular';
+
+    &::-webkit-scrollbar {
+      height: 0px
+      /* 或 width: 0; */
+    }
+
+    .tab-item {
+      margin-left: 10px;
+      display: inline-block;
+      width: 85px;
+      height: 30px;
+      float: none;
+      font-size: 0.9rem;
+      line-height: 30px;
+      text-align: center;
+      background-color: #F1E9D4;
+      border-radius: 5px;
+      // box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.3);
+      color: rgba(0, 0, 0, 0.8);
+      color: #88866F;
+    }
+
+    .active {
+      background: #9D4F0B;
+      color: #F7F3E8;
+    }
+  }
+
+  // padding: 0 10px;\
+  .tab-content {
+    width: 100%;
+    padding: 20px;
+    max-height: calc(100vh - 8vh);
+    overflow: auto;
+
+    &::-webkit-scrollbar {
+      display: none;
+      /* 或 width: 0; */
+    }
+
+    .activity-card {
+      width: 100%;
+      // background: rgba(177, 177, 177, 0.226);
+      border-radius: 15px;
+      // padding: 20px;
+      margin-bottom: 30px;
+
+      .thumb {
+        width: 100%;
+        border-radius: 5px;
+        margin-bottom: 5px;
+      }
+
+      .title {
+        font-size: 1.3em;
+        font-weight: bold;
+        color: #333333;
+        font-family: 'SourceHanSansCN-Medium';
+        line-height: 1.5em;
+        margin-bottom: 10px;
+      }
+
+      .abstract {
+        font-size: 1.1em;
+        color: #88866F;
+        line-height: 1.4em;
+        font-family: 'SourceHanSansCN-Regular';
+        margin-bottom: 5px;
+
+        display: -webkit-box;
+        -webkit-box-orient: vertical;
+        -webkit-line-clamp: 2;
+        line-clamp: 2;
+        overflow: hidden;
+
+      }
+
+      .time {
+        font-size: 0.9em;
+        line-height: 1.4em;
+        width: 100%;
+        color: #9D4F0B;
+        font-family: 'SourceHanSansCN-Regular';
+
+      }
+
+
+    }
+  }
+
+  .no-data {
+    width: 100%;
+    height: calc(100% - 7vh);
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    margin-top: 10%;
+
+
+    img {
+      width: 40%;
+    }
+
+    .tips {
+      color: #88866F;
+      margin-top: 10px;
+      font-family: 'SourceHanSansCN-Regular';
+    }
+
+  }
+
+
+}
+</style>

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 137 - 0
src/views/EnterNingguo/summarize.vue


+ 262 - 0
src/views/ExhibitionService/BookingTime.vue

@@ -0,0 +1,262 @@
+<script setup lang='ts'>
+import { sceneBookingApi } from '@/api/api/sceneBooking';
+import router from '@/router';
+import { useStore } from '@/stores';
+import { showToast } from 'vant';
+
+const dateArray = ref([] as any)
+const store = useStore()
+
+const getFutureDates = (numDays: number) => {
+  const datesArray = [];
+  const today = new Date();
+  const dateFormat = 'yyyy-MM-dd 星期X'; // 新增星期格式
+
+  for (let i = 0; i <= numDays; i++) {
+    const futureDate = new Date(today.getTime());
+    futureDate.setDate(today.getDate() + i);
+    const formattedDate = formatDate(futureDate, dateFormat);
+    datesArray.push({
+      date: formattedDate,
+      isDisabled: false
+    });
+  }
+  return datesArray;
+}
+
+const formatDate = (date: any, format: any) => {
+  const year = date.getFullYear();
+  const month = String(date.getMonth() + 1).padStart(2, '0');
+  const day = String(date.getDate()).padStart(2, '0');
+  const weekDay = getWeekDay(date);
+
+  return format.replace('yyyy', year)
+    .replace('MM', month)
+    .replace('dd', day)
+    .replace('X', weekDay);
+}
+
+const getWeekDay = (date: any) => {
+  const daysOfWeek = ['日', '一', '二', '三', '四', '五', '六'];
+  return daysOfWeek[date.getDay()];
+}
+
+
+const selectDate = ref('' as string)
+const selectTime = ref('' as string)
+const selectId = ref('' as string)
+
+const bookingTimeList = ref([] as any)
+const disableTimeList = ref([] as any)
+
+const noticeText = ref('' as any)
+
+
+// 获得可预约时间段
+const getBookingTime = async () => {
+  const res: any = await sceneBookingApi.getBookingTimeAPI(selectDate.value.slice(0, 10))
+  if (res.code == 0) {
+    bookingTimeList.value = res.data.time
+    disableTimeList.value = res.data.stopDate
+    noticeText.value = res.data.notice
+
+    console.log(noticeText.value)
+  } else {
+    showToast(res.msg)
+  }
+}
+
+watch(selectDate, () => {
+  // 获取time
+  getBookingTime()
+})
+
+watch(disableTimeList, (newVal: any) => {
+  dateArray.value = dateArray.value.map((item: any) => {
+    return {
+      ...item,
+      isDisabled: disableTimeList.value.includes(item.date.slice(0, 10))
+    }
+  })
+  let today = selectDate.value.slice(0, 10)
+  if (newVal.includes(today)) {
+    const newDate = dateArray.value.find((item: any) => {
+      return item.isDisabled == false
+    })
+    selectDate.value = newDate.date
+    console.log(selectDate.value)
+  }
+
+}, { immediate: true })
+
+const selectDateFu = (item: any) => {
+  if (item.isDisabled) {
+    showToast('该日期不可预约,请选择其他日期')
+  } else {
+    selectDate.value = item.date
+    selectTime.value = ''
+    selectId.value=''
+  }
+}
+
+const selectTimeFu = (item: any) => {
+  // router.go(0)
+  if (item.pcs === 0) {
+    showToast('该时段预约已满,请选择其他时段进行预约')
+  } else {
+    selectTime.value = item.time
+    selectId.value=item.id
+  }
+}
+
+const goInpitInfo = async () => {
+  const res: any = await sceneBookingApi.getBookingTimeAPI(selectDate.value.slice(0, 10))
+  if (res.code == 0) {
+    const selectTimeItem = res.data.time.filter((item: any) => {
+      return item.time === selectTime.value
+    })
+    if (selectTimeItem.length > 0 && selectTimeItem[0].pcs > 0) {
+      store.selectDate = selectDate.value
+      store.selectTime = selectTime.value
+      store.selectId = selectId.value
+      router.push({ name: 'bookInputInfo' });
+    } else {
+      router.go(0)
+    }
+  } else {
+    showToast(res.msg)
+  }
+
+}
+
+onMounted(() => {
+  dateArray.value = getFutureDates(13)
+  if (store.selectDate != '' && store.selectTime != '') {
+    selectDate.value = store.selectDate
+    selectTime.value = store.selectTime
+  } else {
+    selectDate.value = dateArray.value[0].date
+  }
+})
+</script>
+
+<template>
+  <div class='time-box'>
+    <div class="date-select-box">
+      <div class="date-select-item" v-for="(item, index) in dateArray" :key="index"
+        :class="{ active: item.date === selectDate, disAble: item.isDisabled }" @click="selectDateFu(item)">
+        {{
+          item.date.slice(5) }}</div>
+    </div>
+    <div class="time-select-box">
+      <div class="time-select-item" v-for="(item, index) in bookingTimeList" :key="index"
+        :class="{ active: item.time === selectTime }" :style="{ color: item.pcs == 0 ? '#9D4F0B' : '' }"
+        @click="() => { selectTimeFu(item) }">
+        <div>{{ item.time }}</div>
+        <div>{{ item.pcs == 0 ? `预约已满` : `剩余${item.pcs}` }}</div>
+      </div>
+
+      <div class="notice-box" v-html="noticeText"></div>
+    </div>
+    <div class="online-box" v-if="selectDate != '' && selectTime != ''" @click="goInpitInfo()">发起预约</div>
+  </div>
+</template>
+
+<style lang='less' scoped>
+.time-box {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  justify-content: start;
+  align-items: center;
+  background: #F1E9D4;
+  margin: 0;
+  box-sizing: border-box;
+  font-family: 'SourceHanSansCN-Regular';
+  max-width: 500px;
+  position: relative;
+  overflow: auto;
+
+
+  .date-select-box {
+    width: 35%;
+    height: 100%;
+    overflow: auto;
+
+
+    .date-select-item {
+      width: 100%;
+      height: 80px;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      color: #88866F;
+      margin-bottom: 3px;
+      background: #F7F3E8;
+    }
+
+    .active {
+      background: #E4DCC5;
+      color: #333333;
+    }
+
+    .disAble {
+      background: #E4DCC5;
+      color: rgba(51, 51, 51, 0.2);
+    }
+  }
+
+  .time-select-box {
+    width: 65%;
+    height: 100%;
+    overflow: auto;
+    padding: 15px;
+    box-sizing: border-box;
+
+    .time-select-item {
+      width: 100%;
+      height: 50px;
+      background: #F7F3E8;
+      border-radius: 5px;
+      color: #88866F;
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      padding: 10px 15px;
+      box-sizing: border-box;
+      margin-bottom: 20px;
+    }
+
+    .active {
+      background: #E4DCC5;
+      color: #333333;
+    }
+
+    .notice-box{
+      width: 100%;
+      white-space: pre-wrap;
+      color: #88866fad;
+    }
+  }
+}
+
+.online-box {
+  width: 80%;
+  max-width: 400px;
+  height: 60px;
+  border-radius: 50px;
+  background: url(@/assets/images/onlineBg.png);
+  background-size: 100% 100%;
+  color: #F1E9D4;
+  position: fixed;
+  left: 50%;
+  transform: translateX(-50%);
+  bottom: 3vh;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  letter-spacing: 2px;
+  font-weight: bold;
+  z-index: 2;
+}
+</style>

+ 191 - 0
src/views/ExhibitionService/active-detail.vue

@@ -0,0 +1,191 @@
+<script setup lang='ts'>
+import { ActiveBookingApi } from '@/api/api/activeBooking';
+import { showToast } from 'vant';
+import {baseIMGUrl} from '@/api/request.ts'
+
+
+const route = useRoute()
+const router = useRouter()
+
+export type activeDetailType = {
+  createTime: string,
+  creatorId: number,
+  creatorName: string,
+  dirCode: string,
+  fileIds: string,
+  id: number,
+  isNeed: number,
+  name: string,
+  publishDate: string,
+  rtf: string,
+  thumb: string,
+  type: string,
+  updateTime: string
+}
+
+const activeDetail = ref({} as activeDetailType)
+
+const videos = ref([] as any)
+
+const getDetailById = async () => {
+  const res: any = await ActiveBookingApi.getDetailById(Number(route.params.id))
+  if (res.code == 0) {
+    activeDetail.value = res.data.entity
+
+    videos.value = res.data.file.filter((item: any) => {
+      return item.filePath.includes('mp4')
+    })
+  } else {
+    showToast(res.msg)
+  }
+}
+
+const goBooking = () => {
+  router.push({
+    name: 'activeBookInfo',
+    params: {
+      id: activeDetail.value.id,
+      title: activeDetail.value.name
+    }
+  })
+}
+
+
+
+const formatrtf = computed(() => {
+  return activeDetail.value.rtf.replace(
+    /<img /g,
+    '<img style="width: 100%;" '
+  ).replace(
+    /<p\b[^>]*>/g,
+    '<p style="width: 100%; word-wrap: break-word;">'
+  );
+})
+
+
+onBeforeMount(() => {
+  getDetailById()
+})
+</script>
+
+<template>
+  <div class='detail-box'>
+    <img class="themb-box" :src=" baseIMGUrl + activeDetail.thumb" alt="">
+    <div class="content-box">
+      <div class="title-box">{{ activeDetail.name }}</div>
+      <div class="time-box">发布时间:{{ activeDetail.publishDate }}</div>
+      <div class="rtf-box" v-html="formatrtf"></div>
+      <div class="video-box">
+        <video :src="baseIMGUrl + item.filePath" controls v-for="(item, index) in videos" :key="index"></video>
+      </div>
+    </div>
+    <div class="booking-btn" v-if="activeDetail.isNeed" @click="goBooking()">
+      发起预约
+    </div>
+    <img class="back-icon" @click="() => { router.back() }" src="@/assets/images/back.png" alt="">
+  </div>
+</template>
+
+<style lang='less' scoped>
+.detail-box {
+  width: 100%;
+  background: #F7F3E8;
+  min-height: 100%;
+  overflow: auto;
+
+
+  .themb-box {
+    width: 100%;
+    height: 30vh;
+    margin-bottom: 10px;
+
+  }
+
+  .content-box {
+    width: 100%;
+    height: calc(70vh + 10px);
+    background: #F7F3E8;
+    border-radius: 20px 20px 0 0;
+    margin-top: -30px;
+    position: relative;
+    z-index: 2;
+    padding: 8% 15px 20% 15px;
+    box-sizing: border-box;
+    overflow: auto;
+
+    .title-box {
+      font-size: 1.4em;
+      font-weight: bold;
+      line-height: 1.5em;
+      color: #333333;
+      margin-bottom: 10px;
+      padding: 0 10px;
+      font-family: 'SourceHanSansCN-Medium';
+      margin-bottom: 5px;
+
+    }
+
+    .time-box {
+      color: #9D4F0B;
+      font-size: 0.9em;
+      margin-bottom: 15px;
+      padding: 0 10px;
+      font-family: 'SourceHanSansCN-Regular';
+    }
+
+    .abstract-box {
+      color: rgba(128, 128, 128, 0.664);
+      font-size: 1em;
+      margin-bottom: 10px;
+      padding: 0 10px;
+
+    }
+
+    .rtf-box {
+      // white-space: pre;
+      margin-bottom: 10px;
+      padding: 0 10px;
+      color: #88866F;
+    }
+  }
+
+
+
+  .video-box {
+    width: 100%;
+
+    video {
+      width: 100%;
+      margin-bottom: 10px;
+    }
+
+  }
+
+  .booking-btn {
+    width: 80%;
+    max-width: 400px;
+    height: 60px;
+    border-radius: 50px;
+    background: url(@/assets/images/onlineBg.png);
+    background-size: 100% 100%;
+    color: #F1E9D4;
+    position: fixed;
+    left: 50%;
+    transform: translateX(-50%);
+    bottom: 3vh;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    letter-spacing: 2px;
+    font-weight: bold;
+    z-index: 2;
+  }
+
+  .back-icon {
+    width: 40px;
+    position: fixed;
+    right: 10px;
+    bottom: 10vh;
+  }
+}
+</style>

+ 226 - 0
src/views/ExhibitionService/active-info.vue

@@ -0,0 +1,226 @@
+<script setup lang='ts'>
+import { VolunteerApi } from '@/api/api/volunteerHome';
+import { showToast } from 'vant';
+
+const route = useRoute()
+const router = useRouter()
+
+
+export type infoDetailType = {
+  name: string,
+  phone: string,
+  identity: string,
+  description: string,
+  type: string
+}
+
+const infoDetail = ref({
+  name: '',
+  phone: '',
+  identity: '',
+  description: '',
+  type: 'activity',
+  title: route.params.title
+} as infoDetailType)
+
+const submit = async () => {
+  if (infoDetail.value.name.length == 0) {
+    showToast('预约姓名不能为空!')
+    return
+  }
+
+  if (infoDetail.value.phone.length == 0) {
+    showToast('预约联系电话不能为空!')
+    return
+  }
+
+  if (!/^((1[3-9]\d{9})|(09\d{8}))$/.test(infoDetail.value.phone)) {
+    showToast('请输入正确的联系电话!')
+    return
+  }
+
+
+  if (infoDetail.value.identity.length == 0) {
+    showToast('预约身份证号不能为空!')
+    return
+  }
+
+  if (!/^(^\d{6}(18|19|20)?\d{2}(0[1-9]|1[012])(0[1-9]|[12]\d|3[01])\d{3}(\d|X|x))$/.test(infoDetail.value.identity)) {
+    showToast('请输入正确的身份证号!')
+    return
+  }
+
+  const res: any = await VolunteerApi.saveApplyAPI(infoDetail.value)
+  if (res.code == 0) {
+    showToast('预约成功')
+    router.back()
+  } else {
+    showToast(res.msg)
+  }
+
+
+}
+
+</script>
+
+<template>
+  <div class='info-box'>
+    <div class="page-title">
+      <img src="@/assets/images/cicle.png" alt="">
+      <div class="title-content">活动预约</div>
+    </div>
+    <div class="title">{{ route.params.title }}</div>
+    <div class="form">
+
+      <div class="input-item">
+        <span style="color: red;">*</span>
+        <div class="label">姓名:</div><input type="text" v-model="infoDetail.name" placeholder="请输入内容">
+      </div>
+      <div class="input-item">
+        <span style="color: red;">*</span>
+        <div class="label">联系电话:</div><input type="text" v-model="infoDetail.phone" placeholder="请输入内容">
+      </div>
+      <div class="input-item">
+        <span style="color: red;">*</span>
+        <div class="label">身份证号:</div><input type="text" v-model="infoDetail.identity" placeholder="请输入内容">
+      </div>
+      <div class="input-item">
+        <!-- <span style="color: red;">*</span> -->
+        <div class="label">预约说明:</div><textarea cols="30" rows="10" v-model="infoDetail.description"
+          placeholder="请输入您想参加的活动或其他需求"></textarea>
+      </div>
+    </div>
+    <div class="commit-box">
+      <div class="tips"> 提交后,请等待工作人员与您取得联系</div>
+      <div class="commit-btn" @click="submit()">提交</div>
+    </div>
+  </div>
+</template>
+
+<style lang='less' scoped>
+* {
+  box-sizing: border-box;
+}
+
+.info-box {
+  width: 100%;
+  height: 100vh;
+  padding: 20px;
+  background: #F7F3E8;
+  overflow: auto;
+
+
+  .page-title {
+    font-size: 1.4em;
+    color: #333333;
+    font-weight: bold;
+    position: relative;
+    // line-height: 1.6em;
+    display: flex;
+    align-items: flex-end;
+    margin-bottom: 10px;
+
+    img {
+      position: absolute;
+      left: 0;
+      top: 0;
+      height: 1.6em;
+    }
+
+    .title-content {
+      // position: absolute;
+      position: relative;
+      left: 0;
+      top: 0;
+      height: 1.4em;
+      z-index: 2;
+      line-height: 1.6em;
+      color: #333333;
+      margin-top: 1%;
+      font-family: SourceHanSansCN-Bold;
+
+    }
+
+  }
+
+  .title {
+    color: #9D4F0B;
+    font-family: 'SourceHanSansCN-Medium';
+    margin-top: 8%;
+    margin-bottom: 5%;
+    font-size: 1.1em;
+  }
+
+  .form {
+    width: 100%;
+
+    .input-item {
+      width: 100%;
+      display: flex;
+      margin: 10px auto;
+      height: 50px;
+
+      span {
+        margin-right: 10px;
+      }
+
+      .label {
+        width: 30%;
+        text-align: left;
+        color: #333333;
+      }
+
+      input {
+        width: 80%;
+        height: 70%;
+        border: 1px solid #9D4F0B;
+        border-radius: 5px;
+        padding: 0 10px;
+        background: #FFFDF6;
+        color: #88866F;
+        font-size: 0.9em;
+      }
+
+      textarea {
+        width: 80%;
+        height: 30vh;
+        border: 1px solid #9D4F0B;
+        border-radius: 5px;
+        padding: 10px;
+        background: #FFFDF6;
+        color: #88866F;
+        font-size: 0.9em;
+      }
+    }
+
+  }
+
+  .commit-box {
+    position: fixed;
+    bottom: 0;
+    width: calc(100% - 40px);
+    max-width: calc(500px - 40px);
+
+    .tips {
+      color: #88866F;
+      width: 100%;
+      text-align: center;
+    }
+
+    .commit-btn {
+      width: 100%;
+      height: 60px;
+      background: url(@/assets/images/onlineBg.png);
+      background-size: 100% 100%;
+      border-radius: 50px;
+      color: white;
+      font-weight: bold;
+      font-size: 1.2em;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      margin: 30px 0 40px 0;
+    }
+  }
+}
+</style>

+ 240 - 0
src/views/ExhibitionService/activeBooking.vue

@@ -0,0 +1,240 @@
+<script setup lang='ts'>
+import { ActiveBookingApi } from '@/api/api/activeBooking';
+import { baseIMGUrl } from '@/api/request';
+import { showToast } from 'vant';
+
+const active = ref(0)
+
+const router = useRouter()
+
+const searchParames = ref({
+  pageNum: 0,
+  pageSize: 0,
+  searchKey: '',
+  dictId: null
+} as {
+  dictId: number | null,
+  pageNum: number,
+  pageSize: number,
+  searchKey: string
+})
+
+export type activeType = {
+  createTime: string,
+  creatorId: number,
+  creatorName: string,
+  dirCode: string,
+  fileIds: string,
+  id: number,
+  isNeed: number,
+  name: string,
+  publishDate: string,
+  rtf: string,
+  thumb: string,
+  type: string,
+  updateTime: string
+}
+
+const allData = ref([] as activeType[])
+
+const typeList = ref([] as any)
+
+
+const getTypeList = async () => {
+  // 获取珍藏类型
+  const resType: any = await ActiveBookingApi.getTypeList()
+  if (resType.code == 0) {
+    typeList.value = resType.data
+    typeList.value.unshift({
+      createTime: null,
+      creatorId: null,
+      creatorName: '',
+      display: null,
+      id: null,
+      name: '全部',
+      parentId: null,
+      rtf: '',
+      sort: null,
+      type: '',
+      typeKey: '',
+      updateTime: null
+    })
+    console.log(typeList.value)
+  } else {
+    showToast(resType.msg)
+  }
+
+  getList()
+}
+
+const getList = async () => {
+  searchParames.value.dictId = active.value == 0 ? null : typeList.value[active.value].id!
+  const res: any = await ActiveBookingApi.getActiveList(searchParames.value)
+  if (res.code == 0) {
+    allData.value = res.data.records
+  } else {
+    showToast(res.msg)
+  }
+}
+
+const goDetai = (id: number) => {
+  router.push({ name: 'activeDetail', params: { id } });
+}
+watch(active, () => {
+  getList()
+})
+
+onMounted(() => {
+  getTypeList()
+})
+
+</script>
+
+<template>
+  <div class='activeBooking'>
+
+    <div class="tabs">
+      <div class="tab-item" :class="{ active: index === active }" v-for="(item, index) in typeList" :key="index"
+        @click="active = index">{{ item.name }}</div>
+    </div>
+    <div class="tab-content" v-if="allData.length > 0">
+      <div class="activity-card" v-for="(item, index) in allData" :key="index" @click="goDetai(item.id)">
+        <img v-show="item.thumb != ''" class="thumb" :src="baseIMGUrl + item.thumb" alt="">
+        <div class="bottom-box">
+          <div class="name">{{ item.name }}</div>
+          <div class="income">了解详情</div>
+        </div>
+      </div>
+    </div>
+    <div class="no-data" v-else>
+      <img src="@/assets/images/no-data.png" alt="">
+      <div class="tips">暂时没有数据 </div>
+    </div>
+  </div>
+</template>
+
+<style lang='less' scoped>
+* {
+  box-sizing: border-box;
+}
+
+.activeBooking {
+  width: 100%;
+  background: #F7F3E8;
+  min-height: 100%;
+  overflow: auto;
+
+
+  .tabs {
+    // height: 40px;
+    white-space: nowrap;
+    overflow-x: auto;
+    padding: 15px 10px 10px 0px;
+    font-family: 'SourceHanSansCN-Regular';
+
+    &::-webkit-scrollbar {
+      display: none;
+      /* 或 width: 0; */
+    }
+
+    .tab-item {
+      margin-left: 10px;
+      display: inline-block;
+      width: 85px;
+      height: 30px;
+      float: none;
+      font-size: 0.9rem;
+      line-height: 30px;
+      text-align: center;
+      background-color: #F1E9D4;
+      border-radius: 5px;
+      // box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.3);
+      color: rgba(0, 0, 0, 0.8);
+      color: #88866F;
+    }
+
+    .active {
+      background: #9D4F0B;
+      color: #F7F3E8;
+    }
+  }
+
+
+  .tab-content {
+    width: 100%;
+    padding: 20px;
+    max-height: calc(100vh - 8vh);
+    overflow: auto;
+    background: #F7F3E8;
+
+
+
+    .activity-card {
+      width: 100%;
+      background: #FFFDF6;
+      border-radius: 15px;
+      margin-bottom: 30px;
+      box-shadow: 0px 0px 20px 0px rgba(222, 216, 199, 0.6);
+
+      .thumb {
+        width: 100%;
+        height: 25vh;
+        border-radius: 15px 15px 0 0;
+      }
+
+      .bottom-box {
+        padding: 10px;
+        display: flex;
+        flex-direction: column;
+        align-items: end;
+
+        .name {
+          font-size: 1.2em;
+          font-weight: bold;
+          line-height: 1.3em;
+          margin-top: 10px;
+          color: #333333;
+          width: 100%;
+        }
+
+        .income {
+          width: 30%;
+          display: flex;
+          justify-content: center;
+          align-items: center;
+          color: #9D4F0B;
+          line-height: 30px;
+          border: 1px solid #9D4F0B;
+          border-radius: 50px;
+        }
+
+      }
+
+
+
+    }
+  }
+
+  .no-data {
+    width: 100%;
+    height: calc(100% - 7vh);
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    margin-top: 10%;
+
+
+    img {
+      width: 40%;
+    }
+
+    .tips {
+      color: #88866F;
+      margin-top: 10px;
+      font-family: 'SourceHanSansCN-Regular';
+    }
+
+  }
+}
+</style>

+ 234 - 0
src/views/ExhibitionService/bookInputInfo.vue

@@ -0,0 +1,234 @@
+<script setup lang='ts'>
+import { sceneBookingApi } from '@/api/api/sceneBooking';
+import { useStore } from '@/stores';
+import { showToast } from 'vant';
+import router from '@/router';
+
+const store = useStore()
+const personList = ref([
+  {
+    bookDate: store.selectDate,
+    bookId:store.selectId,
+    name: '',
+    num: '',
+    phone: '',
+    time: store.selectTime,
+    type: '身份证'
+  }
+])
+
+onMounted(()=>{
+  if(!store.selectId) router.replace('/booking/selectTime')
+})
+
+const deleteFu = (index: number) => {
+  personList.value.splice(index, 1)
+}
+
+const addPerson = () => {
+  personList.value.push({
+    bookDate: store.selectDate,
+    bookId:'',
+    name: '',
+    num: '',
+    phone: '',
+    time: store.selectTime,
+    type: '身份证'
+  })
+}
+
+const submit = async() => {
+
+  
+  let flag1 =false
+  let flag2 =false
+  let flag3 =false
+
+  personList.value.forEach((v:any)=>{
+    
+    if(v.bookDate) v.bookDate=v.bookDate.split(' ')[0]
+
+    const {name,phone,num}=v
+
+    if(name.trim()==='') return  flag1=true 
+
+    if(!/^1[3456789]\d{9}$/.test(phone)) return flag2=true
+
+    const numTest =/^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/
+    if(!numTest.test(num)) return flag3=true
+    
+  },[])
+
+  if(flag1) return showToast('参观人姓名不能为空')
+  if(flag2) return showToast('参观人电话格式错误')
+  if(flag3) return showToast('身份证或社保卡格式错误')
+  
+ 
+
+  const res:any =await sceneBookingApi.submitBookingAPI(personList.value)
+  if(res.code === 0){
+    showToast('预约成功')
+    router.back()
+  }else {
+    showToast('预约失败')
+  }
+}
+
+</script>
+
+<template>
+  <div class='input-box'>
+    <div class="input-card" v-for="(item, index) in personList" :key="index">
+      <div class="top">
+        <div>{{ '参观日期:' + item.bookDate.slice(0, 10) + ' ' + item.time }}</div>
+        <img @click="deleteFu(index)" src="@/assets/images/delect.png" alt="">
+      </div>
+      <div class="content">
+        <div>
+          <div class="label">参观人姓名:</div><input type="text" v-model="item.name" placeholder="请输入内容,不超过6个字">
+        </div>
+        <div>
+          <div class="label">参观人电话:</div><input type="text" v-model="item.phone" placeholder="请输入11个数字">
+        </div>
+        <div>
+          <div class="label">证件号:</div><van-radio-group v-model="item.type" direction="horizontal" shape="dot">
+            <van-radio name="身份证" checked-color="#9D4F0B">身份证</van-radio>
+            <van-radio name="社保卡" checked-color="#9D4F0B">社保卡</van-radio>
+          </van-radio-group>
+        </div>
+        <div>
+          <div class="label"></div><input type="text" v-model="item.num" placeholder="请输入18位证件编码">
+        </div>
+
+
+      </div>
+    </div>
+    <div class="btns">
+      <div class="add" @click="addPerson()">添加参观人</div>
+      <div class="submit" @click="submit()">提交</div>
+    </div>
+  </div>
+
+</template>
+
+<style lang='less' scoped>
+* {
+  box-sizing: border-box;
+}
+
+.input-box {
+  width: 100%;
+  min-height: 100%;
+  background: #F7F3E8;
+  padding: 20px 10px;
+  position: relative;
+  max-width: 500px;
+  overflow: auto;
+
+
+  .input-card {
+    width: 100%;
+    padding: 20px 10px;
+    background: #9D4F0B;
+    border-radius: 15px;
+    font-family: 'SourceHanSansCN-Regular';
+    margin-bottom: 20px;
+
+    .top {
+      display: flex;
+      width: 100%;
+      justify-content: space-between;
+      color: #F7F3E8;
+      margin-bottom: 15px;
+
+      img {
+        width: 25px;
+      }
+    }
+
+    .content {
+      padding: 20px 15px;
+      background: url(@/assets/images/booking-card.png);
+      background-size: 100% 100%;
+
+      div {
+        display: flex;
+        height: 50px;
+        align-items: center;
+        color: #88866F;
+
+        .label {
+          width: 30%;
+          text-align: center;
+          color: #333333;
+          font-family: 0.9em;
+        }
+
+        input {
+          width: 70%;
+          background: #FFFDF6;
+          border: 1px solid #9D4F0B;
+          color: #88866F;
+          border-radius: 5px;
+          height: 75%;
+          padding: 0 5px;
+          font-size: 0.9em;
+        }
+      }
+
+    }
+
+  }
+
+
+  .btns {
+    width: 90%;
+    position: absolute;
+    bottom: 20px;
+    display: flex;
+    justify-content: space-between;
+    left: 50%;
+    transform: translateX(-50%);
+
+    div {
+      display: flex;
+      justify-content: center;
+      align-items: center;
+    }
+
+    .add {
+      width: 35%;
+      height: 5vh;
+      background: #FFFDF6;
+      border: 1px solid #9D4F0B;
+      border-radius: 50px;
+      color: #88866F;
+    }
+
+    .submit {
+      width: 60%;
+      height: 5vh;
+      background: url(@/assets/images/onlineBg.png);
+      background-size: 100% 100%;
+      // border: 1px solid #9D4F0B;
+      border-radius: 50px;
+      color: #F7F3E8;
+    }
+  }
+
+}
+
+@media only screen and (max-width: 400px) {
+  .input-box .input-card .content div .label{
+    font-size: 14px;
+  }
+}
+@media only screen and (max-width: 350px) {
+  .input-box .input-card .content div .label{
+    font-size: 12px;
+  }
+  /deep/.van-radio__label{
+    font-size: 12px;
+  }
+}
+</style>

+ 125 - 0
src/views/ExhibitionService/booked-detail.vue

@@ -0,0 +1,125 @@
+<script setup lang='ts'>
+import { sceneBookingApi } from '@/api/api/sceneBooking';
+
+
+const bookedDetail = ref({} as any)
+
+const route = useRoute()
+
+const getDetailById = async() => {
+  const res:any = await sceneBookingApi.getDetailByIdAPI(route.params.id)
+
+  if(res.code === 0){
+    bookedDetail.value = res.data
+  }
+}
+
+onMounted(() => {
+  getDetailById()
+
+})
+</script>
+
+<template>
+  <div class='detail-box'>
+    <div class="title">
+      <img src="@/assets/images/cicle.png" alt="">
+      <div class="title-content">我的预约</div>
+    </div>
+    <div class="info-box">
+      <div>参观日期:{{ bookedDetail.bookDate + ' ' + bookedDetail.time }}</div>
+      <div>当前状态:{{ bookedDetail.status === 0 ? '未验证' : '已验证' }}</div>
+    </div>
+    <div class="info-box-card" v-for="(item, index) in bookedDetail.rtf" :key="index">
+      <div>参观人姓名:{{ item.name }}</div>
+      <div>参观人电话:{{ item.phone }}</div>
+      <div>{{ item.type + '号:' + item.num }}</div>
+    </div>
+    <div class="code-box" >
+      <img src="@/assets/images/code.png" alt="">
+      <div>验证码:56717</div>
+    </div>
+  </div>
+</template>
+
+<style lang='less' scoped>
+.detail-box {
+  width: 100%;
+  height: 100%;
+  background: #F7F3E8;
+  overflow: auto;
+  padding: 40px 20px;
+  font-family: SourceHanSansCN-Regular;
+
+  .title {
+    font-size: 1.4em;
+    color: #333333;
+    font-weight: bold;
+    position: relative;
+    // line-height: 1.6em;
+    display: flex;
+    align-items: flex-end;
+    margin-bottom: 10px;
+
+    img {
+      position: absolute;
+      left: 0;
+      top: 0;
+      height: 1.6em;
+    }
+
+    .title-content {
+      // position: absolute;
+      position: relative;
+      left: 0;
+      top: 0;
+      height: 1.4em;
+      z-index: 2;
+      line-height: 1.6em;
+      color: #333333;
+      margin-top: 1%;
+      font-family: SourceHanSansCN-Bold;
+
+    }
+
+  }
+
+  .info-box {
+    margin-top: 20px;
+    color: #333333;
+    line-height: 2em;
+    font-family: SourceHanSansCN-Regular;
+  }
+
+  .info-box-card{
+    width: 100%;
+    border-radius: 10px;
+    background: url(@/assets/images/booked-card.png);
+    background-size: 100% 100%;
+    margin-top: 30px;
+    color: #F7F3E8;
+    line-height: 3em;
+    font-size: 1.1em;
+    padding: 5% 8%;
+  }
+
+  .code-box{
+    width: 100%;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    margin-top: 10%;
+    img{
+      width: 50%;
+      margin-bottom: 10px;
+    }
+    div {
+      width: 50%;
+      text-align: center;
+      color: #333333;
+    }
+  }
+
+}
+</style>

+ 250 - 0
src/views/ExhibitionService/bookedList.vue

@@ -0,0 +1,250 @@
+<script setup lang='ts'>
+import { sceneBookingApi } from '@/api/api/sceneBooking';
+import { useStore } from '@/stores';
+import { checkLoginStatus } from '@/utils/weixin'
+
+// import { json } from 'stream/consumers';
+// import BookingTime from './BookingTime.vue'
+
+const bookedList = ref([] as any)
+
+const router = useRouter()
+const store = useStore()
+// 立即预约-时段选择
+const goSelectTime = () => {
+  router.push({
+    name: 'selectTime'
+  })
+}
+
+const goBookedDetail = (id: number) => {
+  router.push({
+    name: 'bookedDetail',
+    params: {
+      id
+    }
+  })
+}
+
+
+// 用户授权成功
+const loginOnNeed = async() => {
+  const query = new URLSearchParams(location.search)
+  const code = query.get('code')
+  if (code) {
+    let res: any = await sceneBookingApi.getCodeAPI(code);
+    if (res.code === 0) {
+      localStorage.setItem("token", res.data.token);
+      localStorage.setItem(
+        "userInfo",
+        JSON.stringify({ ...res.data.wxUser, time: Date.now() })
+      );
+    } else {
+      console.error('登录结果异常!');
+    }
+    location.href = window.location.href
+  }
+}
+
+const searchParames = ref({
+  pageNum: 0,
+  pageSize: 0,
+  searchKey: ''
+} as {
+  pageNum: number,
+  pageSize: number,
+  searchKey: string
+})
+
+const getList = async() => {
+  const res:any = await sceneBookingApi.getBookingListAPi(searchParames)
+  if(res.code === 0) {
+    bookedList.value = res.data.records
+  }
+}
+
+onMounted(() => {
+  // 清除之前的预约选择记录
+  store.selectDate = ''
+  store.selectTime = ''
+
+  loginOnNeed()
+
+  const loginStatus = checkLoginStatus()
+  if (loginStatus) {
+    // 处理我的预约数据
+    getList()
+    
+  } else {
+    // 发起我的授权
+    // getUserCode(`${baseIMGUrl.replace(/^api/, '')}booking`)
+  }
+
+
+  // bookedList.value = [
+  //   {
+  //     bookDate: '2024-04-16',
+  //     bookId: '1',
+  //     createTime: '2024-04-16 16:46:00',
+  //     creatorId: null,
+  //     creatorName: '',
+  //     id: 1,
+  //     name: '小明',
+  //     pcs: 2,
+  //     phone: '',
+  //     rtf: JSON.parse('[{"bookDate":"2024-02-01","name":"小明","num":"1111111","time":"9:00-1:00","type":"身份证"},{"bookDate":"2024-02-01","name":"小明2","num":"222222","time":"9:00-1:00","type":"社保卡"}]'),
+  //     status: 0,
+  //     time: '9:00-1:00',
+  //     updateTime: '2024-04-16 16:46:00'
+  //   },
+  //   {
+  //     bookDate: '2024-04-16',
+  //     bookId: '1',
+  //     createTime: '2024-04-16 16:46:00',
+  //     creatorId: null,
+  //     creatorName: '',
+  //     id: 1,
+  //     name: '小明',
+  //     pcs: 2,
+  //     phone: '12333',
+  //     rtf: JSON.parse('[{"bookDate":"2024-02-01","name":"小明","num":"1111111","time":"9:00-1:00","type":"身份证"},{"bookDate":"2024-02-01","name":"小明2","num":"222222","time":"9:00-1:00","type":"社保卡"}]'),
+  //     status: 1,
+  //     time: '9:00-1:00',
+  //     updateTime: '2024-04-16 16:46:00'
+  //   },
+  // ]
+
+  // userAuthorization()
+
+})
+</script>
+
+<template>
+  <div class='booked-box'>
+    <div class="title">
+      <img src="@/assets/images/cicle.png" alt="">
+      <div class="title-content">我的预约</div>
+    </div>
+    <div class="no-data" v-if="bookedList.length == 0">暂无预约</div>
+    <div v-else>
+      <div class="booking-card" v-for="(item, index) in bookedList" :key="index" @click="goBookedDetail(item.id)">
+        <div>参观日期:{{ `${item.bookDate} ${item.time}` }}</div>
+        <div>当前状态:<span :style="{ color: item.status == 0 ? '' : '#E3C956' }">{{ item.status == 0 ? '未验证' : '已验证'
+            }}</span>
+        </div>
+        <div class="info-box">
+          <div>参观人姓名:{{ item.name }}</div>
+          <div>参观人电话:{{ bookedList[index].rtf[0].phone }}</div>
+          <div>{{ `${bookedList[index].rtf[0].type}号:${bookedList[index].rtf[0].num}` }}</div>
+        </div>
+      </div>
+    </div>
+    <div class="online-box" @click="goSelectTime">发起预约</div>
+    <!-- <BookingTime v-show="showBookingTime" /> -->
+  </div>
+</template>
+
+<style lang='less' scoped>
+* {
+  box-sizing: border-box;
+}
+
+.booked-box {
+  width: 100%;
+  min-height: 100%;
+  background: #F7F3E8;
+  max-width: 500px;
+  padding: 40px 20px;
+  position: relative;
+  overflow: auto;
+
+
+
+  .title {
+    font-size: 1.4em;
+    color: #333333;
+    font-weight: bold;
+    position: relative;
+    // line-height: 1.6em;
+    display: flex;
+    align-items: flex-end;
+    margin-bottom: 10px;
+
+    img {
+      position: absolute;
+      left: 0;
+      top: 0;
+      height: 1.6em;
+    }
+
+    .title-content {
+      // position: absolute;
+      position: relative;
+      left: 0;
+      top: 0;
+      height: 1.4em;
+      z-index: 2;
+      line-height: 1.6em;
+      color: #333333;
+      margin-top: 1%;
+      font-family: SourceHanSansCN-Bold;
+
+    }
+
+  }
+
+  .no-data {
+    color: #88866F;
+    font-family: SourceHanSansCN-Regular;
+  }
+
+  .booking-card {
+    width: 100%;
+    border-radius: 15px;
+    padding: 15px;
+    color: #F7F3E8;
+    font-family: SourceHanSansCN-Regular;
+    line-height: 1.5em;
+    background: #9D4F0B;
+    margin-bottom: 15px;
+
+    div {
+      height: 40px;
+      font-family: SourceHanSansCN-Regular;
+    }
+
+    .info-box {
+      width: 100%;
+      height: auto;
+      padding: 20px 10px;
+      background: url(@/assets/images/booking-card.png);
+      background-size: 100% 100%;
+      color: #333333;
+      // margin-top: 10px;
+
+
+    }
+  }
+
+  .online-box {
+    width: 80%;
+    max-width: 400px;
+    height: 60px;
+    border-radius: 50px;
+    background: url(@/assets/images/onlineBg.png);
+    background-size: 100% 100%;
+    color: #F1E9D4;
+    position: fixed;
+    left: 50%;
+    transform: translateX(-50%);
+    bottom: 3vh;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    letter-spacing: 2px;
+    font-weight: bold;
+    z-index: 2;
+  }
+
+}
+</style>

+ 203 - 0
src/views/SmartTour/exhibition-detail.vue

@@ -0,0 +1,203 @@
+<script setup lang='ts'>
+import { ExhibitionApi } from '@/api/api/exhibition';
+import { showToast } from 'vant';
+import { baseIMGUrl } from '@/api/request.ts'
+
+
+
+const route = useRoute()
+
+export type ExhibitionDetailType = {
+  createTime: string,
+  creatorId: number,
+  creatorName: string,
+  dateEnd: string,
+  dateStart: string,
+  description: string,
+  id: number,
+  link: string,
+  location: string,
+  name: string,
+  publishDate: string,
+  rtf: string,
+  thumb:
+  string,
+  type: string,
+  updateTime: string,
+  videos: any[]
+}
+
+const exhibitionDetail = ref({} as ExhibitionDetailType)
+
+const isShowScene = ref(false)
+
+const getDetailById = async () => {
+  const res: any = await ExhibitionApi.getDetailById(Number(route.params.id))
+  if (res.code == 0) {
+    exhibitionDetail.value = res.data.entity
+    exhibitionDetail.value.videos = res.data.file.filter((item: any) => {
+      return item.type == 'video'
+    })
+  } else {
+    showToast(res.msg)
+  }
+}
+
+
+const formatrtf = computed(() => {
+  return exhibitionDetail.value.rtf.replace(
+    /<img /g,
+    '<img style="width: 100%;" '
+  ).replace(
+    /<p\b[^>]*>/g,
+    '<p style="width: 100%; word-wrap: break-word;">'
+  );
+})
+
+onBeforeMount(() => {
+  getDetailById()
+})
+
+</script>
+
+<template>
+  <div class='detail-box'>
+    <img class="thumb-box" :src="baseIMGUrl + exhibitionDetail.thumb" alt="">
+    <div class="content-box">
+      <div class="name-box">{{ exhibitionDetail.name }}</div>
+      <div class="time-box">
+        <div class="location-box"> {{ exhibitionDetail.location }}</div>
+        <div>{{ exhibitionDetail.dateStart }} - {{ exhibitionDetail.dateEnd }}</div>
+      </div>
+      <div class="rtf-box" v-html="formatrtf"></div>
+      <video :src="baseIMGUrl + item.filePath" v-for="(item, index) in exhibitionDetail.videos" :key="index"
+        controls></video>
+    </div>
+
+    <div class="online-box" @click="() => { isShowScene = true }" v-show="exhibitionDetail.link != ''">体验线上展厅</div>
+    <div v-if="isShowScene" class="iframe-box">
+      <img src="@/assets/images/sceneBack.png" @click="isShowScene = false" alt="">
+      <iframe :src="exhibitionDetail.link" frameborder="0"></iframe>
+    </div>
+  </div>
+</template>
+
+<style lang='less' scoped>
+.detail-box {
+  width: 100%;
+  height: 100vh;
+  max-width: 500px;
+  position: relative;
+  overflow: auto;
+
+  .thumb-box {
+    width: 100%;
+    margin-bottom: 10px;
+    height: 30vh;
+  }
+
+  .content-box {
+    width: 100%;
+    padding: 0 10px;
+    box-sizing: border-box;
+    height: calc(70vh + 25px);
+    overflow: auto;
+    border-radius: 20px 20px 0 0;
+    margin-top: -40px;
+    position: relative;
+    z-index: 2;
+    background: #F7F3E8;
+    padding: 25px 20px 25% 20px;
+    box-sizing: border-box;
+
+
+    .name-box {
+      width: 100%;
+      font-size: 1.2em;
+      font-weight: bold;
+      color: #333333;
+      font-family: 'SourceHanSansCN-Medium';
+      line-height: 1.5em;
+      margin-bottom: 10px;
+    }
+
+    .time-box {
+      width: 100%;
+      display: flex;
+      justify-content: space-between;
+      color: #9D4F0B;
+      margin: 10px auto;
+      font-size: 0.8em;
+      align-items: center;
+
+      .location-box {
+        max-width: 50vw;
+      }
+
+    }
+
+    .rtf-box {
+      color: #88866F;
+    }
+
+  }
+
+  video {
+    width: 100%;
+    margin-top: 10px;
+  }
+
+  .online-box {
+    width: 80%;
+    max-width: 400px;
+    height: 60px;
+    border-radius: 50px;
+    background: url(@/assets/images/onlineBg.png);
+    background-size: 100% 100%;
+    color: #F1E9D4;
+    position: fixed;
+    left: 50%;
+    transform: translateX(-50%);
+    bottom: 3vh;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    letter-spacing: 2px;
+    font-weight: bold;
+    z-index: 2;
+  }
+
+  .back-icon {
+    width: 40px;
+    position: fixed;
+    right: 10px;
+    bottom: 10vh;
+  }
+
+  .iframe-box {
+    width: 100%;
+    height: 100%;
+    position: absolute;
+    top: 0;
+    left: 0;
+    z-index: 10;
+
+    iframe {
+      width: 100%;
+      height: 100%;
+      position: absolute;
+      top: 0;
+      left: 0;
+    }
+
+    img {
+      width: 30px;
+      height: 30px;
+      position: absolute;
+      top: 3%;
+      left: 3%;
+      z-index: 11;
+    }
+  }
+}
+</style>

+ 250 - 0
src/views/SmartTour/exhibition.vue

@@ -0,0 +1,250 @@
+<!-- eslint-disable vue/multi-word-component-names -->
+<script setup lang='ts'>
+import { ExhibitionApi } from '@/api/api/exhibition';
+import { showToast } from 'vant';
+import SearchInput from '@/components/SearchInput/index.vue'
+import { baseIMGUrl } from '@/api/request';
+
+export type exhibitionType = {
+  createTime: string,
+  creatorId: number,
+  creatorName: string,
+  dateEnd: string,
+  dateStart: string,
+  description: string,
+  id: number,
+  link: string,
+  location: string,
+  name: string,
+  publishDate: string,
+  rtf: string,
+  thumb: string,
+  type: string,
+  updateTime: string
+}
+
+const router = useRouter()
+const route = useRoute()
+
+const searchParames = ref({
+  pageNum: 0,
+  pageSize: 0,
+  searchKey: '',
+  type: ''
+} as {
+  pageNum: number,
+  pageSize: number,
+  searchKey: string,
+  type: string | null
+})
+
+const onSearch = (eventData: any) => {
+  searchParames.value.searchKey = eventData
+  getList()
+}
+
+
+const activeIndex = ref(0)
+
+const data = ref([] as exhibitionType[])
+
+const goDetai = (id: number) => {
+  router.push({ name: 'exhibitionDetail', params: { id } });
+}
+
+const dateFormat = (date: string) => {
+  const dateNew = new Date(date)
+  const month = dateNew.getMonth() + 1
+  const day = dateNew.getDate()
+  const formattedWithLeadingZeroes = `${month.toString().padStart(2, '0')}.${day.toString().padStart(2, '0')}`;
+  const finalFormat = formattedWithLeadingZeroes.replace(/^0(\d)\./, '$1.');
+  return finalFormat
+}
+
+// 判断临时展厅是否过时
+const isOverTime = (date: string) => {
+  const dateEnd = new Date(date)
+  const dateNow = new Date()
+
+  if (dateNow > dateEnd) {
+    return true
+  }
+
+  return false
+}
+
+// 写死
+const typeList = ref([
+  '全部',
+  '常设展览',
+  '临时展览'
+] as any)
+
+const getList = async () => {
+  searchParames.value.type = activeIndex.value == 0 ? '' : typeList.value[activeIndex.value]
+  const res: any = await ExhibitionApi.getExhibitionList(searchParames.value)
+  if (res.code == 0) {
+    data.value = res.data.records
+    if (searchParames.value.type == '临时展览') {
+      data.value.sort((a: any, b: any) => {
+        const dateA = new Date(a.dateEnd);
+        const dateB = new Date(b.dateEnd);
+        // @ts-ignore
+        return dateB - dateA
+      })
+    }
+  } else {
+    showToast(res.msg)
+  }
+}
+
+onBeforeMount(() => {
+
+  getList()
+
+  // 0 全部 1 常设展览 2 临时展览
+  activeIndex.value = route.query.type ? Number(route.query.type) : 0
+
+})
+</script>
+
+<template>
+  <div class=''>
+    <div class="treasure-box">
+      <SearchInput @onSearch="onSearch" />
+      <div class="content-box">
+        <van-tabs v-model:active="activeIndex" color="#9D4F0B" background="#F7F3E8" title-active-color="#9D4F0B"
+          title-inactive-color="#88866F" @change="getList()">
+          <van-tab v-for="(item, index) in typeList" :key="index" :title="item">
+            <!-- <div>{{ activeIndex }}</div> -->
+            <div class="tab-content" v-if="data.length > 0">
+              <div class="activity-card" v-for="(item, index) in data" :key="index" @click="goDetai(item.id)">
+                <img v-show="item.thumb != ''" class="thumb" :src=" baseIMGUrl + item.thumb" alt="">
+                <div class="name">{{ item.name }}</div>
+                <div class="abstract">{{ item.description }}</div>
+                <div class="bottom-box">
+                  <div class="location">{{ item.location }}</div>
+                  <div class="time" v-show="item.type == '临时展览'">{{ isOverTime(`${item.dateEnd} 23:59:59`) ? '已结束' :
+                    dateFormat(item.dateStart) +
+                    '-'
+                    + dateFormat(item.dateEnd) }}
+                  </div>
+                </div>
+              </div>
+            </div>
+            <div v-else class="no-data">
+              <img src="@/assets/images/no-data.png" alt="">
+              <div class="tips">暂时没有数据 ,请试一下其他关键字</div>
+            </div>
+          </van-tab>
+        </van-tabs>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style lang='less' scoped>
+.treasure-box {
+  width: 100%;
+  height: 100vh;
+  background: #F7F3E8;
+  overflow: auto;
+
+
+
+  .search-box {
+    width: 100%;
+  }
+
+  .content-box {
+    width: 100%;
+    overflow: auto;
+    padding: 0px 10px;
+    box-sizing: border-box;
+    background: #F7F3E8;
+
+
+    .tab-content {
+      width: 100%;
+
+      .activity-card {
+        width: 100%;
+        padding: 10px;
+        box-sizing: border-box;
+        margin-bottom: 20px;
+
+        img {
+          width: 100%;
+          height: 25vh;
+          border-radius: 10px;
+        }
+
+
+
+        .name {
+          font-size: 1.2em;
+          line-height: 1.5em;
+          font-weight: bold;
+          margin: 10px 0;
+          font-family: 'SourceHanSansCN-Medium';
+          color: #333333;
+        }
+
+        .abstract {
+          color: #88866F;
+          font-size: 0.9em;
+          line-height: 1.2em;
+          margin-bottom: 10px;
+        }
+
+        .bottom-box {
+          display: flex;
+          justify-content: space-between;
+
+          .location {
+            font-family: 'SourceHanSansCN-Regular';
+            color: #9D4F0B;
+            font-size: 0.9em;
+            max-width: 80%;
+          }
+
+          .time {
+            font-family: 'SourceHanSansCN-Regular';
+            color: #9D4F0B;
+            font-size: 0.9em;
+          }
+
+        }
+
+        .detail {
+          font-size: 0.8em;
+          color: gray;
+          margin-top: 10px;
+        }
+      }
+    }
+
+    .no-data {
+      width: 100%;
+      height: calc(100% - 7vh);
+      display: flex;
+      flex-direction: column;
+      justify-content: center;
+      align-items: center;
+      margin-top: 10%;
+
+
+      img {
+        width: 40%;
+      }
+
+      .tips {
+        color: #88866F;
+        margin-top: 10px;
+        font-family: 'SourceHanSansCN-Regular';
+      }
+
+    }
+  }
+}
+</style>

+ 327 - 0
src/views/SmartTour/treasure-detail.vue

@@ -0,0 +1,327 @@
+<script setup lang='ts'>
+import playIcon from '@/assets/images/play.png';
+import pauseIcon from '@/assets/images/pause.png';
+import { showToast } from 'vant';
+import { TreasureApi } from '@/api/api/treasure';
+import { baseIMGUrl } from '@/api/request.ts'
+
+
+
+// import { DynamicApi } from "@/api/api/dynamic/index";
+// import { showToast } from "vant";
+
+const route = useRoute()
+
+
+export type TreasureDetailType = {
+  age: string,
+  createTime: string,
+  creatorId: number,
+  creatorName: string,
+  description: string,
+  dirCode: string,
+  fileIds: string,
+  fileTypes: string,
+  id: number,
+  name: string,
+  publishDate: string,
+  size: string,
+  texture: string,
+  thumb: string,
+  type: string,
+  updateTime: string
+  files: {
+    images: string[],
+    moduleUrl: string,
+    audio: string,
+    // 可能是单个可能是多个
+    videos: string[],
+  }
+}
+
+const dynamicDetail = ref({} as TreasureDetailType)
+
+const audioPlaying = ref(null as any)
+const audioBgUrl = ref(null as any)
+
+
+// 当前显示什么状态的多媒体  module videos images
+const curState = ref(null as any)
+
+// 当前显示的多媒体的列表内容
+const curMediaList = ref(null as any)
+
+// 当前列表的索引
+const curMediaIndex = ref(0)
+
+const initModule = () => {
+  curState.value = 'module'
+  curMediaList.value = [dynamicDetail.value.files.moduleUrl]
+  curMediaIndex.value = 0
+}
+
+const initImages = () => {
+  curState.value = 'images'
+  curMediaList.value = dynamicDetail.value.files.images
+  curMediaIndex.value = 0
+}
+
+const initVideos = () => {
+  curState.value = 'videos'
+  curMediaList.value = dynamicDetail.value.files.videos
+  curMediaIndex.value = 0
+}
+
+const audioChange = () => {
+  audioPlaying.value = !audioPlaying.value
+  const audioDom: any = document.getElementById('audioBg')
+  if (audioPlaying.value) {
+    audioDom.play()
+  } else {
+    audioDom.pause()
+  }
+}
+
+const initShowState = () => {
+  if (dynamicDetail.value.files.moduleUrl) {
+    initModule()
+  } else if (dynamicDetail.value.files.images.length > 0) {
+    initImages()
+  } else if (dynamicDetail.value.files.videos.length > 0) {
+    initVideos()
+  }
+  audioBgUrl.value = dynamicDetail.value.files.audio
+
+}
+
+const getDetailById = async () => {
+  const res: any = await TreasureApi.getDetailById(Number(route.params.id))
+  if (res.code == 0) {
+    dynamicDetail.value = res.data.entity
+
+    dynamicDetail.value.files = {
+      images: [],
+      moduleUrl: '',
+      audio: '',
+      // 可能是单个可能是多个
+      videos: [],
+    }
+
+    // 视频集合
+    dynamicDetail.value.files.videos = res.data.file.filter((item: any) => {
+      return item.filePath.includes('.mp4')
+    })
+    // 图片集合
+    dynamicDetail.value.files.images = res.data.file.filter((item: any) => {
+      return item.filePath.includes('.png') || item.filePath.includes('.jpg') || item.filePath.includes('.jpeg')
+    })
+
+    //  音频链接
+    dynamicDetail.value.files.audio = res.data.file.filter((item: any) => {
+      return item.filePath.includes('.mp3') || item.type === 'audio'
+    })[0]
+
+    //  模型链接
+    dynamicDetail.value.files.moduleUrl = res.data.entity.modelLink
+
+    console.log('模型链接是',res.data.entity.modelLink,dynamicDetail.value.files.moduleUrl)
+
+    initShowState()
+
+    console.log(dynamicDetail.value.files)
+  } else {
+    showToast(res.msg)
+  }
+}
+
+getDetailById()
+</script>
+
+<template>
+  <div class='detail-box'>
+
+    <div class="media-box" v-if="curState != null">
+      <img @click="() => { curMediaIndex-- }" v-show="curMediaIndex != 0 && curMediaList.length > 1" class="left-icon"
+        src="@/assets/images/left.png" alt="">
+      <img @click="() => { curMediaIndex++ }" v-show="curMediaIndex != curMediaList.length - 1"
+        class="left-icon right-icon" src="@/assets/images/right.png" alt="">
+      <img v-if="curState === 'images'" :src="baseIMGUrl + curMediaList[curMediaIndex].filePath" alt="">
+      <video v-if="curState === 'videos'" :src="baseIMGUrl + curMediaList[curMediaIndex].filePath" controls></video>
+      <iframe v-if="curState === 'module'" :src="dynamicDetail.files.moduleUrl" frameborder="0"></iframe>
+
+      <div class="option-box">
+        <div v-if="dynamicDetail.files.moduleUrl" @click="initModule()" :class="{ active: curState === 'module' }">模型
+        </div>
+        <div v-if="dynamicDetail.files.images.length != 0" @click="initImages()"
+          :class="{ active: curState === 'images' }">图片<span v-show="curState === 'images'">{{
+            curMediaIndex + 1 + '/' + curMediaList.length
+          }}</span></div>
+        <div v-if="dynamicDetail.files.videos.length != 0" @click="initVideos()"
+          :class="{ active: curState === 'videos' }">视频<span v-show="curState === 'videos'">{{
+            curMediaIndex + 1 + '/' + curMediaList.length
+          }}</span></div>
+      </div>
+    </div>
+    <div class="info-box" :style="{ marginTop: curState != null ? '' : '0px' }">
+      <div class="title">{{ dynamicDetail.name }}</div>
+      <!-- <div class="age">{{ dynamicDetail.age + '·' + dynamicDetail.texture }}</div> -->
+      <div class="age">
+        <div style="margin-right: 20px;">朝代:{{ dynamicDetail.age }}</div>
+        <div>质地:{{ dynamicDetail.texture }}</div>
+
+      </div>
+
+      <div class="size">尺寸: {{ dynamicDetail.size }}</div>
+      <div class="info">{{ dynamicDetail.description }}</div>
+    </div>
+
+    <audio v-if="audioBgUrl" id="audioBg" :src="baseIMGUrl + audioBgUrl.filePath" style="display: none;"></audio>
+    <img v-if="audioBgUrl" class="playing-icon" :src="audioPlaying ? pauseIcon : playIcon" alt=""
+      @click="audioChange()">
+    <!-- <img class="back-icon" @click="() => { router.back() }" src="@/assets/images/back.png" alt=""> -->
+  </div>
+</template>
+
+<style lang='less' scoped>
+.detail-box {
+  width: 100%;
+  min-height: 100%;
+  background: #F7F3E8;
+  overflow: auto;
+
+
+  .media-box {
+    width: 100%;
+    height: 35vh;
+    position: relative;
+
+    .left-icon {
+      width: 25px;
+      height: auto;
+      position: absolute;
+      left: 0;
+      top: 50%;
+      transform: translateY(-50%);
+      object-fit: fill;
+    }
+
+    .right-icon {
+      right: 0px;
+      left: auto;
+    }
+
+    img {
+      width: 100%;
+      height: 100%;
+      object-fit: cover;
+    }
+
+    video {
+      width: 100%;
+      height: 100%;
+      object-fit: cover;
+
+    }
+
+    iframe {
+      width: 100%;
+      height: 100%;
+    }
+
+    .option-box {
+      width: 70%;
+      position: absolute;
+      left: 50%;
+      transform: translateX(-50%);
+      bottom: 35px;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+
+      div {
+        height: 25px;
+        padding: 0 15px;
+        box-sizing: border-box;
+        line-height: 25px;
+        border-radius: 15px;
+        background: #9F8E8E;
+        color: #5B4848;
+        font-size: 0.8em;
+        margin: auto 2vw;
+
+        span {
+          margin-left: 5px;
+          font-size: 0.8em;
+          font-weight: 400;
+        }
+      }
+
+      .active {
+        background: #5B4848;
+        color: #F7F3E8;
+      }
+    }
+
+  }
+
+  .info-box {
+    background: #F7F3E8;
+    width: 100%;
+    height: calc(65vh + 20px);
+    margin-top: -20px;
+    padding: 20px;
+    box-sizing: border-box;
+    position: relative;
+    z-index: 2;
+    border-radius: 20px 20px 0 0;
+    overflow: auto;
+
+    .title {
+      color: #333333;
+      font-size: 1.2em;
+      font-weight: bold;
+      line-height: 1.5em;
+      font-family: 'SourceHanSansCN-Medium';
+      margin-bottom: 10px;
+    }
+
+    .age {
+      line-height: 1.5em;
+      color: #9D4F0B;
+      font-size: 1em;
+      font-family: 'SourceHanSansCN-Regular';
+      display: flex;
+      margin-bottom: 5px;
+    }
+
+    .size {
+      line-height: 1.5em;
+      color: #9D4F0B;
+      font-size: 1em;
+      font-family: 'SourceHanSansCN-Regular';
+      margin-bottom: 10px;
+    }
+
+    .info {
+      color: #88866F;
+    }
+
+
+  }
+
+  .playing-icon {
+    width: 40px;
+    position: fixed;
+    right: 15px;
+    bottom: 3vh;
+    z-index: 3;
+  }
+
+  // .back-icon {
+  //   width: 40px;
+  //   position: fixed;
+  //   right: 10px;
+  //   bottom: 10vh;
+  // }
+}
+</style>

+ 233 - 0
src/views/SmartTour/treasure.vue

@@ -0,0 +1,233 @@
+<!-- eslint-disable vue/multi-word-component-names -->
+<script setup lang='ts'>
+import { TreasureApi } from '@/api/api/treasure';
+import { showToast } from 'vant';
+import SearchInput from '@/components/SearchInput/index.vue'
+import { baseIMGUrl } from '@/api/request';
+
+export type DictType = {
+  createTime: string | null,
+  creatorId: number | null,
+  creatorName: string | null,
+  display: string | null,
+  id: number | null,
+  name: string | null,
+  parentId: number | null,
+  rtf: string | null,
+  sort: number | null,
+  type: string | null,
+  typeKey: string | null,
+  updateTime: string | null
+}
+
+export type TreasureType = {
+  age: string,
+  createTime: string,
+  creatorId: number,
+  creatorName: string,
+  description: string,
+  dirCode: string,
+  fileIds: string,
+  fileTypes: string,
+  id: 1,
+  name: string,
+  publishDate: string,
+  size: string,
+  texture: string,
+  thumb: string,
+  type: string,
+  updateTime: string
+}
+
+
+const searchParames = ref({
+  dictId: null,
+  pageNum: 0,
+  pageSize: 0,
+  searchKey: '',
+} as {
+  dictId: number | null,
+  pageNum: number,
+  pageSize: number,
+  searchKey: string,
+})
+
+const router = useRouter()
+
+const data = ref([] as TreasureType[])
+
+const typeList = ref([] as DictType[])
+
+
+const activeIndex = ref(0)
+
+const goDetai = (id: number) => {
+  router.push({ name: 'treasureDetail', params: { id } });
+}
+
+
+const getList = async () => {
+  searchParames.value.dictId = activeIndex.value == 0 ? null : typeList.value[activeIndex.value].id!
+  // 获取指定类型的列表
+  const res: any = await TreasureApi.getTreasureList(searchParames.value)
+  if (res.code == 0) {
+    data.value = res.data.records
+  } else {
+    showToast(res.msg)
+  }
+}
+
+
+const getTypeList = async () => {
+  // 获取珍藏类型
+  const resType: any = await TreasureApi.getTypeList()
+  if (resType.code == 0) {
+    typeList.value = resType.data
+    typeList.value.unshift({
+      createTime: null,
+      creatorId: null,
+      creatorName: '',
+      display: null,
+      id: null,
+      name: '全部',
+      parentId: null,
+      rtf: '',
+      sort: null,
+      type: '',
+      typeKey: '',
+      updateTime: null
+    })
+  } else {
+    showToast(resType.msg)
+  }
+
+  getList()
+}
+
+
+const onSearch = (eventData: any) => {
+  searchParames.value.searchKey = eventData
+  getList()
+}
+
+
+onBeforeMount(() => {
+  getTypeList()
+})
+
+
+
+
+</script>
+
+<template>
+  <div class="treasure-box">
+    <SearchInput @onSearch="onSearch" />
+    <div class="content-box">
+      <van-tabs v-model:active="activeIndex" color="#9D4F0B" background="#F7F3E8" title-active-color="#9D4F0B"
+        title-inactive-color="#88866F" @change="getList()">
+        <van-tab v-for="(item, index) in typeList" :key="index" :title="item.name">
+          <!-- <div>{{ activeIndex }}</div> -->
+          <div class="tab-content" v-if="data.length > 0">
+            <div class="activity-card" v-for="(item, index) in data" :key="index" @click="goDetai(item.id)">
+              <img v-show="item.thumb != ''" class="thumb" :src=" baseIMGUrl + item.thumb" alt="">
+              <div class="title">{{ item.name }}</div>
+              <div class="detail">
+                <div style="margin-right: 30px;">朝代:{{ item.age }}</div>
+                <div>质地:{{ item.texture }}</div>
+              </div>
+
+            </div>
+          </div>
+          <div v-else class="no-data">
+            <img src="@/assets/images/no-data.png" alt="">
+            <div class="tips">暂时没有数据 ,请试一下其他关键字</div>
+          </div>
+        </van-tab>
+      </van-tabs>
+    </div>
+  </div>
+</template>
+
+<style lang='less' scoped>
+.van-tabs__content {
+  background-color: #F7F3E8;
+}
+
+.treasure-box {
+  width: 100%;
+  min-height: 100%;
+  background: #F7F3E8;
+  overflow: auto;
+
+
+
+  .content-box {
+    width: 100%;
+    padding: 0px 10px;
+    box-sizing: border-box;
+
+    .van-tabs {
+      .van-tabs__nav {
+        background: #F7F3E8 !important;
+      }
+    }
+
+    .tab-content {
+      width: 100%;
+
+      .activity-card {
+        width: 100%;
+        padding: 10px;
+        box-sizing: border-box;
+        margin-bottom: 20px;
+
+
+        img {
+          width: 100%;
+          border-radius: 10px;
+        }
+
+        .detail {
+          font-size: 1em;
+          color: #9D4F0B;
+          margin-top: 10px;
+          font-family: 'SourceHanSansCN-Regular';
+          display: flex;
+
+        }
+
+        .title {
+          font-size: 1.2em;
+          font-weight: bold;
+          margin-top: 5px;
+          color: #333333;
+          font-family: 'SourceHanSansCN-Medium';
+        }
+      }
+    }
+
+    .no-data {
+      width: 100%;
+      height: calc(100% - 7vh);
+      display: flex;
+      flex-direction: column;
+      justify-content: center;
+      align-items: center;
+      margin-top: 10%;
+
+
+      img {
+        width: 40%;
+      }
+
+      .tips {
+        color: #88866F;
+        margin-top: 10px;
+        font-family: 'SourceHanSansCN-Regular';
+      }
+
+    }
+  }
+}
+</style>

+ 223 - 0
src/views/VolunteerHome/apply.vue

@@ -0,0 +1,223 @@
+<!-- eslint-disable vue/multi-word-component-names -->
+<script setup lang='ts'>
+import { showToast } from 'vant';
+import { VolunteerApi } from '@/api/api/volunteerHome';
+import router from '@/router';
+
+
+const route = useRoute()
+
+export type infoDetailType = {
+  name: string,
+  phone: string,
+  identity: string,
+  description: string,
+  type: string
+}
+
+const infoDetail = ref({
+  name: '',
+  phone: '',
+  identity: '',
+  description: '',
+  type: 'volunteer'
+} as infoDetailType)
+
+const submit = async () => {
+  if (infoDetail.value.name.length == 0) {
+    showToast('预约姓名不能为空!')
+    return
+  }
+
+  if (infoDetail.value.phone.length == 0) {
+    showToast('预约联系电话不能为空!')
+    return
+  }
+
+  if (!/^((1[3-9]\d{9})|(09\d{8}))$/.test(infoDetail.value.phone)) {
+    showToast('请输入正确的联系电话!')
+    return
+  }
+
+
+  if (infoDetail.value.identity.length == 0) {
+    showToast('预约身份证号不能为空!')
+    return
+  }
+
+  if (!/^(^\d{6}(18|19|20)?\d{2}(0[1-9]|1[012])(0[1-9]|[12]\d|3[01])\d{3}(\d|X|x))$/.test(infoDetail.value.identity)) {
+    showToast('请输入正确的身份证号!')
+    return
+  }
+  const res: any = await VolunteerApi.saveApplyAPI(infoDetail.value)
+  if (res.code == 0) {
+    showToast('申请成功')
+    router.back()
+  }else {
+    showToast(res.msg)
+  }
+
+}
+
+</script>
+
+<template>
+  <div class='info-box'>
+    <div class="page-title">
+      <img src="@/assets/images/cicle.png" alt="">
+      <div class="title-content">志愿者申请</div>
+    </div>
+    <div class="title">{{ route.params.title }}</div>
+    <div class="form">
+
+      <div class="input-item">
+        <span style="color: red;">*</span>
+        <div class="label">姓名:</div><input type="text" v-model="infoDetail.name" placeholder="请输入内容">
+      </div>
+      <div class="input-item">
+        <span style="color: red;">*</span>
+        <div class="label">联系电话:</div><input type="text" v-model="infoDetail.phone" placeholder="请输入内容">
+      </div>
+      <div class="input-item">
+        <span style="color: red;">*</span>
+        <div class="label">身份证号:</div><input type="text" v-model="infoDetail.identity" placeholder="请输入内容">
+      </div>
+      <div class="input-item">
+        <!-- <span style="color: red;">*</span> -->
+        <div class="label">预约说明:</div><textarea cols="30" rows="10" v-model="infoDetail.description"
+          placeholder="请输入您想参加的活动或其他需求"></textarea>
+      </div>
+    </div>
+    <div class="commit-box">
+      <div class="tips"> 提交后,请等待工作人员与您取得联系</div>
+      <div class="commit-btn" @click="submit()">提交</div>
+    </div>
+  </div>
+</template>
+
+<style lang='less' scoped>
+* {
+  box-sizing: border-box;
+}
+
+.info-box {
+  width: 100%;
+  height: 100vh;
+  padding: 20px;
+  background: #F7F3E8;
+  overflow: auto;
+
+
+  .page-title {
+    font-size: 1.4em;
+    color: #333333;
+    font-weight: bold;
+    position: relative;
+    // line-height: 1.6em;
+    display: flex;
+    align-items: flex-end;
+    margin-bottom: 10px;
+
+    img {
+      position: absolute;
+      left: 0;
+      top: 0;
+      height: 1.6em;
+    }
+
+    .title-content {
+      // position: absolute;
+      position: relative;
+      left: 0;
+      top: 0;
+      height: 1.4em;
+      z-index: 2;
+      line-height: 1.6em;
+      color: #333333;
+      margin-top: 1%;
+      font-family: SourceHanSansCN-Bold;
+
+    }
+
+  }
+
+  .title {
+    color: #9D4F0B;
+    font-family: 'SourceHanSansCN-Medium';
+    margin-top: 8%;
+    margin-bottom: 5%;
+    font-size: 1.1em;
+  }
+
+  .form {
+    width: 100%;
+
+    .input-item {
+      width: 100%;
+      display: flex;
+      margin: 10px auto;
+      height: 50px;
+
+      span {
+        margin-right: 10px;
+      }
+
+      .label {
+        width: 30%;
+        text-align: left;
+        color: #333333;
+      }
+
+      input {
+        width: 80%;
+        height: 70%;
+        border: 1px solid #9D4F0B;
+        border-radius: 5px;
+        padding: 0 10px;
+        background: #FFFDF6;
+        color: #88866F;
+        font-size: 0.9em;
+      }
+
+      textarea {
+        width: 80%;
+        height: 30vh;
+        border: 1px solid #9D4F0B;
+        border-radius: 5px;
+        padding: 10px;
+        background: #FFFDF6;
+        color: #88866F;
+        font-size: 0.9em;
+      }
+    }
+
+  }
+
+  .commit-box {
+    position: fixed;
+    bottom: 0;
+    width: calc(100% - 40px);
+
+    .tips {
+      color: #88866F;
+      width: 100%;
+      text-align: center;
+    }
+
+    .commit-btn {
+      width: 100%;
+      height: 60px;
+      background: url(@/assets/images/onlineBg.png);
+      background-size: 100% 100%;
+      border-radius: 50px;
+      color: white;
+      font-weight: bold;
+      font-size: 1.2em;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      margin: 30px 0 40px 0;
+    }
+  }
+}
+</style>

+ 145 - 0
src/views/VolunteerHome/detail.vue

@@ -0,0 +1,145 @@
+<!-- eslint-disable vue/multi-word-component-names -->
+<script setup lang='ts'>
+import { VolunteerApi } from '@/api/api/volunteerHome';
+import { showToast } from 'vant';
+// import volunteerType from './home.vue'
+import { baseIMGUrl } from '@/api/request.ts'
+
+
+const route = useRoute()
+// const router = useRouter()
+
+const videos = ref([] as any)
+
+export type volunteerType = {
+  createTime: string,
+  creatorId: number,
+  creatorName: string,
+  dirCode: string,
+  fileIds: string,
+  id: number,
+  name: string,
+  publishDate: string,
+  rtf: string,
+  thumb: string,
+  type: string,
+  updateTime: string
+}
+
+const activeDetail = ref({} as volunteerType)
+
+const getDetailById = async () => {
+  const res: any = await VolunteerApi.getDetailById(Number(route.params.id))
+  if (res.code == 0) {
+    activeDetail.value = res.data.entity
+    videos.value = res.data.file.filter((item: any) => {
+      return item.filePath.includes('.mp4')
+    })
+  } else {
+    showToast(res.msg)
+  }
+}
+
+const formatrtf = computed(() => {
+  return activeDetail.value.rtf.replace(
+    /<img /g,
+    '<img style="width: 100%;" '
+  ).replace(
+    /<p\b[^>]*>/g,
+    '<p style="width: 100%; word-wrap: break-word;">'
+  );
+})
+
+
+
+onBeforeMount(() => {
+  getDetailById()
+})
+</script>
+
+<template>
+  <div class='detail-box'>
+    <img class="thumb-box" :src="baseIMGUrl + activeDetail.thumb" alt="">
+    <div class="content-box">
+      <div class="name-box">{{ activeDetail.name }}</div>
+      <div class="mainbody-box" v-html="formatrtf"></div>
+      <div class="time-box">发布时间:{{ activeDetail.publishDate }}</div>
+      <div class="video-box">
+        <video :src="baseIMGUrl + item.filePath" controls v-for="(item, index) in videos" :key="index"></video>
+      </div>
+    </div>
+    <!-- <img class="back-icon" @click="() => { router.back() }" src="@/assets/images/back.png" alt=""> -->
+  </div>
+</template>
+
+<style lang='less' scoped>
+.detail-box {
+  width: 100%;
+  height: 100%;
+  overflow: auto;
+
+
+  .thumb-box {
+    width: 100%;
+    margin-bottom: 10px;
+    height: 30vh;
+  }
+
+  .content-box {
+    width: 100%;
+    padding: 0 10px;
+    box-sizing: border-box;
+    height: calc(70vh + 25px);
+    overflow: auto;
+    border-radius: 20px 20px 0 0;
+    margin-top: -40px;
+    position: relative;
+    z-index: 2;
+    background: #F7F3E8;
+    padding: 25px 20px;
+    box-sizing: border-box;
+
+    .name-box {
+      width: 100%;
+      font-size: 1.2em;
+      font-weight: bold;
+      color: #333333;
+      font-family: 'SourceHanSansCN-Medium';
+      line-height: 1.5em;
+      margin-bottom: 10px;
+    }
+
+    .time-box {
+      width: 100%;
+      display: flex;
+      justify-content: space-between;
+      color: #9D4F0B;
+      margin: 10px auto;
+      font-size: 0.8em;
+      align-items: center;
+    }
+
+    .abstract-box {
+      color: rgba(128, 128, 128, 0.664);
+      font-size: 1em;
+      margin-bottom: 10px;
+      padding: 0 10px;
+    }
+
+    .mainbody-box {
+      color: #88866F;
+      word-wrap: break-word;
+    }
+
+    .video-box {
+      width: 100%;
+
+      video {
+        width: 100%;
+        margin-top: 10px;
+      }
+
+    }
+  }
+}
+</style>

+ 235 - 0
src/views/VolunteerHome/home.vue

@@ -0,0 +1,235 @@
+<!-- eslint-disable vue/multi-word-component-names -->
+<script setup lang='ts'>
+import { VolunteerApi } from '@/api/api/volunteerHome';
+import { baseIMGUrl } from '@/api/request';
+import { showToast } from 'vant';
+
+const active = ref(0)
+
+const router = useRouter()
+
+
+const searchParames = ref({
+  pageNum: 0,
+  pageSize: 0,
+  searchKey: '',
+  type: ''
+} as {
+  pageNum: number,
+  pageSize: number,
+  searchKey: string,
+  type: string
+})
+
+export type volunteerType = {
+  createTime: string,
+  creatorId: number,
+  creatorName: string,
+  dirCode: string,
+  fileIds: string,
+  id: number,
+  name: string,
+  publishDate: string,
+  rtf: string,
+  thumb: string,
+  type: string,
+  updateTime: string
+}
+
+const data = ref([] as volunteerType[])
+
+const goDetai = (id: number) => {
+  router.push({ name: 'volunteerDetail', params: { id } });
+}
+
+const goApply = () => {
+  router.push({ name: 'activeBookApply' });
+}
+
+const typesList = ref(['志愿者资讯', '志愿者风采'] as string[])
+
+const getList = async () => {
+  searchParames.value.type = typesList.value[active.value]
+  const res: any = await VolunteerApi.getVolunteerList(searchParames.value)
+  if (res.code == 0) {
+    data.value = res.data.records
+    console.log(data.value.length, res.data.records)
+  } else {
+    showToast(res.msg)
+  }
+}
+
+onBeforeMount(() => {
+  getList()
+})
+</script>
+
+<template>
+  <div class='home-box'>
+    <!-- <van-tabs v-model:active="active" id="tabs">
+      <van-tab v-for="(item, index) in data" :key="index" :name="item.type">
+      </van-tab>
+    </van-tabs> -->
+
+    <div class="tabs">
+      <div class="tab-item" :class="{ active: index === active }" v-for="(item, index) in typesList" :key="item"
+        @click="() => { active = index, getList() }">{{ item }}</div>
+    </div>
+
+    <div class="tab-content" v-if="data.length > 0">
+      <div class="activity-card" v-for="(item, index) in data" :key="index" @click="goDetai(item.id)">
+        <img v-show="item.thumb != ''" class="thumb" :src="baseIMGUrl + item.thumb" alt="">
+        <div class="bottom-box">
+          <div class="name">{{ item.name }}</div>
+          <div class="income">了解详情</div>
+        </div>
+      </div>
+    </div>
+    <div class="no-data" v-else>
+      <img src="@/assets/images/no-data.png" alt="">
+      <div class="tips">暂时没有数据</div>
+    </div>
+    <div class="apply-btn" @click="goApply()">我要报名</div>
+  </div>
+</template>
+
+<style lang='less' scoped>
+* {
+  box-sizing: border-box;
+}
+
+.home-box {
+  width: 100%;
+  background: #F7F3E8;
+  min-height: 100%;
+  overflow: auto;
+
+
+  .tabs {
+    // height: 40px;
+    white-space: nowrap;
+    overflow-x: auto;
+    padding: 15px 10px 10px 0px;
+    font-family: 'SourceHanSansCN-Regular';
+
+    &::-webkit-scrollbar {
+      display: none;
+      /* 或 width: 0; */
+    }
+
+    .tab-item {
+      margin-left: 10px;
+      display: inline-block;
+      width: 85px;
+      height: 30px;
+      float: none;
+      font-size: 0.9rem;
+      line-height: 30px;
+      text-align: center;
+      background-color: #F1E9D4;
+      border-radius: 5px;
+      // box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.3);
+      color: rgba(0, 0, 0, 0.8);
+      color: #88866F;
+    }
+
+    .active {
+      background: #9D4F0B;
+      color: #F7F3E8;
+    }
+  }
+
+  .tab-content {
+    width: 100%;
+    padding: 10px;
+
+
+    .activity-card {
+      width: 100%;
+      background: #FFFDF6;
+      border-radius: 15px;
+      margin-bottom: 30px;
+      box-shadow: 0px 0px 20px 0px rgba(222, 216, 199, 0.6);
+
+      .thumb {
+        width: 100%;
+        height: 25vh;
+        border-radius: 15px 15px 0 0;
+      }
+
+      .bottom-box {
+        padding: 10px;
+        display: flex;
+        flex-direction: column;
+        align-items: end;
+
+        .name {
+          font-size: 1.2em;
+          font-weight: bold;
+          line-height: 1.3em;
+          margin-top: 10px;
+          color: #333333;
+          width: 100%;
+        }
+
+        .income {
+          width: 30%;
+          display: flex;
+          justify-content: center;
+          align-items: center;
+          color: #9D4F0B;
+          line-height: 30px;
+          border: 1px solid #9D4F0B;
+          border-radius: 50px;
+        }
+
+      }
+
+
+    }
+  }
+
+  .no-data {
+    width: 100%;
+    height: calc(100% - 7vh);
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    margin-top: 10%;
+
+
+    img {
+      width: 40%;
+    }
+
+    .tips {
+      color: #88866F;
+      margin-top: 10px;
+      font-family: 'SourceHanSansCN-Regular';
+    }
+
+  }
+
+  .apply-btn {
+    width: 80%;
+    max-width: 400px;
+    height: 60px;
+    border-radius: 50px;
+    background: url(@/assets/images/onlineBg.png);
+    background-size: 100% 100%;
+    color: #F1E9D4;
+    position: fixed;
+    left: 50%;
+    transform: translateX(-50%);
+    bottom: 3vh;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    letter-spacing: 2px;
+    font-weight: bold;
+    z-index: 2;
+    font-size: 1.3em;
+  }
+}
+</style>

+ 13 - 0
tsconfig.app.json

@@ -0,0 +1,13 @@
+{
+  "extends": "@vue/tsconfig/tsconfig.dom.json",
+  "include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
+  "exclude": ["src/**/__tests__/*"],
+  "compilerOptions": {
+    "composite": true,
+    "noEmit": true,
+    "baseUrl": ".",
+    "paths": {
+      "@/*": ["./src/*"]
+    }
+  }
+}

+ 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"],
+}

+ 17 - 0
tsconfig.node.json

@@ -0,0 +1,17 @@
+{
+  "extends": "@tsconfig/node18/tsconfig.json",
+  "include": [
+    "vite.config.*",
+    "vitest.config.*",
+    "cypress.config.*",
+    "nightwatch.conf.*",
+    "playwright.config.*"
+  ],
+  "compilerOptions": {
+    "composite": true,
+    "noEmit": true,
+    "module": "ESNext",
+    "moduleResolution": "Bundler",
+    "types": ["node"]
+  }
+}

+ 44 - 0
vite.config.ts

@@ -0,0 +1,44 @@
+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 { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
+
+import { resolve } from 'path'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+  base: './',
+  plugins: [
+    vue(),
+    Components({
+      resolvers: [VantResolver(), ElementPlusResolver()]
+    }),
+    AutoImport({
+      //安装两行后你会发现在组件中不用再导入ref,reactive等
+      imports: ['vue', 'vue-router'],
+      //存放的位置
+      dts: 'src/auto-import.d.ts',
+      resolvers: [ElementPlusResolver(),VantResolver()]
+    })
+  ],
+  resolve: {
+    // ↓路径别名
+    alias: {
+      '@': resolve(__dirname, './src')
+    }
+  },
+  build: {
+    outDir: 'dist',
+    assetsDir: 'assets',
+    sourcemap: false,
+    terserOptions: {
+      compress: {
+        drop_console: true,
+        drop_debugger: true
+      }
+    }
+  }
+})

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 2445 - 0
yarn.lock