Forráskód Böngészése

```
feat: 添加电影演出模块和相关API接口

- 新增电影演出子包,包含选择电影、选座、订单等页面
- 添加电影相关API定义,包括城市列表、电影列表、影院列表等接口
- 配置电影模块路由和页面配置
- 添加相关类型定义和全局API方法声明

chore: 更新项目配置和忽略文件

- 添加IDEA项目配置文件到.gitignore
- 创建项目模块配置文件
- 配置代码检查规则
- 更新版本控制映射配置

refactor: 优化二维码组件功能

- 修改QCode组件支持下载功能
- 添加H5端下载和小程序端保存到相册逻辑
- 支持权限检查和用户授权流程

refactor: 调整购物车页面布局样式

- 更新CSS类名格式,统一使用负值边距语法
- 注释部分导航标签,暂时隐藏视频权益和大牌点餐功能
- 优化页面间距和组件尺寸配置

refactor: 优化个人中心页面样式

- 统一CSS类名格式,修复背景色和边距问题
- 调整头像和文字间距显示效果
- 优化卡片组件和单元格布局

refactor: 更新API类型定义和接口配置

- 扩展API类型定义,添加电影相关数据结构
- 添加电影模块API接口配置
- 更新全局API方法声明和参数类型
- 增加异步导入模块声明

refactor: 优化充电订单列表组件

- 使用异步组件加载订单列表
- 移除重复的订单项点击处理逻辑
- 简化订单列表渲染结构

chore: 调整开发环境配置

- 更新开发环境API基础URL配置
- 启用线上开发环境地址
```

zouzexu 1 hete
szülő
commit
22cad23f28
53 módosított fájl, 7041 hozzáadás és 225 törlés
  1. 8 0
      .idea/.gitignore
  2. 12 0
      .idea/city-gather.iml
  3. 6 0
      .idea/inspectionProfiles/Project_Default.xml
  4. 8 0
      .idea/modules.xml
  5. 6 0
      .idea/vcs.xml
  6. 8 0
      async-component.d.ts
  7. 2 0
      async-import.d.ts
  8. 137 0
      src/api/api.type.d.ts
  9. 14 1
      src/api/apiDefinitions.ts
  10. 114 0
      src/api/globals.d.ts
  11. 62 4
      src/components/QCode.vue
  12. 3 2
      src/config/index.ts
  13. 108 1
      src/pages.json
  14. 38 38
      src/pages/cart/index.vue
  15. 27 27
      src/pages/index/index.vue
  16. 20 20
      src/pages/my/index.vue
  17. 11 3
      src/shims.d.ts
  18. 3 0
      src/store/address.ts
  19. 2 52
      src/subPack-charge/chargeOrderList/chargeOrderList.vue
  20. 610 0
      src/subPack-film/choose-film/index.vue
  21. 117 0
      src/subPack-film/choose-seat/components/pages/CanvasSeatmap.vue
  22. 184 0
      src/subPack-film/choose-seat/components/pages/canvasCalculator.js
  23. 41 0
      src/subPack-film/choose-seat/components/pages/canvasOperator.js
  24. 304 0
      src/subPack-film/choose-seat/components/pages/canvasRenderer.js
  25. 46 0
      src/subPack-film/choose-seat/components/pages/config.js
  26. 286 0
      src/subPack-film/choose-seat/components/pages/main.js
  27. 690 0
      src/subPack-film/choose-seat/index.vue
  28. 64 0
      src/subPack-film/components/choose-time.vue
  29. 66 0
      src/subPack-film/components/tabbar.vue
  30. 144 0
      src/subPack-film/index/index.vue
  31. 463 0
      src/subPack-film/movie-detail/index.vue
  32. 33 0
      src/subPack-film/movie-list/index.vue
  33. 528 0
      src/subPack-film/movie/index.vue
  34. 621 0
      src/subPack-film/order-detail/index.vue
  35. 311 0
      src/subPack-film/order/index.vue
  36. 369 0
      src/subPack-film/select-time/index.vue
  37. 563 0
      src/subPack-film/submit-order/index.vue
  38. 153 0
      src/subPack-film/utils/confirm-order.ts
  39. 149 0
      src/subPack-film/utils/index.ts
  40. 156 0
      src/subPack-film/utils/order-data.ts
  41. 13 6
      src/subPack-smqjh/components/charge-orderList/charge-orderList.vue
  42. 245 0
      src/subPack-smqjh/components/film-orderList/film-orderList.vue
  43. 28 26
      src/subPack-smqjh/order/components/OrderRenderer.vue
  44. 11 10
      src/subPack-smqjh/order/index.vue
  45. 1 1
      src/subPack-smqjh/order/order-data.ts
  46. 49 0
      src/subPack-videoRights/commonTab/components/home.vue
  47. 61 0
      src/subPack-videoRights/commonTab/components/order.vue
  48. 10 5
      src/subPack-videoRights/commonTab/index.vue
  49. 94 0
      src/subPack-videoRights/videoRightsDetail/videoRightsDetail.vue
  50. 27 27
      src/subPack-xsb/order/index.vue
  51. 12 1
      src/uni-pages.d.ts
  52. 2 1
      tsconfig.json
  53. 1 0
      vite.config.ts

+ 8 - 0
.idea/.gitignore

@@ -0,0 +1,8 @@
+# 默认忽略的文件
+/shelf/
+/workspace.xml
+# 基于编辑器的 HTTP 客户端请求
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml

+ 12 - 0
.idea/city-gather.iml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="WEB_MODULE" version="4">
+  <component name="NewModuleRootManager">
+    <content url="file://$MODULE_DIR$">
+      <excludeFolder url="file://$MODULE_DIR$/.tmp" />
+      <excludeFolder url="file://$MODULE_DIR$/temp" />
+      <excludeFolder url="file://$MODULE_DIR$/tmp" />
+    </content>
+    <orderEntry type="inheritedJdk" />
+    <orderEntry type="sourceFolder" forTests="false" />
+  </component>
+</module>

+ 6 - 0
.idea/inspectionProfiles/Project_Default.xml

@@ -0,0 +1,6 @@
+<component name="InspectionProjectProfileManager">
+  <profile version="1.0">
+    <option name="myName" value="Project Default" />
+    <inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
+  </profile>
+</component>

+ 8 - 0
.idea/modules.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectModuleManager">
+    <modules>
+      <module fileurl="file://$PROJECT_DIR$/.idea/city-gather.iml" filepath="$PROJECT_DIR$/.idea/city-gather.iml" />
+    </modules>
+  </component>
+</project>

+ 6 - 0
.idea/vcs.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="" vcs="Git" />
+  </component>
+</project>

+ 8 - 0
async-component.d.ts

@@ -14,3 +14,11 @@ declare module '@/subPack-xsb/components/xsbTabbar/index.vue?async' {
   const component: typeof import('@/subPack-xsb/components/xsbTabbar/index.vue')
   export = component
 }
+declare module '../../subPack-smqjh/components/charge-orderList/charge-orderList.vue?async' {
+  const component: typeof import('../../subPack-smqjh/components/charge-orderList/charge-orderList.vue')
+  export = component
+}
+declare module '@/subPack-smqjh/components/charge-orderList/charge-orderList.vue?async' {
+  const component: typeof import('@/subPack-smqjh/components/charge-orderList/charge-orderList.vue')
+  export = component
+}

+ 2 - 0
async-import.d.ts

@@ -8,6 +8,8 @@ interface ModuleMap {
   '@/subPack-xsb/utils/order-data': typeof import('@/subPack-xsb/utils/order-data')
   '@/subPack-xsb/utils/confirm-order': typeof import('@/subPack-xsb/utils/confirm-order')
   '@/subPack-xsb/store-xsb/sys': typeof import('@/subPack-xsb/store-xsb/sys')
+  '@/subPack-film/utils/order-data': typeof import('@/subPack-film/utils/order-data')
+  '@/subPack-film/utils/confirm-order': typeof import('@/subPack-film/utils/confirm-order')
   [path: string]: any
 }
 

+ 137 - 0
src/api/api.type.d.ts

@@ -1545,6 +1545,143 @@ namespace Api {
     peakTime?: string
   }
 
+  interface filmCityList {
+    id?: string
+    cityId?: string
+    name?: string
+    pinYin?: string
+    letter?: string
+    provinceId?: string
+    isShow?: string
+    districts?: any[]
+
+    [property: string]: any
+  }
+
+  interface filmMovieList {
+
+    id?: string
+    description?: string
+    duration?: string
+    movieId?: string
+    movieCode?: string
+    name?: string
+    posterUrl?: string
+    version?: string
+    isPicShow?: string
+    isGlobalReleased?: string
+    score?: string
+    wish?: string
+    star?: string
+    releaseTime?: string
+    comingTitle?: string
+    showSt?: number
+    category?: string
+    director?: string
+    originPrice?: string
+    sellPrice?: string
+    fastPrice?: string
+
+    [property: string]: any
+  }
+
+  interface filmList {
+
+    id: string
+    distance: string
+    lat: string
+    lng: string
+    halls: string
+    showInfo: string
+    cinemaId: string
+    cinemaCode: string
+    cityId: string
+    districtId: string
+    name: string
+    phone: string
+    sellPrice: string
+    originPrice: string
+    areaName: string
+
+    [property: string]: any
+  }
+
+  interface filmDateItem {
+
+  }
+
+  interface filmDateList {
+    id?: string
+    brandId?: string
+    brandName?: string
+
+    [property: string]: any
+  }
+
+  interface filmBrandList {
+    id?: string
+    brandId?: string
+    brandName?: string
+
+    [property: string]: any
+  }
+
+  interface cinema {
+    distance?: number
+    lat?: number
+    lng?: number
+    halls?: string[]
+    address?: string
+    name?: string
+    phone?: string
+
+  }
+
+  interface filmOwnMovieList {
+    cinemaId?: string
+    movieId?: string
+    cinema?: cinema
+    showInfo?: string
+    address?: string
+    phone?: string
+    name?: string
+    areaName?: string
+    closeTime?: string
+    fastCloseTime?: string
+    movieShows?: any[]
+    showCount?: string
+    [property: string]: any
+  }
+  interface seat {
+    rowCount: number
+    columnCount: number
+    seatRows: any
+  }
+
+  interface filmMovieSeat {
+    seatSection?: seat
+    sessionId?: string
+    showTime?: string
+    endTime?: string
+    language?: string
+    planType?: string
+    movieName?: string
+    movieId?: string
+    hall?: string
+    [property: string]: any
+
+  }
+
+  interface addFilmOrder {
+    [property: string]: any
+
+  }
+
+  interface filmOrder {
+    [property: string]: any
+
+  }
+
   interface chargeStationDetail {
     /**
      * 详细地址

+ 14 - 1
src/api/apiDefinitions.ts

@@ -68,7 +68,20 @@ export default {
   'common.shoppingCartOrderConfirm':['GET', '/smqjh-oms/app-api/v1/shoppingCart/shoppingCartOrderConfirm/{ids}'],
   'common.addOrder':['POST', '/smqjh-oms/api/v1/order/addOrder'],
   'common.hybridPayment':['POST', '/smqjh-oms/service/pay/hybridPayment'],
-  'smqjh.getCategoryList': ['POST', '/smqjh-pms/app-api/v1/categories/getCategoryList'],
+  'smqjh.getCategoryList':['POST', '/smqjh-pms/app-api/v1/categories/getCategoryList'],
+
+  'film.getCityList':['GET', '/smqjh-pms/api/v1/app/city/list'],
+  'film.getMovieList':['GET', '/smqjh-pms/api/v1/app/movie/page'],
+  'film.getMovieDetail':['GET', '/smqjh-pms/api/v1/app/movie/detail'],
+  'film.getFilmList':['GET', '/smqjh-pms/api/v1/app/cinema/list'],
+  'film.getFilmDateList':['GET', '/smqjh-pms/api/v1/app/cinema/date'],
+  'film.getFilmBrandList':['GET', '/smqjh-pms/api/v1/app/cinema/brand'],
+  'film.getFilmMovieList':['GET', '/smqjh-pms/api/v1/app/movie/cinema/movie/screening/list'],
+  'film.getFilmMovieSeat':['GET', '/smqjh-pms/api/v1/app/movie/movie/screening/detail'],
+  'film.addFilmOrder':['POST', '/smqjh-oms/api/v1/movie/order/addOrder'],
+  'film.filmOrderDetail':['GET', '/smqjh-oms/api/v1/movie/order/getOrderDetail'],
+
+
 
   'charge.getStationInfoPage':['POST','/smqjh-system/applet/v1/homePage/getStationInfoPage'],
   'charge.getMemberInfo': ['GET', '/smqjh-system/applet/v1/homePage/getMemberInfo'],

+ 114 - 0
src/api/globals.d.ts

@@ -664,6 +664,120 @@ declare global {
       >(
         config: Config
       ): Alova2Method<apiResData<Api.smqjhCategoryList[]>, 'smqjh.getCategoryList', Config>;
+    },
+    film:{
+       getCityList<
+        Config extends Alova2MethodConfig<ApiResponse<Api.filmCityList>> & {
+
+        }
+      >(
+        config: Config
+      ): Alova2Method<ApiResponse<Api.filmCityList>, 'film.getCityList', Config>;
+      getMovieList<
+        Config extends Alova2MethodConfig<ApiResponse<Api.filmMovieList>> & {
+          data: {
+            showSt?: number;
+            pageNum?: number;
+            pageSize?: number;
+          }
+        }
+      >(
+        config: Config
+      ): Alova2Method<ApiResponse<Api.filmMovieList>, 'film.getMovieList', Config>;
+
+      getMovieDetail<
+        Config extends Alova2MethodConfig<ApiResponse<Api.filmMovieList>> & {
+          data: {
+            id?: number|string;
+          }
+        }
+      >(
+        config: Config
+      ): Alova2Method<ApiResponse<Api.filmMovieList>, 'film.getMovieDetail', Config>;
+      getFilmList<
+        Config extends Alova2MethodConfig<ApiResponse<Api.filmList>> & {
+          data: {
+            cityId?: number|string;
+            cinemaId?: number|string;
+            movieId?: number|string;
+            districtId?: number|string;
+            showDates?: number|string;
+            hall?: number|string;
+            lat?: number|null;
+            lng?: number|null;
+
+          }
+        }
+      >(
+        config: Config
+      ): Alova2Method<ApiResponse<Api.filmList>, 'film.getFilmList', Config>;
+       getFilmDateList<
+        Config extends Alova2MethodConfig<ApiResponse<Api.filmDateList>> & {
+          data: {
+            movieId?:string
+          }
+        }
+      >(
+        config: Config
+      ): Alova2Method<ApiResponse<Api.filmDateList>, 'film.getFilmDateList', Config>;
+
+       getFilmBrandList<
+        Config extends Alova2MethodConfig<ApiResponse<Api.filmBrandList>> & {
+
+        }
+      >(
+        config: Config
+      ): Alova2Method<ApiResponse<Api.filmBrandList>, 'film.getFilmBrandList', Config>;
+
+      getFilmMovieList<
+        Config extends Alova2MethodConfig<ApiResponse<Api.filmOwnMovieList>> & {
+          data: {
+            cinemaId:string
+            movieId:string
+
+          }
+        }
+      >(
+        config: Config
+      ): Alova2Method<ApiResponse<Api.filmOwnMovieList>, 'film.getFilmMovieList', Config>;
+
+      getFilmMovieSeat<
+        Config extends Alova2MethodConfig<ApiResponse<Api.filmMovieSeat>> & {
+          data: {
+            sessionId:string
+            movieId:string
+            cinemaId:string
+
+          }
+        }
+      >(
+        config: Config
+      ): Alova2Method<ApiResponse<Api.filmMovieSeat>, 'film.getFilmMovieSeat', Config>;
+      addFilmOrder<
+        Config extends Alova2MethodConfig<ApiResponse<Api.addFilmOrder>> & {
+          data: {
+            cinemaCode:string
+            hallName:string
+            orderPayMode:string
+            originPrice:number
+            seatNames:string
+            sessionBeginTime:string
+            switchSeat:boolean
+
+          }
+        }
+      >(
+        config: Config
+      ): Alova2Method<ApiResponse<Api.addFilmOrder>, 'film.addFilmOrder', Config>;
+      filmOrderDetail<
+        Config extends Alova2MethodConfig<ApiResponse<Api.filmOrder>> & {
+          data: {
+            id:string
+          }
+        }
+      >(
+        config: Config
+      ): Alova2Method<ApiResponse<Api.filmOrder>, 'film.filmOrderDetail', Config>;
     }
     charge: {
       getStationInfoPage<

+ 62 - 4
src/components/QCode.vue

@@ -4,22 +4,80 @@
 
 import UQRCode from 'uqrcodejs'
 
-const props = defineProps<{ text: string, qwidth: number }>()
+const props = defineProps<{ text: string, qwidth: number, qrKey: string }>()
 const _this = getCurrentInstance()
 function createQRCode() {
   const qr = new UQRCode()
   qr.data = props.text
-  qr.size = props.qwidth || 100
+  qr.size = props.qwidth || 100 // 这里是px,和下面的不匹配
   qr.make()
-  const canvasContext = uni.createCanvasContext('qrcode', _this)
+  const canvasContext = uni.createCanvasContext(props.qrKey, _this)
   qr.canvasContext = canvasContext
   qr.drawCanvas()
 }
 createQRCode()
+
+async function downloadQrcode() {
+  try {
+    // 1. 先确保二维码已绘制完成(短暂延迟,兼容绘制异步性)
+    await new Promise(resolve => setTimeout(resolve, 300))
+
+    // 2. 将Canvas转为临时文件路径(复用原有canvas-id)
+    const tempRes: any = await uni.canvasToTempFilePath({
+      canvasId: props.qrKey, // 与原有Canvas的canvas-id保持一致
+      width: props.qwidth,
+      height: props.qwidth,
+      destWidth: props.qwidth * 2, // 生成高清图
+      destHeight: props.qwidth * 2,
+      fileType: 'png',
+    }, _this)
+
+    // 3. 分端处理下载/保存逻辑
+    const systemInfo = uni.getSystemInfoSync()
+    if (systemInfo.platform === 'h5') {
+      // H5端:浏览器下载
+      const link = document.createElement('a')
+      link.href = tempRes.tempFilePath
+      link.download = `二维码_${Date.now()}.png`
+      link.click()
+      uni.showToast({ title: '二维码下载成功' })
+    }
+    else {
+      // 小程序/APP端:保存到相册(处理授权)
+      // 检查授权状态
+      const settingRes = await uni.getSetting({})
+      if (!settingRes.authSetting['scope.writePhotosAlbum']) {
+        // 未授权则请求授权
+        try {
+          await uni.authorize({ scope: 'scope.writePhotosAlbum' })
+        }
+        catch {
+          uni.showToast({ title: '需授权保存相册才能下载', icon: 'none' })
+          return
+        }
+      }
+
+      // 保存到系统相册
+      await uni.saveImageToPhotosAlbum({
+        filePath: tempRes.tempFilePath,
+      })
+      uni.showToast({ title: '二维码已保存到相册' })
+    }
+  }
+  catch (err) {
+    // const errMsg = (err as any).errMsg
+    uni.showToast({ title: `下载失败`, icon: 'none' })
+    console.error('二维码下载失败:', err)
+  }
+}
+
+defineExpose({
+  downloadQrcode,
+})
 </script>
 
 <template>
-  <canvas id="qrcode" canvas-id="qrcode" :style="{ width: `${props.qwidth}px`, height: `${props.qwidth}px` }" />
+  <canvas :id="qrKey" :canvas-id="qrKey" :style="{ width: `${props.qwidth}px`, height: `${props.qwidth}px` }" />
 </template>
 
 <style scoped>

+ 3 - 2
src/config/index.ts

@@ -6,11 +6,12 @@ const mapEnvVersion = {
   // develop: 'http://192.168.1.101:8080',
   // develop: 'http://192.168.0.157:8080',
   // develop: 'http://192.168.1.253:8080',
-  develop: 'http://192.168.0.19:8080',
+  // develop: 'http://192.168.0.19:8080',
   // develop: 'http://192.168.0.217:8080',
   // develop: 'http://192.168.1.89:8080', // 田
+  // develop: 'http://74949mkfh190.vicp.fun', // 付
   // develop: 'http://47.109.84.152:8081',
-  // develop: 'https://smqjh.api.zswlgz.com',
+  develop: 'https://smqjh.api.zswlgz.com',
   /**
    * 体验版
    */

+ 108 - 1
src/pages.json

@@ -288,6 +288,104 @@
         }
       ]
     },
+    {
+      "root": "subPack-film",
+      "pages": [
+        {
+          "path": "choose-film/index",
+          "name": "film-choose-film",
+          "islogin": false,
+          "style": {
+            "navigationStyle": "custom",
+            "navigationBarTitleText": "我的订单",
+            "backgroundColorBottom": "#fff"
+          }
+        },
+        {
+          "path": "choose-seat/index",
+          "name": "film-choose-seat",
+          "islogin": false,
+          "style": {
+            "navigationBarTitleText": "座次选择",
+            "backgroundColorBottom": "#fff"
+          }
+        },
+        {
+          "path": "index/index",
+          "name": "film-index",
+          "islogin": true,
+          "style": {
+            "navigationBarTitleText": "电影演出",
+            "backgroundColorBottom": "#fff"
+          }
+        },
+        {
+          "path": "movie/index",
+          "name": "film-movie",
+          "islogin": false,
+          "style": {
+            "navigationStyle": "custom",
+            "navigationBarTitleText": "电影演出",
+            "backgroundColorBottom": "#fff"
+          }
+        },
+        {
+          "path": "movie-detail/index",
+          "name": "film-movie-detail",
+          "islogin": false,
+          "style": {
+            "navigationStyle": "custom",
+            "navigationBarTitleText": "电影详情",
+            "backgroundColorBottom": "#fff"
+          }
+        },
+        {
+          "path": "movie-list/index",
+          "name": "film-movieList",
+          "islogin": false,
+          "style": {
+            "navigationBarTitleText": "电影演出",
+            "backgroundColorBottom": "#fff"
+          }
+        },
+        {
+          "path": "order/index",
+          "name": "film-order",
+          "islogin": false,
+          "style": {
+            "navigationBarTitleText": "我的订单",
+            "backgroundColorBottom": "#fff"
+          }
+        },
+        {
+          "path": "order-detail/index",
+          "name": "film-order-detail",
+          "islogin": false,
+          "style": {
+            "navigationBarTitleText": "订单详情",
+            "backgroundColorBottom": "#fff"
+          }
+        },
+        {
+          "path": "select-time/index",
+          "name": "film-select-time",
+          "islogin": false,
+          "style": {
+            "navigationBarTitleText": "",
+            "backgroundColorBottom": "#fff"
+          }
+        },
+        {
+          "path": "submit-order/index",
+          "name": "film-submit-order",
+          "islogin": false,
+          "style": {
+            "navigationBarTitleText": "确认订单",
+            "backgroundColorBottom": "#fff"
+          }
+        }
+      ]
+    },
     {
       "root": "subPack-charge",
       "pages": [
@@ -383,7 +481,16 @@
           "name": "video-rights-tabbar",
           "islogin": false,
           "style": {
-            "navigationBarTitleText": ""
+            "navigationBarTitleText": "",
+            "navigationStyle": "custom"
+          }
+        },
+        {
+          "path": "videoRightsDetail/videoRightsDetail",
+          "name": "video-rights-detail",
+          "islogin": false,
+          "style": {
+            "navigationBarTitleText": "详情"
           }
         }
       ]

+ 38 - 38
src/pages/cart/index.vue

@@ -23,11 +23,11 @@ const navList = ref([
   { title: '全部', id: 1 },
   { title: '星闪豹', id: 2 },
   { title: '电影演出', id: 3 },
-  { title: '视频权益', id: 4 },
-  { title: '大牌点餐', id: 5 },
+  // { title: '视频权益', id: 4 },
+  // { title: '大牌点餐', id: 5 },
   { title: '加油', id: 6 },
-  { title: '酒店民宿', id: 7 },
-  { title: '代驾', id: 8 },
+  // { title: '酒店民宿', id: 7 },
+  // { title: '代驾', id: 8 },
 ])
 
 watch(() => cartList.value, async () => {
@@ -49,53 +49,53 @@ async function handleSelectAddress() {
       safe-area-inset-top fixed
     >
       <template #left>
-        <view class="ml10rpx flex items-center" @click.stop="handleSelectAddress">
-          <view class="mr20rpx text-36rpx font-semibold">
+        <view class="ml-10rpx flex items-center" @click.stop="handleSelectAddress">
+          <view class="mr-20rpx text-36rpx font-semibold">
             购物车
           </view>
-          <image :src="`${StaticUrl}/location-green.png`" class="h33.8rpx w29rpx" />
-          <view class="ml10rpx max-w-280rpx truncate text-32rpx text-#222 font-semibold">
+          <image :src="`${StaticUrl}/location-green.png`" class="h-33.8rpx w-29rpx" />
+          <view class="ml-10rpx max-w-280rpx truncate text-32rpx text-[#222] font-semibold">
             {{ smqjhSelectedAddress?.city || '请选择地址' }}
           </view>
         </view>
       </template>
     </wd-navbar>
-    <view class="xsb-linear h406rpx" />
+    <view class="xsb-linear h-406rpx" />
     <view class="-mt220rpx">
-      <view class="flex items-center justify-between pb16rpx">
-        <view class="w85%">
+      <view class="flex items-center justify-between pb-16rpx">
+        <view class="w-[85%]">
           <wd-tabs v-model="tab">
             <block v-for="tabs in navList" :key="tabs.id">
               <wd-tab :title="tabs.title" />
             </block>
           </wd-tabs>
         </view>
-        <view class="guanli h80rpx w104rpx flex flex-shrink-0 items-center justify-center text-28rpx font-semibold">
+        <view class="guanli h-80rpx w-104rpx flex flex-shrink-0 items-center justify-center text-28rpx font-semibold">
           管理
         </view>
       </view>
-      <scroll-view scroll-y class="content box-border px24rpx">
-        <view v-for="shop in cartList" :key="shop.shopId" class="mb24rpx rounded-16rpx bg-white px24rpx pb18rpx pt28rpx">
+      <scroll-view scroll-y class="content box-border px-24rpx">
+        <view v-for="shop in cartList" :key="shop.shopId" class="mb-24rpx rounded-16rpx bg-white px-24rpx pb-18rpx pt-28rpx">
           <wd-checkbox v-model="shop.AllShopGoods" size="large" @change="cartStore.cartStoreAllChecked($event, shop)">
             <view class="text-28rpx font-semibold">
               {{ shop.shopName }}
             </view>
           </wd-checkbox>
-          <view class="mt20rpx h2rpx w-full bg-#F0F0F0" />
+          <view class="mt-20rpx h-2rpx w-full bg-[#F0F0F0]" />
           <wd-checkbox-group v-model="shop.allGoods" size="large" @change="cartStore.cartGoodsChecked($event, shop)">
             <view
-              v-for="item in shop.skuList" :key="item.id" class="relative mt20rpx flex items-center"
+              v-for="item in shop.skuList" :key="item.id" class="relative mt-20rpx flex items-center"
             >
-              <view class="mr20rpx h32rpx w32rpx">
+              <view class="mr-20rpx h-32rpx w-32rpx">
                 <wd-checkbox :model-value="item.id" />
               </view>
               <view class="flex flex-1">
                 <image
                   :src="item.pic"
-                  class="h206rpx w200rpx flex-shrink-0"
+                  class="h-206rpx w-200rpx flex-shrink-0"
                   @click.stop="router.push({ name: 'xsb-goods', params: { id: String(item.prodId) } })"
                 />
-                <view class="ml20rpx flex-1">
+                <view class="ml-20rpx flex-1">
                   <view class="text-left text-28rpx font-semibold">
                     <!-- <view v-for="i in 2" :key="i" class="mr5px inline-block">
                         <wd-tag type="danger">
@@ -104,26 +104,26 @@ async function handleSelectAddress() {
                       </view> -->
                     {{ item.skuName }}
                   </view>
-                  <view class="mt14rpx text-24rpx text-#AAAAAA">
+                  <view class="mt-14rpx text-24rpx text-[#AAAAAA]">
                     规格:{{ item.spec }}
                   </view>
-                  <view class="mt14rpx flex items-center justify-between">
-                    <view class="text-36rpx text-#FF4A39 font-semibold">
+                  <view class="mt-14rpx flex items-center justify-between">
+                    <view class="text-36rpx text-[#FF4A39] font-semibold">
                       ¥{{ item.price }}
                     </view>
                     <!-- <wd-input-number v-model="item.num" disable-input @change="handleChangeNum($event, item)" /> -->
                     <view class="flex items-center">
                       <image
                         :src="` ${StaticUrl}/sub-cart.png`"
-                        class="h44rpx w44rpx"
+                        class="h-44rpx w-44rpx"
                         @click.stop="cartStore.cartSubGoods(item)"
                       />
-                      <view class="box-border h44rpx w84rpx flex items-center justify-center border border-#F0F0F0 border-solid text-24rpx text-#AAAAAA">
+                      <view class="box-border h-44rpx w-84rpx flex items-center justify-center border border-[#F0F0F0] border-solid text-24rpx text-[#AAAAAA]">
                         {{ item.num }}
                       </view>
                       <image
                         :src="` ${StaticUrl}/add-cart.png`"
-                        class="h44rpx w44rpx"
+                        class="h-44rpx w-44rpx"
                         @click.stop="cartStore.cartAddGoods(item)"
                       />
                     </view>
@@ -131,12 +131,12 @@ async function handleSelectAddress() {
                 </view>
               </view>
               <view v-if="item.shopSkuStocks == '0'" class="absolute left-0 top-0 z-1 h-full w-full flex items-center justify-center bg-[rgba(255,255,255,.6)]">
-                <view class="rounded-16rpx bg-[rgba(0,0,0,.5)] p20rpx text-white">
+                <view class="rounded-16rpx bg-[rgba(0,0,0,.5)] p-20rpx text-white">
                   商品已售罄
                 </view>
               </view>
               <view v-if="item.isDelete == '1'" class="absolute left-0 top-0 z-1 h-full w-full flex items-center justify-center bg-[rgba(255,255,255,.6)]">
-                <view class="rounded-16rpx bg-[rgba(0,0,0,.5)] p20rpx text-white">
+                <view class="rounded-16rpx bg-[rgba(0,0,0,.5)] p-20rpx text-white">
                   商品已删除
                 </view>
               </view>
@@ -145,9 +145,9 @@ async function handleSelectAddress() {
         </view>
 
         <view v-if="!cartList.length" class="box-border w-full flex items-center justify-center">
-          <view class="mt220rpx flex flex-col items-center">
-            <image :src="`${StaticUrl}/cart.png`" class="h110rpx w110rpx" />
-            <view class="mb20rpx mt20rpx text-24rpx">
+          <view class="mt-220rpx flex flex-col items-center">
+            <image :src="`${StaticUrl}/cart.png`" class="h-110rpx w-110rpx" />
+            <view class="mb-20rpx mt-20rpx text-24rpx">
               你还没有添加商品哦~
             </view>
             <wd-button plain @click=" useTabbar().setTabbarItemActive('smqjh-home'), router.pushTab({ name: 'smqjh-home' })">
@@ -155,38 +155,38 @@ async function handleSelectAddress() {
             </wd-button>
           </view>
         </view>
-        <view class="h140rpx" />
+        <view class="h-140rpx" />
       </scroll-view>
     </view>
-    <view v-if="cartList.length" class="fixedShadow fixed bottom-60rpx left-0 z-99 box-border w-full flex items-center justify-between rounded-t-16rpx bg-white px24rpx pb60rpx pt10rpx">
+    <view v-if="cartList.length" class="fixedShadow fixed bottom-60rpx left-0 z-99 box-border w-full flex items-center justify-between rounded-t-16rpx bg-white px-24rpx pb-60rpx pt-10rpx">
       <view class="ios w-full flex items-center justify-between">
         <view class="flex items-center">
           <image
             :src="`${StaticUrl}/cart-lanzi.png`"
-            class="h100rpx w100rpx"
+            class="h-100rpx w-100rpx"
           />
-          <view class="ml16rpx flex items-center">
+          <view class="ml-16rpx flex items-center">
             <wd-checkbox v-model="isCartAllChecked" size="large" @change="cartStore.cartAllChecked">
               全选
             </wd-checkbox>
-            <view class="ml10rpx text-24rpx text-#FF4A39" @click="cartStore.cartDeleteGoods">
+            <view class="ml-10rpx text-24rpx text-[#FF4A39]" @click="cartStore.cartDeleteGoods">
               删除
             </view>
           </view>
         </view>
         <view class="flex items-center">
           <view class="flex items-center font-semibold">
-            <view class="text-22rpx text-#222">
+            <view class="text-22rpx text-[#222]">
               总计:
             </view>
-            <view class="flex items-baseline text-24rpx text-#FF4A39">
+            <view class="flex items-baseline text-24rpx text-[#FF4A39]">
               <text class="text-36rpx">
                 {{ totalProduct?.price || '0.00' }}
               </text>
             </view>
           </view>
-          <view class="ml20rpx w160rpx">
+          <view class="ml-20rpx w-160rpx">
             <wd-button block size="large" @click="cartStore.cartOrderConfirm">
               结算
             </wd-button>

+ 27 - 27
src/pages/index/index.vue

@@ -57,7 +57,7 @@ const navList = computed(() => {
   const list = [
     { icon: `${StaticUrl}/xsb.png`, title: '星闪豹', name: 'xsb-homeTabbar', show: true },
     { icon: `${StaticUrl}/smqjh-index-cd.png`, title: '充电', name: 'charge-index', show: true }, // !isOnlineAudit.value
-    { icon: `${StaticUrl}/smqjh-sp.png`, title: '电影演出', name: '', show: !isOnlineAudit.value },
+    { icon: `${StaticUrl}/smqjh-sp.png`, title: '电影演出', name: 'film-index', show: true },
     { icon: `${StaticUrl}/smqjh-vip.png`, title: '视频权益', name: 'video-rights-tabbar', show: true },
     { icon: `${StaticUrl}/smqjh-diancan.png`, title: '大牌点餐', name: '', show: !isOnlineAudit.value },
     { icon: `${StaticUrl}/smqjh-jiayou.png`, title: '加油', name: '', show: !isOnlineAudit.value },
@@ -99,40 +99,40 @@ function handleGo() {
       safe-area-inset-top fixed
     >
       <template #left>
-        <view class="relative z-10 h48rpx w-full w202rpx opacity-100">
-          <image class="absolute left-0 top-0 h48rpx w202rpx" :src="`${StaticUrl}/logo.png`" />
+        <view class="relative z-10 h-48rpx w-202rpx w-full opacity-100">
+          <image class="absolute left-0 top-0 h-48rpx w-202rpx" :src="`${StaticUrl}/logo.png`" />
         </view>
       </template>
     </wd-navbar>
 
     <view
-      class="header-linear h320rpx px24rpx"
+      class="header-linear h-320rpx px-24rpx"
       :style="{ paddingTop: `${(Number(statusBarHeight) || 44) + MenuButtonHeight + 12}px` }"
     >
       <view class="box-border flex items-center">
         <view class="flex items-center" @click="useAddressStore().getMapAddress()">
-          <image :src="`${StaticUrl}/location-black.png`" class="h33.8rpx min-w28.97rpx w28.97rpx" />
-          <view class="ml18rpx max-w-180rpx truncate text-32rpx">
+          <image :src="`${StaticUrl}/location-black.png`" class="h-33.8rpx min-w-28.97rpx w-28.97rpx" />
+          <view class="ml-18rpx max-w-180rpx truncate text-32rpx">
             {{ name }}
           </view>
         </view>
-        <view class="ml16rpx h60rpx w502rpx flex items-center justify-between rounded-40rpx bg-white pr6rpx">
-          <view class="flex items-center pb14rpx pl24rpx pt16rpx">
+        <view class="ml-16rpx h-60rpx w-502rpx flex items-center justify-between rounded-40rpx bg-white pr-6rpx">
+          <view class="flex items-center pb-14rpx pl-24rpx pt-16rpx">
             <wd-icon name="search" size="14" color="#ccc" />
-            <view class="ml12rpx text-24rpx text-gray">
+            <view class="ml-12rpx text-24rpx text-gray">
               霸王茶姬
             </view>
           </view>
           <view
-            class="h50rpx w96rpx flex items-center justify-center rounded-26rpx bg-[var(--them-color)] text-24rpx text-white font-semibold"
+            class="h-50rpx w-96rpx flex items-center justify-center rounded-26rpx bg-[var(--them-color)] text-24rpx text-white font-semibold"
           >
             搜索
           </view>
         </view>
       </view>
     </view>
-    <view class="px24rpx -mt260rpx">
-      <view class="mt24rpx rounded-16rpx bg-white py24rpx" :class="[loading ? 'px24rpx' : '']">
+    <view class="px-24rpx -mt260rpx">
+      <view class="mt-24rpx rounded-16rpx bg-white py-24rpx" :class="[loading ? 'px-24rpx' : '']">
         <wd-skeleton
           :loading="loading"
           theme="image" :row-col="[
@@ -151,14 +151,14 @@ function handleGo() {
 
           ]"
         >
-          <view class="grid grid-cols-4 gap12rpx">
+          <view class="grid grid-cols-4 gap-12rpx">
             <template v-for="item in navList" :key="item.icon">
               <view
                 v-if="item.show" class="flex flex-col items-center justify-center"
                 @click="handleClick(String(item.name))"
               >
-                <view class="relative h120rpx w120rpx">
-                  <image :src="item.icon" class="h120rpx w120rpx" />
+                <view class="relative h-120rpx w-120rpx">
+                  <image :src="item.icon" class="h-120rpx w-120rpx" />
                   <view
                     v-if="item.name == ''"
                     class="linebg absolute left-0 top-0 h-full w-full flex items-center justify-center rounded-32rpx text-24rpx text-white font-semibold"
@@ -175,14 +175,14 @@ function handleGo() {
         </wd-skeleton>
       </view>
 
-      <view class="mt20rpx w-full flex items-center justify-between" @click="handleGo">
-        <image :src="`${StaticUrl}/smqjh-fl.png`" class="h346rpx w344rpx flex-shrink-0" />
+      <view class="mt-20rpx w-full flex items-center justify-between" @click="handleGo">
+        <image :src="`${StaticUrl}/smqjh-fl.png`" class="h-346rpx w-344rpx flex-shrink-0" />
         <view class="flex flex-1 flex-col items-end justify-center">
-          <image :src="`${StaticUrl}/smqjh-hot-vip.png`" class="h180rpx w344rpx" />
-          <image :src="`${StaticUrl}/smqjh-cd.png`" class="mt14rpx h152rpx w344rpx" />
+          <image :src="`${StaticUrl}/smqjh-hot-vip.png`" class="h-180rpx w-344rpx" />
+          <image :src="`${StaticUrl}/smqjh-cd.png`" class="mt-14rpx h-152rpx w-344rpx" />
         </view>
       </view>
-      <view class="mt20rpx">
+      <view class="mt-20rpx">
         <wd-skeleton
           theme="image" animation="gradient" :loading="loading"
           :row-col="[[{ height: '568rpx', width: '344rpx' }, { height: '568rpx', width: '344rpx' }], [{ height: '568rpx', width: '344rpx' }, { height: '568rpx', width: '344rpx' }]]"
@@ -192,13 +192,13 @@ function handleGo() {
               <grid-view type="masonry" cross-axis-count="2" main-axis-gap="10" cross-axis-gap="10">
                 <view
                   v-for="item in goodsList" :key="item.id"
-                  class="relative overflow-hidden rounded-16rpx bg-white pb16rpx"
+                  class="relative overflow-hidden rounded-16rpx bg-white pb-16rpx"
                 >
                   <view @click="router.push({ name: 'xsb-goods', params: { id: item.prodId } })">
-                    <view class="relative h344rpx">
-                      <image :src="item.pic" class="h344rpx w344rpx" />
+                    <view class="relative h-344rpx">
+                      <image :src="item.pic" class="h-344rpx w-344rpx" />
                       <view class="absolute bottom-0 left-0 flex items-center text-22rpx">
-                        <view class="rounded-tl-8rpx bg-#02ECFD pl4rpx">
+                        <view class="rounded-tl-8rpx bg-[#02ECFD] pl-4rpx">
                           星闪豹
                         </view>
                         <!-- <view class="rounded-tr-8rpx bg-[rgba(0,0,0,0.5)] pl8rpx pr4rpx text-white">
@@ -206,7 +206,7 @@ function handleGo() {
                     </view> -->
                       </view>
                     </view>
-                    <view class="mt16rpx pl20rpx pr6rpx">
+                    <view class="mt-16rpx pl-20rpx pr-6rpx">
                       <view class="flex items-center">
                         <!-- <view class="rounded-8rpx bg-#FF4D3A px12rpx text-24rpx text-white">
                       补贴
@@ -215,7 +215,7 @@ function handleGo() {
                           {{ item.prodName }}
                         </view>
                       </view>
-                      <view class="mt20rpx flex items-end text-#FF4D3A">
+                      <view class="mt-20rpx flex items-end text-[#FF4D3A]">
                         <view class="text-24rpx">
                         </view>
@@ -233,7 +233,7 @@ function handleGo() {
                     class="absolute left-0 top-0 z-1 h-full w-full flex items-center justify-center bg-[rgba(255,255,255,0.6)]"
                   >
                     <view
-                      class="h156rpx w156rpx flex items-center justify-center rounded-full bg-[rgba(0,0,0,.6)] text-28rpx text-white"
+                      class="h-156rpx w-156rpx flex items-center justify-center rounded-full bg-[rgba(0,0,0,.6)] text-28rpx text-white"
                     >
                       商品已售罄
                     </view>

+ 20 - 20
src/pages/my/index.vue

@@ -40,15 +40,15 @@ function handleGo(item: { name: string }) {
 </script>
 
 <template>
-  <view class="page-class bg-#F9F9F9 dark:bg-[var(--wot-dark-background)]">
+  <view class="page-class bg-[#F9F9F9] dark:bg-[var(--wot-dark-background)]">
     <wd-navbar
       title="个人中心" custom-style="background-color: transparent !important;" :bordered="false"
       safe-area-inset-top fixed :z-index="99"
     />
-    <view class="header relative h-408rpx rounded-18px">
-      <view class="absolute bottom-100rpx left-0 box-border w-full flex items-center justify-between pl48rpx pr58rpx">
+    <view class="header relative h-408rpx rounded-[18px]">
+      <view class="absolute bottom-100rpx left-0 box-border w-full flex items-center justify-between pl-48rpx pr-58rpx">
         <template v-if="!token">
-          <image :src="`${StaticUrl}/9.png`" alt="" class="h100rpx w100rpx" />
+          <image :src="`${StaticUrl}/9.png`" alt="" class="h-100rpx w-100rpx" />
           <view class="text-32rpx font-semibold">
             请登录后使用完整功能
           </view>
@@ -58,12 +58,12 @@ function handleGo(item: { name: string }) {
         </template>
         <template v-else>
           <view class="flex items-center">
-            <image :src="getUserAvatar" alt="" class="h100rpx w100rpx flex-shrink-0 rounded-full" />
-            <view class="ml20rpx flex-1">
+            <image :src="getUserAvatar" alt="" class="h-100rpx w-100rpx flex-shrink-0 rounded-full" />
+            <view class="ml-20rpx flex-1">
               <view class="text-32rpx font-semibold">
                 {{ userInfo.nickName }}
               </view>
-              <view class="mt12rpx rounded-8rpx bg-white px12rpx py4rpx text-24rpx text-[var(--them-color)] opacity-70">
+              <view class="mt-12rpx rounded-8rpx bg-white px-12rpx py-4rpx text-24rpx text-[var(--them-color)] opacity-70">
                 {{ userInfo.channelName }}
               </view>
             </view>
@@ -71,9 +71,9 @@ function handleGo(item: { name: string }) {
           <view class="flex flex-col items-center" @click="router.push({ name: 'common-user-center' })">
             <image
               :src="`${StaticUrl}/user-setting.png`"
-              class="h48rpx w48rpx"
+              class="h-48rpx w-48rpx"
             />
-            <view class="mt12rpx text-24rpx text-[var(--them-color)]">
+            <view class="mt-12rpx text-24rpx text-[var(--them-color)]">
               账户设置
             </view>
           </view>
@@ -97,29 +97,29 @@ function handleGo(item: { name: string }) {
         </template>
         <view class="grid grid-cols-4 gap-4">
           <view v-for="item in tabList" :key="item.title" class="flex flex-col items-center justify-center" @click="handleGo(item)">
-            <image :src="item.icon" class="h56rpx w56rpx" />
-            <view class="mt20rpx text-24rpx">
+            <image :src="item.icon" class="h-56rpx w-56rpx" />
+            <view class="mt-20rpx text-24rpx">
               {{ item.title }}
             </view>
           </view>
         </view>
-        <view class="h20rpx" />
+        <view class="h-20rpx" />
       </wd-card>
     </view>
-    <view class="item-cell mt20rpx">
+    <view class="item-cell mt-20rpx">
       <wd-card custom-class="card">
         <wd-cell-group custom-class="cell-group">
           <wd-cell title="收货地址" custom-title-class="cell-title" clickable is-link @click="router.push({ name: 'common-addressList' })">
             <template #icon>
-              <image :src="`${StaticUrl}/4.png`" class="h50rpx w50rpx" />
+              <image :src="`${StaticUrl}/4.png`" class="h-50rpx w-50rpx" />
             </template>
           </wd-cell>
           <wd-cell custom-title-class="cell-title" clickable is-link>
             <template #icon>
-              <image :src="`${StaticUrl}/5.png`" class="h50rpx w50rpx" />
+              <image :src="`${StaticUrl}/5.png`" class="h-50rpx w-50rpx" />
             </template>
             <template #title>
-              <view class="ml20rpx w90%">
+              <view class="ml-20rpx w-[90%]">
                 <Zcontact>联系平台客服</Zcontact>
               </view>
             </template>
@@ -127,23 +127,23 @@ function handleGo(item: { name: string }) {
         </wd-cell-group>
       </wd-card>
     </view>
-    <view class="item-cell mt20rpx">
+    <view class="item-cell mt-20rpx">
       <wd-card custom-class="card">
         <wd-cell-group custom-class="cell-group">
           <wd-cell title="积分" custom-title-class="cell-title" clickable is-link @click="router.push({ name: 'common-integral' })">
             <template #icon>
-              <image :src="`${StaticUrl}/7.png`" class="h50rpx w50rpx" />
+              <image :src="`${StaticUrl}/7.png`" class="h-50rpx w-50rpx" />
             </template>
           </wd-cell>
           <wd-cell title="评价" custom-title-class="cell-title" clickable is-link>
             <template #icon>
-              <image :src="`${StaticUrl}/8.png`" class="h50rpx w50rpx" />
+              <image :src="`${StaticUrl}/8.png`" class="h-50rpx w-50rpx" />
             </template>
           </wd-cell>
         </wd-cell-group>
       </wd-card>
     </view>
-    <view v-if="token" class="mt80rpx flex items-center justify-center">
+    <view v-if="token" class="mt-80rpx flex items-center justify-center">
       <wd-button @click="handleLoginOut">
         退出登录
       </wd-button>

+ 11 - 3
src/shims.d.ts

@@ -1,6 +1,14 @@
+import type Vue from 'vue'
+
 export {}
 
-declare module 'vue' {
-  type Hooks = App.AppInstance & Page.PageInstance
-  interface ComponentCustomOptions extends Hooks {}
+// declare module 'vue' {
+//   type Hooks = App.AppInstance & Page.PageInstance
+//   interface ComponentCustomOptions extends Hooks {}
+// }
+
+declare module '*.vue' {
+  // 关键:Vue2 组件的正确类型声明(兼容 Vue2 选项式 API)
+  const component: Vue.ComponentOptions<Vue>
+  export default component
 }

+ 3 - 0
src/store/address.ts

@@ -12,6 +12,8 @@ interface addressState {
     longitude: number | null
   }
   name: string
+  city: string
+
 }
 export const useAddressStore = defineStore('address', {
   state: (): addressState => ({
@@ -20,6 +22,7 @@ export const useAddressStore = defineStore('address', {
       longitude: null,
     },
     name: '请选择位置',
+    city: '',
   }),
   actions: {
   // 获取地理位置的函数

+ 2 - 52
src/subPack-charge/chargeOrderList/chargeOrderList.vue

@@ -1,9 +1,8 @@
 <script setup lang="ts">
 import { navTabTypeList } from '../utils/order-data'
-import { chargeOrderStatus } from '../utils/index'
+import chargeList from '@/subPack-smqjh/components/charge-orderList/charge-orderList.vue?async'
 import router from '@/router'
 import { createGlobalLoadingMiddleware } from '@/api/core/middleware'
-import { StaticUrl } from '@/config'
 
 const navActiveTab = ref(0)
 const { statusBarHeight, MenuButtonHeight, opcity } = storeToRefs(useSysStore())
@@ -49,20 +48,6 @@ function handleChangeTypeNav(value: number) {
   navActiveTab.value = value
   orderList.value = []
   reload()
-  console.log(navActiveTab.value, '===============')
-}
-
-function handleItemClick(item: Api.xsbOrderList) {
-  const orderNumber = item.orderNumber
-  if (orderNumber) {
-    router.push({
-      name: 'charge-order-detail',
-      params: { orderNo: orderNumber },
-    })
-  }
-  else {
-    console.warn('订单号不存在,无法跳转到详情页')
-  }
 }
 </script>
 
@@ -89,42 +74,7 @@ function handleItemClick(item: Api.xsbOrderList) {
       </view>
     </view>
     <view class="box-border px24rpx">
-      <view v-for="item in orderList" :key="item.orderNumber" class="mt-20rpx mt20rpx rounded-16rpx bg-#FFF p-24rpx" @click="handleItemClick(item)">
-        <view class="flex items-center justify-between">
-          <view>
-            <view class="text-28rpx font-bold">
-              {{ item.powerStationName }}
-            </view>
-            <view class="mt-20rpx text-24rpx text-#AAA">
-              订单时间:{{ item.createTime || '未知' }}
-            </view>
-            <view class="mt-20rpx text-24rpx text-#AAA">
-              终端编号:{{ item.connectorId || '未知' }}
-            </view>
-          </view>
-          <view v-if="item.status" class="text-center text-28rpx text-#4EDC86">
-            <image
-              class="h-64rpx w-64rpx"
-              :src="`${StaticUrl}/${chargeOrderStatus(item.status).icon}.png`"
-            />
-            <view :style="{ color: chargeOrderStatus(item.status).color }">
-              {{ chargeOrderStatus(item.status).text }}
-            </view>
-          </view>
-        </view>
-        <view class="my-20rpx h-2rpx w-full bg-#F0F0F0" />
-        <view class="flex items-center justify-between">
-          <view class="text-28rpx">
-            充电费用:
-          </view>
-          <view class="text-32rpx text-#FF6464 font-800">
-            <text class="text-18rpx">
-              ¥
-            </text>
-            {{ item.actualTotal }}
-          </view>
-        </view>
-      </view>
+      <chargeList :order-list="orderList" />
       <StatusTip v-if="!orderList.length" tip="暂无内容" />
     </view>
   </view>

+ 610 - 0
src/subPack-film/choose-film/index.vue

@@ -0,0 +1,610 @@
+<script setup lang="ts">
+import { ref, watch } from 'vue'
+import selectTime from '../components/choose-time.vue'
+import { timeFormat } from '../utils/index'
+import { StaticUrl } from '@/config'
+
+interface brand {
+  id: number
+  brandId: string
+  brandName: string
+}
+const router = useRouter()
+definePage({
+  name: 'film-choose-film',
+  islogin: false,
+  style: {
+    navigationStyle: 'custom',
+    navigationBarTitleText: '我的订单',
+    backgroundColorBottom: '#fff',
+  },
+})
+
+onLoad((options) => {
+  console.log('onload', options)
+})
+const addressStore = useAddressStore()
+const { Location } = storeToRefs(addressStore)
+const query = ref({
+  cityId: '103',
+  showDates: timeFormat(new Date().getTime(), 'yyyy-MM-dd'),
+  lat: Location.value.latitude,
+  lng: Location.value.longitude,
+  districtId: '',
+  brandId: '',
+  hall: '',
+  movieId: '',
+})
+// const tab = ref(timeFormat(new Date().getTime(), 'yyyy-mm-dd'))
+const dayList = ref([])
+const active = ref<number | null>(null)
+
+const filmList = ref<Api.filmList[]>([])
+const cityList = ref<Api.filmCityList>([])
+const brandList = ref<brand[]>()
+const currentCity = ref(0)
+const show = ref(false)
+const info = ref<Api.filmMovieList>({})
+watch(() => show.value, (val) => {
+  if (!val) {
+    active.value = null
+  }
+})
+function handleChoose(index: number) {
+  show.value = true
+  active.value = index
+}
+
+function close() {
+  show.value = false
+}
+
+function choose(cityId: string, index?: number, districtId?: string) {
+  if (active.value === 0) {
+    query.value.cityId = cityId
+    query.value.districtId = districtId || ''
+    if (index) {
+      currentCity.value = index
+    }
+  }
+  else if (active.value === 1) {
+    query.value.brandId = cityId
+  }
+  // else if (active.value === 2) {
+
+  // }
+  show.value = false
+
+  getData()
+}
+
+function handleFilm(title: string = '保利万和学府影城', cinemaId: string) {
+  console.log(title)
+
+  router.push({
+    name: 'film-select-time',
+    params: {
+      cinemaId,
+      movieId: query.value.movieId,
+      title,
+    },
+  })
+}
+
+async function getData() {
+  uni.showLoading({ title: '加载中' })
+  console.log('qingind', query.value)
+
+  const res = await Apis.film.getFilmList({ data: query.value })
+  uni.hideLoading()
+  if (res.data) {
+    filmList.value = res.data
+  }
+}
+
+async function getDate(movieId: string) {
+  const res = await Apis.film.getFilmDateList({ data: { movieId } })
+  dayList.value = res.data
+}
+
+function changeTime() {
+  getData()
+}
+async function getCityList() {
+  const res = await Apis.film.getCityList({})
+  cityList.value = res.data
+  res.data.forEach((item: any, index: number) => {
+    if (item.name.includes('贵阳')) {
+      currentCity.value = index
+    }
+  })
+}
+getCityList()
+async function getFilmBrandList() {
+  const res = await Apis.film.getFilmBrandList({})
+  brandList.value = res.data
+}
+getFilmBrandList()
+
+onLoad((options) => {
+  query.value.movieId = options?.movieId
+  info.value = JSON.parse(options?.info)
+  getDate(options?.movieId)
+  getData()
+})
+</script>
+
+<template>
+  <view class="choose-film">
+    <wd-navbar
+      title="" custom-style="background-color: transparent;" :bordered="false" :z-index="99"
+      safe-area-inset-top left-arrow fixed @click-left="router.back()"
+    />
+    <view class="bg-box">
+      <image class="img" :src="info.posterUrl" mode="scaleToFill" />
+    </view>
+    <view class="info-box">
+      <view class="info">
+        <image class="icon" :src="info.posterUrl" />
+        <view class="box">
+          <view class="name">
+            {{ info.name }}
+          </view>
+          <view class="score-box">
+            <view class="score">
+              评分 <view class="num">
+                {{ info.score }}
+              </view>
+            </view>
+            <view class="want-num">
+              {{ info.wish }}人想看
+            </view>
+          </view>
+
+          <view class="type-box">
+            {{ info.category }}
+            <view class="tag">
+              {{ info.version }}
+            </view>
+          </view>
+          <view class="publish-info">
+            时长: {{ info.duration }}分钟
+          </view>
+          <view class="publish-info">
+            上映:{{ info.releaseTime }}
+          </view>
+          <view class="publish-info">
+            导演:{{ info.director }}
+          </view>
+          <view class="publish-info">
+            演员:{{ info.star }}
+          </view>
+        </view>
+      </view>
+    </view>
+    <view class="px-24rpx">
+      <!-- 影院时间 -->
+      <selectTime v-model="query.showDates" :data="dayList" @change="changeTime" />
+      <view class="choose-list">
+        <view class="choose-item" :class="[active == 0 ? 'active' : '']" @click="handleChoose(0)">
+          <view class="choose-item-title">
+            全城
+          </view>
+          <image
+            class="icon" :src="StaticUrl + (active == 0 ? '/film-choose-active.png' : '/film-choose.png')"
+            mode="scaleToFill"
+          />
+        </view>
+        <view class="choose-item" :class="[active == 1 ? 'active' : '']" @click="handleChoose(1)">
+          <view class="choose-item-title">
+            品牌
+          </view>
+          <image
+            class="icon" :src="StaticUrl + (active == 1 ? '/film-choose-active.png' : '/film-choose.png')"
+            mode="scaleToFill"
+          />
+        </view>
+        <view class="choose-item" :class="[active == 2 ? 'active' : '']" @click="handleChoose(2)">
+          <view class="choose-item-title">
+            特色
+          </view>
+          <image
+            class="icon" :src="StaticUrl + (active == 2 ? '/film-choose-active.png' : '/film-choose.png')"
+            mode="scaleToFill"
+          />
+        </view>
+      </view>
+    </view>
+
+    <view class="film-list">
+      <view
+        v-for="(item, index) in filmList" :key="index" class="film-item"
+        @click="handleFilm(item.name, item.cinemaId)"
+      >
+        <view class="name-box">
+          <view class="name w-450rpx overflow-hidden text-ellipsis whitespace-nowrap">
+            {{ item.name }}
+          </view>
+          <view class="price-box">
+            <view class="num">
+              ¥{{ item.sellPrice }}
+            </view>
+            <div class="label">
+              起
+            </div>
+          </view>
+        </view>
+        <view class="address-box">
+          <view class="address overflow-hidden text-ellipsis whitespace-nowrap">
+            {{ item.address }}
+          </view>
+          <view class="distance">
+            {{ item.distance }}km
+          </view>
+        </view>
+        <div class="cantact">
+          {{ item.phone || '暂无联系方式' }}
+        </div>
+        <div class="time-box">
+          近期场次:{{ item.showInfo }}
+        </div>
+      </view>
+    </view>
+
+    <wd-loadmore state="finished" :loading-props="{ color: '#9ED605', size: 20 }" />
+
+    <!-- 弹窗 -->
+    <wd-action-sheet v-if="show" v-model="show" @close="close">
+      <view class="content">
+        <!-- 影院时间 -->
+        <selectTime v-model="query.showDates" :data="dayList" @change="changeTime" />
+        <view class="choose-list">
+          <view class="choose-item" :class="[active == 0 ? 'active' : '']" @click="handleChoose(0)">
+            <view class="choose-item-title">
+              全城
+            </view>
+            <image
+              class="icon" :src="StaticUrl + (active == 0 ? '/film-choose-active.png' : '/film-choose.png')"
+              mode="scaleToFill"
+            />
+          </view>
+          <view class="choose-item" :class="[active == 1 ? 'active' : '']" @click="handleChoose(1)">
+            <view class="choose-item-title">
+              品牌
+            </view>
+            <image
+              class="icon" :src="StaticUrl + (active == 1 ? '/film-choose-active.png' : '/film-choose.png')"
+              mode="scaleToFill"
+            />
+          </view>
+          <view class="choose-item" :class="[active == 2 ? 'active' : '']" @click="handleChoose(2)">
+            <view class="choose-item-title">
+              特色
+            </view>
+            <image
+              class="icon" :src="StaticUrl + (active == 2 ? '/film-choose-active.png' : '/film-choose.png')"
+              mode="scaleToFill"
+            />
+          </view>
+        </view>
+        <view v-if="active == 0" class="choose-box">
+          <view class="city-content">
+            <view
+              v-for="(item, index) in cityList" :key="index" class="name"
+              :class="[query.cityId == item.cityId ? 'active' : '']" @click="choose(item.cityId, index as number)"
+            >
+              {{ item.name }}
+            </view>
+          </view>
+          <view class="choose-content">
+            <view class="choose-item" @click="choose(query.cityId)">
+              <view class="name">
+                全城
+              </view>
+              <image
+                v-if="query.districtId === ''" class="icon" :src="`${StaticUrl}/film-choose-icon.png`"
+                mode="scaleToFill"
+              />
+            </view>
+            <view
+              v-for="(item, index) in cityList[currentCity].districts" :key="index" class="choose-item"
+              @click="choose(item.cityId, currentCity, item.districtId)"
+            >
+              <view class="name" :class="[query.districtId == item.districtId ? 'active' : '']">
+                {{ item.districtName }}
+              </view>
+              <image
+                v-if="query.districtId == item.districtId" class="icon" :src="`${StaticUrl}/film-choose-icon.png`"
+                mode="scaleToFill"
+              />
+            </view>
+          </view>
+        </view>
+        <view v-if="active == 1" class="choose-box">
+          <view class="choose-content">
+            <view class="choose-item" @click="choose('')">
+              <view class="name">
+                全部
+              </view>
+              <image
+                v-if="query.brandId === ''" class="icon" :src="`${StaticUrl}/film-choose-icon.png`"
+                mode="scaleToFill"
+              />
+            </view>
+            <view v-for="(item, index) in brandList" :key="index" class="choose-item" @click="choose(item.brandId)">
+              <view class="name" :class="[query.brandId == item.brandId ? 'active' : '']">
+                {{ item.brandName }}
+              </view>
+              <image
+                v-if="query.brandId == item.brandId" class="icon" :src="`${StaticUrl}/film-choose-icon.png`"
+                mode="scaleToFill"
+              />
+            </view>
+          </view>
+        </view>
+      </view>
+    </wd-action-sheet>
+  </view>
+</template>
+
+<style lang="scss" scoped>
+.choose-film{
+  background: #fff;
+  min-height: 100vh;
+
+  .bg-box {
+      width: 100%;
+      height: 596rpx;
+      position: relative;
+
+      .img {
+        width: 100%;
+        height: 596rpx;
+        vertical-align: top;
+      }
+    }
+
+    .info-box {
+      padding: 24rpx;
+      padding-top: 300rpx;
+      border-radius: 16rpx 16rpx 16rpx 16rpx;
+      margin-top: -400rpx;
+      position: relative;
+      background: linear-gradient(179deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.8) 29.54%, #FFFFFF 46.99%, #FFFFFF 100%);
+
+      .info {
+        display: flex;
+        position: relative;
+
+        .icon {
+          width: 218rpx;
+          height: 298rpx;
+          background: #aaa;
+          border-radius: 16rpx;
+          flex-shrink: 0;
+        }
+
+        .box {
+          display: flex;
+          flex-direction: column;
+          justify-content: space-between;
+          margin-left: 20rpx;
+
+          .name {
+            font-weight: 600;
+            font-size: 28rpx;
+            color: #222222;
+            width: 400rpx;
+            white-space: nowrap;
+            overflow: hidden;
+            text-overflow: ellipsis;
+          }
+
+          .score-box {
+            font-size: 24rpx;
+            color: #AAAAAA;
+            position: absolute;
+            top: 0;
+            right: 0;
+            text-align: right;
+
+            .score {
+              display: flex;
+              align-items: center;
+              justify-content: flex-end;
+
+              .num {
+                font-size: 28rpx;
+                color: #FF4D3A;
+                padding: 0 12rpx;
+              }
+            }
+
+            .want-num {
+              padding: 0 12rpx;
+              align-self: flex-end;
+              margin-top: 24rpx;
+              // font-size: 24rpx;
+              // color: #AAAAAA;
+            }
+          }
+
+          .type-box {
+            display: flex;
+            align-items: center;
+            font-size: 24rpx;
+            color: #AAAAAA;
+
+            .tag {
+              padding: 6rpx 10rpx;
+              background: #F0F0F0;
+              border-radius: 8rpx 8rpx 8rpx 8rpx;
+              font-size: 24rpx;
+              color: #AAAAAA;
+              margin-left: 10rpx;
+            }
+          }
+
+          .publish-info {
+            font-size: 24rpx;
+            color: #AAAAAA;
+            white-space: nowrap;
+            text-overflow: ellipsis;
+            overflow: hidden;
+            max-width: 460rpx;
+          }
+        }
+      }
+
+      .title-box {
+        display: flex;
+        justify-content: space-between;
+        margin-top: 28rpx;
+
+        .title {
+          font-weight: 600;
+          font-size: 28rpx;
+          color: #222222;
+        }
+
+        .open {
+          display: flex;
+          align-items: center;
+          font-size: 24rpx;
+          color: #AAAAAA;
+
+          .icon {
+            width: 24rpx;
+            height: 24rpx;
+          }
+        }
+      }
+
+      .desc {
+        font-size: 24rpx;
+        color: #222222;
+        font-weight: 300;
+        margin-top: 20rpx;
+        line-height: 40rpx;
+      }
+    }
+
+  .choose-list{
+    display: flex;
+    padding: 24rpx 0;
+    background: #fff;
+    .choose-item{
+      display: flex;
+      align-items: center;
+      margin-right: 80rpx;
+      .choose-item-title{
+        font-weight: bold;
+          font-size: 32rpx;
+          color: #222222;
+      }
+      .icon{
+        width: 30rpx;
+        height: 30rpx;
+        margin-left: 12rpx;
+      }
+      &.active{
+        .choose-item-title{
+          color: #9ED605;
+        }
+      }
+    }
+
+  }
+
+  .film-list{
+    padding: 0 24rpx;
+    .film-item{
+      background: #F9F9F9;
+      border-radius: 16rpx 16rpx 16rpx 16rpx;
+      padding: 24rpx;
+      margin-bottom: 20rpx;
+      .name-box{
+        display: flex;
+        justify-content: space-between;
+        margin-bottom: 16rpx;
+        .name{
+          font-weight: bold;
+            font-size: 28rpx;
+            color: #222222;
+        }
+        .price-box{
+          display: flex;
+          align-items: center;
+          .num{
+            font-weight: bold;
+              font-size: 36rpx;
+              color: #FF4D3A;
+          }
+          .label{
+            font-size: 24rpx;
+            color: #AAAAAA;
+          }
+        }
+      }
+      .address-box{
+        display: flex;
+        justify-content: space-between;
+        font-size: 24rpx;
+        color: #AAAAAA;
+        margin-bottom: 16rpx;
+
+          .address {
+          font-size: 24rpx;
+          color: #AAAAAA;
+        }
+        .distance{
+
+        }
+      }
+      .cantact{
+        font-size: 24rpx;
+        color: #AAAAAA;
+        margin-bottom: 16rpx;
+      }
+      .time-box{
+        font-size: 24rpx;
+        color: #222222;
+      }
+    }
+  }
+  .content{
+    padding: 0 24rpx;
+    .choose-box{
+      min-height: 500rpx;
+      display: flex;
+      .city-content{
+        width: 30%;
+        .name{
+          font-size: 28rpx;
+          color: #222222;
+          padding: 14rpx 0;
+        }
+      }
+      .choose-content{
+        flex: 1;
+        .choose-item{
+          display: flex;
+          justify-content: space-between;
+          padding: 14rpx 0;
+          .name{
+            font-size: 28rpx;
+              color: #222222;
+          }
+          .icon{
+            width: 30rpx;
+              height: 30rpx;
+          }
+        }
+      }
+      .name.active{
+          color: #9ED605;
+      }
+    }
+  }
+}
+</style>

+ 117 - 0
src/subPack-film/choose-seat/components/pages/CanvasSeatmap.vue

@@ -0,0 +1,117 @@
+<script>
+import Seatmap from './main.js'
+
+export default {
+  name: 'CanvasSeatmap',
+  emits: ['onSeatTap', 'error'], // 添加 emits 声明
+  data() {
+    return {
+      pixelRatio: 1,
+      canvasWidth: 0,
+      canvasHeight: 0,
+      chooseSeatList: [],
+      colorList: [
+        '#ffc284', // 浅黄色
+        '#97cafc', // 浅蓝色
+        '#C1A3A3', // 干枯玫瑰
+        '#ffb0bc', // 浅粉色
+        '#b6e67f', // 浅绿色
+      ],
+      moveing: false,
+      options: [],
+    }
+  },
+  methods: {
+    async init({ seatList, options }) {
+      this.seatList = seatList
+      this.options = options
+      console.log(options)
+
+      // #ifdef MP-WEIXIN || H5 || MP-ALIPAY
+      this.onCanvasReady()
+      // #endif
+    },
+    async onCanvasReady() {
+      const canvasRect = await this.getElementRect()
+      const { windowWidth, windowHeight, pixelRatio } = uni.getWindowInfo()
+      this.canvasWidth = windowWidth
+      this.canvasHeight = windowHeight - (canvasRect.top || 0)
+      this.options = { ...this.options, ...{ pixelRatio, canvasWidth: this.canvasWidth, canvasHeight: this.canvasHeight }, topHeight: canvasRect.top || 0 }
+
+      uni.createSelectorQuery().in(this).select('#canvas-seatmap').node().exec((res) => {
+        const canvas = res[0].node
+        const { seatList, options } = this
+        // #ifndef H5
+        const ctx = canvas.getContext('2d')
+        const { pixelRatio, canvasWidth, canvasHeight } = options
+        canvas.width = canvasWidth * pixelRatio
+        canvas.height = canvasHeight * pixelRatio
+        ctx.scale(pixelRatio, pixelRatio)
+        // #endif
+        this.seatmapInstance = new Seatmap({
+          canvas,
+          seatList,
+          options,
+        })
+        console.log('onCanvasReady:画布初始化完成')
+      })
+    },
+    onTouchStart(e) {
+      this.seatmapInstance.onTouchStart(e)
+    },
+    onTouchMove(e) {
+      this.seatmapInstance.onTouchMove(e)
+    },
+    onTouchEnd(e) {
+      this.seatmapInstance.onTouchEnd(e)
+    },
+    onTap(e) {
+      console.log('onTap', e)
+      if (e.detail === undefined)
+        throw new Error('支付宝小程序开发者工具点击事件不返回坐标,请使用支付宝小程序真机调试')
+      let { x, y } = e.detail // 微信 h5是相对于文档的坐标 支付宝是相对于画布的坐标
+      // #ifdef MP-WEIXIN || H5
+      y -= this.options.topHeight
+      // #endif
+      this.seatmapInstance.onSeatTap({ x, y })
+        .then(({ ok, msg }) => {
+          this.$emit('onSeatTap', { chooseSeatList: this.seatmapInstance.chooseSeatList() })
+          if (!ok)
+            this.$emit('error', { message: msg, details: msg })
+        })
+    },
+    cancelSeat(seat) {
+      this.seatmapInstance.cancelSeat(seat)
+      this.$emit('onSeatTap', { chooseSeatList: this.seatmapInstance.chooseSeatList() })
+    },
+    validateIsolates() {
+      return this.seatmapInstance.validateIsolates()
+    },
+
+    async  getElementRect() {
+      return new Promise((resolve) => {
+        uni.createSelectorQuery().in(this)
+          .select(`#canvas-seatmap`)
+          .boundingClientRect((res) => {
+            resolve(res)
+          })
+          .exec()
+      })
+    },
+  },
+
+}
+</script>
+
+<template>
+  <canvas
+    id="canvas-seatmap" canvas-id="canvas-seatmap" type="2d" :hidpi="true" :disable-scroll="true"
+    :style="{ width: `${canvasWidth}px`, height: `${canvasHeight}px` }"
+    @touchstart="onTouchStart"
+    @touchmove="onTouchMove"
+    @touchcancel="onTouchEnd"
+    @touchend="onTouchEnd"
+    @click="onTap"
+    @ready="onCanvasReady"
+  />
+</template>

+ 184 - 0
src/subPack-film/choose-seat/components/pages/canvasCalculator.js

@@ -0,0 +1,184 @@
+export default class SeatCalculator {
+  constructor(seatList, options) {
+    this.seatList = seatList
+    this.options = options
+    this._buildSeatArray()
+    this._buildIndexMap()
+  }
+
+  /* ========== 基础数据结构 ========== */
+  _buildSeatArray() {
+    const { ROW_ID, COLUMN_ID } = this.options.SEAT_FIELDS
+    const arr = []
+    this.seatList.forEach((s) => {
+      const r = Number(s[ROW_ID])
+      const c = Number(s[COLUMN_ID])
+      if (!arr[r])
+        arr[r] = []
+      arr[r][c] = s
+    })
+    this.seatArray = arr
+    this.seatRow = arr.length
+    this.seatCol = Math.max(...arr.map(r => r.length).filter(Boolean), 1)
+  }
+
+  _buildIndexMap() {
+    this.seatMap = new Map()
+    const { ROW_ID, COLUMN_ID } = this.options.SEAT_FIELDS
+    this.seatList.forEach((s, idx) =>
+      this.seatMap.set(`${s[ROW_ID]}-${s[COLUMN_ID]}`, idx),
+    )
+  }
+
+  /* ========== 尺寸与边界 ========== */
+  calcCellSize(canvasWidth) {
+    const { seatMinWidth, seatMaxWidth } = this.options
+    const raw = (canvasWidth) / this.seatCol
+    return Math.max(seatMinWidth, Math.min(raw, seatMaxWidth))
+  }
+
+  calcSeatRowCol() {
+    return {
+      seatRow: this.seatRow,
+      seatCol: this.seatCol,
+    }
+  }
+
+  calcInitialPan(canvasWidth, canvasHeight, cellSize) {
+    const { hallHeight } = this.options
+    return {
+      panX: (canvasWidth - cellSize * this.seatCol) / 2,
+      panY: hallHeight,
+    }
+  }
+
+  getBoundaries(canvasWidth, canvasHeight, footerHeight, cellSize) {
+    const { hallHeight } = this.options
+    const contentW = cellSize * this.seatCol
+    const contentH = cellSize * this.seatRow
+    const availH = canvasHeight - hallHeight - footerHeight
+
+    return {
+      maxOffsetX: contentW < canvasWidth ? canvasWidth - contentW : 0,
+      minOffsetX: contentW > canvasWidth ? canvasWidth - contentW : 0,
+      maxOffsetY: hallHeight,
+      minOffsetY: contentH > availH ? availH - contentH : 0,
+    }
+  }
+
+  clampOffset(panX, panY, boundaries) {
+    const { maxOffsetX, minOffsetX, maxOffsetY, minOffsetY } = boundaries
+    return {
+      targetX: Math.max(minOffsetX, Math.min(maxOffsetX, panX)),
+      targetY: Math.max(minOffsetY, Math.min(maxOffsetY, panY)),
+    }
+  }
+
+  /* ========== 缩放 ========== */
+  calcPinchZoom({ oldCellSize, scale, centerX, centerY, panX, panY, seatCol, canvasWidth, seatMaxWidth, _seatMinWidth }) {
+    const newSize = oldCellSize * scale
+    if (newSize < (canvasWidth) / seatCol || newSize > seatMaxWidth)
+      return null // 越界
+
+    const oldContentCX = ((centerX - panX) / oldCellSize)
+    const oldContentCY = ((centerY - panY) / oldCellSize)
+
+    return {
+      cellSize: newSize,
+      panX: centerX - oldContentCX * newSize,
+      panY: centerY - oldContentCY * newSize,
+    }
+  }
+
+  /* ========== 获取与给定座位相关的所有座位索引 ========== */
+  getRelatedSeatIndices(seat) {
+    const { SINGLE, COUPLE_LEFT } = this.options.SEAT_TYPE
+    const { TYPE_NAME, ROW_ID, COLUMN_ID } = this.options.SEAT_FIELDS
+    const list = [this.seatMap.get(`${seat[ROW_ID]}-${seat[COLUMN_ID]}`)]
+    if (seat[TYPE_NAME] === SINGLE)
+      return list
+
+    const offset = seat[TYPE_NAME] === COUPLE_LEFT ? 1 : -1
+    const partnerKey = `${seat[ROW_ID]}-${Number(seat[COLUMN_ID]) + offset}`
+    if (this.seatMap.has(partnerKey)) {
+      const partnerSeat = this.seatList[this.seatMap.get(partnerKey)]
+      if (partnerSeat[TYPE_NAME] !== SINGLE)
+        list.push(this.seatMap.get(partnerKey))
+    }
+    return list
+  }
+
+  /* ========== 更新座位状态 ========== */
+  updateSeatStatus(targetStatus, indices) {
+    const { STATUS_NAME } = this.options.SEAT_FIELDS
+    indices.forEach((idx) => {
+      this.seatList[idx][STATUS_NAME] = targetStatus
+      this.seatList[idx].clickTime = Date.now()
+    })
+  }
+
+  cancelSeat(seat) {
+    const { AVAILABLE } = this.options.SEAT_STATUS
+    const { STATUS_NAME } = this.options.SEAT_FIELDS
+    this.getRelatedSeatIndices(seat).forEach((idx) => {
+      this.seatList[idx][STATUS_NAME] = AVAILABLE
+    })
+  }
+
+  /* ========== 验票 ========== */
+  validateSeat(seat) {
+    const { ROW_ID, COLUMN_ID, STATUS_NAME } = this.options.SEAT_FIELDS
+    const row = this.seatArray[seat[ROW_ID]]
+    if (!row)
+      return 0
+    const col = Number(seat[COLUMN_ID])
+    const { AVAILABLE, SELECTED } = this.options.SEAT_STATUS
+
+    let err = 0
+    const prev = []
+    const next = []
+    // 左侧连续空位
+    for (let c = col - 1; c >= 0; c--) {
+      const s = row[c]
+      if (!s || ![AVAILABLE, SELECTED].includes(s[STATUS_NAME]))
+        break
+      if (s[STATUS_NAME] === AVAILABLE)
+        prev.push(s)
+    }
+    // 右侧连续空位
+    for (let c = col + 1; c < row.length; c++) {
+      const s = row[c]
+      if (!s || ![AVAILABLE, SELECTED].includes(s[STATUS_NAME]))
+        break
+      if (s[STATUS_NAME] === AVAILABLE)
+        next.push(s)
+    }
+    if (!((prev.length === 0 && next.length === 0) || (prev.length >= 2 && next.length >= 2))) {
+      if (prev.length === 1 && next.length !== 0)
+        err++
+      if (next.length === 1 && prev.length !== 0)
+        err++
+    }
+    // 相邻已选中间夹一个空位
+    row.forEach((s, i) => {
+      if (s?.[STATUS_NAME] === SELECTED
+        && row[i + 1]?.[STATUS_NAME] === AVAILABLE
+        && row[i + 2]?.[STATUS_NAME] === SELECTED) {
+        err++
+      }
+    })
+    return err
+  }
+
+  /* ---------- 坐标转换 ---------- */
+  clientToCanvas({ x, y, panX, panY }) {
+    const canvasX = x - panX
+    const canvasY = y - panY
+    return { canvasX, canvasY }
+  }
+
+  /* ---------- 距离计算 ---------- */
+  calcDistance(t1, t2) {
+    return Math.hypot(t1.x - t2.x, t1.y - t2.y)
+  }
+}

+ 41 - 0
src/subPack-film/choose-seat/components/pages/canvasOperator.js

@@ -0,0 +1,41 @@
+export default class CanvasOperator {
+  constructor(canvas) {
+    this.canvas = canvas
+  }
+
+  /* ---------- RAF 封装 ---------- */
+  raf(cb) {
+    // #ifdef MP-WEIXIN || MP-ALIPAY
+    return this.canvas.requestAnimationFrame(cb)
+    // #endif
+  }
+
+  cancelRaf(id) {
+    // #ifdef MP-WEIXIN || MP-ALIPAY
+    return this.canvas.cancelAnimationFrame(id)
+    // #endif
+  }
+
+  /* ---------- 图片加载 ---------- */
+  async loadImage(filePath) {
+    let img
+    if (typeof this.canvas.createImage === 'function') {
+      img = this.canvas.createImage()
+    }
+    if (typeof Image !== 'undefined') {
+      img = new Image()
+    }
+    // if (typeof wx !== 'undefined') {
+    //   filePath = `/${filePath}` // 微信环境下添加前缀/
+    // }
+    img.crossOrigin = 'anonymous'
+    return new Promise((resolve, reject) => {
+      img.src = filePath
+      img.onload = () => resolve(img)
+      img.onerror = () => {
+        console.error('[loadImage] 图片加载失败:', filePath, '请检查图片路径')
+        reject(new Error('图片加载失败'))
+      }
+    })
+  }
+}

+ 304 - 0
src/subPack-film/choose-seat/components/pages/canvasRenderer.js

@@ -0,0 +1,304 @@
+export default class CanvasRenderer {
+  constructor(ctx, options) {
+    this.ctx = ctx
+    this.options = options || {}
+    this.areaListMap = this._getAreaListMap(this.options?.areaList || [])
+  }
+
+  /* ---------- 工具 ---------- */
+  _roundRect(x, y, w, h, r) {
+    const ctx = this.ctx
+    ctx.beginPath()
+    ctx.moveTo(x + r, y)
+    ctx.arcTo(x + w, y, x + w, y + h, r)
+    ctx.arcTo(x + w, y + h, x, y + h, r)
+    ctx.arcTo(x, y + h, x, y, r)
+    ctx.arcTo(x, y, x + w, y, r)
+    ctx.closePath()
+  }
+
+  _roundRect_left(x, y, w, h, r) {
+    const ctx = this.ctx
+    ctx.beginPath()
+    ctx.moveTo(x + w, y + h)
+    ctx.arcTo(x, y + h, x, y + h - r, r)
+    ctx.lineTo(x, y + r)
+    ctx.arcTo(x, y, x + r, y, r)
+    ctx.lineTo(x + w - r, y)
+    ctx.arcTo(x + w, y, x + w, y + r, r)
+    ctx.lineTo(x + w, y + h / 2)
+  }
+
+  _roundRect_right(x, y, w, h, r) {
+    const ctx = this.ctx
+    ctx.beginPath()
+    ctx.moveTo(x, y + h)
+    ctx.arcTo(x + w, y + h, x + w, y + h - r, r)
+    ctx.lineTo(x + w, y + r)
+    ctx.arcTo(x + w, y, x + w - r, y, r)
+    ctx.lineTo(x + r, y)
+    ctx.arcTo(x, y, x, y + r, r)
+    ctx.lineTo(x, y + h / 2)
+  }
+
+  // 座位状态样式缓存
+  _getAreaListMap(areaList) {
+    const { AREA_ID } = this.options.SEAT_FIELDS
+    return areaList.reduce((obj, area) => {
+      obj[area[AREA_ID]] = area
+      return obj
+    }, {})
+  }
+
+  /* ---------- 1. 清屏 ---------- */
+  clear(x, y, width, height) {
+    this.ctx.clearRect(x, y, width, height)
+  }
+
+  /* ---------- 2. 座位主入口 ---------- */
+  drawSeatMap({ panX, panY, cellSize, _seatCol, seatList, _seatRow, imageGou }) {
+    const ctx = this.ctx
+    const { ROW_ID, COLUMN_ID } = this.options.SEAT_FIELDS
+    ctx.save()
+    ctx.translate(panX, panY)
+    ctx.lineWidth = 1
+    seatList.forEach((seat) => {
+      seat.points = this.drawSeat(seat, seat[COLUMN_ID] * cellSize, seat[ROW_ID] * cellSize, cellSize, imageGou)
+    })
+    ctx.restore()
+  }
+
+  /* ---------- 3. 单个座位 ---------- */
+  drawSeat(seat, startX, startY, cellSize, imageGou) {
+    const ctx = this.ctx
+    const { SINGLE, COUPLE_LEFT, COUPLE_MIDDLE, COUPLE_RIGHT } = this.options.SEAT_TYPE
+    const { SELECTED } = this.options.SEAT_STATUS
+    const { STATUS_NAME, TYPE_NAME } = this.options.SEAT_FIELDS
+    const radius = cellSize / 5
+    const margin = radius
+    const seatType = seat[TYPE_NAME]
+    // 颜色
+    const colors = this._getSeatColors(seat)
+    ctx.fillStyle = colors.fillStyle
+    ctx.strokeStyle = colors.strokeStyle
+
+    let x, y, w, h
+    if (seatType === SINGLE) {
+      x = startX + margin / 2
+      y = startY + margin / 2
+      w = cellSize - margin
+      h = cellSize - margin
+      this._roundRect(x, y, w, h, radius)
+      ctx.fill()
+      ctx.stroke()
+    }
+    if (seatType === COUPLE_LEFT) {
+      x = startX + margin / 2
+      y = startY + margin / 2
+      w = cellSize - margin / 2
+      h = cellSize - margin
+
+      this._roundRect_left(x, y, w, h, radius)
+      ctx.fill()
+      ctx.stroke()
+    }
+    if (seatType === COUPLE_MIDDLE || seatType === COUPLE_RIGHT) {
+      x = startX
+      y = startY + margin / 2
+      w = cellSize - margin / 2
+      h = cellSize - margin
+      this._roundRect_right(x, y, w, h, radius)
+      ctx.fill()
+      ctx.stroke()
+    }
+
+    // 已选勾勾
+    if (seat[STATUS_NAME] === SELECTED) {
+      // ctx.drawImage(imageGou, startX, startY, cellSize, cellSize)
+      ctx.drawImage(imageGou, startX + cellSize / 4, startY + cellSize / 4, cellSize / 2, cellSize / 2)
+    }
+    return {
+      startX: x,
+      startY: y,
+      endX: x + w,
+      endY: y + h,
+    }
+  }
+
+  _getSeatColors(seat) {
+    const { STATUS_NAME, AREA_ID } = this.options.SEAT_FIELDS
+    const seatStatus = seat[STATUS_NAME]
+    const seatArea = seat[AREA_ID]
+    if (!this.statusStyleMap)
+      this.statusStyleMap = new Map()
+    if (this.statusStyleMap.has(`${seatStatus}-${seatArea}`)) {
+      return this.statusStyleMap.get(`${seatStatus}-${seatArea}`)
+    };
+
+    const { DISABLED, LOCKED, SOLD, AVAILABLE, SELECTED } = this.options.SEAT_STATUS
+    const { SEAT_STYLE } = this.options
+
+    this.statusStyleMap.set(`${seatStatus}-${seatArea}`, { fillStyle: 'transparent', strokeStyle: 'transparent' })
+    this.statusStyleMap.set(`${DISABLED}-${seatArea}`, SEAT_STYLE.DISABLED)
+    this.statusStyleMap.set(`${LOCKED}-${seatArea}`, SEAT_STYLE.LOCKED)
+    this.statusStyleMap.set(`${SOLD}-${seatArea}`, SEAT_STYLE.SOLD)
+    this.statusStyleMap.set(`${SELECTED}-${seatArea}`, SEAT_STYLE.SELECTED)
+    this.statusStyleMap.set(`${AVAILABLE}-${seatArea}`, {
+      fillStyle: SEAT_STYLE.AVAILABLE.fillStyle,
+      strokeStyle: this.areaListMap[seat[AREA_ID]]?.strokeStyle || SEAT_STYLE.AVAILABLE.strokeStyle,
+    })
+    return this.statusStyleMap.get(`${seatStatus}-${seatArea}`)
+  }
+
+  /* ---------- 4. 中线 + 文字 ---------- */
+  drawCenterLine({ panX, panY, seatCol, seatRow, cellSize, title }) {
+    const ctx = this.ctx
+    ctx.save()
+    ctx.translate(panX, panY)
+    ctx.beginPath()
+    ctx.setLineDash([3, 3])
+    ctx.strokeStyle = '#999'
+    ctx.moveTo(seatCol * cellSize / 2, 0)
+    ctx.lineTo(seatCol * cellSize / 2, seatRow * cellSize + 20)
+    ctx.stroke()
+    ctx.setLineDash([])
+
+    ctx.beginPath()
+    ctx.arc(seatCol * cellSize / 2, seatRow * cellSize + 20, 0.5, 0, 2 * Math.PI) // 绘制圆
+    ctx.stroke()
+    ctx.restore()
+
+    if (!title)
+      return
+    ctx.save()
+    ctx.translate(panX, panY)
+    ctx.fillStyle = '#d8d8d8'
+    ctx.font = '14px Arial'
+    ctx.textAlign = 'center'
+    ctx.textBaseline = 'middle'
+    ctx.fillText(title, seatCol * cellSize / 2, seatRow * cellSize + 40)
+    ctx.restore()
+  }
+
+  /* ---------- 5. 排号侧边栏 ---------- */
+  drawRowLabels({ panY, cellSize, seatArray }) {
+    const ctx = this.ctx
+    const x = 4
+    const w = 16
+    const radius = w / 2
+    const h = cellSize * seatArray.length
+    ctx.save()
+    ctx.translate(0, panY)
+    ctx.beginPath()
+    ctx.moveTo(x + radius, 0)
+    ctx.arcTo(x + w, 0, x + w, radius, radius)
+    ctx.arcTo(x + w, h, x + w - radius, h, radius)
+    ctx.arcTo(x, h, x, h - radius, radius)
+    ctx.arcTo(x, 0, x + radius, 0, radius)
+    ctx.fillStyle = 'rgba(150,150,150,0.8)'
+    ctx.fill()
+    ctx.fillStyle = '#fff'
+    ctx.font = '10px Arial'
+    ctx.textAlign = 'center'
+    ctx.textBaseline = 'middle'
+    seatArray.forEach((_, rowId) => {
+      ctx.fillText(String(rowId), x + w / 2, rowId * cellSize + cellSize / 2)
+    })
+    ctx.restore()
+  }
+
+  /* ---------- 6. 屏幕 ---------- */
+  drawScreen({ panX, imageScreen, hallName, cellSize, seatCol, canvasWidth, hallHeight }) {
+    const ctx = this.ctx
+    const screenW = canvasWidth / 2
+    const screenH = (imageScreen.height / imageScreen.width) * screenW
+    const centerX = cellSize * seatCol / 2
+    ctx.save()
+    ctx.clearRect(0, 0, canvasWidth, hallHeight)
+    ctx.translate(panX, 0)
+    ctx.drawImage(imageScreen, centerX - screenW / 2, 0, screenW, screenH)
+    ctx.fillStyle = '#000'
+    ctx.font = '10px Arial'
+    ctx.textAlign = 'center'
+    ctx.textBaseline = 'middle'
+    ctx.fillText(hallName || '', centerX, screenH - 5)
+    ctx.restore()
+  }
+
+  /* ---------- 8. 虚拟缩略图 ---------- */
+  drawMiniMap({ miniMapShow, seatList, seatCol, seatRow, canvasWidth, hallHeight, panX, cellSize: realCellSize }) {
+    if (!miniMapShow)
+      return
+
+    const ctx = this.ctx
+    const miniCellSize = 6
+    const margin = 1
+
+    const { SOLD, AVAILABLE, SELECTED } = this.options.SEAT_STATUS
+    const { SINGLE, COUPLE_MIDDLE, COUPLE_RIGHT } = this.options.SEAT_TYPE
+    const { STATUS_NAME, TYPE_NAME, ROW_ID, COLUMN_ID } = this.options.SEAT_FIELDS
+    const contentWidth = miniCellSize * seatCol
+    const contentHeight = miniCellSize * seatRow
+    const virtualWidth = miniCellSize * (seatCol + 2)
+    const virtualHeight = miniCellSize * (seatRow + 2)
+    const startX = (virtualWidth - contentWidth) / 2
+    const startY = hallHeight - 40 + miniCellSize
+
+    ctx.save()
+
+    /* 1. 背景 */
+    ctx.fillStyle = 'rgba(0,0,0,0.6)'
+    ctx.fillRect(0, hallHeight - 40, virtualWidth, virtualHeight)
+
+    /* 2. 画座位 */
+    seatList.forEach((seat) => {
+      const x = startX + seat[COLUMN_ID] * miniCellSize
+      const y = startY + seat[ROW_ID] * miniCellSize
+
+      let rectStartX = x + margin
+      const rectStartY = y + margin
+      let rectWidth = miniCellSize - margin * 2
+      const rectHeight = miniCellSize - margin * 2
+      if (seat[TYPE_NAME] !== SINGLE) {
+        rectWidth = miniCellSize - margin
+      }
+      if (seat[TYPE_NAME] === COUPLE_MIDDLE || seat[TYPE_NAME] === COUPLE_RIGHT) {
+        rectStartX = x
+      }
+
+      let fillStyle = 'transparent'
+      if (seat[STATUS_NAME] === AVAILABLE)
+        fillStyle = '#fff'
+      if (seat[STATUS_NAME] === SELECTED)
+        fillStyle = '#0ed7b8'
+      if (seat[STATUS_NAME] === SOLD)
+        fillStyle = 'red'
+
+      ctx.fillStyle = fillStyle
+      ctx.fillRect(rectStartX, rectStartY, rectWidth, rectHeight)
+    })
+
+    /* 3. 中线 */
+    ctx.beginPath()
+    ctx.setLineDash([3, 3])
+    ctx.strokeStyle = '#999'
+    ctx.moveTo(startX + contentWidth / 2, startY)
+    ctx.lineTo(startX + contentWidth / 2, startY + contentHeight)
+    ctx.stroke()
+    ctx.setLineDash([])
+
+    /* 4. 可视区域红框 */
+    const realContentWidth = realCellSize * seatCol
+    const virtualBoxWidth = (canvasWidth / realContentWidth) * contentWidth
+    const virtualBoxHeight = contentHeight
+
+    let virtualStartX = startX + ((-panX) / realContentWidth) * contentWidth
+    const virtualStartY = startY
+    virtualStartX = Math.min(virtualStartX, virtualWidth - virtualBoxWidth)
+
+    ctx.strokeStyle = 'red'
+    ctx.strokeRect(virtualStartX, virtualStartY, virtualBoxWidth, virtualBoxHeight)
+
+    ctx.restore()
+  }
+}

+ 46 - 0
src/subPack-film/choose-seat/components/pages/config.js

@@ -0,0 +1,46 @@
+/**
+ * 默认配置选项
+ */
+export default {
+  title: '', // 座位图的标题
+  hallName: '屏幕', // 影厅名称
+  canvasWidth: 414, // 画布宽度
+  canvasHeight: 414, // 画布高度
+  topHeight: 40, // 画布距离视口顶部高度 用于计算坐标。
+  hallHeight: 40, // 画布中荧幕高度
+  footerHeight: 0, // 底部区域的高度
+  maxSelectNum: 4, // 最大可选座位数
+  seatMaxWidth: 35, // 座位的最大宽度
+  seatMinWidth: 5, // 座位的最小宽度
+  miniMapShowTime: 2000, // 停止操作后,过多久隐藏小图
+  isolateSeats: false, // 孤座检查是否开启
+  areaList: [], // 座位区域 格式见md文档
+  SEAT_FIELDS: {
+    ROW_ID: 'rowId', // 行ID字段名
+    COLUMN_ID: 'columnId', // 列ID字段名
+    STATUS_NAME: 'status', // 座位状态字段名
+    TYPE_NAME: 'seatType', // 座位类型字段名
+    AREA_ID: 'areaId', // 座位区域字段名
+  },
+  SEAT_TYPE: {
+    SINGLE: 0, // 单座
+    COUPLE_LEFT: 1, // 情侣座 - 左
+    COUPLE_MIDDLE: 2, // 情侣座 - 中/右
+    COUPLE_RIGHT: 3, // 情侣座 - 右
+  },
+  SEAT_STATUS: {
+    DISABLED: -2, // 不可用
+    LOCKED: -1, // 锁定
+    SOLD: 0, // 已售
+    AVAILABLE: 1, // 可选
+    SELECTED: 10, // 已选
+  },
+
+  SEAT_STYLE: {
+    DISABLED: { fillStyle: '#f7f9fc', strokeStyle: '#eeeff1' }, // 不可用
+    LOCKED: { fillStyle: '#f7f9fc', strokeStyle: '#eeeff1' }, // 锁定
+    SOLD: { fillStyle: '#AAAAAA', strokeStyle: 'transparent' }, // 已售
+    AVAILABLE: { fillStyle: 'transparent', strokeStyle: '#97cafc' }, // 可选
+    SELECTED: { fillStyle: '#4DD266', strokeStyle: 'transparent' }, // 已选
+  },
+}

+ 286 - 0
src/subPack-film/choose-seat/components/pages/main.js

@@ -0,0 +1,286 @@
+import CanvasOperator from './canvasOperator.js'
+import CanvasRenderer from './canvasRenderer.js'
+import SeatCalculator from './canvasCalculator.js'
+import seatmapConfig from './config.js'
+import { StaticUrl } from '@/config'
+
+export default class seatmap {
+  constructor({ canvas, seatList, options }) {
+    this.ctx = canvas.getContext('2d')
+    this.seatList = seatList
+    this.options = this._deepMerge(seatmapConfig, options)
+    this.checkOptions()
+    this.operator = new CanvasOperator(canvas)
+    this.renderer = new CanvasRenderer(this.ctx, this.options)
+    this.calc = new SeatCalculator(this.seatList, this.options)
+
+    // 运行时状态
+    this.cellSize = 0
+    this.panX = 0
+    this.panY = 0
+    this.boundaries = {}
+    this.imageScreen = null
+    this.imageGou = null
+    this._drawing = false
+    this._rafId = null
+    this.seatRow = 0
+    this.seatCol = 0
+    this.miniMapShow = false
+    this.firstClick = true
+    this.init()
+  }
+
+  checkOptions() {
+    const { SEAT_FIELDS, canvasWidth, canvasHeight, footerHeight, SEAT_STYLE, areaList } = this.options
+
+    if (!SEAT_FIELDS.ROW_ID || typeof SEAT_FIELDS.ROW_ID !== 'string')
+      throw new Error(`options.SEAT_FIELDS.ROW_ID 配置不正确`)
+    if (!SEAT_FIELDS.COLUMN_ID || typeof SEAT_FIELDS.COLUMN_ID !== 'string')
+      throw new Error(`options.SEAT_FIELDS.COLUMN_ID 配置不正确`)
+    if (!SEAT_FIELDS.STATUS_NAME || typeof SEAT_FIELDS.STATUS_NAME !== 'string')
+      throw new Error(`options.SEAT_FIELDS.STATUS_NAME 配置不正确`)
+    if (!SEAT_FIELDS.TYPE_NAME || typeof SEAT_FIELDS.TYPE_NAME !== 'string')
+      throw new Error(`options.SEAT_FIELDS.TYPE_NAME 配置不正确`)
+    if (!SEAT_FIELDS.AREA_ID || typeof SEAT_FIELDS.AREA_ID !== 'string')
+      throw new Error(`options.SEAT_FIELDS.AREA_ID 配置不正确`)
+    if (!canvasWidth || canvasWidth <= 0)
+      throw new Error(`options.canvasWidth 必须大于0`)
+    if (!canvasHeight || canvasHeight <= 0)
+      throw new Error(`options.canvasHeight 必须大于0`)
+    if (footerHeight < 0)
+      throw new Error(`options.footerHeight 不能为负`)
+
+    const validateSeatStyle = SEAT_STYLE => ['DISABLED', 'LOCKED', 'SOLD', 'AVAILABLE', 'SELECTED'].every(prop =>
+      SEAT_STYLE[prop]?.fillStyle !== undefined && SEAT_STYLE[prop]?.strokeStyle !== undefined,
+    )
+    if (validateSeatStyle(SEAT_STYLE) === false)
+      throw new Error(`options.SEAT_STYLE 配置不正确,每个状态都需要配置 fillStyle 和 strokeStyle`)
+
+    areaList.forEach((item) => {
+      if (item[SEAT_FIELDS.AREA_ID] === undefined)
+        throw new Error(`options.areaList 中缺少 AREA_ID 字段`)
+      if (item.strokeStyle === undefined)
+        throw new Error(`options.areaList 中缺少 strokeStyle 字段(颜色)`)
+    })
+  }
+
+  async init() {
+    const { canvasWidth, canvasHeight, footerHeight } = this.options
+
+    // 加载图片
+    // this.imageScreen = await this.operator.loadImage(`${StaticUrl}/film-choose-icon.png`)
+    // this.imageGou = await this.operator.loadImage(`${StaticUrl}/film-choose-icon.png`)
+    this.imageScreen = await this.operator.loadImage(`${StaticUrl}/film-hall.png`)
+    this.imageGou = await this.operator.loadImage(`${StaticUrl}/film-gou-old.png`)
+
+    // 初始布局
+    this.cellSize = this.calc.calcCellSize(canvasWidth)
+    const { seatRow, seatCol } = this.calc.calcSeatRowCol()
+    const { panX, panY } = this.calc.calcInitialPan(canvasWidth, canvasHeight, this.cellSize)
+    this.boundaries = this.calc.getBoundaries(canvasWidth, canvasHeight, footerHeight, this.cellSize)
+    this.seatRow = seatRow
+    this.seatCol = seatCol
+    this.panX = panX
+    this.panY = panY
+    this.requestDraw()
+  }
+
+  /* -------------------- 座位图绘制 -------------------- */
+  requestDraw() {
+    if (this._drawing)
+      return
+    this._drawing = true
+    this._rafId = this.operator.raf(() => {
+      const { panX, panY, cellSize, seatRow, seatCol, seatList, imageGou, imageScreen, miniMapShow } = this
+      const { title, canvasWidth, hallHeight, areaColorMap, hallName } = this.options
+      this.renderer.clear(0, 0, this.options.canvasWidth, this.options.canvasHeight)
+      this.renderer.drawSeatMap({ panX, panY, cellSize, seatRow, seatCol, seatList, areaColorMap, imageGou })
+      this.renderer.drawCenterLine({ panX, panY, cellSize, seatRow, seatCol, title })
+      this.renderer.drawScreen({ panX, panY, cellSize, seatRow, seatCol, imageScreen, hallName, canvasWidth, hallHeight })
+      this.renderer.drawRowLabels({ panX, panY, cellSize, seatRow, seatCol, seatArray: this.calc.seatArray })
+      this.renderer.drawMiniMap({ panX, panY, cellSize, seatRow, seatCol, seatList, canvasWidth, miniMapShow, hallHeight })
+      this._drawing = false
+    })
+  }
+
+  /* -------------------- 触摸事件 -------------------- */
+  onTouchStart(e) {
+    this._startPanX = this.panX
+    this._startPanY = this.panY
+    this._lastTouch = e.touches[0]
+    this._startTouchEvent = e
+    this._isPinching = false
+
+    if (e.touches.length === 2) {
+      this._isPinching = true
+      this._lastPinchDist = this.calc.calcDistance(e.touches[0], e.touches[1])
+    }
+  }
+
+  onTouchMove(e) {
+    if (e.touches.length === 1 && !this._isPinching) {
+      this.miniMapShow = true // 拖拽时显示小图
+      const dx = e.touches[0].x - this._lastTouch.x
+      const dy = e.touches[0].y - this._lastTouch.y
+      this.panX = this._startPanX + dx
+      this.panY = this._startPanY + dy
+      this.requestDraw()
+      return
+    }
+
+    if (e.touches.length === 2 && this._isPinching) {
+      this.miniMapShow = true
+      if (!this._lastPinchDist)
+        return
+      const dist = this.calc.calcDistance(e.touches[0], e.touches[1])
+      const centerX = (e.touches[0].x + e.touches[1].x) / 2
+      const centerY = (e.touches[0].y + e.touches[1].y) / 2
+      const scale = dist / this._lastPinchDist
+      const cellSize = this.cellSize * scale
+      this.applyZoom(cellSize, centerX, centerY)
+      this._lastPinchDist = dist
+      this.requestDraw()
+    }
+  }
+
+  onTouchEnd() {
+    this._afterDrawTimer()
+    const { targetX, targetY } = this.calc.clampOffset(this.panX, this.panY, this.boundaries)
+    if (this.panX === targetX && this.panY === targetY)
+      return
+    this._animateTo(targetX, targetY)
+  }
+
+  /* -------------------- 选座 -------------------- */
+  async onSeatTap({ x, y }) {
+    const { panX, panY } = this
+    const { AVAILABLE, SELECTED } = this.options.SEAT_STATUS
+    const { STATUS_NAME } = this.options.SEAT_FIELDS
+    const { seatMaxWidth, maxSelectNum, isolateSeats } = this.options
+    const { canvasX, canvasY } = this.calc.clientToCanvas({ x, y, panX, panY })
+
+    const seat = this.seatList.find((item) => {
+      const { points: { startX, startY, endX, endY } } = item
+      return (canvasX >= startX && canvasX <= endX && canvasY >= startY && canvasY <= endY)
+    })
+    if (!seat || ![AVAILABLE, SELECTED].includes(seat[STATUS_NAME])) {
+      return { ok: true, msg: '不是有效座位' }
+    }
+    const indices = this.calc.getRelatedSeatIndices(seat)
+    const adjustedNum = seat[STATUS_NAME] === SELECTED ? -indices.length : indices.length
+    if (maxSelectNum && this.chooseSeatList().length + adjustedNum > maxSelectNum) {
+      return { ok: false, msg: `最多可选${maxSelectNum}个座位` }
+    };
+    const targetStatus = seat[STATUS_NAME] === SELECTED ? AVAILABLE : SELECTED
+    this.calc.updateSeatStatus(targetStatus, indices)
+
+    if (this.firstClick) {
+      this.applyZoom(seatMaxWidth, canvasX, canvasY)
+      this.firstClick = false
+    }
+    this.requestDraw()
+
+    if (seat[STATUS_NAME] === SELECTED && isolateSeats && this.calc.validateSeat(seat)) {
+      return { ok: false, msg: '左右两边不要留空哦' }
+    }
+    return { ok: true, msg: '点击座位成功' }
+  }
+
+  applyZoom(cellSize, centerX, centerY) {
+    const { panX, panY, cellSize: oldCellSize, seatCol } = this
+    const { canvasWidth, seatMaxWidth, seatMinWidth } = this.options
+    const zoom = this.calc.calcPinchZoom({ oldCellSize, scale: cellSize / oldCellSize, centerX, centerY, panX, panY, seatCol, canvasWidth, seatMaxWidth, seatMinWidth })
+    if (!zoom)
+      return
+    this.boundaries = this.calc.getBoundaries(this.options.canvasWidth, this.options.canvasHeight, this.options.footerHeight, zoom.cellSize)
+    const { targetX, targetY } = this.calc.clampOffset(zoom.panX, zoom.panY, this.boundaries)
+    this.cellSize = cellSize
+    this.panX = targetX
+    this.panY = targetY
+  }
+
+  chooseSeatList() {
+    const { SELECTED } = this.options.SEAT_STATUS
+    const { STATUS_NAME } = this.options.SEAT_FIELDS
+    const chooseList = this.seatList.filter((item) => {
+      return item[STATUS_NAME] === SELECTED
+    })
+    chooseList.sort((a, b) => {
+      return a.clickTime - b.clickTime
+    })
+    return chooseList
+  }
+
+  cancelSeat(seat) {
+    this.calc.cancelSeat(seat)
+    this.requestDraw()
+  }
+
+  /* ========== 孤座检测 ========== */
+  validateIsolates() {
+    if (this.options.isolateSeats === false)
+      return 0
+    let errNo = 0
+    this.chooseSeatList().forEach((seat) => {
+      errNo += this.calc.validateSeat(seat)
+    })
+    return errNo
+  }
+
+  /* -------------------- 工具 -------------------- */
+
+  _afterDrawTimer() {
+    if (this._timer)
+      clearTimeout(this._timer)
+    this._timer = setTimeout(() => {
+      this.miniMapShow = false
+      this.requestDraw()
+    }, this.options.miniMapShowTime)
+  }
+
+  _animateTo(tx, ty) {
+    const startX = this.panX
+    const startY = this.panY
+    const duration = 300
+    const startTime = Date.now()
+    const animate = () => {
+      const elapsed = Date.now() - startTime
+      const prog = Math.min(elapsed / duration, 1)
+      const ease = 1 - (1 - prog) ** 3
+      this.panX = startX + (tx - startX) * ease
+      this.panY = startY + (ty - startY) * ease
+      this.requestDraw()
+      if (prog < 1)
+        this.operator.raf(animate)
+    }
+    this.operator.raf(animate)
+  }
+
+  /* ---------- 深度合并 ---------- */
+  _deepMerge(target, source) {
+    // 如果目标或源不是对象,直接返回源
+    if (typeof target !== 'object' || target === null || Array.isArray(target)) {
+      return source
+    }
+    // 遍历源对象的每个属性
+    for (const key in source) {
+      if (Object.prototype.hasOwnProperty.call(source, key)) {
+        const targetValue = target[key]
+        const sourceValue = source[key]
+        // 如果源属性是对象,递归合并
+        if (typeof sourceValue === 'object' && sourceValue !== null && !Array.isArray(sourceValue)) {
+          if (typeof targetValue !== 'object' || targetValue === null || Array.isArray(targetValue)) {
+            target[key] = {}
+          }
+          this._deepMerge(target[key], sourceValue)
+        }
+        else {
+          // 否则,直接覆盖目标属性
+          target[key] = sourceValue
+        }
+      }
+    }
+
+    return target
+  }
+}

+ 690 - 0
src/subPack-film/choose-seat/index.vue

@@ -0,0 +1,690 @@
+<script setup lang="ts">
+// 1. 导入 Vue3 核心 API 和 uni-app 生命周期
+import { computed, nextTick, ref } from 'vue'
+import { onLoad } from '@dcloudio/uni-app'
+
+// 2. 导入原组件和模拟数据
+import { getArrayFieldMax, isWithin45Minutes } from '../utils'
+import canvasSeatmap from './components/pages/CanvasSeatmap.vue'
+import { StaticUrl } from '@/config'
+import router from '@/router'
+
+definePage({
+  name: 'film-choose-seat',
+  islogin: false,
+  style: {
+    navigationBarTitleText: '座次选择',
+    backgroundColorBottom: '#fff',
+  },
+})
+
+// ************************ 核心:定义所有缺失的接口(解决类型报错的关键) ************************
+// /**
+//  * 影院信息接口
+//  */
+// interface CinemaInfo {
+//   cinema_name: string
+//   [key: string]: any // 兼容其他未知属性,避免后续扩展报错
+// }
+
+/**
+ * 区域列表项接口
+ */
+interface AreaItem {
+  area: string | number
+  areaId: string | number
+  market_price: number | string
+  strokeStyle: string
+  [key: string]: any // 兼容组件所需的其他未知属性
+}
+
+// /**
+//  * 场次信息接口
+//  */
+// interface ScheduleInfo {
+//   schedule_id: string | number
+//   hall_name: string
+//   cinema: CinemaInfo | null
+//   schedule_area: AreaItem[]
+//   [key: string]: any // 兼容其他未知属性
+// }
+
+/**
+ * 座位信息接口
+ */
+interface SeatInfo {
+  seatName: string
+  marketPrice: number | string
+  [key: string]: any // 兼容组件所需的其他未知属性
+}
+
+/**
+ * 座位图配置项接口
+ */
+interface SeatmapOptions {
+  title: string
+  hallName: string
+  areaList: AreaItem[]
+  footerHeight: number
+  maxSelectNum?: number // 可选属性,对应注释的配置
+  isolateSeats?: boolean // 可选属性,对应注释的配置
+  [key: string]: any // 兼容组件所需的其他未知配置
+}
+
+/**
+ * 座位点击回调事件接口
+ */
+interface SeatTapEvent {
+  chooseSeatList: SeatInfo[]
+}
+
+/**
+ * 座位图组件实例接口(定义组件的公开方法,解决 ref 调用方法的类型报错)
+ */
+interface CanvasSeatmapInstance {
+  init: (params: { seatList: SeatInfo[], options: SeatmapOptions }) => void
+  validateIsolates: () => boolean
+  cancelSeat: (seat: SeatInfo) => void
+}
+
+const isLoging = ref(false)
+const loading = ref<boolean>(false)
+const seatInfo = ref<Api.filmMovieSeat>({})
+const areaList = ref<AreaItem[]>([])
+const seatList = ref<SeatInfo[]>([])
+const chooseSeatList = ref<SeatInfo[]>([])
+const options = ref<SeatmapOptions>({
+  title: '',
+  hallName: '',
+  areaList: [],
+  footerHeight: 0,
+})
+const areaArr = ref(['贵宾区', '普通区', '特价区1', '特价区2', '特价区3', '特价区4', '特价区5', '特价区6'])
+const { userInfo } = storeToRefs(useUserStore())
+const phone = ref('')
+const query = ref({
+  memberId: unref(userInfo).id,
+  channelId: unref(userInfo).channelId,
+  shopId: '1',
+  channelName: unref(userInfo).channelName,
+  cinemaName: '',
+  cinemaCode: '',
+  movieCode: '',
+  hallName: '',
+  orderPayMode: '1',
+  originPrice: 0,
+  seatNames: '',
+  sessionBeginTime: '',
+  switchSeat: false,
+  movieName: '',
+  postImageUrl: '',
+  planType: '',
+
+})
+const active = ref('')
+const showTime = ref(false)
+const screenList = ref()
+// ************************ 非响应式常量(类型自动推断,无需额外注解) ************************
+const colorList = [
+  '#449DFE', // 浅黄色
+  '#FFB639', // 浅蓝色
+  '#FF4D3A', // 干枯玫瑰
+  '#b6e67f', // 浅绿色
+  '#ffb0bc', // 浅粉色
+] as const // as const 锁定数组元素,禁止修改,类型更严格
+
+const totalPrice = computed(() => {
+  let total = 0
+  const scale = 10000
+  if (!seatInfo.value.showTime)
+    return 0
+  const key = isWithin45Minutes(new Date(), seatInfo.value.showTime) ? 'fastPrice' : 'ticketPrice'
+  const price = getArrayFieldMax(chooseSeatList.value, key) || 0
+  total = (price * scale) * chooseSeatList.value.length / scale
+  return total
+})
+
+// ************************ 组件 ref(添加明确实例类型,解决调用方法的类型报错) ************************
+const seatmapRef = ref<CanvasSeatmapInstance | null>(null)
+
+// ************************ 所有方法(添加参数/返回值类型注解,消除隐式 any) ************************
+// /**
+//  * 金额转换(分转元,保留 2 位小数)
+//  * @param total 分单位金额
+//  * @returns 元单位金额(字符串,保留 2 位小数)
+//  */
+// function priceConversion(total: number | string): string {
+//   const numTotal = Number(total)
+//   // 处理 NaN 情况,避免返回 NaN.toFixed(2) 报错
+//   if (Number.isNaN(numTotal))
+//     return '0.00'
+//   return (numTotal / 100).toFixed(2)
+// }
+
+async function getScreenList(cinemaId: string, movieId: string) {
+  const res = await Apis.film.getFilmMovieList({ data: { cinemaId, movieId } })
+  res.data.data.movieShows?.forEach((i: any) => {
+    if (i.shows.length) {
+      i.shows.forEach((it: any, idx: number) => {
+        if (active.value.includes(it.showDate)) {
+          screenList.value = i.shows[idx].sessions
+        }
+      })
+    }
+  })
+  console.log(2222222, screenList.value)
+}
+
+/**
+ * 获取座位详情
+ */
+async function getData(sessionId: string, movieId: string, cinemaId: string): Promise<void> {
+  uni.showLoading({ title: '加载中...' })
+  const res = await Apis.film.getFilmMovieSeat({ data: { sessionId, movieId, cinemaId } })
+  uni.hideLoading()
+  getScreenList(res.data.cinemaId, res.data.movieId)
+  active.value = res.data.showTime
+  seatInfo.value = res.data
+  // seatInfo.value.seatSection = []
+  query.value.hallName = res.data.hall
+  query.value.cinemaCode = res.data.cinemaCode
+  query.value.movieCode = res.data.movieCode
+  query.value.sessionBeginTime = res.data.showTime
+  query.value.cinemaName = res.data.cinemaName
+  query.value.movieName = res.data.movieName
+  query.value.postImageUrl = res.data.postImageUrl
+  areaList.value = res.data.areaPriceList
+  areaList.value.sort((a, b) => Number(b.originPrice) - Number(a.originPrice))
+  areaList.value.forEach((item: AreaItem, index: number) => {
+    item.strokeStyle = colorList[index % colorList.length] || '#ccc'
+    item.areaName = areaArr.value[index]
+  })
+  for (let index = 0; index < res.data.seatSection.seatRows.length; index++) {
+    const item = res.data.seatSection.seatRows[index]
+    for (let i = 0; i < item.columns.length; i++) {
+      const seat = item.columns[i]
+      let price = 0
+      let marketPrice = 0
+      let fastPrice = 0
+      for (let d = 0; d < res.data.areaPriceList.length; d++) {
+        const el = res.data.areaPriceList[d]
+        if (el.areaId === seat.areaId) {
+          // 价格
+          price = el.price
+          marketPrice = el.originPrice
+          fastPrice = el.fastPrice
+        }
+      }
+      // 桌椅状态
+
+      let status = 0
+      if (seat.state === 0 || seat.state === 4) {
+        status = -2
+      }
+      else if (seat.state === 1) {
+        status = 1
+      }
+      else if (seat.state === 2) {
+        status = 0
+      }
+      else if (seat.state === 3) {
+        status = 10
+      }
+      else if (seat.state === 10) {
+        status = 1
+      }
+      else if (seat.state === 11) {
+        status = 1
+      }
+      // 座位类型
+      let seatType = 0
+      if (seat.seatType === 2) {
+        seatType = 1
+      }
+      else if (seat.seatType === 3) {
+        seatType = 3
+      }
+
+      seatList.value.push({
+        status,
+        seatType,
+        seatName: seat.seatName,
+        seatId: seat.originSeatID,
+        columnId: seat.colId,
+        rowId: item.rowsId,
+        areaId: seat.areaId,
+        marketPrice,
+        ticketPrice: price,
+        fastPrice,
+        serviceFee: 0,
+      })
+    }
+  }
+
+  console.log(2222, areaList.value)
+
+  // 赋值座位图配置项(严格符合 SeatmapOptions 接口)
+  options.value = {
+    title: '',
+    hallName: seatInfo.value.hall as string,
+    areaList: areaList.value,
+    footerHeight: 470,
+    // maxSelectNum: 4,   // 可选属性,开启后无类型报错
+    // isolateSeats: true, // 可选属性,开启后无类型报错
+  }
+
+  // 可选链访问,避免 cinema 为 null 时报错
+  // const cinemaName = schedule.value.cinema?.cinema_name || '默认影院'
+  // uni.setNavigationBarTitle({ title: cinemaName })
+}
+
+/**
+ * 选定并购买座位
+ */
+async function buySeat() {
+  if (loading.value)
+    return false
+  if (chooseSeatList.value.length === 0) {
+    return uni.showToast({
+      title: '请选择座位',
+      icon: 'none',
+      duration: 2000,
+    })
+  }
+
+  // 可选链调用,避免组件实例为 null 时报错
+  if (seatmapRef.value?.validateIsolates()) {
+    uni.showToast({
+      title: '左右座位请不要留空!',
+      icon: 'none',
+      duration: 2000,
+    })
+    return false
+  }
+
+  query.value.seatNames = chooseSeatList.value.map(item => item.seatName).join(',')
+  query.value.planType = seatInfo.value?.language as string + seatInfo.value?.planType
+  uni.showToast({
+    title: '开始锁座',
+    icon: 'none',
+    duration: 2000,
+  })
+  uni.setStorageSync('film-info', { showTime: active.value, endTime: seatInfo.value.endTime, cinemaName: seatInfo.value.cinemaName, chooseSeatList: chooseSeatList.value, language: seatInfo.value.language, planType: seatInfo.value.planType, totalPrice: totalPrice.value, phone: phone.value })
+  router.push({ name: 'film-submit-order', params: { query: JSON.stringify(query.value) } })
+  console.log(query.value)
+}
+
+/**
+ * 取消选定座位
+ * @param seat 要取消的座位信息
+ */
+function cancelSeat(seat: SeatInfo): void {
+  seatmapRef.value?.cancelSeat(seat)
+}
+
+/**
+ * 座位图错误回调
+ * @param e 错误事件对象
+ */
+function onError(e: { message: string }): void {
+  uni.showToast({
+    title: e.message || '座位图加载失败',
+    icon: 'none',
+    duration: 2000,
+  })
+}
+
+/**
+ * 座位点击回调
+ * @param event 座位点击事件对象
+ */
+function onSeatTap(event: SeatTapEvent): void {
+  chooseSeatList.value = event.chooseSeatList
+  console.log('zuoshsjd', chooseSeatList.value)
+  // 遍历已选座位列表,累加价格(严格类型访问)
+  let price = 0
+  for (const item of chooseSeatList.value) {
+    if (item.marketPrice as number > price) {
+      price = item.marketPrice as number
+    }
+  }
+  query.value.originPrice = price
+}
+
+// ************************ 页面生命周期(严格类型,无报错) ************************
+onLoad(async (opt): Promise<void> => {
+  phone.value = opt?.phone
+  await getData(opt?.sessionId, opt?.movieId, opt?.cinemaId)
+
+  // 等待 DOM 渲染完成后初始化座位图
+  await nextTick()
+
+  // 可选链 + 类型守卫,确保调用安全无报错
+  if (seatmapRef.value) {
+    seatmapRef.value.init({
+      seatList: seatList.value,
+      options: options.value,
+    })
+  }
+})
+
+function handleTime(time: string, sessionId: string) {
+  if (time === active.value) {
+    return
+  }
+  active.value = time
+  router.push({ name: 'film-choose-seat', params: { sessionId, movieId: seatInfo.value.movieId as string, cinemaId: seatInfo.value.cinemaId } })
+}
+function handleSwitch() {
+  console.log(11111)
+
+  showTime.value = !showTime.value
+}
+
+// 获取时间
+function judgeNearDaysSimple(time: any) {
+  const today = new Date(new Date().setHours(0, 0, 0, 0)).getTime()
+  const target = new Date(new Date(time).setHours(0, 0, 0, 0)).getTime()
+  const dayDiff = Math.floor((target - today) / 86400000)
+
+  return { 0: '今天', 1: '明天', 2: '后天' }[dayDiff] || ''
+}
+</script>
+
+<template>
+  <view class="view-page">
+    <!-- 顶部价格区域 -->
+    <view v-if="seatInfo.cinemaId" class="areaList">
+      <view class="area-item">
+        <view class="area" style="background-color:#AAAAAA" />
+        <view class="area-price">
+          已售
+        </view>
+      </view>
+      <view v-for="(item, index) in areaList" :key="index" class="area-item">
+        <view class="area" :style="`border:1px solid ${item.strokeStyle}`" />
+        <view class="area-price">
+          {{ `${item.areaName} ${isWithin45Minutes(new Date(), seatInfo.showTime) ? item.fastPrice : item.price}` }}
+        </view>
+      </view>
+    </view>
+    <canvasSeatmap ref="seatmapRef" @error="onError" @on-seat-tap="onSeatTap" />
+
+    <!-- 底部信息 -->
+    <view v-if="seatInfo.cinemaId" class="footer">
+      <view class="schedule bg-white">
+        <view class="name-box">
+          <view class="film-name">
+            {{ seatInfo.movieName }}
+          </view>
+          <view class="change-box" @click="handleSwitch">
+            <view>切换场次</view>
+            <image class="arrow" :src="`${StaticUrl}/film-black-arrow.png`" mode="scaleToFill" />
+          </view>
+        </view>
+        <view class="film-info">
+          <text style="color:red">
+            {{ judgeNearDaysSimple(seatInfo.showTime) || seatInfo.showTime?.split('T')[0] }}
+          </text>
+          {{ seatInfo.showTime?.split('T')[1] }} -
+          {{ seatInfo.endTime?.split('T')[1] }}
+          {{ seatInfo.planType }} {{ seatInfo.language }}
+        </view>
+        <!-- 切换场次 -->
+        <view v-if="showTime" class="time-list">
+          <view
+            v-for="(item, index) in screenList" :key="index" class="time-item"
+            :class="[active == item.showTime ? 'active' : '']" @click="handleTime(item.showTime, item.sessionId)"
+          >
+            <view class="time">
+              {{ item.showTime.split('T')[1] }}
+            </view>
+            <view class="type">
+              {{ item.language }} {{ item.planType }}
+            </view>
+            <view class="price">
+              ¥{{ item.sellPrice }}起
+            </view>
+          </view>
+        </view>
+        <view v-if="chooseSeatList.length" class="seat-list">
+          <view v-for="(seat, optindex) in chooseSeatList" :key="optindex" class="seat-item">
+            <view class="seat">
+              <view class="seat-name">
+                {{ seat.seatName }}
+              </view>
+              <view class="seat-price">
+                <!-- ¥{{ priceConversion(seat.marketPrice) }} -->
+                ¥{{ isWithin45Minutes(new Date(), seatInfo.showTime) ? seat.fastPrice : seat.ticketPrice }}
+              </view>
+            </view>
+            <view class="delete-seat" @click="cancelSeat(seat)">
+              ×
+            </view>
+          </view>
+        </view>
+      </view>
+      <view class="notice bg-white">
+        请尽快选座,座位可能被抢!
+      </view>
+
+      <wd-button
+        custom-class="confirm" block :loading="isLoging" :class="{ unbtn: !chooseSeatList.length || loading }"
+        @click="buySeat"
+      >
+        {{ chooseSeatList.length ? (`¥${totalPrice} 确认座位`) : '请选座位' }}
+      </wd-button>
+    </view>
+  </view>
+</template>
+
+<style lang="scss" scoped>
+.view-page {
+  background-color: #f7f9fc;
+}
+
+.bg-white {
+  background-color: #fff;
+}
+
+/* 核心样式 */
+.areaList {
+  display: flex;
+  align-items: center;
+  /* 上下居中 */
+  /* 左右居中(可选) */
+  height: 40px;
+  white-space: nowrap;
+  overflow-x: auto;
+  scrollbar-width: none;
+  /* Firefox */
+  -ms-overflow-style: none;
+  /* IE/Edge */
+}
+
+.areaList::-webkit-scrollbar {
+  display: none;
+  /* Chrome、Safari、微信小程序-web-view */
+}
+
+.area-item {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  white-space: nowrap;
+  margin: 0 8px;
+}
+
+.area {
+  width: 20px;
+  height: 20px;
+  border-radius: 4px;
+}
+
+.area-price {
+  margin-left: 10rpx;
+  font-size: 20rpx;
+  line-height: 20px;
+}
+
+.footer {
+  position: fixed;
+  left: 0;
+  bottom: var(--window-bottom);
+  padding: 0 15px 15px 15px;
+  width: 100vw;
+  box-sizing: border-box;
+  border-radius: 5px 5px 0% 0%;
+  z-index: 999999999;
+  background: #FFFFFF;
+  border-radius: 16rpx 16rpx 0rpx 0rpx;
+  padding: 28rpx 24rpx;
+  box-sizing: border-box;
+  .notice{
+    font-size: 24rpx;
+    color: #FF4A39;
+    margin-top: 24rpx;
+    margin-bottom: 24rpx;
+  }
+
+  .schedule {
+
+    .name-box {
+      display: flex;
+      justify-content: space-between;
+      .film-name {
+        font-weight: bold;
+        font-size: 32rpx;
+        color: #222222;
+      }
+      .change-box {
+        display: flex;
+        align-items: center;
+        font-size: 24rpx;
+        color: #222222;
+        .arrow{
+          width: 32rpx;
+          height: 32rpx;
+          transform: rotate(90deg);
+        }
+      }
+    }
+
+      .film-info {
+        font-size: 24rpx;
+        color: #AAAAAA;
+        margin-top: 20rpx;
+      }
+
+  }
+}
+.time-list{
+  display: flex;
+  flex-wrap: nowrap;
+  scrollbar-width: none;
+  margin-top: 24rpx;
+  overflow-x: auto;
+  white-space: nowrap;
+    /* Firefox */
+  -ms-overflow-style: none;
+  /* IE/Edge */
+  .time-item{
+    flex-shrink: 0;
+    width: 160rpx;
+    height: 114rpx;
+    background: #F9F9F9;
+    border-radius: 16rpx 16rpx 16rpx 16rpx;
+    box-sizing: border-box;
+    padding: 10rpx 0;
+    text-align: center;
+    margin-right: 20rpx;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: space-between;
+    .time{
+      font-size: 24rpx;
+      color: #222222;
+    }
+    .type{
+      font-size: 22rpx;
+      color: #AAAAAA;
+    }
+    .price{
+      font-size: 22rpx;
+      color: #222222;
+    }
+  }
+    .time-item.active {
+      background: #FFE7E5;
+    }
+
+}
+
+.seat-list {
+  display: flex;
+  align-items: center;
+  justify-content: flex-start;
+  overflow-x: auto;
+  white-space: nowrap;
+  margin-top: 5px;
+  scrollbar-width: none;
+  margin-top: 24rpx;
+  /* Firefox */
+  -ms-overflow-style: none;
+  /* IE/Edge */
+}
+
+.seat-list::-webkit-scrollbar {
+  display: none;
+  /* Chrome、Safari、微信小程序-web-view */
+}
+
+.seat-item {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  height: 80rpx;
+  background: #F9F9F9;
+  border-radius: 16rpx 16rpx 16rpx 16rpx;
+  box-sizing: border-box;
+  padding: 10rpx 16rpx;
+  margin-right: 20rpx;
+  .seat-name {
+    font-size: 24rpx;
+    color: #222222;
+  }
+
+  .seat-price {
+    font-size: 22rpx;
+    color: #222222;
+    margin-top: 10rpx;
+
+  }
+
+  .delete-seat {
+    width: 20px;
+    height: 20px;
+    line-height: 20px;
+    color: #727272;
+    text-align: center;
+    margin-left: 16rpx;
+  }
+
+}
+
+.confirm {
+  width: 100%;
+  height: 80rpx;
+  line-height: 80rpx;
+  text-align: center;
+  border-radius: 10px;
+  font-size: 14px;
+  color: #fff;
+  background-color: #e54f4f;
+}
+
+.unbtn {
+  background-color: #f69c9c;
+}
+</style>

+ 64 - 0
src/subPack-film/components/choose-time.vue

@@ -0,0 +1,64 @@
+<script setup lang="ts">
+const props = withDefaults(defineProps<{
+  data?: [string, string][]
+}>(), {
+  data: () => [],
+})
+
+const emits = defineEmits(['change'])
+const active = defineModel()
+function handleTab(val: string) {
+  active.value = val
+  emits('change', val)
+  console.log(active)
+}
+</script>
+
+<template>
+  <view>
+    <!-- 影院时间 -->
+    <scroll-view class="tabList" :scroll-x="true" scroll-with-animation :scroll-into-view="`tab${active}`">
+      <view
+        v-for="(item, index) in props.data" :id="`tab${item[0]}`" :key="index" class="tab"
+        :class="[active == item[0] ? 'active' : '']" @click="handleTab(item[0])"
+      >
+        <!-- {{ filterDay(item) }} {{ timeFormat(new Date(item).getTime(), 'MM-dd') }} -->
+        {{ `${item[1]} ${item[0]}` }}
+      </view>
+    </scroll-view>
+  </view>
+</template>
+
+<style lang="scss" scoped>
+    .tabList {
+      white-space: nowrap;
+      width: 100%;
+      border-bottom: 1rpx solid #F0F0F0;
+      padding-top: 20rpx;
+      background: #fff;
+      .tab {
+        display: inline-block;
+        padding: 0 12rpx 20rpx;
+        font-weight: 400;
+        font-size: 28rpx;
+        color: #AAAAAA;
+      }
+
+      .tab.active {
+        position: relative;
+        color: #222222;
+
+        &::after {
+          content: '';
+          position: absolute;
+          bottom: 0%;
+          left: 50%;
+          transform: translateX(-50%);
+          width: 28rpx;
+          height: 6rpx;
+          background: #9ED605;
+          border-radius: 4rpx 4rpx 4rpx 4rpx;
+        }
+      }
+    }
+</style>

+ 66 - 0
src/subPack-film/components/tabbar.vue

@@ -0,0 +1,66 @@
+<script setup lang="ts">
+import { StaticUrl } from '@/config'
+import router from '@/router'
+
+defineProps({
+  active: {
+    type: Number,
+    default: 0,
+  },
+})
+
+const tabList = reactive([
+  { title: '首页', src: `${StaticUrl}/film-home.png`, activeSrc: `${StaticUrl}/film-active-home.png`, path: 'film-index' },
+  { title: '电影/影院', src: `${StaticUrl}/film-movie.png`, activeSrc: `${StaticUrl}/film-active-movie.png`, path: 'film-movie' },
+  { title: '订单', src: `${StaticUrl}/film-order.png`, activeSrc: `${StaticUrl}/film-active-order.png`, path: 'film-order' },
+])
+
+function handleItem(name: string) {
+  router.replace({ name })
+}
+</script>
+
+<template>
+  <view class="tabbar">
+    <view v-for="(item, index) in tabList" :key="index" class="item" @click="handleItem(item.path)">
+      <image class="img" :src="active == index ? item.activeSrc : item.src" mode="aspectFill" />
+      <view class="title" :class="[active == index ? 'active' : '']">
+        {{ item.title }}
+      </view>
+    </view>
+  </view>
+</template>
+
+<style lang="scss" scoped>
+.tabbar{
+    display: flex;
+    justify-content: space-around;
+    width: 750rpx;
+    height: 166rpx;
+    background: #FFFFFF;
+    box-shadow: 0rpx -6rpx 12rpx 2rpx rgba(0, 0, 0, 0.09);
+    border-radius: 32rpx 32rpx 0rpx 0rpx;
+    position: fixed;
+    bottom: 0;
+    left: 0;
+    z-index: 99999;
+    .item{
+        flex: 1;
+        text-align: center;
+        .img{
+            width: 44rpx;
+            height: 44rpx;
+            margin-top: 20rpx;
+        }
+        .title{
+            font-size: 20rpx;
+            color: #AAAAAA;
+            margin-top: 8rpx;
+            &.active{
+                color: #222222;
+            }
+        }
+
+    }
+}
+</style>

+ 144 - 0
src/subPack-film/index/index.vue

@@ -0,0 +1,144 @@
+<script setup lang="ts">
+import Tabbar from '../components/tabbar.vue'
+import router from '@/router'
+
+definePage({
+  name: 'film-index',
+  islogin: true,
+  style: {
+    navigationBarTitleText: '电影演出',
+    backgroundColorBottom: '#fff',
+  },
+})
+
+const hotList = ref<Api.filmMovieList>([
+
+])
+
+const comingSoonList = ref<Api.filmMovieList>([
+
+])
+
+function jump(active: any) {
+  router.push({ name: 'film-movie', params: { active } })
+}
+
+function handleBuy(item: Api.filmMovieList) {
+  console.log('goumai')
+  router.push({ name: 'film-movie-detail', params: { id: item.id as string, movieId: item.movieId as string } })
+}
+
+async function getList(showSt: number) {
+  uni.showLoading({ title: '加载中' })
+  const res = await Apis.film.getMovieList({ data: { showSt, pageNum: 1, pageSize: 8 } })
+  console.log(res, '请求')
+  if (!res.data) {
+    useGlobalToast().show('暂无该商品查看权限!')
+  }
+  if (showSt === 1) {
+    hotList.value = res.data?.records
+  }
+  else {
+    comingSoonList.value = res.data?.records
+  }
+  uni.hideLoading()
+}
+getList(1)
+getList(2)
+onMounted(() => {
+
+})
+</script>
+
+<template>
+  <view class="film-page px-24rpx pb-300rpx pt-20rpx">
+    <view class="mb-20rpx rounded-16rpx bg-[#fff] px-24rpx pt-24rpx">
+      <view class="flex justify-between">
+        <view class="text-[#222] font-bold">
+          热映电影
+        </view>
+        <view class="flex items-center text-center text-24rpx text-[#AAAAAA]" @click="jump(0)">
+          查看全部
+          <wd-icon name="chevron-right" size="24rpx" color="#AAA" />
+        </view>
+      </view>
+      <view class="p-0r gap-y-10r grid grid-cols-4 w-full gap-x-20rpx">
+        <view
+          v-for="(item, index) in hotList" :key="index" class="item relative aspect-square w-152rpx"
+          @click="handleBuy(item)"
+        >
+          <view class="tag absolute rounded-8rpx bg-[rgba(34,34,34,0.7)] px-10rpx text-18rpx text-[#fff] leading-26rpx">
+            {{ item.version }}
+          </view>
+          <image class="h-208rpx w-152rpx rounded-16rpx" :src="item.posterUrl" />
+          <view class="w-full overflow-hidden text-ellipsis whitespace-nowrap text-center text-24rpx">
+            {{ item.name }}
+          </view>
+          <view class="btn" @click.stop="handleBuy(item)">
+            购票
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <view class="mb-20rpx rounded-16rpx bg-[#fff] px-24rpx pt-24rpx">
+      <view class="flex justify-between">
+        <view class="text-[#222] font-bold">
+          即将上映
+        </view>
+        <view class="flex items-center text-center text-24rpx text-[#AAAAAA]" @click="jump(2)">
+          查看全部
+          <wd-icon name="chevron-right" size="24rpx" color="#AAA" />
+        </view>
+      </view>
+      <view class="gap-y-10r p-0r grid grid-cols-4 w-full gap-x-20rpx">
+        <view
+          v-for="(item, index) in comingSoonList" :key="index" class="item relative aspect-square w-152rpx"
+          @click="handleBuy(item)"
+        >
+          <view class="tag absolute rounded-8rpx bg-[rgba(34,34,34,0.7)] px-10rpx text-18rpx text-[#fff] leading-26rpx">
+            {{ item.version }}
+          </view>
+          <image class="h-208rpx w-152rpx rounded-16rpx" :src="item.posterUrl" />
+          <view class="w-full overflow-hidden text-ellipsis whitespace-nowrap text-center text-24rpx">
+            {{ item.name }}
+          </view>
+          <!-- <view class="btn" @click.stop="handleBuy(item)">
+            购票
+          </view> -->
+        </view>
+      </view>
+    </view>
+
+    <Tabbar :active="0" />
+  </view>
+</template>
+
+<style lang="scss" scoped>
+.film-page {
+
+    .item {
+        padding-bottom: 24rpx;
+        margin-top: 26rpx;
+        .tag {
+            top: 4rpx;
+            right: 10rpx;
+            background: rgba(34, 34, 34, 0.7);
+            text-align: center;
+            font-size: 18rpx;
+        }
+
+        .btn {
+            width: 96rpx;
+            height: 44rpx;
+            line-height: 44rpx;
+            text-align: center;
+            background: #9ED605;
+            border-radius: 26rpx 26rpx 26rpx 26rpx;
+            font-size: 28rpx;
+            color: #FFFFFF;
+            margin: 16rpx auto 0;
+        }
+    }
+}
+</style>

+ 463 - 0
src/subPack-film/movie-detail/index.vue

@@ -0,0 +1,463 @@
+<script setup lang="ts">
+import router from '@/router'
+
+definePage({
+  name: 'film-movie-detail',
+  islogin: false,
+  style: {
+    navigationStyle: 'custom',
+    navigationBarTitleText: '电影详情',
+    backgroundColorBottom: '#fff',
+  },
+})
+
+const info = ref<Api.filmMovieList>({})
+
+async function getInfo(id: number) {
+  uni.showLoading({ title: '加载中' })
+  const res = await Apis.film.getMovieDetail({ data: { id } })
+  console.log(res, '请求')
+  if (!res.data) {
+    return useGlobalToast().show('暂无查看权限!')
+  }
+  info.value = res.data
+  uni.hideLoading()
+}
+
+function buy() {
+  console.log('dianji')
+  router.push({ name: 'film-choose-film', params: { movieId: info.value.movieId as string, info: JSON.stringify(info.value) } })
+}
+
+onLoad((options) => {
+  getInfo(options?.id)
+  info.value.movieId = options?.movieId
+})
+</script>
+
+<template>
+  <view class="movieDetail">
+    <wd-navbar
+      title="" custom-style="background-color: transparent;" :bordered="false" :z-index="99"
+      safe-area-inset-top left-arrow fixed @click-left="router.back()"
+    />
+    <view class="bg-box">
+      <image class="img" :src="info.posterUrl" mode="scaleToFill" />
+    </view>
+    <view class="info-box">
+      <view class="info">
+        <image class="icon" :src="info.posterUrl" />
+        <view class="box">
+          <view class="name">
+            {{ info.name }}
+          </view>
+          <view class="score-box">
+            <view class="score">
+              评分 <view class="num">
+                {{ info.score }}
+              </view>
+            </view>
+            <view class="want-num">
+              {{ info.wish }}人想看
+            </view>
+          </view>
+
+          <view class="type-box">
+            {{ info.category }}
+            <view class="tag">
+              {{ info.version }}
+            </view>
+          </view>
+          <view class="publish-info">
+            时长: {{ info.duration }}分钟
+          </view>
+          <view class="publish-info">
+            上映:{{ info.releaseTime }}
+          </view>
+          <view class="publish-info">
+            导演:{{ info.director }}
+          </view>
+          <view class="publish-info">
+            演员:{{ info.star }}
+          </view>
+        </view>
+      </view>
+
+      <view class="title-box">
+        <view class="title">
+          简介
+        </view>
+        <!-- <view class="open">
+展开
+<image class="icon" src="@/static/shop-desc.png" mode=""></image>
+</view> -->
+      </view>
+
+      <view class="desc">
+        {{ info.description }}
+      </view>
+    </view>
+
+    <!-- <view v-if="info.workerList.length" class="actor-box">
+      <view class="title-box1">
+        <view class="title">
+          演员列表
+        </view>
+        <view class="total">
+          全部{{ info.workerList.length }}
+          <wd-icon name="arrow-left" size="22px" color="#000" />
+        </view>
+      </view>
+
+      <scroll-view class="actor-list" :scroll-x="true">
+        <view v-for="item in info.workerList" :key="item.id" class="item">
+          <zs-img class="icon" :src="item.avatarUrl" radius="full" width="120rpx" height="150rpx" mode="widthFix" />
+          <view class="name">
+            {{ item.characterName }}
+          </view>
+          <view class="sub-desc">
+            {{ item.actorName }}
+          </view>
+        </view>
+      </scroll-view>
+    </view> -->
+
+    <view v-if="info.showSt as number == 1 " class="btn-box">
+      <button class="buy-btn" type="default" @click="buy">
+        特惠购票
+      </button>
+    </view>
+  </view>
+</template>
+
+<style lang="scss" scoped>
+.movieDetail {
+  background: #F9F9F9;
+  // padding: 20rpx 24rpx;
+  min-height: 100vh;
+  .bg-box{
+    width: 100%;
+    height: 596rpx;
+    position: relative;
+    .img{
+      width: 100%;
+      height: 596rpx;
+      vertical-align: top;
+    }
+  }
+  .info-box {
+    padding: 24rpx;
+    padding-top: 300rpx;
+    border-radius: 16rpx 16rpx 16rpx 16rpx;
+    margin-top: -400rpx;
+    position: relative;
+    background: linear-gradient(179deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.8) 29.54%, #FFFFFF 46.99%, #FFFFFF 100%);
+    .info {
+      display: flex;
+      position: relative;
+      .icon {
+        width: 218rpx;
+        height: 298rpx;
+        background: #aaa;
+        border-radius: 16rpx;
+        flex-shrink: 0;
+      }
+
+      .box {
+        display: flex;
+        flex-direction: column;
+        justify-content: space-between;
+        margin-left: 20rpx;
+
+        .name {
+          font-weight: 600;
+          font-size: 28rpx;
+          color: #222222;
+          width: 400rpx;
+          white-space: nowrap;
+          overflow: hidden;
+          text-overflow: ellipsis;
+        }
+
+        .score-box {
+          font-size: 24rpx;
+          color: #AAAAAA;
+          position: absolute;
+          top: 0;
+          right: 0;
+          text-align: right;
+          .score {
+            display: flex;
+            align-items: center;
+            justify-content: flex-end;
+            .num {
+              font-size: 28rpx;
+              color: #FF4D3A;
+              padding: 0 12rpx;
+            }
+          }
+
+          .want-num {
+            padding: 0 12rpx;
+            align-self: flex-end;
+            margin-top: 24rpx;
+            // font-size: 24rpx;
+            // color: #AAAAAA;
+          }
+        }
+
+        .type-box {
+          display: flex;
+          align-items: center;
+          font-size: 24rpx;
+          color: #AAAAAA;
+
+          .tag {
+            padding: 6rpx 10rpx;
+            background: #F0F0F0;
+            border-radius: 8rpx 8rpx 8rpx 8rpx;
+            font-size: 24rpx;
+            color: #AAAAAA;
+            margin-left: 10rpx;
+          }
+        }
+
+        .publish-info {
+          font-size: 24rpx;
+          color: #AAAAAA;
+          white-space: nowrap;
+          text-overflow: ellipsis;
+          overflow: hidden;
+          max-width: 460rpx;
+        }
+      }
+    }
+
+    .title-box {
+      display: flex;
+      justify-content: space-between;
+      margin-top: 28rpx;
+
+      .title {
+        font-weight: 600;
+        font-size: 28rpx;
+        color: #222222;
+      }
+
+      .open {
+        display: flex;
+        align-items: center;
+        font-size: 24rpx;
+        color: #AAAAAA;
+
+        .icon {
+          width: 24rpx;
+          height: 24rpx;
+        }
+      }
+    }
+
+    .desc {
+      font-size: 24rpx;
+      color: #222222;
+      font-weight: 300;
+      margin-top: 20rpx;
+      line-height: 40rpx;
+    }
+  }
+
+  .btn-box {
+    position: fixed;
+    left: 0%;
+    bottom: 0%;
+    width: 100%;
+    padding: 10rpx 24rpx 76rpx;
+    box-sizing: border-box;
+    background: #FFFFFF;
+    border-top: 1rpx solid #EEEEEE;
+
+    .buy-btn {
+      width: 702rpx;
+      height: 80rpx;
+      line-height: 80rpx;
+      background: #EE4320;
+      border-radius: 46rpx 46rpx 46rpx 46rpx;
+      font-weight: 600;
+      font-size: 28rpx;
+      color: #FFFFFF;
+      padding: 0;
+      margin: 0;
+    }
+  }
+
+  .content {
+    padding: 24rpx;
+
+    .tabList {
+      white-space: nowrap;
+      width: 100%;
+
+      .tab {
+        display: inline-block;
+        padding: 0 12rpx 20rpx;
+        font-weight: 400;
+        font-size: 28rpx;
+        color: #222222;
+      }
+
+      .tab.active {
+        position: relative;
+
+        &::after {
+          content: '';
+          position: absolute;
+          bottom: 0%;
+          left: 50%;
+          transform: translateX(-50%);
+          width: 60rpx;
+          height: 8rpx;
+          background: #EE4320;
+          border-radius: 4rpx 4rpx 4rpx 4rpx;
+        }
+      }
+    }
+
+    .list {
+      max-height: 900rpx;
+      overflow: auto;
+
+      .cinema-item {
+        background: #FFFFFF;
+        border-radius: 16rpx 16rpx 16rpx 16rpx;
+        padding: 28rpx 0;
+        border-top: 1rpx solid #F0F0F0;
+
+        .name-box {
+          display: flex;
+          justify-content: space-between;
+
+          .name {
+            font-weight: 600;
+            font-size: 28rpx;
+            color: #222222;
+            width: 450rpx;
+            white-space: nowrap;
+            overflow: hidden;
+            text-overflow: ellipsis;
+          }
+
+          .price {
+            display: flex;
+            align-items: center;
+            font-size: 28rpx;
+            color: $uni-color-primary;
+
+            .label {
+              font-size: 24rpx;
+              color: #AAAAAA;
+              margin-left: 6rpx;
+            }
+          }
+        }
+
+        .address-box {
+          display: flex;
+          justify-content: space-between;
+          font-size: 24rpx;
+          color: #AAAAAA;
+          margin-top: 16rpx;
+
+          .address {
+            width: 460rpx;
+            white-space: nowrap;
+            overflow: hidden;
+            text-overflow: ellipsis;
+          }
+
+          .distance {}
+        }
+
+        .movie-name {
+          font-size: 24rpx;
+          color: #222222;
+          margin-top: 16rpx;
+          width: 100%;
+          white-space: nowrap;
+          overflow: hidden;
+          text-overflow: ellipsis;
+        }
+      }
+
+      .movie-item {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        padding: 34rpx 0;
+        border-bottom: 1rpx solid #F0F0F0;
+
+        .info {
+          display: flex;
+
+          .time-box {
+            display: flex;
+            flex-direction: column;
+            justify-content: space-between;
+
+            .start-time {
+              font-weight: 600;
+              font-size: 36rpx;
+              color: #222222;
+            }
+
+            .end-time {
+              font-size: 24rpx;
+              color: #AAAAAA;
+              margin-top: 20rpx;
+            }
+          }
+
+          .type-box {
+            display: flex;
+            flex-direction: column;
+            justify-content: space-between;
+            margin-left: 80rpx;
+
+            .type {
+              font-size: 28rpx;
+              color: #222222;
+            }
+
+            .room {
+              font-size: 24rpx;
+              color: #AAAAAA;
+            }
+          }
+        }
+
+        .price-box {
+          display: flex;
+          align-items: center;
+
+          .price {
+            font-size: 36rpx;
+            color: #FF4D3A;
+          }
+
+          .btn {
+            width: 120rpx;
+            height: 52rpx;
+            line-height: 52rpx;
+            text-align: center;
+            background: rgba(238, 67, 32, 0.1);
+            border-radius: 26rpx;
+            margin-left: 20rpx;
+            font-size: 28rpx;
+            color: #EE4320;
+          }
+        }
+      }
+    }
+  }
+
+}
+</style>

+ 33 - 0
src/subPack-film/movie-list/index.vue

@@ -0,0 +1,33 @@
+<script setup lang="ts">
+import Tabbar from '../components/tabbar.vue'
+
+const route = useRoute()
+
+definePage({
+  name: 'film-movieList',
+  islogin: false,
+  style: {
+    navigationBarTitleText: '电影演出',
+    backgroundColorBottom: '#fff',
+  },
+})
+
+// function handleItem() {
+//   console.log('dianji')
+// }
+
+uni.setNavigationBarTitle({
+  title: route.query?.title || '电影演出',
+})
+onMounted(() => {
+})
+</script>
+
+<template>
+  <view class="" />
+  <Tabbar :active="1" />
+</template>
+
+<style lang="scss" scoped>
+
+</style>

+ 528 - 0
src/subPack-film/movie/index.vue

@@ -0,0 +1,528 @@
+<script setup lang="ts">
+import Tabbar from '../components/tabbar.vue'
+import { StaticUrl } from '@/config'
+
+const route = useRoute()
+const router = useRouter()
+const addressStore = useAddressStore()
+const { Location } = storeToRefs(addressStore)
+definePage({
+  name: 'film-movie',
+  islogin: false,
+  style: {
+    navigationStyle: 'custom',
+    navigationBarTitleText: '电影演出',
+    backgroundColorBottom: '#fff',
+  },
+})
+const active = ref(route.query?.active || 0)
+const tabList = reactive([
+  { title: '热映', path: 'film-index' },
+  { title: '影院', path: 'film-movie' },
+  { title: '待映', path: 'film-order' },
+])
+
+const { data: hotList, isLastPage, page, reload, error, refresh } = usePagination((pageNum, pageSize) =>
+  Apis.film.getMovieList({ data: { showSt: 1, pageNum, pageSize } }), {
+  data: resp => resp.data?.records,
+  initialData: [],
+  initialPage: 1,
+  initialPageSize: 10,
+  append: true,
+  immediate: false,
+})
+
+const { data: filmList, isLastPage: isLastPage1, page: page1, reload: reload1, error: error1, refresh: refresh1 } = usePagination(() =>
+  Apis.film.getFilmList({ data: { movieId: '2062', lat: Location.value.latitude, lng: Location.value.longitude } }), {
+  data: resp => resp.data,
+  initialData: [],
+  initialPage: 1,
+  initialPageSize: 10,
+  append: true,
+  immediate: false,
+})
+
+const { data: comingSoonList, isLastPage: isLastPage2, page: page2, reload: reload2, error: error2, refresh: refresh2 } = usePagination((pageNum, pageSize) =>
+  Apis.film.getMovieList({ data: { showSt: 2, pageNum, pageSize } }), {
+  data: resp => resp.data?.records,
+  initialData: [],
+  initialPage: 1,
+  initialPageSize: 10,
+  append: true,
+  immediate: false,
+})
+
+const state = computed(() => {
+  if (active.value === 0) {
+    return error.value ? 'error' : !isLastPage.value ? 'loading' : 'finished'
+  }
+  else if (active.value === 1) {
+    return error1.value ? 'error' : !isLastPage2.value ? 'loading' : 'finished'
+  }
+  else {
+    return error2.value ? 'error' : !isLastPage2.value ? 'loading' : 'finished'
+  }
+})
+function loadData() {
+  console.log('loadData')
+  if (active.value === 0) {
+    reload()
+  }
+  else if (active.value === 1) {
+    reload1()
+  }
+  else if (active.value === 2) {
+    reload2()
+  }
+}
+
+function refreshData() {
+  if (Number(active.value) === 0) {
+    refresh()
+  }
+  else if (Number(active.value) === 1) {
+    refresh1()
+  }
+  else if (Number(active.value) === 2) {
+    refresh2()
+  }
+}
+function addPageNum() {
+  if (active.value === 0) {
+    if (!isLastPage.value) {
+      page.value++
+    }
+  }
+  else if (active.value === 1) {
+    if (!isLastPage1.value) {
+      page1.value++
+    }
+  }
+  else if (active.value === 2) {
+    if (!isLastPage2.value) {
+      page2.value++
+    }
+  }
+}
+
+onLoad((options) => {
+  console.log('onload', options)
+
+  active.value = Number(options?.active || 0)
+  refreshData()
+})
+onReachBottom(() => {
+  console.log('onReachBottom', state.value)
+  addPageNum()
+})
+
+function handleItem(index: number) {
+  console.log('handle')
+
+  active.value = index
+  loadData()
+}
+
+function handleFilm(title: string, movieId: string, cinemaId: string) {
+  router.push({
+    name: 'film-select-time',
+    params: {
+      id: '2221',
+      title,
+      movieId,
+      cinemaId,
+    },
+  })
+}
+
+function handleBuy(item: Api.filmMovieList) {
+  console.log('goumai')
+  router.push({ name: 'film-movie-detail', params: { id: item.id as string, movieId: item.movieId as string } })
+}
+
+function handleWant() {
+  useGlobalToast().show('敬请期待')
+}
+
+onMounted(() => {
+  if (!Location.value.latitude) {
+    useAddressStore().getLocation()
+  }
+})
+</script>
+
+<template>
+  <view class="movies">
+    <wd-navbar
+      title="" :bordered="false" :z-index="99" safe-area-inset-top left-arrow placeholder fixed
+      @click-left="router.back()"
+    >
+      <template #left>
+        <view class="flex items-center">
+          <wd-icon name="arrow-left" size="22px" color="#000" />
+        </view>
+      </template>
+      <template #title>
+        <view class="tabbar">
+          <view v-for="(item, index) in tabList" :key="index" class="item" @click="handleItem(index)">
+            <view class="title" :class="[active == index ? 'active' : '']">
+              {{ item.title }}
+            </view>
+          </view>
+        </view>
+      </template>
+    </wd-navbar>
+
+    <!-- 热映 列表 -->
+    <view v-if="active == 0" class="list">
+      <view v-for="(item, index) in hotList" :key="index" class="item">
+        <view class="img-box">
+          <image class="img" :src="item.posterUrl" />
+          <view class="tag">
+            {{ item.version }}
+          </view>
+        </view>
+        <view class="info">
+          <view class="title">
+            {{ item.name }}
+          </view>
+          <view class="num-box">
+            <view class="score-box">
+              <view class="label">
+                评分
+              </view>
+              <view class="score">
+                {{ item.score }}
+              </view>
+            </view>
+            <view class="num">
+              {{ item.wish }}人想看
+            </view>
+          </view>
+          <view class="type-box">
+            <view class="type">
+              {{ item.category }} |
+            </view>
+            <view class="time">
+              {{ item.duration }}分钟
+            </view>
+          </view>
+          <view class="director">
+            导演:{{ item.director }}
+          </view>
+          <view class="actor">
+            演员:{{ item.star }}
+          </view>
+        </view>
+        <view class="btn-box">
+          <image class="img" :src="`${StaticUrl}/film-movie-icon.png`" mode="scaleToFill" />
+          <view class="btn" @click="handleBuy(item)">
+            购票
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <!-- 影院 列表 -->
+    <view v-else-if="active == 1" class="film-list">
+      <view v-for="(item, index) in filmList" :key="index" class="film-item" @click="handleFilm(item.name, '', item.cinemaId)">
+        <view class="name-box">
+          <view class="name w-450rpx overflow-hidden text-ellipsis whitespace-nowrap">
+            {{ item.name }}
+          </view>
+          <view class="price-box">
+            <view class="num">
+              ¥{{ item.sellPrice }}
+            </view>
+            <div class="label">
+              起
+            </div>
+          </view>
+        </view>
+        <view class="address-box">
+          <view class="address overflow-hidden text-ellipsis whitespace-nowrap">
+            {{ item.address }}
+          </view>
+          <view class="distance">
+            {{ item.distance }}km
+          </view>
+        </view>
+        <div class="cantact">
+          {{ item.phone || '暂无联系方式' }}
+        </div>
+        <div class="movie-box">
+          <!-- 今日热映:《疯狂动物城2》《阿凡达3》《匿杀》《惊天魔... -->
+        </div>
+      </view>
+    </view>
+
+    <!-- 待映映 列表 -->
+    <view v-else-if="active == 2" class="list soon-list">
+      <view v-for="(item, index) in comingSoonList" :key="index" class="item">
+        <view class="img-box">
+          <image class="img" :src="item.posterUrl" />
+          <view class="tag">
+            {{ item.version }}
+          </view>
+        </view>
+        <view class="info">
+          <view class="title">
+            {{ item.name }}
+          </view>
+          <view class="num-box">
+            <view class="score-box">
+              <view class="label">
+                评分
+              </view>
+              <view class="score">
+                {{ item.score }}
+              </view>
+            </view>
+            <view class="num">
+              {{ item.wish }}人想看
+            </view>
+          </view>
+          <view class="type-box">
+            <view class="type">
+              {{ item.category }} |
+            </view>
+            <view class="time">
+              {{ item.duration }}分钟
+            </view>
+          </view>
+          <view class="director">
+            导演:{{ item.director }}
+          </view>
+          <view class="actor">
+            演员:{{ item.star }}
+          </view>
+        </view>
+        <view class="btn-box">
+          <image class="img" :src="`${StaticUrl}/film-movie-icon.png`" mode="scaleToFill" />
+          <view class="btn" @click="handleWant()">
+            想看
+          </view>
+        </view>
+      </view>
+    </view>
+    <wd-loadmore :state="state" :loading-props="{ color: '#9ED605', size: 20 }" @reload="reload" />
+
+    <Tabbar :active="1" />
+  </view>
+</template>
+
+<style lang="scss" scoped>
+.movies{
+  padding: 16rpx 24rpx 350rpx;
+  background: #F9F9F9;
+
+  .tabbar {
+      display: flex;
+      justify-content: space-between;
+      padding: 0 60rpx;
+      .item {
+          flex: 1;
+          text-align: center;
+
+          .title {
+              font-weight: bold;
+              font-size: 36rpx;
+              color: #AAAAAA;
+
+              &.active {
+                  color: #222222;
+              }
+          }
+
+      }
+  }
+  .list{
+    .item{
+      background: #FFFFFF;
+      border-radius: 16rpx 16rpx 16rpx 16rpx;
+      display: flex;
+      padding: 28rpx 24rpx;
+      position: relative;
+      margin-bottom: 20rpx;
+      .img-box{
+        position: relative;
+        margin-right: 24rpx;
+        .img{
+          width: 152rpx;
+          height: 208rpx;
+          border-radius: 16rpx;
+          vertical-align: top;
+        }
+        .tag{
+          line-height: 26rpx;
+          background: rgba(34, 34, 34, 0.7);
+          border-radius: 8rpx 8rpx 8rpx 8rpx;
+          font-size: 18rpx;
+          color: #FFFFFF;
+          padding: 0 10rpx;
+          position: absolute;
+          top: 4rpx;
+          right: 4rpx;
+
+        }
+      }
+      .info{
+        .title{
+          font-weight: bold;
+          font-size: 32rpx;
+          color: #222222;
+          width: 330rpx;
+          white-space: nowrap;
+          text-overflow: ellipsis;
+          overflow: hidden;
+        }
+        .num-box{
+          display: flex;
+          align-items: flex-end;
+          margin-top: 8rpx;
+          line-height: 28rpx;
+          .score-box{
+            display: flex;
+            align-items: flex-end;
+            .label{
+              font-size: 24rpx;
+              color: #AAAAAA;
+            }
+            .score{
+              font-weight: bold;
+              font-size: 28rpx;
+              color: #FF4D3A;
+              margin-left: 6rpx;
+            }
+          }
+          .num{
+            font-size: 24rpx;
+            color: #AAAAAA;
+            margin-left: 8rpx;
+          }
+        }
+        .type-box{
+          display: flex;
+          margin-top: 8rpx;
+
+            .type {
+            font-size: 24rpx;
+            color: #AAAAAA;
+          }
+          .time{
+            font-size: 24rpx;
+            color: #AAAAAA;
+          }
+        }
+        .director{
+          font-size: 24rpx;
+          color: #AAAAAA;
+          margin-top: 8rpx;
+          }
+        .actor{
+          font-size: 24rpx;
+          color: #AAAAAA;
+          margin-top: 8rpx;
+          width: 470rpx;
+          white-space: nowrap;
+          text-overflow: ellipsis;
+          overflow: hidden;
+          }
+      }
+      .btn-box{
+        position: absolute;
+        top: 0;
+        right: 0;
+        .img{
+          width: 172rpx;
+          height: 150rpx;
+        }
+        .btn{
+          position: absolute;
+          bottom: 0;
+          left: calc(50% - 60rpx);
+          width: 120rpx;
+          height: 52rpx;
+          line-height: 52rpx;
+          text-align: center;
+          background: #9ED605;
+          border-radius: 26rpx 26rpx 26rpx 26rpx;
+          font-size: 28rpx;
+          color: #FFFFFF;
+        }
+      }
+    }
+  }
+  .list.soon-list .btn-box .btn{
+    background: #FFB639;
+  }
+
+    .film-list {
+
+      .film-item {
+        background: #FFF;
+        border-radius: 16rpx 16rpx 16rpx 16rpx;
+        padding: 24rpx;
+        margin-bottom: 20rpx;
+
+        .name-box {
+          display: flex;
+          justify-content: space-between;
+          margin-bottom: 16rpx;
+
+          .name {
+            font-weight: bold;
+            font-size: 28rpx;
+            color: #222222;
+          }
+
+          .price-box {
+            display: flex;
+            align-items: center;
+
+            .num {
+              font-weight: bold;
+              font-size: 36rpx;
+              color: #FF4D3A;
+            }
+
+            .label {
+              font-size: 24rpx;
+              color: #AAAAAA;
+            }
+          }
+        }
+
+        .address-box {
+          display: flex;
+          justify-content: space-between;
+          font-size: 24rpx;
+          color: #AAAAAA;
+          margin-bottom: 16rpx;
+
+          .address {
+            font-size: 24rpx;
+            color: #AAAAAA;
+          }
+
+          .distance {}
+        }
+
+        .cantact {
+          font-size: 24rpx;
+          color: #AAAAAA;
+          margin-bottom: 16rpx;
+        }
+
+        .movie-box {
+          font-size: 24rpx;
+          color: #222222;
+          width: 100%;
+          white-space: nowrap;
+          text-overflow: ellipsis;
+          overflow: hidden;
+        }
+      }
+    }
+}
+</style>

+ 621 - 0
src/subPack-film/order-detail/index.vue

@@ -0,0 +1,621 @@
+<script setup lang="ts">
+import { filterDay, timeFormat } from '../utils'
+import { OrderStatus, handleCommonOrderStatusText } from '../utils/order-data'
+import { StaticUrl } from '@/config'
+
+definePage({
+  name: 'film-order-detail',
+  islogin: false,
+  style: {
+    navigationBarTitleText: '订单详情',
+    backgroundColorBottom: '#fff',
+  },
+})
+
+const orderInfo = ref()
+const current = ref(0)
+const qrRefs = ref<Record<string, any>>({})
+function handleCopy(data: string) {
+  uni.setClipboardData({
+    data,
+    success() {
+      uni.showToast({
+        title: '复制成功',
+        icon: 'none',
+      })
+    },
+  })
+}
+
+async function downloadImg() {
+  await nextTick()
+  const qrInstance = qrRefs.value[current.value]
+  console.log(qrInstance, current.value)
+
+  qrInstance.downloadQrcode()
+}
+
+async function getData(orderNo: string) {
+  const res = await Apis.xsb.orderInfo({ data: { orderNo } })
+  // const res = await Apis.film.filmOrderDetail({ data: { id } })
+
+  orderInfo.value = res.data
+  if (orderInfo.value.orderMovieItems[0].ticketCode) {
+    orderInfo.value.codeList = orderInfo.value.orderMovieItems[0].ticketCode?.split(',')
+  }
+  else {
+    orderInfo.value.codeList = []
+  }
+}
+function call() {
+  uni.makePhoneCall({
+    phoneNumber: orderInfo.value.phone,
+  })
+}
+
+function handleNav() {
+  uni.openLocation({
+    latitude: Number(orderInfo.value?.latitude),
+    longitude: Number(orderInfo.value?.longitude),
+    name: orderInfo.value.cinemaName,
+    address: orderInfo.value.cinemaName,
+  })
+}
+function changeSwiper(e: any) {
+  console.log(e.detail.current)
+  current.value = e.detail.current
+}
+
+onLoad((options) => {
+  getData(options?.orderNo as string)
+})
+</script>
+
+<template>
+  <view class="film-order-detail">
+    <view class="status">
+      <!-- 支付成功,待出票  -->
+      {{ orderInfo.hbOrderStatus == 0 ? "待支付" : handleCommonOrderStatusText(orderInfo) }}
+    </view>
+    <view class="status-desc">
+      如需改签、退款,请直接联系商家
+    </view>
+    <!-- 出票状态 -->
+    <view v-if="orderInfo.hbOrderStatus === OrderStatus.OrderAccepted" class="status-box block">
+      <view class="icon-box">
+        <image class="icon" :src="`${StaticUrl}/film-ing-icon.png`" />
+        <view class="state">
+          正在出票
+        </view>
+      </view>
+      <view class="notice">
+        预计1-15分钟内出票,若遇到影院系统导致出票失败,系统
+        将自动为您退款
+      </view>
+    </view>
+
+    <!-- 取票码 -->
+    <view class="ticket-info block">
+      <view class="ticket-item">
+        <view class="seat">
+          座位:
+          <view v-for="(item, index) in orderInfo.orderMovieItems" :key="index">
+            {{ `${item.name} ` }} &nbsp;
+          </view>
+        </view>
+        <!-- <view class="code-box">
+          <view class="code">
+            取票码:{{ item.ticketCode || '暂无' }}
+          </view>
+          <image class="icon" :src="`${StaticUrl}/film-copy.png`" @click="handleCopy(item.ticketCode)" />
+        </view>
+        <view class="notice">
+          (前为取票码,后为验票码)
+        </view> -->
+        <template v-if="orderInfo.codeList.length > 0">
+          <swiper class="swiper w-[100%]" :indicator-dots="true" @change="changeSwiper">
+            <swiper-item
+              v-for="(item, index) in orderInfo.codeList" :key="index"
+              class="flex items-center justify-center"
+            >
+              <QCode
+                :ref="(el) => { if (el) qrRefs[index] = el; }" class="my-20rpx" :text="item" :qwidth="90"
+                :qr-key="`qr${index}`"
+              />
+            </swiper-item>
+          </swiper>
+          <view class="qrcode-box">
+            <!-- <view class="qrcode">
+            </view> -->
+            <view class="download-btn" @click="downloadImg">
+              下载到手机
+            </view>
+          </view>
+        </template>
+      </view>
+      <!-- <view class="open-box">
+        <view class="text">
+          展开(共{{ orderInfo.orderMovieItems.length }}个)
+        </view>
+        <image class="arrow" :src="`${StaticUrl}/film-black-arrow.png`" mode="scaleToFill" />
+      </view> -->
+    </view>
+
+    <!-- 退款信息 -->
+    <view v-if="false" class="refund-info block">
+      <!-- 全退 -->
+      <view class="sub-title">
+        退款信息
+      </view>
+      <!-- 部分退 -->
+      <view class="flex items-center">
+        <image class="mr-16rpx h-30rpx w-30rpx" :src="`${StaticUrl}/film-error.png`" mode="scaleToFill" />
+        <view class="sub-title">
+          出票失败已退款:6排6座、6排7座
+        </view>
+      </view>
+      <view class="item">
+        <view class="label gray">
+          退款金额
+        </view>
+        <view class="value">
+          ¥159.9
+        </view>
+      </view>
+      <view class="desc">
+        退还金额:159.9元 退还积分:100(已过期10)
+      </view>
+      <view class="item">
+        <view class="label gray">
+          退款方式
+        </view>
+        <view class="value">
+          原支付方式退回
+        </view>
+      </view>
+      <view class="item">
+        <view class="label gray">
+          退款时间
+        </view>
+        <view class="value">
+          2024-12-13 11:12:30
+        </view>
+      </view>
+      <view class="item">
+        <view class="label gray">
+          预计到帐
+        </view>
+        <view class="value">
+          1-3个工作日
+        </view>
+      </view>
+    </view>
+
+    <!-- 退款原因 -->
+    <view v-if="false" class="refund-reason block">
+      <view class="sub-title">
+        退款原因
+      </view>
+      <view class="reason">
+        影院场次临时调整
+      </view>
+    </view>
+
+    <!-- 影片信息 -->
+    <view class="movie-info block">
+      <image class="img" :src="orderInfo.orderImage" />
+      <view class="title-box">
+        <view class="title">
+          {{ orderInfo.movieName }}
+        </view>
+
+        <view class="time-box">
+          {{ filterDay(timeFormat(orderInfo.session, 'yyyy-MM-dd')) == '今天' ? (`今天${timeFormat(orderInfo.session, 'HH:mm')}`) : timeFormat(orderInfo.session) }} 英语3D
+        </view>
+      </view>
+    </view>
+
+    <!-- 影院信息 -->
+    <view class="film-info block">
+      <view class="title">
+        {{ orderInfo.cinemaName }}
+      </view>
+      <view class="icon-box">
+        <view class="icon-item" @click="handleNav">
+          <image class="icon" :src="`${StaticUrl}/film-nav-icon.png`" />
+          <view class="text">
+            导航
+          </view>
+        </view>
+        <view class="icon-item" @click="call">
+          <image class="icon" :src="`${StaticUrl}/film-phone.png`" />
+          <view class="text">
+            电话
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <!-- 座位信息 -->
+    <view class="seat-info block">
+      <view class="room-num">
+        {{ orderInfo.hallName }}
+      </view>
+      <!-- <view class="area-price">
+        普通区¥29.9
+      </view> -->
+      <view class="seat-list">
+        <view v-for="(item, index) in orderInfo.orderMovieItems" :key="index" class="item">
+          <view class="label">
+            {{ item.name }}
+          </view>
+          <view class="value">
+            ¥{{ item.price }}
+          </view>
+        </view>
+      </view>
+    </view>
+
+    <!-- 价格信息 -->
+    <view class="goods-price-box block">
+      <view class="item">
+        <view class="label">
+          商品金额
+        </view>
+        <view class="value">
+          ¥{{ orderInfo.orderMoney }}
+        </view>
+      </view>
+      <view class="notice">
+        座位跨区,按最高价格计算。订单总价=最高座位价格×座位数
+      </view>
+      <view class="item">
+        <view class="label">
+          积分({{ orderInfo.offsetPoints || 0 }})
+        </view>
+        <view class="value price">
+          -¥{{ orderInfo.offsetPoints / 100 }}
+        </view>
+      </view>
+      <!-- <view class="item">
+        <view class="label">
+          平台券
+        </view>
+        <view class="value price">
+          -¥14
+        </view>
+      </view> -->
+      <view class="line" />
+      <view class="total-box">
+        <view class="reduce">
+          总计¥{{ orderInfo.orderMoney }}共减¥{{ orderInfo.offsetPoints / 100 }}
+        </view>
+        <view class="text">
+          实际付款
+        </view>
+        <view class="total">
+          ¥{{ orderInfo.total }}
+        </view>
+      </view>
+    </view>
+
+    <view class="order-info block">
+      <view class="sub-title">
+        订单信息
+      </view>
+      <view class="item">
+        <view class="label gray">
+          订单编号
+        </view>
+        <view class="value">
+          {{ orderInfo.orderNumber }}
+          <image class="icon" :src="`${StaticUrl}/film-copy.png`" @click="handleCopy(orderInfo.orderNumber)" />
+        </view>
+      </view>
+      <view class="item">
+        <view class="label gray">
+          支付方式
+        </view>
+        <view class="value">
+          微信支付
+        </view>
+      </view>
+      <view class="item">
+        <view class="label gray">
+          支付时间
+        </view>
+        <view class="value">
+          {{ orderInfo.payTime || '-' }}
+        </view>
+      </view>
+      <view class="item">
+        <view class="label gray">
+          下单时间
+        </view>
+        <view class="value">
+          {{ orderInfo.createTime }}
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<style lang="scss" scoped>
+.film-order-detail{
+  padding: 20rpx 24rpx 300rpx;
+  background: #F9F9F9;
+  .block{
+    background: #FFFFFF;
+    border-radius: 16rpx 16rpx 16rpx 16rpx;
+    padding: 24rpx;
+    margin-bottom: 20rpx;
+  }
+  .sub-title {
+    font-weight: bold;
+    font-size: 32rpx;
+    color: #222222;
+  }
+
+  .item{
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-top: 20rpx;
+    .label{
+      font-size: 28rpx;
+      color: #222222;
+    }
+    .label.gray{
+      color: #AAAAAA;
+    }
+    .value{
+      font-size: 28rpx;
+      color: #222222;
+    }
+    .value.price {
+      color: #FF4A39;
+    }
+  }
+  .item:first-child {
+    margin-top: 0 !important;
+  }
+  .status{
+    font-weight: bold;
+    font-size: 36rpx;
+    color: #222222;
+    margin-bottom: 20rpx;
+  }
+  .status-desc{
+    font-size: 28rpx;
+    color: #AAAAAA;
+    margin-bottom: 20rpx;
+  }
+  .status-box{
+    .icon-box{
+        margin-bottom: 20rpx;
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        .icon {
+          width: 80rpx;
+          height: 80rpx;
+          margin-bottom: 20rpx;
+        }
+      .state{
+        font-weight: bold;
+        font-size: 32rpx;
+        color: #222222;
+      }
+    }
+    .notice{
+      font-size: 24rpx;
+      color: #AAAAAA;
+      line-height: 40rpx;
+    }
+  }
+
+  .ticket-info{
+    .seat{
+      font-weight: bold;
+      font-size: 28rpx;
+      color: #222222;
+      display: flex;
+    }
+    .code-box{
+      display: flex;
+      align-items: center;
+      margin-top: 20rpx;
+      .code{
+        font-weight: bold;
+        font-size: 28rpx;
+        color: #222222;
+      }
+      .icon{
+        width: 30rpx;
+        height: 30rpx;
+        margin-left: 20rpx;
+      }
+    }
+    .notice{
+      font-size: 24rpx;
+      color: #AAAAAA;
+      margin-top: 20rpx;
+    }
+    .qrcode-box{
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      justify-content: center;
+      margin-top: 24rpx;
+      .qrcode{
+        width: 160rpx;
+        height: 160rpx;
+        background: #AAAAAA;
+        border-radius: 16rpx;
+      }
+      .download-btn{
+        width: 180rpx;
+        height: 44rpx;
+        line-height: 44rpx;
+        text-align: center;
+        background: #9ED605;
+        border-radius: 26rpx 26rpx 26rpx 26rpx;
+        font-size: 28rpx;
+        color: #FFFFFF;
+        margin-top: 24rpx;
+      }
+    }
+
+    .open-box{
+      display: flex;
+      align-items: center;
+      .text{
+        font-size: 24rpx;
+        color: #222222;
+      }
+      .arrow{
+        width: 32rpx;
+        height: 32rpx;
+        transform: rotate(90deg);
+      }
+    }
+  }
+
+  .refund-info{
+    .desc{
+      font-size: 24rpx;
+      color: #AAAAAA;
+      margin-top: 20rpx;
+    }
+  }
+
+  .refund-reason{
+    .reason{
+      font-size: 28rpx;
+      color: #AAAAAA;
+      margin-top: 20rpx;
+    }
+  }
+
+  .movie-info{
+    display: flex;
+    align-items: center;
+    .img{
+      width: 152rpx;
+      height: 208rpx;
+      margin-right: 24rpx;
+      border-radius: 16rpx;
+      vertical-align: bottom;
+    }
+    .title-box{
+      display: flex;
+      flex-direction: column;
+      justify-content: center;
+
+      .title{
+        font-weight: bold;
+        font-size: 32rpx;
+        color: #222222;
+        margin-bottom: 20rpx;
+      }
+      .time-box{
+        font-size: 24rpx;
+        color: #AAAAAA;
+      }
+    }
+  }
+
+  .film-info{
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    .title{
+      font-weight: bold;
+      font-size: 32rpx;
+      color: #222222;
+      white-space: nowrap;
+      overflow: hidden;
+      text-overflow: ellipsis;
+    }
+    .icon-box{
+      display: flex;
+      justify-content: space-between;
+      width: 140rpx;
+      .icon-item{
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        .icon{
+          width: 40rpx;
+          height: 40rpx;
+          margin-bottom: 20rpx;
+        }
+        .text{
+          font-size: 28rpx;
+          color: #222222;
+        }
+    }
+  }
+  }
+
+  .seat-info{
+    .room-num{
+      font-size: 28rpx;
+      color: #222222;
+      margin-bottom: 20rpx;
+    }
+    .area-price{
+      font-size: 24rpx;
+      color: #AAAAAA;
+      margin: 20rpx 0;
+    }
+    .seat-list{
+
+    }
+  }
+
+  .goods-price-box{
+    .notice{
+      font-size: 24rpx;
+      color: #AAAAAA;
+      margin-top: 16rpx;
+    }
+    .line{
+      height: 2rpx;
+      background: #F0F0F0;
+      margin-top: 20rpx;
+    }
+
+    .total-box{
+      display: flex;
+      justify-content: flex-end;
+      align-items: center;
+      margin-top: 30rpx;
+      .reduce{
+        font-size: 24rpx;
+        color: #222222;
+      }
+      .text{
+        font-weight: bold;
+        font-size: 32rpx;
+        color: #222222;
+        margin: 0 12rpx;
+      }
+      .total{
+        font-weight: bold;
+        font-size: 32rpx;
+        color: #FF4A39;
+      }
+    }
+  }
+
+  .order-info{
+
+    .icon{
+      width: 30rpx;
+      height: 30rpx;
+      margin-left: 20rpx;
+    }
+  }
+}
+</style>

+ 311 - 0
src/subPack-film/order/index.vue

@@ -0,0 +1,311 @@
+<script setup lang="ts">
+import Tabbar from '../components/tabbar.vue'
+import { OrderStatus, handleCommonCancelOrder, handleCommonOrderStatusText } from '../utils/order-data'
+import { getWxCommonPayment, handleCommonPayMent } from '../utils/confirm-order'
+// import { StaticUrl } from '@/config'
+import { timeFormat } from '../utils'
+import { createGlobalLoadingMiddleware } from '@/api/core/middleware'
+import router from '@/router'
+
+definePage({
+  name: 'film-order',
+  islogin: false,
+  style: {
+    navigationBarTitleText: '我的订单',
+    backgroundColorBottom: '#fff',
+  },
+})
+
+const orderStatus = ref('all')
+const tabList = reactive([
+  { title: '全部', value: 'all' },
+  { title: '待支付', value: 'paddingPay' },
+  { title: '已支付', value: 'ing' },
+  { title: '已完成', value: 'completed' },
+  { title: '已退款', value: 'cancel' },
+])
+
+const { data: orderList, refresh, isLastPage, page, error, reload } = usePagination((pageNum, pageSize) => Apis.xsb.orderList({
+  data: {
+    businessType: 'DYY',
+    orderStatus: orderStatus.value,
+    pageNum,
+    pageSize,
+  },
+}), {
+  immediate: false,
+  pageNum: 1,
+  pageSize: 10,
+  initialData: [],
+  data: res => res.data?.list,
+  append: true,
+  middleware: createGlobalLoadingMiddleware(),
+})
+
+const state = computed(() => {
+  return error.value ? 'error' : !isLastPage.value ? 'loading' : 'finished'
+})
+
+function handleItem(value: string) {
+  orderStatus.value = value
+  reload()
+}
+
+async function handlePay(orderNumber: string) {
+  const res = await handleCommonPayMent(orderNumber)
+  if (res.payType !== 1) {
+    await getWxCommonPayment(res)
+  }
+  else {
+    reload()
+  }
+}
+async function handleCancel(order: Api.xsbOrderList) {
+  await handleCommonCancelOrder(order)
+  reload()
+}
+function handleCopy(data: string) {
+  uni.setClipboardData({
+    data,
+    showToast: true,
+  })
+}
+function handleOrder(orderNo: string) {
+  router.push({
+    name: 'film-order-detail',
+    params: {
+      orderNo,
+    },
+  })
+}
+
+onReachBottom(() => {
+  if (!isLastPage.value) {
+    page.value++
+  }
+})
+
+onLoad(() => {
+  refresh()
+})
+</script>
+
+<template>
+  <view class="film-order">
+    <!-- 顶部切换 -->
+    <view class="tabbar">
+      <view v-for="(item, index) in tabList" :key="index" class="item" @click="handleItem(item.value)">
+        <view class="title" :class="[orderStatus == item.value ? 'active' : '']">
+          {{ item.title }}
+        </view>
+      </view>
+    </view>
+
+    <!-- 列表 -->
+    <view class="order-list">
+      <view
+        v-for="(item, index) in orderList" :key="index" class="order-item block"
+        @click.self="handleOrder(item.orderNumber as string)"
+      >
+        <view class="top-box">
+          <view class="title">
+            {{ item.movieName }}
+          </view>
+          <view class="status">
+            <template v-if="item.hbOrderStatus === OrderStatus.PaddingPay">
+              <view class="flex items-center">
+                待支付( 还剩 <wd-count-down :time="handleCommonOrderStatusText(item)" @finish="refresh" /> )
+              </view>
+            </template>
+            <text v-else>
+              {{ handleCommonOrderStatusText(item) }}
+            </text>
+          </view>
+        </view>
+        <!-- <view class="status-box">
+          <view class="reason-box">
+            <image class="icon" :src="`${StaticUrl}/film-error.png`" mode="scaleToFill" />
+            <view class="reason">
+              订单取消原因
+            </view>
+          </view>
+          <view class="info">
+            退款金额 ¥240 预计1-3个工作日到账
+          </view>
+        </view> -->
+
+        <view class="info-box">
+          <image class="img" :src="item.orderImage" mode="scaleToFill" />
+          <view class="info">
+            <view class="info-item">
+              影院:{{ item.cinemaName }}
+            </view>
+            <view class="info-item">
+              场次:{{ timeFormat(item.session) }}
+            </view>
+            <view class="info-item">
+              数量:{{ item.orderMovieItems?.length }}张
+            </view>
+            <view class="info-item">
+              总价:¥{{ item.orderMoney }}
+            </view>
+          </view>
+        </view>
+        <view class="btn-box">
+          <template v-if="item.hbOrderStatus === OrderStatus.PaddingPay">
+            <!-- 待支付 -->
+            <view
+              class="btn cancel"
+              custom-style="width: 128rpx;height: 38rpx;font-size: 24rpx;color: #AAAAAA;box-sizing: border-box;"
+              @click.stop="handleCancel(item)"
+            >
+              取消订单
+            </view>
+            <view
+              class="btn"
+              custom-style="width: 128rpx;height: 38rpx;font-size: 24rpx;color: #222222;box-sizing: border-box"
+              @click.stop="handlePay(item.orderNumber as string)"
+            >
+              付款
+            </view>
+          </template>
+          <wd-button
+            v-if="item.orderMovieItems[0].ticketCode"
+            custom-style="width: 188rpx;height: 44rpx;font-size: 28rpx;box-sizing: border-box"
+            @click.stop="handleCopy(item.orderMovieItems[0].ticketCode as string)"
+          >
+            复制取票码
+          </wd-button>
+        </view>
+      </view>
+    </view>
+
+    <wd-loadmore :state="state" :loading-props="{ color: '#9ED605', size: 20 }" @reload="reload" />
+  </view>
+  <Tabbar :active="2" />
+</template>
+
+<style lang="scss" scoped>
+.film-order{
+  background: #F9F9F9;
+  padding: 80rpx 24rpx 300rpx;
+  .tabbar {
+    display: flex;
+    justify-content: space-between;
+    padding-top: 10rpx;
+    background: #fff;
+    position: fixed;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 60rpx;
+    box-sizing: border-box;
+    .item {
+      flex: 1;
+      text-align: center;
+      .title {
+        font-size: 32rpx;
+        color: #222222;
+        &.active {
+          color: #9ED605;
+        }
+      }
+
+    }
+  }
+
+  .block {
+    background: #FFFFFF;
+    border-radius: 16rpx 16rpx 16rpx 16rpx;
+    padding: 24rpx;
+    margin-bottom: 20rpx;
+  }
+
+  .order-list{
+    .order-item{
+      .top-box{
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        .title{
+          font-size: 32rpx;
+          color: #222222;
+          font-weight: bold;
+        }
+        .status{
+          font-size: 28rpx;
+          color: #222222;
+        }
+      }
+      .status-box{
+        margin-top: 24rpx;
+        background: #F9F9F9;
+        border-radius: 16rpx 16rpx 16rpx 16rpx;
+        padding: 24rpx;
+        .reason-box{
+          display: flex;
+          align-items: center;
+          .icon{
+            width: 30rpx;
+            height: 30rpx;
+          }
+          .reason{
+            font-size: 28rpx;
+            color: #999999;
+            margin-left: 10rpx;
+          }
+        }
+        .info{
+          font-size: 24rpx;
+          color: #AAAAAA;
+          margin-top: 20rpx;
+          line-height: 44rpx;
+        }
+      }
+      .info-box{
+        margin-top: 20rpx;
+        display: flex;
+        .img{
+          width: 160rpx;
+          height: 160rpx;
+          border-radius: 16rpx 16rpx 16rpx 16rpx;
+          background: #999999;
+          margin-right: 20rpx;
+        }
+        .info{
+          display: flex;
+          flex-direction: column;
+          justify-content: space-between;
+          .info-item{
+            font-size: 24rpx;
+            color: #222222;
+          }
+        }
+      }
+      .btn-box{
+        display: flex;
+        align-items: center;
+        justify-content: flex-end;
+        margin-top: 20rpx;
+        .btn{
+          width: 128rpx;
+          height: 48rpx;
+          line-height: 48rpx;
+          text-align: center;
+          background: #FFFFFF;
+          border-radius: 24rpx 24rpx 24rpx 24rpx;
+          border: 1rpx solid #222222;
+          font-size: 24rpx;
+          color: #222222;
+        }
+        .btn.cancel{
+          color: #AAAAAA;
+          border: 1rpx solid #AAAAAA;
+          margin-right: 20rpx;
+        }
+      }
+    }
+  }
+
+}
+</style>

+ 369 - 0
src/subPack-film/select-time/index.vue

@@ -0,0 +1,369 @@
+<script setup lang="ts">
+import selectTime from '../components/choose-time.vue'
+import { isWithin45Minutes, timeFormat } from '../utils/index'
+import { StaticUrl } from '@/config'
+import router from '@/router'
+
+// interface movieInfoItem {
+//   score: number
+//   name: string
+//   category: string
+//   director: string
+// }
+
+definePage({
+  name: 'film-select-time',
+  islogin: false,
+  style: {
+    navigationBarTitleText: '',
+    backgroundColorBottom: '#fff',
+  },
+})
+const title = ref('')
+const current = ref(0)
+
+const currentTime = ref(timeFormat(new Date().getTime(), 'yyyy-MM-dd'))// 今天
+const query = ref({
+  cinemaId: '',
+  movieId: '',
+
+})
+const dayList = ref([])
+const filmInfo = ref<Api.filmOwnMovieList>({})
+
+const movieInfo = computed(() => {
+  // let list: movieInfoItem
+  const list = filmInfo.value.movieShows?.[current.value]
+  return list
+})
+
+const movieShows = computed(() => {
+  let list: any[] | undefined = []
+  filmInfo.value.movieShows?.forEach((i, d) => {
+    if (i.shows.length) {
+      current.value = d
+      list = i.shows
+    }
+  })
+
+  const sessions = list.filter(i => i.showDate === currentTime.value)
+  console.log('2222', list.filter(i => i.showDate === currentTime.value))
+
+  return sessions.length ? sessions[0].sessions : []
+})
+
+function handleClick(index: number, movieId: string) {
+  current.value = index
+  query.value.movieId = movieId
+  getData().then(() => {
+    getDate(movieId)
+  })
+  console.log(333333, current.value)
+}
+function changeTime(val: string) {
+  console.log(val)
+}
+
+function call(phoneNumber: string) {
+  uni.makePhoneCall({
+    phoneNumber,
+  })
+}
+
+function handleNav() {
+  uni.openLocation({
+    latitude: Number(filmInfo.value?.cinema?.lat),
+    longitude: Number(filmInfo.value?.cinema?.lng),
+    name: filmInfo.value.cinema?.name,
+    address: filmInfo.value.cinema?.address,
+  })
+}
+
+function goChooseSeat(sessionId: string) {
+  router.push({ name: 'film-choose-seat', params: { sessionId, cinemaId: query.value.cinemaId, movieId: query.value.movieId, phone: filmInfo.value.cinema?.phone as string } })
+}
+
+async function getData() {
+  uni.showLoading({ title: '加载中' })
+  const res = await Apis.film.getFilmMovieList({ data: query.value })
+  uni.hideLoading()
+  if (res.data) {
+    filmInfo.value = res.data.data
+  }
+}
+
+async function getDate(movieId: string) {
+  const res = await Apis.film.getFilmDateList({ data: { movieId, cinemaId: query.value.cinemaId } })
+  dayList.value = res.data
+}
+
+onLoad((options) => {
+  console.log('dadas', options)
+  title.value = options?.title
+  uni.setNavigationBarTitle({ title: options?.title })
+  query.value.movieId = options?.movieId
+  query.value.cinemaId = options?.cinemaId
+  getData().then(() => {
+    getDate(options?.movieId || filmInfo.value.movieShows?.[0].movieId)
+  })
+})
+</script>
+
+<template>
+  <view class="select-time">
+    <image class="bg" :src="`${StaticUrl}/film-bg.png`" mode="scaleToFill" />
+    <!-- 详情 -->
+    <view class="relative top-[-50rpx] ml-24rpx box-border w-702rpx rounded-16rpx bg-[#fff] p-24rpx">
+      <view class="flex items-center justify-between">
+        <view class="max-w-400rpx overflow-hidden text-ellipsis whitespace-nowrap text-32rpx font-bold">
+          {{ filmInfo.cinema?.name }}
+        </view>
+        <image
+          class="h-40rpx w-40rpx" :src="`${StaticUrl}/film-phone.png`" mode="scaleToFill"
+          @click="call(filmInfo.cinema?.phone as string)"
+        />
+      </view>
+      <view class="mt-16rpx flex text-24rpx">
+        <view class="max-w-500rpx overflow-hidden text-ellipsis whitespace-nowrap leading-48rpx">
+          {{ filmInfo.cinema?.address }}
+        </view>
+        <view class="flex items-center">
+          <view>·1.2km</view>
+          <image
+            class="h-48rpx w-48rpx" :src="`${StaticUrl}/film-black-arrow.png`" mode="scaleToFill"
+            @click="handleNav"
+          />
+        </view>
+      </view>
+    </view>
+    <scroll-view
+      class="swiper" :scroll-x="true" scroll-with-animation enable-flex :scroll-into-view="`item${current}`"
+      scroll-into-view-offset="-130"
+    >
+      <image
+        v-for="(item, index) in filmInfo.movieShows" :id="`item${index}`" :key="index" class="img"
+        :class="[current == index ? 'active' : '']" :src="item.posterUrl" @click="handleClick(index, item.movieId)"
+      />
+    </scroll-view>
+
+    <!-- 电影信息 -->
+    <view class="info-box">
+      <view class="title-box">
+        <view class="title">
+          {{ movieInfo.name }}
+        </view>
+        <view class="score">
+          评分{{ movieInfo.score }}
+        </view>
+      </view>
+      <view class="info">
+        <view class="time">
+          {{ movieInfo.duration }}分钟 |
+        </view>
+        <view class="type">
+          {{ movieInfo.category }}
+        </view>
+        <view v-if="movieInfo.director" class="director">
+          | {{ movieInfo.director }}
+        </view>
+      </view>
+    </view>
+
+    <view class="bg-[#fff] px-24rpx pb-24rpx">
+      <selectTime v-model="currentTime" :data="dayList" @change="changeTime" />
+      <view v-for="(item, index) in movieShows" :key="index" class="time-item">
+        <view class="left">
+          <view class="start">
+            {{ timeFormat(item.showTime, 'HH:mm') }}
+          </view>
+          <view class="end">
+            {{ timeFormat(item.endTime, 'HH:mm') }}散场
+          </view>
+        </view>
+
+        <view class="mid">
+          <view class="lang">
+            {{ item.language }} {{ item.planType }}
+          </view>
+          <view class="room">
+            {{ item.hall }}
+          </view>
+        </view>
+        <view class="right" @click="goChooseSeat(item.sessionId)">
+          <!-- <view class="state">
+            满座
+          </view> -->
+          <view class="price">
+            ¥{{ item.sellPrice }}
+          </view>
+          <image class="icon" :src="`${StaticUrl}/film-buy-bg.png`" />
+          <view v-if="!isWithin45Minutes(new Date(), item.showTime)" class="text">
+            特惠
+          </view>
+        </view>
+      </view>
+      <view v-if="!movieShows?.length" class="empty">
+        <image class="empty-icon" :src="`${StaticUrl}/film-empty.png`" />
+        <view class="desc">
+          今天场次已映完
+        </view>
+      </view>
+    </view>
+  </view>
+</template>
+
+<style lang="scss">
+.select-time ::v-deep swiper-item {
+  width: 140rpx;
+}
+.select-time{
+  .bg{
+    width: 100%;
+    height: 266rpx;
+  }
+  .swiper{
+    width: 100%;
+    // display: flex;
+    height: 208rpx;
+    // align-items: flex-end;
+    white-space: nowrap;
+    .img{
+      display: inline-block;
+      width: 140rpx;
+      height: 192rpx;
+      border-radius: 16rpx;
+      vertical-align: bottom;
+      flex-shrink: 0;
+      margin-right: 20rpx;
+    }
+    .img.active{
+      width: 152rpx;
+      height: 208rpx;
+    }
+  }
+  .info-box{
+    margin-top: 20rpx;
+    margin-bottom: 20rpx;
+    .title-box{
+      display: flex;
+      justify-content: center;
+      .title{
+        font-weight: bold;
+        font-size: 32rpx;
+        color: #222222;
+      }
+      .score{
+        font-weight: bold;
+        font-size: 32rpx;
+        color: #FF4D3A;
+        margin-left: 20rpx;
+      }
+    }
+    .info{
+      display: flex;
+      justify-content: center;
+      font-size: 24rpx;
+      color: #AAAAAA;
+      margin-top: 20rpx;
+
+    }
+
+  }
+
+  .time-item{
+    display: flex;
+    background: #F9F9F9;
+    border-radius: 16rpx 16rpx 16rpx 16rpx;
+    padding: 18rpx 24rpx;
+    margin-top: 24rpx;
+    .left{
+      width: 150rpx;
+      display: flex;
+      flex-direction: column;
+      justify-content: space-between;
+      .start{
+        font-weight: bold;
+        font-size: 36rpx;
+        color: #222222;
+      }
+      .end{
+        font-size: 24rpx;
+        color: #AAAAAA;
+      }
+    }
+    .mid{
+      flex: 1;
+      padding-left: 80rpx;
+      display: flex;
+      flex-direction: column;
+      justify-content: space-between;
+      .lang{
+        font-size: 28rpx;
+          color: #222222;
+      }
+      .room{
+        font-size: 24rpx;
+        color: #AAAAAA;
+      }
+    }
+    .right{
+      width: 104rpx;
+      height: 112rpx;
+      position: relative;
+      border-radius: 16rpx 16rpx 14rpx 14rpx;
+      .price{
+        position: absolute;
+        top: 12rpx;
+        left: 0;
+        width: 104rpx;
+        text-align: center;
+        font-size: 32rpx;
+        color: #FF4D3A;
+      }
+      .icon{
+        width: 104rpx;
+        height: 112rpx;
+        border-radius: 16rpx 16rpx 14rpx 14rpx;
+      }
+      .state{
+        width: 104rpx;
+        height: 112rpx;
+        line-height: 112rpx;
+        text-align: center;
+        position: absolute;
+        top: 0;
+        left: 0;
+        background: rgba($color: #222, $alpha: .7);
+        font-size: 24rpx;
+        color: #FFFFFF;
+        border-radius: 16rpx 16rpx 14rpx 14rpx;
+      }
+      .text{
+        position: absolute;
+        bottom: 4rpx;
+        left: 0;
+        width: 104rpx;
+        text-align: center;
+        font-size: 20rpx;
+        color: #FFFFFF;
+      }
+    }
+  }
+  .empty{
+    height: 700rpx;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    .empty-icon{
+      width: 106rpx;
+      height: 122rpx;
+    }
+    .desc{
+      font-size: 36rpx;
+      color: #222222;
+      margin-top: 22rpx;
+    }
+  }
+}
+</style>

+ 563 - 0
src/subPack-film/submit-order/index.vue

@@ -0,0 +1,563 @@
+<script setup lang="ts">
+import { getWxCommonPayment, handleCommonPayMent, paySuccess } from '../utils/confirm-order'
+import { getArrayFieldMax, isWithin45Minutes, timeFormat } from '../utils/index'
+import router from '@/router'
+import { StaticUrl } from '@/config'
+
+definePage({
+  name: 'film-submit-order',
+  islogin: false,
+  style: {
+    navigationBarTitleText: '确认订单',
+    backgroundColorBottom: '#fff',
+  },
+})
+
+// const isDifferent = computed(() => {
+//   return info.choose
+// })
+const showProtocol = ref(true)
+const isAgree = ref(false)
+const loading = ref(false)
+const { userInfo } = storeToRefs(useUserStore())
+
+const query = ref({
+  memberId: unref(userInfo).id,
+  channelId: unref(userInfo).channelId,
+  shopId: '1',
+  channelName: unref(userInfo).channelName,
+  cinemaName: '',
+  cinemaCode: '',
+  movieCode: '',
+  hallName: '',
+  orderPayMode: '1',
+  originPrice: 0,
+  seatNames: '',
+  sessionBeginTime: '',
+  switchSeat: false,
+  movieOrderItems: [] as Array<{ name: string, price: number }>,
+  session: '',
+  postImageUrl: '',
+  movieName: '',
+  fastTicket: false,
+})
+
+const info = ref({
+  cinemaName: '',
+  totalPrice: 0,
+  phone: '',
+  points: 0,
+  language: '',
+  planType: '',
+  showTime: '',
+  hallName: '',
+  chooseSeatList: [] as Array<{ seatName: string, ticketPrice: number, fastPrice: number, areaId: string }>,
+})
+
+const isDiffrent = computed(() => {
+  let flag = false
+  const areaId = info.value.chooseSeatList[0].areaId
+  for (const item of info.value.chooseSeatList) {
+    if (item.areaId !== areaId) {
+      flag = true
+      break
+    }
+  }
+  return flag
+})
+
+const totalPrice = computed(() => {
+  let total = 0
+  const scale = 10000
+  if (!info.value.showTime)
+    return 0
+  const key = isWithin45Minutes(new Date(), info.value.showTime) ? 'fastPrice' : 'ticketPrice'
+  const price = getArrayFieldMax(info.value.chooseSeatList, key) || 0
+  total = (price * scale) * info.value.chooseSeatList.length / scale
+  return total
+})
+function back() {
+  showProtocol.value = false
+  router.back()
+}
+
+function next() {
+  if (!isAgree.value) {
+    return useGlobalToast().show('请先勾选协议')
+  }
+  showProtocol.value = false
+}
+
+async function pay() {
+  loading.value = true
+  console.log(info.value.chooseSeatList)
+  query.value.fastTicket = isWithin45Minutes(new Date(), info.value.showTime)
+  query.value.movieOrderItems = info.value.chooseSeatList.map((item: any) => {
+    if (isWithin45Minutes(new Date(), info.value.showTime)) { // 如果在45分钟内,快速出票
+      return {
+        name: item.seatName,
+        price: item.fastPrice,
+      }
+    }
+    else {
+      return {
+        name: item.seatName,
+        price: item.ticketPrice,
+      }
+    }
+  })
+  const { data: orderNumber } = await Apis.film.addFilmOrder({ data: query.value })
+  const res = await handleCommonPayMent(orderNumber)
+  loading.value = false
+  if (res.payType !== 1) {
+    await getWxCommonPayment(res)
+    await paySuccess()
+  }
+  else {
+    await paySuccess()
+  }
+}
+
+function call() {
+  uni.makePhoneCall({
+    phoneNumber: info.value.phone,
+  })
+}
+async function getPoints() {
+  const res = await Apis.xsb.findUserPoints({})
+  if (res.data) {
+    info.value.points = res.data.availablePointsTotal as number
+  }
+  else {
+    info.value.points = 0
+  }
+  console.log(11111111, info.value.points)
+}
+getPoints()
+
+function usePoints() {
+  return (totalPrice.value * 100) >= info.value.points ? info.value.points : ((totalPrice.value * 10000) / 100)
+}
+
+onLoad((options) => {
+  query.value = JSON.parse(options?.query)
+  info.value = uni.getStorageSync('film-info')
+  console.log(11111, info.value.chooseSeatList)
+})
+</script>
+
+<template>
+  <view class="film-submit-order">
+    <!-- 影片信息 -->
+    <view class="movie-info-box block">
+      <view class="movie-info">
+        <image class="img" :src="query.postImageUrl" />
+        <view class="title-box">
+          <view class="title">
+            {{ query.movieName }}
+          </view>
+          <view class="time-box">
+            {{ timeFormat(info.showTime) }} {{ info.language }} {{ info.planType }}
+          </view>
+        </view>
+      </view>
+      <view class="phone-box">
+        <view class="notice-box">
+          <image class="icon" :src="`${StaticUrl}/film-error.png`" mode="scaleToFill" />
+          <view class="text">
+            不支持线上改签、退款,具体请咨询商家。
+          </view>
+        </view>
+        <image class="phone" :src="`${StaticUrl}/film-phone.png`" mode="scaleToFill" @click="call" />
+      </view>
+    </view>
+
+    <!-- 座位信息 -->
+    <view class="seat-info block">
+      <view class="sub-title">
+        {{ info.cinemaName }}
+      </view>
+      <view class="room-num">
+        {{ query.hallName }}
+      </view>
+      <!-- <view class="area-price">
+        普通区¥29.9
+      </view> -->
+      <view class="seat-list">
+        <view v-for="(item, index) in info.chooseSeatList" :key="index" class="item">
+          <view class="label">
+            {{ item.seatName }}
+          </view>
+          <view class="value">
+            ¥{{ isWithin45Minutes(new Date(), info.showTime) ? item.fastPrice : item.ticketPrice }}
+          </view>
+        </view>
+      </view>
+      <!--
+      <view class="area-price">
+        贵宾区¥39.9
+      </view>
+      <view class="seat-list">
+        <view class="item">
+          <view class="label">
+            2排11座
+          </view>
+          <view class="value">
+            ¥29.9
+          </view>
+        </view>
+        <view class="item">
+          <view class="label">
+            2排11座
+          </view>
+          <view class="value">
+            ¥29.9
+          </view>
+        </view>
+      </view> -->
+    </view>
+
+    <!-- 价格信息 -->
+    <view class="goods-price-box block">
+      <view class="item">
+        <view class="label">
+          商品金额
+        </view>
+        <view class="value">
+          ¥{{ totalPrice || info.totalPrice }}
+        </view>
+      </view>
+      <view v-if="isDiffrent" class="notice">
+        座位跨区,按最高价格计算。订单总价=最高座位价格×座位数
+      </view>
+      <view class="item">
+        <view class="label">
+          积分({{ usePoints() }})
+        </view>
+        <view class="value price">
+          -¥{{ usePoints() / 100 }}
+        </view>
+      </view>
+      <!-- <view class="item">
+        <view class="label">
+          平台券
+        </view>
+        <view class="value price">
+          -¥14
+        </view>
+      </view> -->
+      <view class="line" />
+      <view class="total-box">
+        <view class="text">
+          总计
+        </view>
+        <view class="total">
+          ¥{{ (totalPrice * 100 - usePoints()) / 100 }}
+        </view>
+      </view>
+    </view>
+
+    <!-- 购票须知 -->
+    <view class="notice-box block">
+      <view class="sub-title">
+        购票须知
+      </view>
+      <view class="content-text">
+        1.请提前30分钟左右到达影院现场,通过影院自助取票机完成
+        取票。
+
+        2.若取票过程中遇到无法取票等其它问题,请联系影院工作人
+        员进行处理。
+
+        3.请及时关注电影开场时间,凭票有序检票入场。
+
+        4.如需开具电影票发票,可联系影院工作人员凭当日票根进行
+        开具,若遇到特殊情况请及时联系猫眼客服人员。
+
+        5.退票、改签服务请参考影院具体政策要求,特殊场次及部分
+        使用卡、券场次订单可能不支持此服务。
+
+        6.由于设备故障等不可抗力因素,存在少量影院调整/取消场次
+        的情况,系统将会自动退票退款。
+
+        7.由于影院系统不稳定等因素,存在少量出票失败的情况系统
+        将会自动退款请注意查收。
+
+        8.如有其他问题可联系在线客服,工作时间:9:00-22:00。
+      </view>
+    </view>
+
+    <!-- 底部 -->
+    <view class="footer-box">
+      <view class="price-box">
+        <view class="total">
+          ¥{{ (totalPrice * 100 - usePoints()) / 100 }}
+        </view>
+        <view class="reduce">
+          共减¥{{ usePoints() / 100 }}
+        </view>
+      </view>
+      <wd-button
+        custom-style="width: 180rpx;height: 80rpx;background: #9ED605;border-radius: 40rpx 40rpx 40rpx 40rpx;" :loading="loading" @click="pay"
+      >
+        立即支付
+      </wd-button>
+    </view>
+
+    <!-- 协议弹窗 -->
+    <wd-popup v-model="showProtocol" :close-on-click-modal="false" custom-style="border-radius:16rpx;">
+      <view class="popup-box">
+        <view class="title">
+          退改签协议
+        </view>
+        <view class="content-text">
+          用户点击同意本协议之前,请务必认真阅读完全理解本协议中
+          全部条款,特别是其中与用户权益有或可能具有重大关系的条
+          款(包括但不限于第1.2条、第1.3条、第2条、第3.2条)。当用
+          户按照页面提示阅读、点击确认同意本协议及完成支付购票时,
+          即表示用户已经充分阅读、理解并接受本协议的全部内容。如
+          用户不同意接受本协议的任何条款,或无法理解本协议相关条
+          款含义的,请不要进行后续操用户点击同意本协议之前,请务
+          必认真阅读完全理解木协议中全部条款 特别是其中与用户权
+          益有或可能,用户点击同意本协议之前,请务必认真阅读
+        </view>
+
+        <view class="radio-box">
+          <wd-checkbox v-model="isAgree">
+            我已阅读并同意以上协议
+          </wd-checkbox>
+        </view>
+        <view class="btn-box">
+          <wd-button custom-class="btn" @click="next">
+            继续购票
+          </wd-button>
+          <wd-button custom-class="btn" type="text" @click="back">
+            暂不购票
+          </wd-button>
+        </view>
+      </view>
+    </wd-popup>
+  </view>
+</template>
+
+<style lang="scss" scoped>
+.film-submit-order{
+  padding: 20rpx 24rpx 300rpx;
+  background: #F9F9F9;
+  .block {
+    background: #FFFFFF;
+    border-radius: 16rpx 16rpx 16rpx 16rpx;
+    padding: 24rpx;
+    margin-bottom: 20rpx;
+  }
+  .sub-title {
+    font-weight: bold;
+    font-size: 32rpx;
+    color: #222222;
+  }
+  .item {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-top: 20rpx;
+    .label {
+      font-size: 28rpx;
+      color: #222222;
+    }
+    .label.gray {
+      color: #AAAAAA;
+    }
+    .value {
+      font-size: 28rpx;
+      color: #222222;
+    }
+    .value.price {
+      color: #FF4A39;
+    }
+  }
+
+  .item:first-child {
+    margin-top: 0 !important;
+  }
+
+  .movie-info {
+    display: flex;
+    align-items: center;
+
+    .img {
+      width: 152rpx;
+      height: 208rpx;
+      margin-right: 24rpx;
+      border-radius: 16rpx;
+      vertical-align: bottom;
+    }
+
+    .title-box {
+      display: flex;
+      flex-direction: column;
+      justify-content: center;
+
+      .title {
+        font-weight: bold;
+        font-size: 32rpx;
+        color: #222222;
+        margin-bottom: 20rpx;
+      }
+
+      .time-box {
+        font-size: 24rpx;
+        color: #AAAAAA;
+      }
+    }
+  }
+  .phone-box{
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-top: 24rpx;
+    .notice-box{
+      display: flex;
+      align-items: center;
+      .icon{
+        width: 32rpx;
+        height: 32rpx;
+        margin-right: 16rpx;
+      }
+      .text{
+        font-size: 24rpx;
+        color: #222222;
+      }
+    }
+
+    .phone{
+      width: 40rpx;
+      height: 40rpx;
+    }
+  }
+
+  .seat-info {
+    .room-num {
+      font-size: 28rpx;
+      color: #222222;
+      margin: 20rpx 0;
+    }
+
+    .area-price {
+      font-size: 24rpx;
+      color: #AAAAAA;
+      margin: 20rpx 0;
+    }
+
+    .seat-list {}
+  }
+
+  .goods-price-box {
+    .notice {
+      font-size: 24rpx;
+      color: #AAAAAA;
+      margin-top: 16rpx;
+    }
+
+    .line {
+      height: 2rpx;
+      background: #F0F0F0;
+      margin-top: 20rpx;
+    }
+
+    .total-box {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      margin-top: 30rpx;
+
+      .text {
+        font-weight: bold;
+        font-size: 32rpx;
+        color: #222222;
+      }
+
+      .total {
+        font-weight: bold;
+        font-size: 32rpx;
+        color: #FF4A39;
+      }
+    }
+  }
+
+  .notice-box{
+    .content-text{
+      font-size: 24rpx;
+      color: #222222;
+      line-height: 42rpx;
+      margin-top: 24rpx;
+    }
+  }
+
+  .footer-box{
+    position: fixed;
+    bottom: 0;
+    left: 0;
+    box-sizing: border-box;
+    padding: 12rpx 24rpx 76rpx;
+    background: #fff;
+    width: 100%;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    border-top: 1rpx solid #EEEEEE;
+    .price-box{
+      display: flex;
+      align-items: center;
+      .total{
+        font-weight: bold;
+        font-size: 40rpx;
+        color: #FF4A39;
+      }
+      .reduce{
+        font-size: 24rpx;
+        color: #FF4A39;
+        margin-left: 20rpx;
+      }
+    }
+  }
+
+}
+.btn-box::v-deep .btn {
+  width: 654rpx;
+  height: 80rpx;
+  margin-top: 24rpx;
+}
+.popup-box{
+  width: 702rpx;
+  height: 826rpx;
+  background: #FFFFFF;
+  border-radius: 16rpx 16rpx 16rpx 16rpx;
+  padding: 28rpx 24rpx;
+  box-sizing: border-box;
+  .title{
+    font-weight: bold;
+    font-size: 28rpx;
+    color: #222222;
+    text-align: center;
+  }
+  .content-text{
+    height: 500rpx;
+    overflow-y: auto;
+    font-size: 24rpx;
+    color: #222222;
+    line-height: 42rpx;
+    margin-top: 24rpx;
+  }
+  .radio-box{
+    margin-top: 24rpx;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+  .btn-box{
+    display: flex;
+    flex-direction: column;
+
+  }
+}
+</style>

+ 153 - 0
src/subPack-film/utils/confirm-order.ts

@@ -0,0 +1,153 @@
+import type { wxpay } from '@/api/globals'
+import router from '@/router'
+
+export function handleCommonPayMent(orderNumber: string): Promise<wxpay> {
+  return new Promise((resolve, reject) => {
+    if (!orderNumber) {
+      useGlobalToast().show({ msg: '订单号为空!请联系管理员' })
+      reject(new Error('订单号为空!请联系管理员'))
+      return
+    }
+    uni.showLoading({ mask: true })
+    Apis.common.hybridPayment({ data: { orderNumber } }).then((res) => {
+      resolve(res.data)
+      uni.hideLoading()
+    }).catch((err) => {
+      uni.hideLoading()
+      reject(err)
+    })
+  })
+}
+const { selectedAddress, userInfo } = storeToRefs(useUserStore())
+
+/**
+ *
+ * @param businessType
+ * @param dvyType  配送类型 1:快递 2:自提 3:及时配送
+ * @param remarks
+ * @param shopId
+ * @param orderItemList
+ * @returns 下单获取待支付订单号
+ */
+export function getOrderPayMent(freightFee: number, businessType: string, dvyType: number, shopId: number, orderItemList: {
+  prodCount?: number
+  skuId?: number
+}[], remarks?: string): Promise<string> {
+  uni.showLoading({ mask: true })
+  return new Promise((resolve, reject) => {
+    if (!selectedAddress.value) {
+      reject(new Error('请选择收货地址'))
+      return
+    }
+    Apis.common.addOrder({
+      data: {
+        channelId: Number(unref(userInfo).channelId),
+        businessType,
+        addressId: selectedAddress.value.id,
+        dvyType,
+        freightAmount: freightFee,
+        shopId,
+        orderItemList,
+        remarks,
+      },
+    }).then((res) => {
+      resolve(res.data)
+      uni.hideLoading()
+    }).catch((err) => {
+      uni.hideLoading()
+      reject(err)
+    })
+  })
+}
+/**
+ *
+ * @param orderInfo
+ * @returns 统一拉起微信支付
+ */
+
+export function getWxCommonPayment(orderPay: wxpay) {
+  uni.showLoading({ mask: true })
+  return new Promise((resolve, reject) => {
+    const orderInfo = {
+      appid: orderPay.appId,
+      timeStamp: orderPay.timeStamp,
+      nonceStr: orderPay.nonceStr,
+      package: orderPay.package,
+      signType: orderPay.signType,
+      paySign: orderPay.paySign,
+    }
+    uni.requestPayment({
+      provider: 'wxpay',
+      orderInfo,
+      ...orderInfo,
+      success(res) {
+        resolve(res)
+      },
+      fail(e) {
+        console.log(e, '失败')
+
+        reject(e)
+      },
+      complete() {
+        uni.hideLoading()
+      },
+    })
+  })
+}
+
+/**
+ * 电影
+ * 支付成功统一跳转
+ */
+export function paySuccess() {
+  return new Promise((resolve) => {
+    const { paySuccessPath, payBackIndexPath } = storeToRefs(useSysStore())
+    paySuccessPath.value = 'film-order'
+    payBackIndexPath.value = 'subPack-flim/index/index'
+    router.replace({ name: 'common-paySuccess' })
+    setTimeout(() => {
+      resolve(1)
+    }, 2000)
+  })
+}
+/**
+ * 电影
+ * 支付成功统一跳转
+ */
+export function payError() {
+  return new Promise((resolve) => {
+    const { paySuccessPath, payBackIndexPath } = storeToRefs(useSysStore())
+    paySuccessPath.value = 'xsb-order'
+    payBackIndexPath.value = 'subPack-xsb/commonTab/index'
+    router.replace({ name: 'common-payError' })
+    setTimeout(() => {
+      resolve(1)
+    }, 2000)
+  })
+}
+
+/**
+ * 支付成功清空购买的商品的购物车
+ */
+
+export async function clearCart(skuList: Api.CartSkuVo[]) {
+  uni.showLoading({ mask: true })
+  return new Promise((resolve) => {
+    const skuids = skuList.map(item => item.id).join(',')
+    if (skuids.length) {
+      Apis.common.deleteShoppingCart({
+        pathParams: {
+          ids: skuids,
+        },
+      }).then(async (res) => {
+        resolve(res)
+        await useSmqjhCartStore().getCartList('XSB')
+        uni.hideLoading()
+      }).finally(() => resolve(1))
+    }
+    else {
+      resolve(1)
+      uni.hideLoading()
+    }
+  })
+}

+ 149 - 0
src/subPack-film/utils/index.ts

@@ -0,0 +1,149 @@
+// export function timeFormat(
+//   time: number | string | Date, // 入参:时间戳/日期字符串/Date 对象
+//   format: string = 'yyyy-mm-dd', // 格式化模板,默认 yyyy-mm-dd
+// ): string {
+//   // 容错处理:非法时间值直接返回空字符串
+//   let date: Date
+//   if (typeof time === 'number') {
+//     // 处理时间戳(支持 13 位毫秒级,兼容 10 位秒级)
+//     date = new Date(time.toString().length === 10 ? time * 1000 : time)
+//   }
+//   else if (typeof time === 'string') {
+//     // 处理日期字符串(如 '2026-01-13'、'2026/01/13')
+//     date = new Date(time.replace(/-/g, '/')) // 兼容横杠分隔符,避免部分浏览器报错
+//   }
+//   else if (time instanceof Date) {
+//     // 处理 Date 对象
+//     date = time
+//   }
+//   else {
+//     console.error('非法的时间入参:', time)
+//     return ''
+//   }
+
+//   // 提取日期组件(年、月、日,月份从 0 开始,需 +1)
+//   const year = date.getFullYear()
+//   const month = padZero(date.getMonth() + 1)
+//   const day = padZero(date.getDate())
+
+//   // 解析格式化模板,替换占位符
+//   const formatMap: Record<string, string> = {
+//     yyyy: year.toString(),
+//     mm: month,
+//     dd: day,
+//   }
+
+//   // 替换模板中的占位符(支持 yyyy、mm、dd 核心格式)
+//   return format.replace(/yyyy|mm|dd/g, match => formatMap[match] || match)
+// }
+
+export function filterDay(val: string) {
+  if (val === timeFormat(new Date().getTime(), 'yyyy-MM-dd')) {
+    return '今天'
+  }
+  else if (val === timeFormat(new Date().getTime() + 1000 * 60 * 60 * 24, 'yyyy-MM-dd')) {
+    return '明天'
+  }
+  else if (val === timeFormat(new Date().getTime() + 1000 * 60 * 60 * 24 * 2, 'yyyy-MM-dd')) {
+    return '后天'
+  }
+  else {
+    const arr = ['日', '一', '二', '三', '四', '五', '六']
+    const num = new Date(val).getDay()
+    return `周${arr[num]}`
+  }
+}
+
+/**
+ * 判断两个时间的间隔是否在 45 分钟以内(包含 45 分钟整)
+ * @param {string | number | Date} time1 - 第一个时间(支持 ISO 字符串、时间戳、Date 对象)
+ * @param {string | number | Date} time2 - 第二个时间(支持 ISO 字符串、时间戳、Date 对象)
+ * @returns {boolean} - 间隔 ≤ 45 分钟返回 true,否则返回 false;时间格式非法返回 false
+ */
+export function isWithin45Minutes(time1: any, time2: any) {
+  // 1. 解析两个时间为 Date 对象
+  const date1 = new Date(time1)
+  const date2 = new Date(time2)
+
+  // 2. 合法性校验:判断两个时间是否能被正确解析
+  if (Number.isNaN(date1.getTime()) || Number.isNaN(date2.getTime())) {
+    console.error('传入的时间格式非法,无法解析')
+    return false
+  }
+
+  // 3. 定义 45 分钟对应的毫秒数(1 分钟 = 60 * 1000 毫秒,45 分钟 = 45 * 60 * 1000)
+  const fortyFiveMinutesMs = 45 * 60 * 1000 // 结果:2700000 毫秒
+
+  // 4. 计算两个时间的时间戳差值,取绝对值(忽略时间先后顺序)
+  const timeDiff = Math.abs(date1.getTime() - date2.getTime())
+
+  // 5. 判断差值是否 ≤ 45 分钟,返回布尔值
+  return timeDiff <= fortyFiveMinutesMs
+}
+
+/**
+ * 从对象数组中获取指定字段的最大值(兼容 ESLint,使用 Number.isNaN() 替代全局 isNaN())
+ * @param {Array} arr - 传入的对象数组(不能为空数组,且元素为对象)
+ * @param {string} field - 要获取最大值的字段名(支持简单字段,如 'age'、'price')
+ * @returns {number | null} - 返回字段的最大值(无有效数据返回 null)
+ */
+export function getArrayFieldMax(arr: any, field: string) {
+  // 1. 合法性校验:判断是否为有效数组
+  if (!Array.isArray(arr) || arr.length === 0) {
+    console.error('传入的不是有效数组或数组为空')
+    return null
+  }
+
+  // 2. 提取数组中所有元素的目标字段值,转换为数字并过滤无效值
+  const validValues = arr
+    .map((item) => {
+      // 排除非对象元素,避免访问字段报错
+      if (typeof item !== 'object' || item === null) {
+        return Number.NaN
+      }
+      // 提取字段值并转换为数字(处理字符串类型数字,如 '99' → 99)
+      return Number(item[field])
+    })
+    .filter((value) => {
+      // 优化核心:替换 isNaN() 为 Number.isNaN(),同时保留 isFinite() 过滤 Infinity
+      // 逻辑:不是 NaN 且是有限数值,才视为有效值
+      return !Number.isNaN(value) && Number.isFinite(value)
+    })
+
+  // 3. 判断有效数值数组是否为空,为空返回 null
+  if (validValues.length === 0) {
+    console.warn('数组中无有效的目标字段数值')
+    return null
+  }
+
+  // 4. 计算并返回最大值(扩展运算符将数组转为参数传入 Math.max)
+  return Math.max(...validValues)
+}
+
+/**
+ * 格式化任意时间为指定格式的字符串(核心支持 yyyy-mm-dd hh:mm 格式)
+ * @param {string | number | Date} time - 任意时间(ISO字符串、时间戳、Date对象、普通日期字符串)
+ * @param {string} format - 目标格式(如 'yyyy-mm-dd hh:mm',支持占位符:yyyy/年, mm/月, dd/日, hh/时, mm/分)
+ * @returns {string} - 格式化后的时间字符串,时间非法返回空字符串
+ */
+export function timeFormat(time: any, format = 'yyyy-MM-dd HH:mm') {
+  const targetDate = new Date(time)
+  if (Number.isNaN(targetDate.getTime()))
+    return ''
+
+  const padZero = (num: any) => num.toString().padStart(2, '0')
+  const timeObj = {
+    yyyy: targetDate.getFullYear().toString(),
+    MM: padZero(targetDate.getMonth() + 1), // 月份(大写MM)
+    dd: padZero(targetDate.getDate()),
+    HH: padZero(targetDate.getHours()), // 小时(大写HH)
+    mm: padZero(targetDate.getMinutes()), // 分钟(小写mm)
+  }
+
+  // 按规范占位符替换
+  return format.replace(/yyyy/g, timeObj.yyyy)
+    .replace(/MM/g, timeObj.MM)
+    .replace(/dd/g, timeObj.dd)
+    .replace(/HH/g, timeObj.HH)
+    .replace(/mm/g, timeObj.mm)
+}

+ 156 - 0
src/subPack-film/utils/order-data.ts

@@ -0,0 +1,156 @@
+import { dayjs } from 'wot-design-uni'
+
+export const navTabTypeList = [
+  { name: '全部', value: 0 },
+  { name: '配送(外卖)', value: 3 },
+  { name: '快递', value: 1 },
+]
+
+export const orderStatusList = [
+  { name: '全部', value: 'all' },
+  { name: '待支付', value: 'paddingPay' },
+  { name: '进行中', value: 'ing' },
+  { name: '已完成', value: 'completed' },
+  { name: '已取消', value: 'cancel' },
+]
+export enum OrderStatus {
+  /**
+   * 待支付
+   */
+  PaddingPay = 0,
+  /**
+   * 订单已接单
+   */
+  OrderAccepted = 20,
+  /**
+   * 订单待配送
+   */
+  OrderWaitDelivery = 30,
+  /**
+   * 订单配送中
+   */
+  OrderDelivering = 40,
+  /**
+   * 订单取消审核
+   */
+  OrderCancelAudit = 50,
+  /**
+   * 订单取消
+   */
+  OrderCancel = 60,
+  /**
+   * 订单已送达
+   */
+  OrderArrived = 70,
+  /**
+   * 订单完成
+   */
+  OrderCompleted = 80,
+}
+
+/**
+ * 订单状态文字统一处理
+ * @param order
+ *
+ */
+export function handleCommonOrderStatusText(order: Api.xsbOrderList): any {
+  if (order.hbOrderStatus === OrderStatus.PaddingPay) {
+    const endTime = dayjs(order.createTime).add(16, 'minutes')
+    const remaining = dayjs(endTime).valueOf() - dayjs().valueOf()
+    return Math.max(0, remaining) // 确保不会返回负数
+  }
+
+  if (order.hbOrderStatus === OrderStatus.OrderAccepted) {
+    return '待出票'
+  }
+  if (order.hbOrderStatus === OrderStatus.OrderWaitDelivery) {
+    return '订单待配送'
+  }
+  if (order.hbOrderStatus === OrderStatus.OrderDelivering) {
+    return '出票中'
+  }
+  if (order.hbOrderStatus === OrderStatus.OrderCancelAudit) {
+    return '订单取消审核'
+  }
+  if (order.hbOrderStatus === OrderStatus.OrderCancel) {
+    return '订单取消'
+  }
+  if (order.hbOrderStatus === OrderStatus.OrderArrived) {
+    return '订单完成'
+  }
+  if (order.hbOrderStatus === OrderStatus.OrderCompleted) {
+    return '订单完成'
+  }
+  return '未知订单状态'
+}
+
+/**
+ *  统一取消订单逻辑处理
+ * @param order
+ */
+export function handleCommonCancelOrder(order: Api.xsbOrderList) {
+  console.log(order, '取消顶顶顶')
+
+  return new Promise((resolve, reject) => {
+    useGlobalMessage().confirm({
+      title: '取消订单',
+      msg: '确定要取消订单吗?',
+      success: async () => {
+        uni.showLoading({ mask: true })
+        Apis.xsb.cancelOrder({
+          data: {
+            orderNo: String(order.orderNumber),
+          },
+        }).then(res => resolve(res)).catch(err => reject(err)).finally(() => uni.hideLoading())
+      },
+      fail: async (err) => {
+        reject(err)
+      },
+    })
+  })
+}
+
+/**
+ * 统一删除订单
+ */
+export function handleCommonDeleteOrder(order: Api.xsbOrderList) {
+  return new Promise((resolve, reject) => {
+    useGlobalMessage().confirm({
+      title: '删除订单',
+      msg: '确定要删除该订单吗?',
+      success: async () => {
+        uni.showLoading({ mask: true })
+        Apis.xsb.deleteOrder({
+          pathParams: {
+            ids: String(order.orderId),
+          },
+        }).then(res => resolve(res)).catch(err => reject(err)).finally(() => uni.hideLoading())
+      },
+      fail: async (err) => {
+        reject(err)
+      },
+    })
+  })
+}
+
+/**
+ * 统一确认收货
+ */
+export function handleCommonOrderReceive(order: Api.xsbOrderList) {
+  return new Promise((resolve, reject) => {
+    useGlobalMessage().confirm({
+      title: '确认收货',
+      msg: '确定要确认收货吗?',
+      success: async () => {
+        uni.showLoading({ mask: true })
+        await Apis.xsb.confirmReceipt({
+          data: {
+            orderNumber: String(order.orderNumber),
+          },
+        })
+        resolve(1)
+      },
+      fail: err => reject(err),
+    })
+  })
+}

+ 13 - 6
src/subPack-smqjh/components/charge-orderList/charge-orderList.vue

@@ -1,20 +1,27 @@
 <script setup lang="ts">
-import { chargeOrderStatus } from '../../order/order-data'
+import { chargeOrderStatus } from '@/subPack-charge/utils/index'
 import router from '@/router'
 import { StaticUrl } from '@/config'
 
 defineProps<{
   orderList: Api.xsbOrderList[]
-  subPackOrder: any
 }>()
 
 function handleItemClick(item: Api.xsbOrderList) {
   const orderNumber = item.orderNumber
   if (orderNumber) {
-    router.push({
-      name: 'charge-order-detail',
-      params: { orderNo: orderNumber },
-    })
+    if (item.hbOrderStatus !== 80) {
+      router.push({
+        name: 'chargeing',
+        params: { orderNo: orderNumber },
+      })
+    }
+    else {
+      router.push({
+        name: 'charge-order-detail',
+        params: { orderNo: orderNumber },
+      })
+    }
   }
   else {
     console.warn('订单号不存在,无法跳转到详情页')

+ 245 - 0
src/subPack-smqjh/components/film-orderList/film-orderList.vue

@@ -0,0 +1,245 @@
+<script setup lang="ts">
+// import { StaticUrl } from '@/config'
+import router from '@/router'
+
+const props = defineProps<{
+  orderList: Api.xsbOrderList[]
+  subPackOrder?: typeof import('@/subPack-film/utils/order-data')
+  subPackConfirm?: typeof import('@/subPack-film/utils/confirm-order')
+}>()
+
+const _emit = defineEmits<{
+  'after-sale': [item: Api.xsbOrderList]
+  'refresh': []
+}>()
+
+function resolveModule<T = any>(maybeRef: any): T | undefined {
+  if (!maybeRef)
+    return undefined
+  if (maybeRef.value !== undefined)
+    return maybeRef.value as T
+  return maybeRef as T
+}
+async function handlePay(orderNumber: string) {
+  const spc = resolveModule(props.subPackConfirm)
+  if (!spc)
+    return
+  const res = await spc.handleCommonPayMent(orderNumber)
+  if (res.payType !== 1) {
+    await getWxCommonPayment(res)
+  }
+  else {
+    _emit('refresh')
+  }
+}
+async function handleCancel(order: Api.xsbOrderList) {
+  const sp = resolveModule(props.subPackOrder)
+  if (!sp)
+    return
+  await handleCommonCancelOrder(order)
+  _emit('refresh')
+}
+function handleCopy(data: string) {
+  uni.setClipboardData({
+    data,
+    showToast: true,
+  })
+}
+
+function handleOrder(orderNo: string) {
+  router.push({
+    name: 'film-order-detail',
+    params: {
+      orderNo,
+    },
+  })
+}
+</script>
+
+<template>
+  <view
+    v-for="(item, index) in orderList" :key="index" class="order-item block"
+    @click.self="handleOrder(item.orderNumber as string)"
+  >
+    <view class="top-box">
+      <view class="title">
+        {{ item.movieName }}
+      </view>
+      <view class="status">
+        <template v-if="item.hbOrderStatus === props.subPackOrder?.OrderStatus.PaddingPay">
+          <view class="flex items-center">
+            待支付( 还剩 <wd-count-down
+              :time="props.subPackOrder?.handleCommonOrderStatusText(item)"
+              @finish="() => _emit('refresh')"
+            /> )
+          </view>
+        </template>
+        <text v-else>
+          {{ props.subPackOrder?.handleCommonOrderStatusText(item) }}
+        </text>
+      </view>
+    </view>
+    <!-- <view class="status-box">
+          <view class="reason-box">
+            <image class="icon" :src="`${StaticUrl}/film-error.png`" mode="scaleToFill" />
+            <view class="reason">
+              订单取消原因
+            </view>
+          </view>
+          <view class="info">
+            退款金额 ¥240 预计1-3个工作日到账
+          </view>
+        </view> -->
+
+    <view class="info-box">
+      <image class="img" :src="item.orderImage" mode="scaleToFill" />
+      <view class="info">
+        <view class="info-item">
+          影院:{{ item.cinemaName }}
+        </view>
+        <view class="info-item">
+          场次:{{ item.session.replace('T', ' ') }}
+        </view>
+        <view class="info-item">
+          数量:{{ item.orderMovieItems?.length }}张
+        </view>
+        <view class="info-item">
+          总价:¥{{ item.orderMoney }}
+        </view>
+      </view>
+    </view>
+    <view class="btn-box">
+      <template v-if="item.hbOrderStatus === props.subPackOrder?.OrderStatus.PaddingPay">
+        <!-- 待支付 -->
+        <view
+          class="btn cancel"
+          custom-style="width: 128rpx;height: 38rpx;font-size: 24rpx;color: #AAAAAA;box-sizing: border-box;"
+          @click.stop="handleCancel(item)"
+        >
+          取消订单
+        </view>
+        <view
+          class="btn"
+          custom-style="width: 128rpx;height: 38rpx;font-size: 24rpx;color: #222222;box-sizing: border-box"
+          @click.stop="handlePay(item.orderNumber as string)"
+        >
+          付款
+        </view>
+      </template>
+      <wd-button
+        v-if="item.orderMovieItems[0].ticketCode"
+        custom-style="width: 188rpx;height: 44rpx;font-size: 28rpx;box-sizing: border-box"
+        @click.stop="handleCopy(item.orderMovieItems[0].ticketCode as string)"
+      >
+        复制取票码
+      </wd-button>
+    </view>
+  </view>
+</template>
+
+<style lang="scss" scoped>
+.block {
+  background: #FFFFFF;
+  border-radius: 16rpx 16rpx 16rpx 16rpx;
+  padding: 24rpx;
+  margin-bottom: 20rpx;
+}
+
+  .order-item {
+    .top-box {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+
+      .title {
+        font-size: 32rpx;
+        color: #222222;
+        font-weight: bold;
+      }
+
+      .status {
+        font-size: 28rpx;
+        color: #222222;
+      }
+    }
+
+    .status-box {
+      margin-top: 24rpx;
+      background: #F9F9F9;
+      border-radius: 16rpx 16rpx 16rpx 16rpx;
+      padding: 24rpx;
+
+      .reason-box {
+        display: flex;
+        align-items: center;
+
+        .icon {
+          width: 30rpx;
+          height: 30rpx;
+        }
+
+        .reason {
+          font-size: 28rpx;
+          color: #999999;
+          margin-left: 10rpx;
+        }
+      }
+
+      .info {
+        font-size: 24rpx;
+        color: #AAAAAA;
+        margin-top: 20rpx;
+        line-height: 44rpx;
+      }
+    }
+
+    .info-box {
+      margin-top: 20rpx;
+      display: flex;
+
+      .img {
+        width: 160rpx;
+        height: 160rpx;
+        border-radius: 16rpx 16rpx 16rpx 16rpx;
+        background: #999999;
+        margin-right: 20rpx;
+      }
+
+      .info {
+        display: flex;
+        flex-direction: column;
+        justify-content: space-between;
+
+        .info-item {
+          font-size: 24rpx;
+          color: #222222;
+        }
+      }
+    }
+
+    .btn-box {
+      display: flex;
+      align-items: center;
+      justify-content: flex-end;
+      margin-top: 20rpx;
+
+      .btn {
+        width: 128rpx;
+        height: 48rpx;
+        line-height: 48rpx;
+        text-align: center;
+        background: #FFFFFF;
+        border-radius: 24rpx 24rpx 24rpx 24rpx;
+        border: 1rpx solid #222222;
+        font-size: 24rpx;
+        color: #222222;
+      }
+
+      .btn.cancel {
+        color: #AAAAAA;
+        border: 1rpx solid #AAAAAA;
+        margin-right: 20rpx;
+      }
+    }
+  }
+</style>

+ 28 - 26
src/subPack-smqjh/order/components/OrderRenderer.vue

@@ -1,6 +1,7 @@
 <script setup lang="ts">
 import chargeList from '../../components/charge-orderList/charge-orderList.vue'
 import xsbList from '../../components/xsb-orderList/xsb-orderList.vue'
+import filmList from '../../components/film-orderList/film-orderList.vue'
 
 interface Props {
   orderList: Api.xsbOrderList[]
@@ -10,6 +11,7 @@ interface Props {
 }
 
 defineProps<Props>()
+
 defineEmits<{
   'cancel': [order: Api.xsbOrderList]
   'pay': [orderNumber: string]
@@ -18,45 +20,45 @@ defineEmits<{
   'after-sale': [item: Api.xsbOrderList]
   'refresh': []
 }>()
+
+const subPackFilmOrder = ref<typeof import('@/subPack-film/utils/order-data')>()
+const subPackFilmComfirm = ref<typeof import('@/subPack-film/utils/confirm-order')>()
+
+onMounted(async () => {
+  subPackFilmOrder.value = await AsyncImport('@/subPack-film/utils/order-data')
+  subPackFilmComfirm.value = await AsyncImport('@/subPack-film/utils/confirm-order')
+})
 </script>
 
 <template>
   <template v-if="navActiveTab === 'all'">
     <template v-for="order in orderList" :key="order.orderNumber">
       <xsbList
-        v-if="order.businessType === 'XSB'"
-        :order-list="[order]"
-        :sub-pack-order="subPackOrder"
-        :sub-pack-confirm="subPackConfirm"
-        @cancel="$emit('cancel', $event)"
-        @pay="$emit('pay', $event)"
-        @submit="$emit('submit', $event)"
-        @del="$emit('del', $event)"
-        @after-sale="$emit('after-sale', $event)"
+        v-if="order.businessType === 'XSB'" :order-list="[order]" :sub-pack-order="subPackOrder"
+        :sub-pack-confirm="subPackConfirm" @cancel="$emit('cancel', $event)" @pay="$emit('pay', $event)"
+        @submit="$emit('submit', $event)" @del="$emit('del', $event)" @after-sale="$emit('after-sale', $event)"
         @refresh="$emit('refresh')"
       />
-      <chargeList
-        v-else-if="order.businessType === 'CD'"
-        :order-list="[order]"
-        :sub-pack-order="subPackOrder"
+      <chargeList v-else-if="order.businessType === 'CD'" :order-list="[order]" />
+      <filmList
+        v-else-if="order.businessType === 'DYY'" :order-list="[order]" :sub-pack-order="subPackFilmOrder"
+        :sub-pack-confirm="subPackFilmComfirm" @cancel="$emit('cancel', $event)" @pay="$emit('pay', $event)"
+        @submit="$emit('submit', $event)" @del="$emit('del', $event)" @after-sale="$emit('after-sale', $event)"
+        @refresh="$emit('refresh')"
       />
     </template>
   </template>
   <xsbList
-    v-else-if="navActiveTab === 'XSB'"
-    :order-list="orderList"
-    :sub-pack-order="subPackOrder"
-    :sub-pack-confirm="subPackConfirm"
-    @cancel="$emit('cancel', $event)"
-    @pay="$emit('pay', $event)"
-    @submit="$emit('submit', $event)"
-    @del="$emit('del', $event)"
-    @after-sale="$emit('after-sale', $event)"
+    v-else-if="navActiveTab === 'XSB'" :order-list="orderList" :sub-pack-order="subPackOrder"
+    :sub-pack-confirm="subPackConfirm" @cancel="$emit('cancel', $event)" @pay="$emit('pay', $event)"
+    @submit="$emit('submit', $event)" @del="$emit('del', $event)" @after-sale="$emit('after-sale', $event)"
     @refresh="$emit('refresh')"
   />
-  <chargeList
-    v-else-if="navActiveTab === 'CD'"
-    :order-list="orderList"
-    :sub-pack-order="subPackOrder"
+  <chargeList v-else-if="navActiveTab === 'CD'" :order-list="orderList" />
+  <filmList
+    v-else-if="navActiveTab === 'DYY'" :order-list="orderList" :sub-pack-order="subPackFilmOrder"
+    :sub-pack-confirm="subPackFilmComfirm" @cancel="$emit('cancel', $event)" @pay="$emit('pay', $event)"
+    @submit="$emit('submit', $event)" @del="$emit('del', $event)" @after-sale="$emit('after-sale', $event)"
+    @refresh="$emit('refresh')"
   />
 </template>

+ 11 - 10
src/subPack-smqjh/order/index.vue

@@ -61,6 +61,7 @@ const currentOrderList = computed(() => {
   }
   return []
 })
+
 onMounted(async () => {
   subPackOrder.value = await AsyncImport('@/subPack-xsb/utils/order-data')
   subPackComfirm.value = await AsyncImport('@/subPack-xsb/utils/confirm-order')
@@ -98,30 +99,30 @@ async function handleAfterSale(item: Api.xsbOrderList) {
 
 <template>
   <view class="page-smqjh">
-    <view class="nav sticky top-0 z-10 bg-white px24rpx py18rpx">
+    <view class="nav sticky top-0 z-10 bg-white px-24rpx py-18rpx">
       <scroll-view
-        scroll-x class="whitespace-nowrap" :scroll-into-view="`id-${scrollViewId}`"
-        :scroll-into-view-offset="-150" scroll-with-animation enable-passive
+        class="whitespace-nowrap" :scroll-into-view="`id-${scrollViewId}`"
+        :scroll-into-view-offset="-150" scroll-with-animation scroll-x enable-passive
       >
         <view class="flex items-center">
           <view
             v-for="item in navTabTypeList" :id="`id-${item.value}`" :key="item.value"
-            class="mr64rpx flex flex-col items-center whitespace-nowrap text-32rpx"
-            :class="[navActiveTab == item.value ? 'font-semibold ' : 'text-#AAAAAA']"
+            class="mr-64rpx flex flex-col items-center whitespace-nowrap text-32rpx"
+            :class="[navActiveTab == item.value ? 'font-semibold ' : 'text-[#AAAAAA]']"
             @click="handleChangeTypeNav(item.value)"
           >
             {{ item.name }}
             <view
-              class="mt10rpx bg-[var(--them-color)] transition-all transition-duration-400 ease-in"
-              :class="[navActiveTab == item.value ? 'w40rpx h8rpx rounded-4rpx' : '']"
+              class="mt-10rpx bg-[var(--them-color)] transition-all transition-duration-400 ease-in"
+              :class="[navActiveTab == item.value ? 'w-40rpx h-8rpx rounded-4rpx' : '']"
             />
           </view>
         </view>
       </scroll-view>
-      <view class="mt20rpx flex items-center">
+      <view class="mt-20rpx flex items-center">
         <view
           v-for="item in orderStatusList" :key="item.value"
-          class="mr16rpx rounded-24rpx bg-#F6F6F6 px16rpx py6rpx text-24rpx"
+          class="mr-16rpx rounded-24rpx bg-[#F6F6F6] px-16rpx py-6rpx text-24rpx"
           :class="[orderStatusActive == item.value ? 'bg-[var(--them-color)] text-white' : '']"
           @click="handleChangeStatus(item.value)"
         >
@@ -129,7 +130,7 @@ async function handleAfterSale(item: Api.xsbOrderList) {
         </view>
       </view>
     </view>
-    <view class="px24rpx">
+    <view class="px-24rpx">
       <OrderRenderer
         :order-list="currentOrderList"
         :nav-active-tab="navActiveTab"

+ 1 - 1
src/subPack-smqjh/order/order-data.ts

@@ -2,7 +2,7 @@ export const navTabTypeList = [
   { name: '全部', value: 'all' },
   { name: '星闪豹', value: 'XSB' },
   { name: '充电', value: 'CD' },
-  { name: '电影演出', value: 'MOVIE' },
+  { name: '电影演出', value: 'DYY' },
   { name: '视频权益', value: 'VIDEO' },
   { name: '大牌点餐', value: 'SHOP' },
 ]

+ 49 - 0
src/subPack-videoRights/commonTab/components/home.vue

@@ -0,0 +1,49 @@
+<script setup lang="ts">
+import router from '@/router'
+
+const { statusBarHeight, MenuButtonHeight } = storeToRefs(useSysStore())
+</script>
+
+<template>
+  <view class="video-rights-home-page">
+    <wd-navbar
+      title="视频权益" custom-style="background-color:#FFF" :bordered="false" :z-index="99"
+      safe-area-inset-top left-arrow fixed @click-left="router.back()"
+    />
+    <view :style="{ paddingTop: `${(Number(statusBarHeight) || 44) + MenuButtonHeight + 12}px` }" />
+    <view class="box-border px24rpx">
+      <scroll-view scroll-y type="custom">
+        <grid-view type="masonry" cross-axis-count="2" main-axis-gap="10" cross-axis-gap="10">
+          <view class="mt-18rpx rounded-16rpx bg-#FFF" @click="router.push({ name: 'video-rights-detail' })">
+            <view>
+              <image
+                class="h-342rpx w-342rpx rounded-16rpx"
+                src="../../../static/tab/class-tab1.png"
+              />
+            </view>
+            <view class="mx20rpx my-24rpx">
+              <view class="text-28rpx font-bold">
+                网易云黑胶VIP月卡
+              </view>
+              <view class="mt-30rpx flex items-center gap-18rpx">
+                <view class="text-#FF4D3A">
+                  <text class="text-22rpx font-500">
+                    ¥
+                  </text>
+                  <text class="text-36rpx font-800">
+                    18
+                  </text>
+                </view>
+                <view class="text-24rpx text-#AAA decoration-line-through">
+                  市场价¥30
+                </view>
+              </view>
+            </view>
+          </view>
+        </grid-view>
+      </scroll-view>
+    </view>
+  </view>
+</template>
+
+<style lang="scss" scoped></style>

+ 61 - 0
src/subPack-videoRights/commonTab/components/order.vue

@@ -0,0 +1,61 @@
+<script setup lang="ts">
+const { statusBarHeight, MenuButtonHeight } = storeToRefs(useSysStore())
+const tab = ref<number>(0)
+const tabsList = ref(['全部', '成功', '失败'])
+</script>
+
+<template>
+  <view class="video-rights-order-page">
+    <wd-navbar
+      title="订单" custom-style="background-color:#FFF" :bordered="false" :z-index="99"
+      safe-area-inset-top fixed
+    />
+    <view :style="{ paddingTop: `${(Number(statusBarHeight) || 44) + MenuButtonHeight + 12}px` }" />
+    <wd-tabs v-model="tab" animated>
+      <block v-for="item in tabsList" :key="item">
+        <wd-tab :title="item">
+          <view class="box-border bg-#f6f6f6 px24rpx">
+            <view class="h-28rpx" />
+            <view class="rounded-16rpx bg-#FFF p-24rpx">
+              <view class="flex items-center justify-between">
+                <view class="text-28rpx">
+                  2024-12-13 11:12:30
+                </view>
+                <view class="text-28rpx font-bold">
+                  充值成功
+                </view>
+              </view>
+              <view class="mt-24rpx h-2rpx w-full bg-#F0F0F0" />
+              <view class="mt-24rpx flex items-center justify-between">
+                <view class="flex items-center gap-20rpx">
+                  <view class="h-160rpx w-160rpx">
+                    image
+                  </view>
+                  <view>
+                    <view class="text-32rpx font-bold">
+                      网易云黑胶VIP月卡
+                    </view>
+                    <view class="mt-16rpx text-#FF4D3A">
+                      <text class="text-22rpx font-500">
+                        ¥
+                      </text>
+                      <text class="text-36rpx font-800">
+                        18
+                      </text>
+                    </view>
+                    <view class="mt-16rpx text-24rpx">
+                      XN123458625484359845
+                    </view>
+                  </view>
+                </view>
+                <wd-icon name="chevron-right" size="22px" color="#222222" />
+              </view>
+            </view>
+          </view>
+        </wd-tab>
+      </block>
+    </wd-tabs>
+  </view>
+</template>
+
+<style lang="scss" scoped></style>

+ 10 - 5
src/subPack-videoRights/commonTab/index.vue

@@ -1,20 +1,25 @@
 <script setup lang="ts">
+import home from './components/home.vue'
+import order from './components/order.vue'
+
 definePage({
   name: 'video-rights-tabbar',
   islogin: false,
   style: {
     navigationBarTitleText: '',
+    navigationStyle: 'custom',
   },
 })
-const tabbar = ref(1)
+const tabbar = ref(0)
 </script>
 
 <template>
+  <home v-if="tabbar === 0" />
+  <order v-if="tabbar === 1" />
   <view class="">
-    <wd-tabbar v-model="tabbar">
-      <wd-tabbar-item title="首页" icon="home" />
-      <wd-tabbar-item title="分类" icon="cart" />
-      <wd-tabbar-item title="我的" icon="user" />
+    <wd-tabbar v-model="tabbar" placeholder safe-area-inset-bottom fixed :bordered="false" :z-index="99999">
+      <wd-tabbar-item title="商品" icon="goods" />
+      <wd-tabbar-item title="订单记录" icon="list" />
     </wd-tabbar>
   </view>
 </template>

+ 94 - 0
src/subPack-videoRights/videoRightsDetail/videoRightsDetail.vue

@@ -0,0 +1,94 @@
+<script setup lang="ts">
+import { StaticUrl } from '@/config'
+
+definePage({
+  name: 'video-rights-detail',
+  islogin: false,
+  style: {
+    navigationBarTitleText: '详情',
+  },
+})
+</script>
+
+<template>
+  <view class="video-rights-detail-page">
+    <view class="relative h-750rpx">
+      <image class="h-750rpx w-full" src="https://picx.zhimg.com/v2-ab23d513fab6abd0a27cda9ba9676383_720w.jpg?source=172ae18b" />
+      <view class="absolute bottom-0 left-24rpx right-24rpx rounded-16rpx bg-#FFF p-24rpx">
+        <view class="flex items-center gap-18rpx">
+          <view class="text-#FF4D3A">
+            <text class="text-22rpx font-500">
+              ¥
+            </text>
+            <text class="text-36rpx font-800">
+              18
+            </text>
+          </view>
+          <view class="text-24rpx text-#AAA decoration-line-through">
+            ¥30
+          </view>
+        </view>
+        <view class="mt-20rpx text-32rpx font-bold">
+          网易云黑胶VIP月卡
+        </view>
+      </view>
+    </view>
+    <view class="mt-20rpx px24rpx">
+      <view class="rounded-16rpx bg-#FFF p-24rpx">
+        <view class="text-28rpx font-bold">
+          购买流程
+        </view>
+        <view class="mt-24rpx flex items-center gap-20rpx">
+          <view class="text-center">
+            <image class="h-40rpx w-40rpx" :src="`${StaticUrl}/videoRight-selectGoods.png`" />
+            <view class="text-24rpx text-#9ED605">
+              选择商品
+            </view>
+          </view>
+          <view class="text-#9ED605 font-500">
+            ····>
+          </view>
+          <view class="text-center">
+            <image class="h-40rpx w-40rpx" :src="`${StaticUrl}/videoRight-accect.png`" />
+            <view class="text-24rpx text-#9ED605">
+              输入账户
+            </view>
+          </view>
+          <view class="text-#9ED605 font-500">
+            ····>
+          </view>
+          <view class="text-center">
+            <image class="h-40rpx w-40rpx" :src="`${StaticUrl}/videoRight-select-pay.png`" />
+            <view class="text-24rpx text-#9ED605">
+              确认支付
+            </view>
+          </view>
+          <view class="text-#9ED605 font-500">
+            ····>
+          </view>
+          <view class="text-center">
+            <image class="h-40rpx w-40rpx" :src="`${StaticUrl}/videoRight-success.png`" />
+            <view class="text-24rpx text-#9ED605">
+              充值成功
+            </view>
+          </view>
+        </view>
+      </view>
+      <view class="mt-20rpx rounded-16rpx bg-#FFF p-24rpx">
+        <view class="text-28rpx font-bold">
+          温馨提示
+        </view>
+        <view class="mt-20rpx text-24rpx">
+          充值前请确认充值账号无误,虚拟商品充值成功无法退款。
+        </view>
+      </view>
+    </view>
+    <view class="fixed bottom-0 h-174rpx w-full border-[1rpx_solid_#EEEEEE] bg-#FFF px-24rpx">
+      <view class="mt-10rpx h-80rpx w-702rpx rounded-46rpx bg-#9ED605 text-center text-#FFF line-height-[80rpx]">
+        立即充值
+      </view>
+    </view>
+  </view>
+</template>
+
+<style lang="scss" scoped></style>

+ 27 - 27
src/subPack-xsb/order/index.vue

@@ -86,24 +86,24 @@ async function handleAfterSale(item: Api.xsbOrderList) {
 
 <template>
   <view class="page-xsb">
-    <view class="nav sticky top-0 z10 bg-white px24rpx py18rpx">
-      <view class="h80rpx flex items-center">
+    <view class="nav sticky top-0 z10 bg-white px-24rpx py-18rpx">
+      <view class="h-80rpx flex items-center">
         <view
-          v-for="item in navTabTypeList" :key="item.value" class="mr64rpx h-full flex flex-col items-center justify-center text-32rpx"
-          :class="[navActiveTab == item.value ? 'font-semibold ' : 'text-#AAAAAA']"
+          v-for="item in navTabTypeList" :key="item.value" class="mr-64rpx h-full flex flex-col items-center justify-center text-32rpx"
+          :class="[navActiveTab == item.value ? 'font-semibold ' : 'text-[#AAAAAA]']"
           @click="handleChangeTypeNav(item.value)"
         >
           {{ item.name }}
           <view
-            class="mt10rpx bg-[var(--them-color)] transition-all transition-duration-400 ease-in"
-            :class="[navActiveTab == item.value ? 'w40rpx h8rpx rounded-4rpx' : '']"
+            class="mt-10rpx bg-[var(--them-color)] transition-all transition-duration-400 ease-in"
+            :class="[navActiveTab == item.value ? 'w-40rpx h-8rpx rounded-4rpx' : '']"
           />
         </view>
       </view>
-      <view class="mt20rpx flex items-center">
+      <view class="mt-20rpx flex items-center">
         <view
           v-for="item in orderStatusList" :key="item.value"
-          class="mr16rpx rounded-24rpx bg-#F6F6F6 px16rpx py6rpx text-24rpx"
+          class="mr-16rpx rounded-24rpx bg-[#F6F6F6] px-16rpx py-6rpx text-24rpx"
           :class="[orderStatusActive == item.value ? 'bg-[var(--them-color)] text-white' : '']"
           @click="handleChangeStatus(item.value)"
         >
@@ -111,16 +111,16 @@ async function handleAfterSale(item: Api.xsbOrderList) {
         </view>
       </view>
     </view>
-    <view class="px24rpx">
-      <view v-for="item in orderList" :key="item.orderNumber" class="mt20rpx rounded-16rpx bg-white p24rpx">
+    <view class="px-24rpx">
+      <view v-for="item in orderList" :key="item.orderNumber" class="mt-20rpx rounded-16rpx bg-white p-24rpx">
         <view class="w-full flex items-center justify-between">
           <view class="flex items-center">
-            <image :src="`${StaticUrl}/order-icon.png`" class="h36rpx w36rpx" />
-            <view class="ml20rpx text-32rpx font-semibold">
+            <image :src="`${StaticUrl}/order-icon.png`" class="h-36rpx w-36rpx" />
+            <view class="ml-20rpx text-32rpx font-semibold">
               {{ item.shopName }}
             </view>
           </view>
-          <view class="text-24rpx text-#FF4D3A">
+          <view class="text-24rpx text-[#FF4D3A]">
             <template v-if="item.hbOrderStatus === OrderStatus.PaddingPay">
               <view class="flex items-center">
                 待支付( 还剩 <wd-count-down :time="handleCommonOrderStatusText(item)" @finish="refresh" /> )
@@ -131,7 +131,7 @@ async function handleAfterSale(item: Api.xsbOrderList) {
             </text>
           </view>
         </view>
-        <view class="my24rpx h2rpx w-full bg-#F0F0F0" />
+        <view class="my-24rpx h-2rpx w-full bg-[#F0F0F0]" />
         <view>
           <!-- <view class="mb20rpx box-border rounded-16rpx bg-#F9F9F9 p24rpx">
             <view class="flex items-center">
@@ -147,41 +147,41 @@ async function handleAfterSale(item: Api.xsbOrderList) {
               2025-03-26 11:56:07
             </view>
           </view> -->
-          <view class="box-border h176rpx w-full flex items-center justify-between rounded-16rpx bg-#F9F9F9" @click="handleClick(item)">
-            <view class="box-border h-full w480rpx py28rpx pl20rpx">
+          <view class="box-border h-176rpx w-full flex items-center justify-between rounded-16rpx bg-[#F9F9F9]" @click="handleClick(item)">
+            <view class="box-border h-full w-480rpx py-28rpx pl-20rpx">
               <scroll-view scroll-x class="h-full w-full whitespace-nowrap">
                 <view class="flex items-center">
-                  <view v-for="goods in item.orderItemList" :key="goods.skuId" class="mr50rpx">
-                    <image :src="goods.pic" class="h120rpx w120rpx" />
+                  <view v-for="goods in item.orderItemList" :key="goods.skuId" class="mr-50rpx">
+                    <image :src="goods.pic" class="h-120rpx w-120rpx" />
                   </view>
                 </view>
               </scroll-view>
             </view>
-            <view class="box-shadow box-border h-full flex-1 flex-shrink-0 px14rpx py40rpx">
-              <view class="text-center text-32rpx text-#FF4D3A font-semibold">
+            <view class="box-shadow box-border h-full flex-1 flex-shrink-0 px-14rpx py-40rpx">
+              <view class="text-center text-32rpx text-[#FF4D3A] font-semibold">
                 ¥{{ item.actualTotal }}
               </view>
-              <view class="text-center text-28rpx text-#AAAAAA">
+              <view class="text-center text-28rpx text-[#AAAAAA]">
                 共{{ item.goodsTotal }}件
               </view>
             </view>
           </view>
         </view>
-        <view class="mt24rpx flex items-center justify-end">
+        <view class="mt-24rpx flex items-center justify-end">
           <template v-if="item.hbOrderStatus === OrderStatus.PaddingPay">
-            <view class="mr20rpx">
+            <view class="mr-20rpx">
               <wd-button size="small" plain type="info" @click.stop="handleCancel(item)">
                 取消订单
               </wd-button>
             </view>
-            <view class="mr20rpx">
+            <view class="mr-20rpx">
               <wd-button size="small" plain type="error" @click.stop="handlePay(item.orderNumber as string)">
                 付款
               </wd-button>
             </view>
           </template>
           <template v-if="[OrderStatus.OrderCancel, OrderStatus.OrderCompleted].includes(item.hbOrderStatus) ">
-            <view class="mr20rpx">
+            <view class="mr-20rpx">
               <wd-button size="small" plain type="info" @click.stop="handleDel(item)">
                 删除订单
               </wd-button>
@@ -193,14 +193,14 @@ async function handleAfterSale(item: Api.xsbOrderList) {
             </view> -->
           </template>
           <template v-if="item.hbOrderStatus === OrderStatus.OrderArrived">
-            <view class="mr20rpx">
+            <view class="mr-20rpx">
               <wd-button size="small" plain type="info" @click.stop="handleSubmitOrder(item)">
                 确认收货
               </wd-button>
             </view>
           </template>
           <template v-if="item.refundStatus != 2 || [OrderStatus.OrderCompleted, OrderStatus.OrderWaitDelivery, OrderStatus.OrderAccepted].includes(item.hbOrderStatus) ">
-            <view class="mr20rpx">
+            <view class="mr-20rpx">
               <wd-button size="small" plain type="info" @click.stop="handleAfterSale(item)">
                 申请售后
               </wd-button>

+ 12 - 1
src/uni-pages.d.ts

@@ -25,6 +25,16 @@ interface NavigateToOptions {
        "/subPack-common/revalueSuccess/index" |
        "/subPack-common/user-center/index" |
        "/subPack-smqjh/order/index" |
+       "/subPack-film/choose-film/index" |
+       "/subPack-film/choose-seat/index" |
+       "/subPack-film/index/index" |
+       "/subPack-film/movie/index" |
+       "/subPack-film/movie-detail/index" |
+       "/subPack-film/movie-list/index" |
+       "/subPack-film/order/index" |
+       "/subPack-film/order-detail/index" |
+       "/subPack-film/select-time/index" |
+       "/subPack-film/submit-order/index" |
        "/subPack-charge/chargeDetail/chargeDetail" |
        "/subPack-charge/chargeing/chargeing" |
        "/subPack-charge/chargeMap/chargeMap" |
@@ -34,7 +44,8 @@ interface NavigateToOptions {
        "/subPack-charge/chargeSiteDetail/chargeSiteDetail" |
        "/subPack-charge/chargeStart/chargeStart" |
        "/subPack-charge/index/index" |
-       "/subPack-videoRights/commonTab/index";
+       "/subPack-videoRights/commonTab/index" |
+       "/subPack-videoRights/videoRightsDetail/videoRightsDetail";
 }
 interface RedirectToOptions extends NavigateToOptions {}
 

+ 2 - 1
tsconfig.json

@@ -26,6 +26,7 @@
     "src/**/*.tsx",
     "src/**/*.vue",
     "async-import.d.ts",
-    "async-component.d.ts"
+    "async-component.d.ts",
+    "src/**/*.js"
   ]
 }

+ 1 - 0
vite.config.ts

@@ -30,6 +30,7 @@ export default async () => {
           'subPack-xsb',
           'subPack-common',
           'subPack-smqjh',
+          'subPack-film',
           'subPack-charge',
           'subPack-videoRights',
         ],