瀏覽代碼

feat(map): 添加高德地图支持并优化地图相关功能

- 在 .env.development 中添加高德地图 key
- 在 package.json 中添加 @amap/amap-jsapi-loader 依赖
- 新增 useMap.ts 和 map.ts 文件实现地图相关功能
- 优化 ZtCustomTable 组件,增加删除操作和相关逻辑
- 修复 ApiSelect 组件的多个问题
- 新增 competition.api.ts 文件实现比赛管理相关 API
zhangtao 7 小時之前
父節點
當前提交
0fadfc3541
共有 30 個文件被更改,包括 1479 次插入337 次删除
  1. 5 0
      .env.development
  2. 2 0
      package.json
  3. 27 0
      pnpm-lock.yaml
  4. 3 0
      src/components/Form/src/components/ApiSelect.vue
  5. 2 0
      src/components/Form/src/jeecg/components/JAreaSelect.vue
  6. 4 2
      src/components/Table/src/components/editable/EditableCell.vue
  7. 30 7
      src/components/ZtCustomTable/index.vue
  8. 319 0
      src/hooks/web/useMap.ts
  9. 7 2
      src/utils/index.ts
  10. 122 0
      src/utils/map.ts
  11. 35 0
      src/views/businessManagement/competition/competition.api.ts
  12. 191 69
      src/views/businessManagement/competition/competition.data.ts
  13. 268 11
      src/views/businessManagement/competition/competitionCommon.vue
  14. 46 11
      src/views/businessManagement/competition/index.vue
  15. 0 78
      src/views/businessManagement/courses/components/coursesModal.vue
  16. 12 14
      src/views/businessManagement/courses/courses.data.ts
  17. 10 22
      src/views/businessManagement/courses/index.vue
  18. 8 5
      src/views/businessManagement/courses/publishcourses.vue
  19. 46 0
      src/views/businessManagement/gymnasiumBag/gymnasiumBag.api.ts
  20. 1 0
      src/views/businessManagement/gymnasiumBag/gymnasiumBag.data.ts
  21. 180 64
      src/views/businessManagement/gymnasiumBag/index.vue
  22. 12 6
      src/views/businessManagement/gymnasiumNoFixed/DataRuleModal.vue
  23. 32 0
      src/views/businessManagement/gymnasiumNoFixed/gymnasiumNoFixed.api.ts
  24. 18 15
      src/views/businessManagement/gymnasiumNoFixed/gymnasiumNoFixed.data.ts
  25. 81 25
      src/views/businessManagement/gymnasiumNoFixed/index.vue
  26. 3 5
      src/views/demo/table/EditRowTable.vue
  27. 3 0
      src/views/system/staff/staff.api.ts
  28. 1 0
      src/views/system/staff/staffList.vue
  29. 10 0
      types/map.api.d.ts
  30. 1 1
      types/utils.d.ts

+ 5 - 0
.env.development

@@ -10,11 +10,13 @@ VITE_PUBLIC_PATH = /
 # VITE_PROXY = [["/jeecgboot","http://192.168.1.34:8080/jeecg-boot"],["/upload","http://192.168.1.34:8080/jeecg-boot"]]
 VITE_PROXY = [["/jeecgboot","http://192.168.0.11:8080/jeecg-boot"],["/upload","http://192.168.0.11:8080/upload"]]
 # VITE_PROXY = [["/jeecgboot","http://192.168.1.253:8080/jeecg-boot"],["/upload","http://192.168.1.253:8080/upload"]]
+# VITE_PROXY = [["/jeecgboot","http://192.168.1.166:8080/jeecg-boot"],["/upload","http://192.168.1.166:8080/upload"]]
 
 #后台接口全路径地址(必填)
 # VITE_GLOB_DOMAIN_URL=http://192.168.1.34:8080/jeecg-boot #//黄、
 VITE_GLOB_DOMAIN_URL=http://192.168.0.11:8080/jeecg-boot  #李
 # VITE_GLOB_DOMAIN_URL=http://192.168.1.253:8080/jeecg-boot  #张
+# VITE_GLOB_DOMAIN_URL=http://192.168.1.166:8080/jeecg-boot  #张
 
 
 #后台接口父地址(必填)
@@ -37,3 +39,6 @@ VITE_APP_SUB_jeecg-app-1 = '//localhost:8092'
 
 # 在线文档编辑版本。可选属性:wps, offlineWps(离线版) ,onlyoffice
 VITE_GLOB_ONLINE_DOCUMENT_VERSION=wps
+
+#高德地图key
+VITE_MAPKEY = e3d844f4a51aaa401f0abcebc6e0ec19

+ 2 - 0
package.json

@@ -21,6 +21,7 @@
     "husky:install": "husky install"
   },
   "dependencies": {
+    "@amap/amap-jsapi-loader": "^1.0.1",
     "@ant-design/colors": "^7.2.0",
     "@ant-design/icons-vue": "^7.0.1",
     "@fullcalendar/core": "^6.1.18",
@@ -81,6 +82,7 @@
     "xss": "^1.0.15"
   },
   "devDependencies": {
+    "@amap/amap-jsapi-types": "^0.0.15",
     "@commitlint/cli": "^18.6.1",
     "@commitlint/config-conventional": "^18.6.3",
     "@iconify/json": "^2.2.292",

+ 27 - 0
pnpm-lock.yaml

@@ -8,6 +8,9 @@ importers:
 
   .:
     dependencies:
+      '@amap/amap-jsapi-loader':
+        specifier: ^1.0.1
+        version: 1.0.1
       '@ant-design/colors':
         specifier: ^7.2.0
         version: 7.2.1
@@ -183,6 +186,9 @@ importers:
         specifier: ^1.0.15
         version: 1.0.15
     devDependencies:
+      '@amap/amap-jsapi-types':
+        specifier: ^0.0.15
+        version: 0.0.15
       '@commitlint/cli':
         specifier: ^18.6.1
         version: 18.6.1(@types/node@20.19.2)(typescript@4.9.5)
@@ -426,6 +432,12 @@ importers:
 
 packages:
 
+  '@amap/amap-jsapi-loader@1.0.1':
+    resolution: {integrity: sha512-nPyLKt7Ow/ThHLkSvn2etQlUzqxmTVgK7bIgwdBRTg2HK5668oN7xVxkaiRe3YZEzGzfV2XgH5Jmu2T73ljejw==}
+
+  '@amap/amap-jsapi-types@0.0.15':
+    resolution: {integrity: sha512-oqyRqHpVDZh5bUe2mAJh41ZsziSj0eUzwcfIbiaBNB0eiTJnZNhKsTdk77VOklOjwuwNfsblpKW9LjmWNpeQ7A==}
+
   '@ampproject/remapping@2.3.0':
     resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
     engines: {node: '>=6.0.0'}
@@ -1238,56 +1250,67 @@ packages:
     resolution: {integrity: sha512-n0edDmSHlXFhrlmTK7XBuwKlG5MbS7yleS1cQ9nn4kIeW+dJH+ExqNgQ0RrFRew8Y+0V/x6C5IjsHrJmiHtkxQ==}
     cpu: [arm]
     os: [linux]
+    libc: [glibc]
 
   '@rollup/rollup-linux-arm-musleabihf@4.44.1':
     resolution: {integrity: sha512-8WVUPy3FtAsKSpyk21kV52HCxB+me6YkbkFHATzC2Yd3yuqHwy2lbFL4alJOLXKljoRw08Zk8/xEj89cLQ/4Nw==}
     cpu: [arm]
     os: [linux]
+    libc: [musl]
 
   '@rollup/rollup-linux-arm64-gnu@4.44.1':
     resolution: {integrity: sha512-yuktAOaeOgorWDeFJggjuCkMGeITfqvPgkIXhDqsfKX8J3jGyxdDZgBV/2kj/2DyPaLiX6bPdjJDTu9RB8lUPQ==}
     cpu: [arm64]
     os: [linux]
+    libc: [glibc]
 
   '@rollup/rollup-linux-arm64-musl@4.44.1':
     resolution: {integrity: sha512-W+GBM4ifET1Plw8pdVaecwUgxmiH23CfAUj32u8knq0JPFyK4weRy6H7ooxYFD19YxBulL0Ktsflg5XS7+7u9g==}
     cpu: [arm64]
     os: [linux]
+    libc: [musl]
 
   '@rollup/rollup-linux-loongarch64-gnu@4.44.1':
     resolution: {integrity: sha512-1zqnUEMWp9WrGVuVak6jWTl4fEtrVKfZY7CvcBmUUpxAJ7WcSowPSAWIKa/0o5mBL/Ij50SIf9tuirGx63Ovew==}
     cpu: [loong64]
     os: [linux]
+    libc: [glibc]
 
   '@rollup/rollup-linux-powerpc64le-gnu@4.44.1':
     resolution: {integrity: sha512-Rl3JKaRu0LHIx7ExBAAnf0JcOQetQffaw34T8vLlg9b1IhzcBgaIdnvEbbsZq9uZp3uAH+JkHd20Nwn0h9zPjA==}
     cpu: [ppc64]
     os: [linux]
+    libc: [glibc]
 
   '@rollup/rollup-linux-riscv64-gnu@4.44.1':
     resolution: {integrity: sha512-j5akelU3snyL6K3N/iX7otLBIl347fGwmd95U5gS/7z6T4ftK288jKq3A5lcFKcx7wwzb5rgNvAg3ZbV4BqUSw==}
     cpu: [riscv64]
     os: [linux]
+    libc: [glibc]
 
   '@rollup/rollup-linux-riscv64-musl@4.44.1':
     resolution: {integrity: sha512-ppn5llVGgrZw7yxbIm8TTvtj1EoPgYUAbfw0uDjIOzzoqlZlZrLJ/KuiE7uf5EpTpCTrNt1EdtzF0naMm0wGYg==}
     cpu: [riscv64]
     os: [linux]
+    libc: [musl]
 
   '@rollup/rollup-linux-s390x-gnu@4.44.1':
     resolution: {integrity: sha512-Hu6hEdix0oxtUma99jSP7xbvjkUM/ycke/AQQ4EC5g7jNRLLIwjcNwaUy95ZKBJJwg1ZowsclNnjYqzN4zwkAw==}
     cpu: [s390x]
     os: [linux]
+    libc: [glibc]
 
   '@rollup/rollup-linux-x64-gnu@4.44.1':
     resolution: {integrity: sha512-EtnsrmZGomz9WxK1bR5079zee3+7a+AdFlghyd6VbAjgRJDbTANJ9dcPIPAi76uG05micpEL+gPGmAKYTschQw==}
     cpu: [x64]
     os: [linux]
+    libc: [glibc]
 
   '@rollup/rollup-linux-x64-musl@4.44.1':
     resolution: {integrity: sha512-iAS4p+J1az6Usn0f8xhgL4PaU878KEtutP4hqw52I4IO6AGoyOkHCxcc4bqufv1tQLdDWFx8lR9YlwxKuv3/3g==}
     cpu: [x64]
     os: [linux]
+    libc: [musl]
 
   '@rollup/rollup-win32-arm64-msvc@4.44.1':
     resolution: {integrity: sha512-NtSJVKcXwcqozOl+FwI41OH3OApDyLk3kqTJgx8+gp6On9ZEt5mYhIsKNPGuaZr3p9T6NWPKGU/03Vw4CNU9qg==}
@@ -6293,6 +6316,10 @@ packages:
 
 snapshots:
 
+  '@amap/amap-jsapi-loader@1.0.1': {}
+
+  '@amap/amap-jsapi-types@0.0.15': {}
+
   '@ampproject/remapping@2.3.0':
     dependencies:
       '@jridgewell/gen-mapping': 0.3.11

+ 3 - 0
src/components/Form/src/components/ApiSelect.vue

@@ -85,6 +85,7 @@
       // Embedded in the form, just use the hook binding to perform form verification
       const [state, setState] = useRuleFormItem(props, 'value', 'change', emitData);
       // update-begin--author:liaozhiyang---date:20230830---for:【QQYUN-6308】解决警告
+
       let vModalValue: any;
       const attrs_ = computed(() => {
         let obj: any = unref(attrs) || {};
@@ -92,6 +93,7 @@
           vModalValue = obj['onUpdate:value'];
           delete obj['onUpdate:value'];
         }
+
         // update-begin--author:liaozhiyang---date:20231017---for:【issues/5467】ApiSelect修复覆盖了用户传递的方法
         if (obj['filterOption'] === undefined) {
           // update-begin--author:liaozhiyang---date:20230904---for:【issues/5305】无法按照预期进行搜索
@@ -198,6 +200,7 @@
         } else {
           state.value = value;
         }
+
         // update-end--author:liaozhiyang---date:20250407---for:【issues/8037】初始化值单选的值被错误地写入数组值
       }
 

+ 2 - 0
src/components/Form/src/jeecg/components/JAreaSelect.vue

@@ -114,6 +114,8 @@
        * 省份change事件
        */
       function proChange(val) {
+        console.log(val, '设置');
+
         pca.city = val && getDataByCode(val)[0]?.value;
         pca.area = pca.city && getDataByCode(pca.city)[0]?.value;
         state.value = props.level <= 1 ? val : props.level <= 2 ? pca.city : pca.area;

+ 4 - 2
src/components/Table/src/components/editable/EditableCell.vue

@@ -106,19 +106,21 @@
         if (component === 'ApiSelect') {
           apiSelectProps.cache = true;
         }
-
         const isCheckValue = unref(getIsCheckComp);
 
         const valueField = isCheckValue ? 'checked' : 'value';
         const val = unref(currentValueRef);
+        //update-begin---author:zhangtao--修复ApiSelect组件为多选的时候默认值是null的情况2025-07-23
+        const isMultiple = compProps.mode == 'multiple' || compProps.mode == 'tags';
+        const value = isCheckValue ? (isNumber(val) && isBoolean(val) ? val : !!val) : isMultiple ? compProps.defaultValue : val;
 
-        const value = isCheckValue ? (isNumber(val) && isBoolean(val) ? val : !!val) : val;
         //update-begin---author:wangshuai---date:2024-09-19---for:【issues/7136】单元格上的tooltip提示,如果表格有滚动条,会不跟着单元格滚动---
         let tooltipPosition: any = unref(table?.wrapRef.value)?.parentElement?.querySelector('.ant-table-body');
         if (tooltipPosition) {
           tooltipPosition.style.position = 'relative';
         }
         //update-end---author:wangshuai---date:2024-09-19---for:【issues/7136】单元格上的tooltip提示,如果表格有滚动条,会不跟着单元格滚动---
+
         return {
           size: 'small',
           //update-begin---author:wangshuai---date:2024-09-19---for:【issues/7136】单元格上的tooltip提示,如果表格有滚动条,会不跟着单元格滚动---

+ 30 - 7
src/components/ZtCustomTable/index.vue

@@ -8,8 +8,12 @@
       </template>
       <template v-else-if="column.dataIndex != 'operation'"> {{ column.customTitle }} </template>
     </template>
-    <template #bodyCell="{ column, index }">
-      <div class="text-18px cursor-pointer text-#eb5050" @click="handleRemove(index)" v-if="column.dataIndex == 'operation' && showAction">
+    <template #bodyCell="{ column, index, record }">
+      <div
+        class="text-18px cursor-pointer text-#eb5050"
+        @click="handleRemove(index, record)"
+        v-if="column && column.dataIndex == 'operation' && showAction"
+      >
         <minus-circle-outlined />
       </div>
     </template>
@@ -21,7 +25,6 @@
   import { BasicTable, BasicColumn } from '/@/components/Table';
   import { useListPage } from '/@/hooks/system/useListPage';
   import _ from 'lodash-es';
-  import dayjs from 'dayjs';
   import { useMessage } from '/@/hooks/web/useMessage';
   interface Props {
     tableColumn: BasicColumn[];
@@ -29,6 +32,7 @@
     showAction?: boolean;
     showIndex?: boolean;
     count?: number;
+    deleteId?: string[];
   }
   const props = withDefaults(defineProps<Props>(), {
     tableColumn: () => [],
@@ -36,8 +40,10 @@
     showAction: () => true,
     showIndex: () => false,
     count: () => 9999,
+    isSoftdeletion: () => false,
+    deleteId: () => [],
   });
-  const emit = defineEmits(['update:value']);
+  const emit = defineEmits(['update:value', 'update:deleteId']);
   const modelValue = computed({
     get() {
       console.log(props.value, 'props.value');
@@ -45,7 +51,7 @@
       return props.value;
     },
     set(val) {
-      console.log(val, '重新设置');
+      // console.log(val, '重新设置');
 
       emit('update:value', val);
     },
@@ -61,6 +67,19 @@
     },
   });
 
+  const deletIdVaule = computed({
+    get() {
+      // console.log(props.deleteId, 'props.deleteId');
+
+      return props.deleteId;
+    },
+    set(val) {
+      // console.log(val, '获取删除id、');
+
+      emit('update:deleteId', val);
+    },
+  });
+
   //BasicTable绑定注册
   const [registerTable] = tableContext;
 
@@ -79,7 +98,7 @@
     const newRow: DataRow = {};
     inputFields.forEach((field) => {
       newRow[String(field)] = null;
-      newRow['id'] = dayjs().valueOf();
+      // newRow['id'] = dayjs().valueOf();
     });
     return newRow;
   };
@@ -88,7 +107,11 @@
     modelValue.value.push(addEmptyRow(props.tableColumn));
     modelValue.value = [].concat(modelValue.value);
   }
-  function handleRemove(index) {
+  function handleRemove(index, column) {
     modelValue.value.splice(index, 1);
+    if (column.id) {
+      deletIdVaule.value.push(column.id);
+      deletIdVaule.value = ([] as string[]).concat(deletIdVaule.value);
+    }
   }
 </script>

+ 319 - 0
src/hooks/web/useMap.ts

@@ -0,0 +1,319 @@
+import { ref, toRefs } from 'vue';
+import AMapLoader from '@amap/amap-jsapi-loader';
+import { message } from 'ant-design-vue';
+
+interface EnhancedAMapOptions {
+  key?: string;
+  version?: string;
+  plugins?: string[];
+  securityJsCode?: string;
+}
+
+interface MapInstance {
+  AMap: any;
+  map: AMap.Map | null;
+}
+
+// 全局单例变量
+let globalAMap: any = null;
+let globalMap: AMap.Map | null = null;
+let isInitialized = false;
+let initPromise: Promise<MapInstance> | null = null;
+const currentObj = ref({
+  currentAdcode: '',
+  currentAddress: '',
+});
+const lnglat = ref<number[]>([]);
+
+export function useAMapEnhanced(options: EnhancedAMapOptions = {}) {
+  const isLoading = ref(false);
+  const error = ref<string | null>(null);
+  // 默认配置
+  const defaultOptions: EnhancedAMapOptions = {
+    key: import.meta.env.VITE_MAPKEY || '',
+    version: '2.0',
+    plugins: ['AMap.PlaceSearch', 'AMap.Geolocation'],
+    securityJsCode: 'cbbb175f5c77d9ddf74c9b7d37f52f1c',
+  };
+
+  // 合并配置
+  const mergedOptions = { ...defaultOptions, ...options };
+
+  // 设置安全密钥
+  if (mergedOptions.securityJsCode) {
+    window._AMapSecurityConfig = {
+      securityJsCode: mergedOptions.securityJsCode,
+    };
+  }
+
+  /**
+   * 初始化地图(全局单例)
+   * @param container 地图容器ID或元素
+   * @param mapOptions 地图配置选项
+   */
+  const initMap = async (container: string | HTMLElement, mapOptions: AMap.MapOptions = {}): Promise<MapInstance> => {
+    // 如果已经在初始化中,则返回该Promise
+    if (initPromise) {
+      await initPromise;
+      return {
+        AMap: globalAMap,
+        map: globalMap,
+      };
+    }
+
+    // 如果已经初始化完成,直接返回
+    if (isInitialized) {
+      return {
+        AMap: globalAMap,
+        map: globalMap,
+      };
+    }
+
+    // 检查是否提供了key
+    if (!mergedOptions.key) {
+      error.value = '缺少高德地图key';
+      throw new Error('Missing AMap key');
+    }
+
+    isLoading.value = true;
+    error.value = null;
+
+    // 创建初始化Promise
+    initPromise = new Promise(async (resolve, reject) => {
+      try {
+        // 加载AMap API
+        const loadedAMap = await AMapLoader.load({
+          key: mergedOptions.key!,
+          version: mergedOptions.version || '2.0',
+          plugins: mergedOptions.plugins,
+        });
+
+        globalAMap = loadedAMap;
+
+        // 默认地图配置
+        const defaultMapOptions: AMap.MapOptions = {
+          zoom: 11, // 默认缩放级别
+          ...mapOptions,
+        };
+
+        // 创建地图实例
+        globalMap = new globalAMap.Map(container, defaultMapOptions);
+
+        isInitialized = true;
+        resolve({
+          AMap: globalAMap,
+          map: globalMap,
+        });
+      } catch (err: any) {
+        error.value = err.message || '地图加载失败';
+        reject(err);
+      } finally {
+        isLoading.value = false;
+        initPromise = null;
+      }
+    });
+
+    return initPromise;
+  };
+
+  /**
+   * 搜索POI并定位到结果
+   * @param keyword 搜索关键词
+   */
+  const searchAndLocate = async (keyword: string): Promise<void> => {
+    if (!globalAMap || !globalMap) {
+      throw new Error('地图未初始化');
+    }
+
+    return new Promise((resolve, reject) => {
+      globalAMap.plugin(['AMap.PlaceSearch'], () => {
+        const placeSearch = new globalAMap.PlaceSearch({
+          pageSize: 10,
+          pageIndex: 1,
+          autoFitView: true,
+        });
+
+        placeSearch.search(keyword, (status: string, result: any) => {
+          if (status === 'complete' && result.poiList && result.poiList.pois.length > 0) {
+            const firstPoi = result.poiList.pois[0];
+            if (firstPoi.location) {
+              globalMap?.setCenter(firstPoi.location);
+              globalMap?.setZoom(18);
+
+              // // 添加标记点
+              // const marker = new globalAMap.Marker({
+              //   position: firstPoi.location,
+              //   title: firstPoi.name,
+              // });
+              // globalMap?.add(marker);
+            }
+            resolve(firstPoi);
+          } else {
+            reject(new Error('未找到相关地点'));
+          }
+        });
+      });
+    });
+  };
+
+  /**
+   * 根据城市码定位地图
+   * @param cityCode 城市编码
+   */
+  const setCityByCode = async (cityCode: string): Promise<void> => {
+    if (!globalAMap || !globalMap) {
+      throw new Error('地图未初始化');
+    }
+
+    return new Promise((resolve, reject) => {
+      globalMap?.setCity(cityCode, (result) => {
+        if (result) {
+          resolve(result);
+        } else {
+          reject(new Error('设置城市失败'));
+        }
+      });
+    });
+  };
+  /**
+   * 获取当前位置
+   */
+  const getCurrentPosition = async (): Promise<any> => {
+    if (!globalAMap) {
+      throw new Error('AMap未加载');
+    }
+
+    return new Promise((resolve, reject) => {
+      globalAMap.plugin(['AMap.Geolocation'], () => {
+        const geolocation = new globalAMap.Geolocation({
+          enableHighAccuracy: true,
+          timeout: 10000,
+        });
+
+        geolocation.getCurrentPosition((status: string, result: any) => {
+          if (status === 'complete') {
+            // 定位到当前位置
+            getAddressByLngLat(result.position.lng, result.position.lat);
+            if (result.position) {
+              globalMap?.setCenter(result.position);
+              globalMap?.setZoom(18);
+            }
+            resolve(result);
+          } else {
+            reject(result);
+          }
+        });
+      });
+    });
+  };
+  /**
+   * 通过经纬度获取省市区信息
+   * @param longitude 经度
+   * @param latitude 纬度
+   * @returns 省市区信息
+   */
+  const getAddressByLngLat = async (longitude: number, latitude: number): Promise<any> => {
+    if (!globalAMap) {
+      throw new Error('AMap未加载');
+    }
+
+    return new Promise((resolve, reject) => {
+      globalAMap.plugin(['AMap.Geocoder'], () => {
+        const geocoder = new globalAMap.Geocoder({
+          city: '全国',
+          radius: 1000,
+          extensions: 'all',
+        });
+
+        lnglat.value = [longitude, latitude];
+        geocoder.getAddress(lnglat.value, (status: string, result: any) => {
+          if (status === 'complete' && result.regeocode) {
+            currentObj.value.currentAddress = result.regeocode.formattedAddress;
+            if (result.regeocode.addressComponent) {
+              currentObj.value.currentAdcode = result.regeocode.addressComponent.adcode;
+            }
+            resolve(result.regeocode);
+          } else {
+            reject(new Error('无法获取地址信息'));
+          }
+        });
+      });
+    });
+  };
+
+  /**
+   * 根据城市码和详细地址精确定位
+   * @param cityCode 城市编码
+   * @param address 详细地址
+   */
+  const locateByCityAndAddress = async (cityCode: string, address: string): Promise<any> => {
+    if (!globalAMap || !globalMap) {
+      throw new Error('地图未初始化');
+    }
+
+    // 先根据城市码设置地图城市
+    await setCityByCode(cityCode);
+
+    // 然后在指定城市内搜索详细地址
+    return new Promise((resolve, reject) => {
+      globalAMap.plugin(['AMap.PlaceSearch'], () => {
+        const placeSearch = new globalAMap.PlaceSearch({
+          city: cityCode,
+          citylimit: true,
+          pageSize: 10,
+          pageIndex: 1,
+          autoFitView: true,
+        });
+
+        placeSearch.search(address, (status: string, result: any) => {
+          if (status === 'complete' && result.poiList && result.poiList.pois.length > 0) {
+            const firstPoi = result.poiList.pois[0];
+            if (firstPoi.location) {
+              globalMap?.setCenter(firstPoi.location);
+              globalMap?.setZoom(18);
+            }
+            resolve(firstPoi);
+          } else {
+            reject(new Error('未找到相关地点'));
+            message.error('该区域下暂无该地点');
+          }
+        });
+      });
+    });
+  };
+
+  /**
+   * 销毁地图
+   */
+  const destroyMap = () => {
+    if (globalMap) {
+      globalMap.destroy();
+      globalMap = null;
+    }
+    isInitialized = false;
+    initPromise = null;
+    currentObj.value.currentAdcode = ''; // 清空adcode数据
+    currentObj.value.currentAddress = '';
+    lnglat.value = [];
+  };
+
+  return {
+    // 响应式数据
+    isLoading,
+    error,
+    currentObj,
+    lnglat,
+    // 方法
+    initMap,
+    searchAndLocate,
+    getCurrentPosition,
+    destroyMap,
+    setCityByCode,
+    locateByCityAndAddress,
+    getAddressByLngLat,
+    // 提供获取全局地图实例的方法
+    getGlobalMap: () => globalMap,
+    getGlobalAMap: () => globalAMap,
+    isMapInitialized: () => isInitialized,
+  };
+}

+ 7 - 2
src/utils/index.ts

@@ -635,7 +635,7 @@ export const split = (str) => {
  * @returns 所有字段有值返回 true,否则 false
  */
 function areAllFieldsFilled(obj: Record<string, any>): boolean {
-  return Object.values(obj).every((value) => value !== null && value !== undefined);
+  return Object.values(obj).every((value) => unref(value) !== null && unref(value) !== undefined);
 }
 
 /**
@@ -644,5 +644,10 @@ function areAllFieldsFilled(obj: Record<string, any>): boolean {
  * @returns 所有对象字段都有值返回 true,否则 false
  */
 export function areAllItemsAllFieldsFilled(arr: Array<Record<string, any>>): boolean {
-  return arr.every((item) => areAllFieldsFilled(item));
+  // return arr.every((item) => areAllFieldsFilled(item));
+  //修复新增的时候id校验
+  return arr.every((item) => {
+    const { id, ...rest } = item;
+    return areAllFieldsFilled(rest);
+  });
 }

+ 122 - 0
src/utils/map.ts

@@ -0,0 +1,122 @@
+import '@amap/amap-jsapi-types';
+import { pcaa } from './areaData/pcaUtils';
+/**
+ * 高德地图使用的是 GCJ-02 坐标系,若采用的其他坐标系请初始化配置此处转换
+ */
+type CoordinateType = 'gcj02' | 'wgs84' | 'bd09';
+type MyMapOptions = {
+  _AmapCoordinate: CoordinateType;
+} & AMap.MapOptions;
+
+class myMap {
+  public mapConfig: AMap.MapOptions = {
+    zoom: 11,
+    center: [121.732487, 31.086897],
+    viewMode: '3D',
+    mapStyle: 'amap://styles/2a268264f80b6efc60d848838404e726',
+    rotateEnable: false,
+    pitchEnable: false,
+  };
+  public aMap!: AMap.Map; //地图对象
+  /**
+   * 地图初始化
+   * @param {dom元素} el    传入地图容器 DIV 的 ID 值或者 DIV 对象
+   * @param {配置参数} config 对传入的配置合并
+   * @return {Promise<void>} 返回一个 Promise 对象,表示地图初始化完成
+   */
+  public init(el: string | HTMLDivElement, config: MyMapOptions): Promise<typeof AMap> {
+    Object.assign(this.mapConfig, config);
+    if (config._AmapCoordinate) {
+      this._AmapCoordinate = config._AmapCoordinate;
+    }
+    return new Promise((resolve, reject) => {
+      initMap().then((AMap) => {
+        this.aMap = new AMap.Map(el, {
+          ...this.mapConfig,
+        });
+        this.aMap.on('complete', () => {
+          resolve(AMap);
+        });
+      });
+    });
+  }
+
+  /**
+   * 对外暴露的获取地图对象方法
+   * @returns {AMap.Map} 返回地图对象
+   */
+  public getAmapObj(): AMap.Map {
+    return this.aMap;
+  }
+}
+export function initMap(): Promise<typeof AMap> {
+  return new Promise((resolve, reject) => {
+    //如果全局已经存在AMap对象 则直接返回
+    if (window.AMap) {
+      resolve(window.AMap);
+      return;
+    }
+    AMapLoader.load<typeof AMap>({
+      key: import.meta.env.VITE_MAPKEY, // 申请好的Web端开发者Key,首次调用 load 时必填
+      version: '2.0', // 指定要加载的 JS API 的版本,缺省时默认为 1.4.15
+      plugins: ['AMap.PlaceSearch,'], // 需要使用的的插件列表,如比例尺'AMap.Scale'等
+      // "AMapUI": { // 是否加载 AMapUI,缺省不加载
+      //     "version": '1.1', // AMapUI 版本
+      //     "plugins": [], // 需要加载的 AMapUI ui插件
+      // },
+      // "Loca": { // 是否加载 Loca, 缺省不加载
+      //     "version": '2.0' // Loca 版本
+      // },
+    })
+      .then((AMap) => {
+        window.AMap = AMap;
+        resolve(AMap); //防止上方全局添加amap失败手动返回Amap
+      })
+      .catch((e) => {
+        console.error(e); //加载错误提示
+        reject(e);
+      });
+  });
+}
+export default myMap;
+
+export function matchProvinceByFirstThreeDigits(code: string | number): { code: string; name: string }[] {
+  const codeStr = code.toString();
+  if (codeStr.length < 3) {
+    throw new Error('Code must have at least 3 digits');
+  }
+
+  const firstThree = codeStr.substring(0, 3);
+  const provinces: { code: string; name: string }[] = [];
+
+  // 查找匹配的省份
+  Object.keys(pcaa['86']).forEach((provinceCode) => {
+    if (provinceCode.startsWith(firstThree)) {
+      provinces.push({
+        code: provinceCode,
+        name: pcaa['86'][provinceCode],
+      });
+    }
+  });
+
+  return provinces;
+}
+export function matchCityByFirstFourDigits(
+  cities: { value: string; label: string }[],
+  code: string | number
+): { value: string; label: string } | null {
+  const codeStr = code.toString();
+  if (codeStr.length < 4) {
+    return null;
+  }
+
+  const firstFour = codeStr.substring(0, 4);
+
+  for (const city of cities) {
+    if (city.value.startsWith(firstFour)) {
+      return city;
+    }
+  }
+
+  return null;
+}

+ 35 - 0
src/views/businessManagement/competition/competition.api.ts

@@ -0,0 +1,35 @@
+import { defHttp } from '/@/utils/http/axios';
+import { useMessage } from '/@/hooks/web/useMessage';
+
+const { createConfirm } = useMessage();
+
+enum Api {
+  list = '/app/appGame/list',
+  save = '/app/appGame/add',
+  edit = '/app/appGame/edit',
+  deleteOne = '/app/appGame/delete',
+  detaile = '/app/appGame/queryById',
+}
+export const list = (params) => defHttp.get({ url: Api.list, params });
+
+/**
+ * 删除单个
+ */
+export const deleteOne = (params, handleSuccess) => {
+  return defHttp.delete({ url: Api.deleteOne, params }, { joinParamsToUrl: true }).then(() => {
+    handleSuccess();
+  });
+};
+
+/**
+ * 保存或者更新
+ * @param params
+ */
+export const saveOrUpdate = (params, isUpdate) => {
+  let url = isUpdate ? Api.edit : Api.save;
+  return defHttp.post({ url: url, params });
+};
+
+export const getDetaile = (params) => {
+  return defHttp.get({ url: Api.detaile, params });
+};

+ 191 - 69
src/views/businessManagement/competition/competition.data.ts

@@ -1,33 +1,64 @@
+import { options } from '../../monitor/mynews/XssWhiteList';
+import { getSprotProject } from '/@/api/common/api';
 import { FormSchema } from '/@/components/Table';
 import { BasicColumn } from '/@/components/Table';
-
+import { getAddress } from '../courses/courses.api';
+import { Image, RadioChangeEvent } from 'ant-design-vue';
+import { h, ref, unref, VNode } from 'vue';
+import dayjs from 'dayjs';
+import { useAMapEnhanced } from '/@/hooks/web/useMap';
 /**
  * 列表columns
  */
 export const columns = [
   {
     title: '赛事名称',
-    dataIndex: 'roleName',
+    dataIndex: ['game', 'name'],
     width: 100,
   },
   {
     title: '封面',
-    dataIndex: 'roleCode',
+    dataIndex: ['game', 'cover'],
+    width: 100,
+    customRender: ({ text }) => {
+      return h(Image, { src: text, width: 80, height: 80 });
+    },
+  },
+  {
+    title: '比赛地点',
+    dataIndex: ['game', 'address'],
     width: 100,
   },
   {
     title: '比赛时间',
-    dataIndex: 'createTime',
+    dataIndex: ['game', 'startTime'],
     width: 100,
+    customRender: ({ record }) => {
+      const start = dayjs(record.game.startTime);
+      const end = dayjs(record.game.endTime);
+      if (start.isSame(end, 'day')) {
+        return start.format('YYYY-MM-DD');
+      } else {
+        return `${start.format('YYYY-MM-DD')}-${end.format('YYYY-MM-DD')}`;
+      }
+    },
   },
   {
     title: '比赛项目与价格',
     dataIndex: 'createTime',
     width: 100,
+    customRender: ({ record }) => {
+      const viewlist: VNode[] = [];
+      record.gamePriceRules.forEach((item) => {
+        const formattedString = `${item.categoryName} ¥${item.sellingPrice}/${item.peopleNum ? item.peopleNum : 0}人`;
+        viewlist.push(h('div', formattedString));
+      });
+      return h('div', viewlist);
+    },
   },
   {
     title: '报名结束时间',
-    dataIndex: 'createTime',
+    dataIndex: ['game', 'applicationEndTime'],
     width: 100,
   },
 ];
@@ -65,67 +96,78 @@ export const formSchema: FormSchema[] = [
     label: '赛事名称',
     component: 'Input',
     required: true,
+    labelWidth: 200,
+    colProps: { xxl: 8 },
   },
   {
-    field: 'file',
+    field: 'cover',
     label: '封面',
     component: 'JImageUpload',
     required: true,
+    labelWidth: 200,
     componentProps: {
       tipText: '单张图片,比例 9:16,支持格式:.jpg .png .svg .gif ,单个文件不能超过5MB ',
     },
   },
   {
-    field: 'fileimg',
+    field: 'backgroundImage',
     label: '背景图',
     component: 'JImageUpload',
     componentProps: {
       tipText: '单张图片,比例 5:4,支持格式:.jpg .png .svg .gif ,单个文件不能超过5MB。',
     },
     required: true,
+    labelWidth: 200,
   },
   {
-    field: 'name',
+    field: 'organizers',
     label: '主办单位',
     component: 'Input',
     required: true,
-    labelWidth: 210,
-    colProps: { lg: 14 },
+    labelWidth: 200,
+    colProps: { xxl: 13 },
   },
   {
-    field: 'time',
+    field: 'applicationEndTime',
     label: '报名结束时间',
     component: 'DatePicker',
     required: true,
-    labelWidth: 210,
-    colProps: { lg: 14 },
     componentProps: {
       format: 'YYYY-MM-DD hh:mm:ss',
       showTime: true,
     },
+    labelWidth: 200,
+    colProps: { xxl: 13 },
   },
   {
     field: 'dirver',
-    slot: 'RangePicker',
+    colSlot: 'RangePicker',
     label: '',
     component: 'RangePicker',
     labelWidth: 0,
   },
   {
-    field: 'RangePicker',
+    field: 'competitionTime',
     label: '比赛时间',
     component: 'RangePicker',
     required: true,
-    labelWidth: 210,
-    colProps: { lg: 14 },
+    colProps: { xxl: 8 },
+    labelWidth: 200,
   },
   {
-    field: 'ScheduleData1',
+    field: 'scheduleDTOS',
     label: '赛程安排',
     component: 'Input',
     slot: 'ZtCustomTable1',
     defaultValue: [],
+    ifShow(e) {
+      if (e.model.competitionTime) {
+        return true;
+      }
+      return false;
+    },
     required: true,
+    labelWidth: 200,
   },
   {
     field: 'title3',
@@ -135,42 +177,105 @@ export const formSchema: FormSchema[] = [
     labelWidth: 0,
   },
   {
-    field: 'ScheduleData2',
+    field: 'gamePriceRules',
     label: '比赛项目与价格',
     component: 'Input',
     slot: 'ZtCustomTable2',
     defaultValue: [],
     required: true,
+    labelWidth: 200,
   },
   {
-    field: 'address',
+    field: 'siteType',
     label: '比赛地点',
     component: 'RadioGroup',
     required: true,
+    labelWidth: 200,
     componentProps: {
       options: [
-        { label: '平台场地', value: 1 },
-        { label: '其他场地', value: 0 },
+        { label: '平台场地', value: 0 },
+        { label: '其他场地', value: 1 },
       ],
+      async onchange(e: RadioChangeEvent) {
+        const { initMap, destroyMap, getCurrentPosition } = useAMapEnhanced();
+        if (e.target.value == 1) {
+          await initMap('container');
+          await getCurrentPosition();
+        } else {
+          destroyMap();
+        }
+      },
     },
+    defaultValue: 0,
   },
   {
     field: 'address',
+    label: '地址',
+    component: 'JAreaSelect',
+    required: true,
+    slot: 'address',
+    labelWidth: 200,
+    ifShow(model) {
+      return model.model.siteType == 1;
+    },
+  },
+  {
+    field: 'provinceCode',
+    label: '省份code',
+    component: 'Input',
+    show: false,
+  },
+  {
+    field: 'addressString',
+    label: '输入地址',
+    component: 'Input',
+    show: false,
+  },
+  {
+    field: 'cityCode',
+    label: '	城市code',
+    component: 'Input',
+    show: false,
+  },
+  {
+    field: 'areaCode',
+    label: '	城市code',
+    component: 'Input',
+    show: false,
+  },
+  {
+    field: 'latitude',
+    label: '纬度',
+    component: 'Input',
+    show: false,
+  },
+  {
+    field: 'longitude',
+    label: '经度',
+    component: 'Input',
+    show: false,
+  },
+  {
+    field: 'siteId',
     label: '平台场地',
-    component: 'Select',
+    component: 'ApiSelect',
     required: true,
+    labelWidth: 200,
     componentProps: {
-      options: [
-        { label: '平台场地', value: 1 },
-        { label: '其他场地', value: 0 },
-      ],
+      api: getAddress,
+      labelField: 'address',
+      valueField: 'siteId',
+    },
+    ifShow(e) {
+      return e.model.siteType == 0;
     },
   },
   {
-    field: 'address',
+    field: 'reminder',
     label: '图文说明',
     component: 'JEditor',
     required: true,
+    labelWidth: 200,
   },
   {
     field: 'title4',
@@ -180,82 +285,99 @@ export const formSchema: FormSchema[] = [
     labelWidth: 0,
   },
   {
-    field: 'baoxain',
+    field: 'insureIds',
     label: '配套保险',
     component: 'RadioGroup',
     required: true,
+    labelWidth: 200,
     componentProps: {
       options: [
-        { label: '平台场地', value: 1 },
-        { label: '其他场地', value: 0 },
+        { label: '意外伤害险1', value: '1' },
+        { label: '大额意外伤害险', value: '0' },
       ],
     },
   },
+  {
+    field: 'aptitudes',
+    label: '报名所需资质(图片)',
+    component: 'RadioGroup',
+    slot: 'aptitudes',
+    defaultValue: [],
+    labelWidth: 200,
+  },
+  {
+    field: 'gamePriceRulesList',
+    label: ' 比赛项目与价格删除的id',
+    component: 'RadioGroup',
+    defaultValue: [],
+    show: false,
+  },
+  {
+    field: 'scheduleDTOSList',
+    label: '赛程安排删除的id',
+    component: 'RadioGroup',
+    defaultValue: [],
+    show: false,
+  },
 ];
 
-export const ScheduleArrangementColums: BasicColumn[] = [
+export const SchedulePricesColums: BasicColumn[] = [
   {
-    title: '比赛项目',
-    dataIndex: 'project',
-    width: 220,
+    title: '个人/团队赛',
+    dataIndex: 'type',
     editComponent: 'Select',
     editRule: true,
     editRow: true,
     editable: true,
     editComponentProps: {
       size: 'middle',
+      options: [
+        {
+          label: '个人赛',
+          value: '0',
+        },
+        {
+          label: '团队赛',
+          value: '1',
+        },
+      ],
     },
   },
   {
-    title: '时间段',
-    dataIndex: 'time',
-    width: 450,
-    editComponent: 'RangePicker',
-    editRule: true,
-    editComponentProps: {
-      showTime: true,
-      size: 'middle',
-      placeholder: ['开始时间', '结束时间'],
-      valueFormat: 'YYYY-MM-DD HH:mm:ss',
-    },
-    editRow: true,
-    editable: true,
-  },
-  {
-    title: '安排',
-    dataIndex: 'Schedule',
-    width: 220,
-    editComponent: 'Input',
-    editRule: true,
+    title: '比赛项目',
+    dataIndex: 'categoryId',
+    editComponent: 'ApiSelect',
+    // editRule: true,
     editRow: true,
     editable: true,
     editComponentProps: {
       size: 'middle',
+      api: getSprotProject,
+      labelField: 'name',
+      valueField: 'id',
+      resultField: 'records',
     },
   },
   {
-    key: 'op',
-    dataIndex: 'operation',
-    title: 'operation',
-    fixed: 'right',
-    width: 80,
-  },
-];
-export const SchedulePricesColums: BasicColumn[] = [
-  {
-    title: '比赛项目',
-    dataIndex: 'project',
-    editComponent: 'Select',
-    editRule: true,
+    title: '要求人数',
+    dataIndex: 'peopleNum',
+    editComponent: 'InputNumber',
     editRow: true,
     editable: true,
     editComponentProps: {
       size: 'middle',
+      min: 1,
+    },
+    editRule: async (text, record) => {
+      if (record.editValueRefs.type == 0 && text != '1') {
+        return '个人赛最多只能1人,当前输入人数:' + text + '人';
+      }
+      return '';
     },
   },
   {
     title: '价格',
-    dataIndex: 'price',
+    dataIndex: 'sellingPrice',
     editComponent: 'InputNumber',
     editRule: true,
     editRow: true,

+ 268 - 11
src/views/businessManagement/competition/competitionCommon.vue

@@ -11,22 +11,65 @@
           <Divider></Divider>
         </template>
         <template #ZtCustomTable1="{ model, field }">
-          <ZtCustomTable :tableColumn="ScheduleArrangementColums" v-model:value="model[field]"></ZtCustomTable>
+          <ZtCustomTable
+            :tableColumn="ScheduleArrangementColums"
+            v-model:value="model[field]"
+            v-model:deleteId="model['scheduleDTOSList']"
+          ></ZtCustomTable>
         </template>
         <template #title3>
           <TypographyTitle :level="4">比赛项目、价格、地点、说明</TypographyTitle>
           <Divider></Divider>
         </template>
         <template #ZtCustomTable2="{ model, field }">
-          <ZtCustomTable :tableColumn="SchedulePricesColums" v-model:value="model[field]"></ZtCustomTable>
+          <ZtCustomTable
+            :tableColumn="SchedulePricesColums"
+            v-model:value="model[field]"
+            v-model:deleteId="model['gamePriceRulesList']"
+          ></ZtCustomTable>
         </template>
         <template #title4>
           <TypographyTitle :level="4">配套保险</TypographyTitle>
           <Divider></Divider>
         </template>
+        <template #address="{ model, field }">
+          <FormItem>
+            <JAreaSelect
+              v-model:province="model['provinceCode']"
+              v-model:city="model['cityCode']"
+              v-model:area="model['areaCode']"
+              @change="handleSearch(0)"
+            ></JAreaSelect>
+            <div class="mt20px">
+              <Input v-model:value="model['addressString']">
+                <template #suffix>
+                  <Button @click="handleSearch(1)">定位</Button>
+                </template>
+              </Input>
+            </div>
+            <div class="mt20px mb20px relative">
+              <div id="container" class="h-500px"></div>
+              <div id="panel"></div>
+            </div>
+          </FormItem>
+        </template>
+        <template #aptitudes="{ model, field }">
+          <div class="flex flex-wrap">
+            <a-button type="primary" @click="model[field].push({ name: '' })">添加资质</a-button>
+            <div class="ml20px flex mb-20px" v-for="(item, idx) in model[field]" :key="idx">
+              <FormItem>
+                <a-input v-model:value="item.name" placeholder="请输入资质">
+                  <template #suffix>
+                    <a-button type="primary" danger @click="model[field].splice(idx, 1)">删除</a-button>
+                  </template>
+                </a-input>
+              </FormItem>
+            </div>
+          </div>
+        </template>
         <template #formFooter>
           <div style="margin: 0 auto">
-            <a-button type="primary" @click="save" class="mr-2"> 保存 </a-button>
+            <a-button type="primary" @click="save" class="mr-2" :loading="isSubmit"> 保存 </a-button>
             <a-button type="error" @click="back" class="mr-2"> 关闭 </a-button>
           </div>
         </template>
@@ -36,16 +79,230 @@
   </div>
 </template>
 <script lang="ts" setup name="business-management-competitionCommon">
-  import { TypographyTitle, Divider } from 'ant-design-vue';
-  import { BasicForm, useForm } from '/@/components/Form/index';
+  import { TypographyTitle, Divider, FormItem, Input, Button } from 'ant-design-vue';
+  import { BasicForm, useForm, JAreaSelect } from '/@/components/Form/index';
   import ZtCustomTable from '/@/components/ZtCustomTable/index.vue';
-  import { formSchema, ScheduleArrangementColums, SchedulePricesColums } from './competition.data';
-  const [registerForm, { setProps, resetFields, setFieldsValue, updateSchema, validate, clearValidate }] = useForm({
+  import { formSchema, SchedulePricesColums } from './competition.data';
+  import { BasicColumn } from '/@/components/Table';
+  import { getSprotProject } from '@/api/common/api';
+  import { getDetaile, saveOrUpdate } from './competition.api';
+  import dayjs from 'dayjs';
+  import { ref, toRefs, unref, watch, watchEffect } from 'vue';
+  import { areAllItemsAllFieldsFilled } from '/@/utils';
+  import { message } from 'ant-design-vue/lib';
+  import { useRoute } from 'vue-router';
+  import { useTabs } from '/@/hooks/web/useTabs';
+  import { useAMapEnhanced } from '/@/hooks/web/useMap';
+  import { getDataByCode } from '@/components/Form/src/utils/areaDataUtil';
+  import { matchCityByFirstFourDigits, matchProvinceByFirstThreeDigits } from '/@/utils/map';
+  const { close: closeTab } = useTabs();
+  const route = useRoute();
+  const { locateByCityAndAddress, setCityByCode, currentObj, lnglat } = useAMapEnhanced();
+
+  const [registerForm, { setProps, resetFields, setFieldsValue, updateSchema, validate, clearValidate, getFieldsValue }] = useForm({
     schemas: formSchema,
     showActionButtonGroup: false,
-    labelCol: { span: 4 },
-    wrapperCol: { span: 18 },
   });
-  function back() {}
-  function save() {}
+  const isSubmit = ref(false);
+  async function back() {
+    await closeTab();
+  }
+  async function save() {
+    await validate();
+    const form = getFieldsValue();
+    const gamePriceRules = form.gamePriceRules.map((it) => {
+      return {
+        ...extractRefs(it.editValueRefs),
+        id: it.id,
+      };
+    });
+    const scheduleRules = form.scheduleDTOS.map((it) => {
+      return {
+        ...extractRefs(it.editValueRefs),
+        id: it.id,
+      };
+    });
+    console.log(form, '表单数据');
+
+    if (!valiDataCustom(gamePriceRules) || !valiDataCustom(scheduleRules)) {
+      return message.error('请填写完整');
+    }
+    const newObj = {
+      game: {
+        ...form,
+        aptitudes: form.aptitudes ? form.aptitudes.map((it) => it.name).join(',') : null,
+        endTime: form.competitionTime.split(',')[1],
+        startTime: form.competitionTime.split(',')[0],
+        id: route.query.id,
+        gamePriceRules: null,
+        scheduleDTOS: null,
+      },
+      gamePriceRules: gamePriceRules,
+      scheduleDTOSList: form.scheduleDTOSList,
+      gamePriceRulesList: form.gamePriceRulesList,
+      scheduleDTOS: scheduleRules.map((it) => {
+        return {
+          ...it,
+          startTime: it.time[0],
+          endTime: it.time[1],
+        };
+      }),
+    };
+    isSubmit.value = true;
+    try {
+      await saveOrUpdate(newObj, Number(route.query.type) == 1);
+      isSubmit.value = false;
+      back();
+    } catch (error) {
+      console.log(error);
+      isSubmit.value = false;
+    }
+  }
+  const ScheduleArrangementColums: BasicColumn[] = [
+    {
+      title: '名称',
+      dataIndex: 'name',
+      width: 220,
+      editComponent: 'Input',
+      editRule: true,
+      editRow: true,
+      editable: true,
+      editComponentProps: {
+        placeholder: '请输入名称',
+        size: 'middle',
+      },
+    },
+    {
+      title: '时间段',
+      dataIndex: 'time',
+      width: 450,
+      editComponent: 'RangePicker',
+      editRule: true,
+      editComponentProps: {
+        showTime: true,
+        size: 'middle',
+        placeholder: ['开始时间', '结束时间'],
+        valueFormat: 'YYYY-MM-DD HH:mm:ss',
+        disabledDate: (current) => {
+          const fields = getFieldsValue();
+          if (fields.competitionTime && fields.competitionTime.length >= 2) {
+            const [startDate, endDate] = fields.competitionTime.split(',');
+            return current && (current < dayjs(startDate).startOf('day') || current > dayjs(endDate).endOf('day'));
+          }
+          return current && current < dayjs().endOf('day');
+        },
+      },
+      editRow: true,
+      editable: true,
+    },
+    {
+      title: '安排',
+      dataIndex: 'arrange',
+      width: 220,
+      editComponent: 'Input',
+      editRule: true,
+      editRow: true,
+      editable: true,
+      editComponentProps: {
+        size: 'middle',
+      },
+    },
+    {
+      key: 'op',
+      dataIndex: 'operation',
+      title: 'operation',
+      fixed: 'right',
+      width: 80,
+    },
+  ];
+  const extractRefs = (obj) => {
+    const result = {};
+    Object.keys(obj).forEach((key) => {
+      result[key] = unref(obj[key]);
+    });
+    return result;
+  };
+
+  function valiDataCustom(data) {
+    if (!areAllItemsAllFieldsFilled(data)) {
+      return false;
+    }
+    return true;
+  }
+
+  async function getDetaileData() {
+    const res = await getDetaile({ id: route.query.id });
+    setFieldsValue({
+      ...res.game,
+      competitionTime: [res.game.startTime, res.game.endTime],
+      gamePriceRules: res.gamePriceRules.map((it) => {
+        return {
+          ...it,
+          type: String(it.type),
+        };
+      }),
+      scheduleDTOS: res.scheduleDTOS.map((it) => {
+        return {
+          ...it,
+          time: [it.startTime, it.endTime],
+        };
+      }),
+    });
+  }
+
+  watch(
+    () => route.query.type,
+    () => {
+      if (Number(route.query.type) != 0) {
+        getDetaileData();
+      }
+    },
+    { immediate: true }
+  );
+  watch(
+    () => currentObj.value.currentAdcode,
+    () => {
+      if (currentObj.value.currentAdcode) {
+        const proviceCode = matchProvinceByFirstThreeDigits(currentObj.value.currentAdcode); //获取省编码
+        const proce = getDataByCode(Number(proviceCode[0].code));
+        const cityCode = matchCityByFirstFourDigits(proce, currentObj.value.currentAdcode);
+        setFieldsValue({
+          provinceCode: Number(proviceCode[0].code),
+          cityCode: cityCode?.value,
+          areaCode: currentObj.value.currentAdcode,
+          addressString: currentObj.value.currentAddress,
+          longitude: lnglat.value[0],
+          latitude: lnglat.value[1],
+        });
+      }
+    },
+    { immediate: true }
+  );
+  async function handleSearch(type: number) {
+    const { addressString, areaCode } = getFieldsValue();
+    if (type == 0) {
+      setCityByCode(areaCode);
+    }
+    if (addressString && type == 1 && areaCode) {
+      const res = await locateByCityAndAddress(areaCode, addressString);
+      setFieldsValue({
+        longitude: res.location.lng,
+        latitude: res.location.lat,
+      });
+    }
+    if (!areaCode) {
+      message.error('请选择省市区');
+    }
+  }
 </script>
+<style>
+  #panel {
+    position: absolute;
+    background-color: white;
+    max-height: 300px;
+    overflow-y: auto;
+    top: 10px;
+    right: 10px;
+    width: 280px;
+  }
+</style>

+ 46 - 11
src/views/businessManagement/competition/index.vue

@@ -9,13 +9,6 @@
         >
           新增</a-button
         >
-        <a-button
-          type="primary"
-          preIcon="ant-design:plus-outlined"
-          @click="router.push({ name: 'businessManagement-competitionCommon', query: { type: 1 } })"
-        >
-          编辑</a-button
-        >
         <a-dropdown>
           <template #overlay>
             <a-menu>
@@ -27,15 +20,17 @@
           </template>
         </a-dropdown>
       </template>
-    </BasicTable></div
-  >
+      <template #action="{ record }">
+        <TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)" />
+      </template> </BasicTable
+  ></div>
 </template>
 
 <script setup lang="ts" name="businessManagement-competition">
   import { BasicTable, TableAction } from '/@/components/Table';
   import { useListPage } from '/@/hooks/system/useListPage';
   import { searchFormSchema, columns } from './competition.data';
-  import { getMenuList } from '@/api/sys/menu';
+  import { deleteOne, list } from './competition.api';
   import { getcompetition } from '@/api/businessManagement/competition';
   import { router } from '/@/router';
 
@@ -43,7 +38,7 @@
     designScope: 'competition-template',
     tableProps: {
       title: '赛场列表',
-      api: getMenuList,
+      api: list,
       columns: columns,
       formConfig: {
         // update-begin--author:liaozhiyang---date:20230803---for:【QQYUN-5873】查询区域lablel默认居左
@@ -71,6 +66,46 @@
     // },
   });
   const [registerTable, { reload, expandAll, collapseAll }] = tableContext;
+  function getTableAction(record) {
+    return [
+      {
+        label: '查看',
+        onClick: handleView.bind(null, record),
+        // auth: 'courses:nm_courses:edit',
+      },
+      {
+        label: '编辑',
+        onClick: handleEdit.bind(null, record),
+        // auth: 'courses:nm_courses:edit',
+      },
+    ];
+  }
+  /**
+   * 下拉操作栏
+   */
+  function getDropDownAction(record) {
+    return [
+      {
+        label: '删除',
+        popConfirm: {
+          title: '是否确认删除',
+          confirm: handleDelete.bind(null, record),
+          placement: 'topLeft',
+        },
+        // auth: 'courses:nm_courses:delete',
+      },
+    ];
+  }
+  async function handleDelete(record) {
+    await deleteOne({ id: record.id }, reload);
+  }
+
+  function handleView(record) {
+    router.push({ name: 'businessManagement-competitionCommon', query: { type: 2, id: record.id } });
+  }
+  function handleEdit(record) {
+    router.push({ name: 'businessManagement-competitionCommon', query: { type: 1, id: record.id } });
+  }
 </script>
 
 <script lang="ts">

+ 0 - 78
src/views/businessManagement/courses/components/coursesModal.vue

@@ -1,78 +0,0 @@
-<template>
-  <BasicModal v-bind="$attrs" @register="registerModal" destroyOnClose :title="title" :width="800" @ok="handleSubmit">
-    <BasicForm @register="registerForm" name="coursesForm" />
-  </BasicModal>
-</template>
-
-<script lang="ts" setup>
-  import { ref, computed, unref } from 'vue';
-  import { BasicModal, useModalInner } from '/@/components/Modal';
-  import { BasicForm, useForm } from '/@/components/Form/index';
-  import { formSchema } from '../courses.data';
-  import { saveOrUpdate } from '../courses.api';
-  import { useMessage } from '/@/hooks/web/useMessage';
-  const { createMessage } = useMessage();
-  // Emits声明
-  const emit = defineEmits(['register', 'success']);
-  const isUpdate = ref(true);
-  const isDetail = ref(false);
-  //表单配置
-  const [registerForm, { setProps, resetFields, setFieldsValue, validate, scrollToField }] = useForm({
-    labelWidth: 150,
-    schemas: formSchema,
-    showActionButtonGroup: false,
-    baseColProps: { span: 24 },
-  });
-  //表单赋值
-  const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
-    //重置表单
-    await resetFields();
-    setModalProps({ confirmLoading: false, showCancelBtn: !!data?.showFooter, showOkBtn: !!data?.showFooter });
-    isUpdate.value = !!data?.isUpdate;
-    isDetail.value = !!data?.showFooter;
-    if (unref(isUpdate)) {
-      //表单赋值
-      await setFieldsValue({
-        ...data.record,
-      });
-    }
-    // 隐藏底部时禁用整个表单
-    setProps({ disabled: !data?.showFooter });
-  });
-  //设置标题
-  const title = computed(() => (!unref(isUpdate) ? '新增' : !unref(isDetail) ? '详情' : '编辑'));
-  //表单提交事件
-  async function handleSubmit(v) {
-    try {
-      let values = await validate();
-      setModalProps({ confirmLoading: true });
-      //提交表单
-      await saveOrUpdate(values, isUpdate.value);
-      //关闭弹窗
-      closeModal();
-      //刷新列表
-      emit('success');
-    } catch ({ errorFields }) {
-      if (errorFields) {
-        const firstField = errorFields[0];
-        if (firstField) {
-          scrollToField(firstField.name, { behavior: 'smooth', block: 'center' });
-        }
-      }
-      return Promise.reject(errorFields);
-    } finally {
-      setModalProps({ confirmLoading: false });
-    }
-  }
-</script>
-
-<style lang="less" scoped>
-  /** 时间和数字输入框样式 */
-  :deep(.ant-input-number) {
-    width: 100%;
-  }
-
-  :deep(.ant-calendar-picker) {
-    width: 100%;
-  }
-</style>

+ 12 - 14
src/views/businessManagement/courses/courses.data.ts

@@ -4,7 +4,6 @@ import { rules } from '/@/utils/helper/validator';
 import { render } from '/@/utils/common/renderUtils';
 import { getWeekMonthQuarterYear } from '/@/utils';
 import { getSprotProject } from '/@/api/common/api';
-const sportList = await getSprotProject({ pageSize: 20 });
 import { defHttp } from '/@/utils/http/axios';
 import { getAddress, getCoachList } from './courses.api';
 import { useUserStore } from '/@/store/modules/user';
@@ -26,12 +25,13 @@ export const columns: BasicColumn[] = [
   {
     title: '课时节数及时间',
     align: 'center',
-    dataIndex: 'tenantId',
+    dataIndex: 'totalNum',
+    slots: { customRender: 'totalNum' },
   },
   {
     title: '上课地点',
     align: 'center',
-    dataIndex: 'siteId',
+    dataIndex: 'address',
   },
   {
     title: '销售价/原价(元)',
@@ -42,13 +42,13 @@ export const columns: BasicColumn[] = [
   {
     title: '上下架状态',
     align: 'center',
-    dataIndex: 'priceType',
+    dataIndex: 'rackingStatus',
     slots: { customRender: 'priceType' },
   },
   {
     title: '课程类型',
     align: 'center',
-    dataIndex: 'siteId',
+    dataIndex: 'categoryIds',
   },
 ];
 //查询数据
@@ -112,16 +112,14 @@ export const formSchema: FormSchema[] = [
   {
     label: '课程类型',
     field: 'categoryId',
-    component: 'CheckboxGroup',
+    component: 'ApiSelect',
     required: true,
-
     componentProps: {
-      options: sportList.records.map((it) => {
-        return {
-          label: it.name,
-          value: it.id,
-        };
-      }),
+      api: getSprotProject,
+      labelField: 'name',
+      valueField: 'id',
+      mode: 'tags',
+      resultField: 'records',
     },
   },
   {
@@ -141,7 +139,7 @@ export const formSchema: FormSchema[] = [
   },
   {
     label: '上课地点',
-    field: 'siteId',
+    field: 'addressSiteId',
     component: 'ApiSelect',
     required: true,
     componentProps: {

+ 10 - 22
src/views/businessManagement/courses/index.vue

@@ -26,24 +26,28 @@
       <template v-slot:bodyCell="{ column, record, index, text }"> </template>
       <template #price="{ record }"> {{ record.sellingPrice }}/{{ record.originalPrice }} </template>
       <template #priceType="{ record }">
-        <Switch v-model:checked="record.priceType" :checked-value="0" :un-checked-value="1"></Switch>
+        <Switch v-model:checked="record.rackingStatus" :checked-value="0" :un-checked-value="1"></Switch>
+      </template>
+      <template #totalNum="{ record }">
+        <div>{{ record.totalNum }}课时</div>
+        {{ record.startTime }}-{{ record.endTime }}
       </template>
       <template #img="{ text }">
         <TableImg :img-list="[text]" :size="60" simpleShow></TableImg>
       </template>
     </BasicTable>
     <!-- 表单区域 -->
-    <coursesModal @register="registerModal" @success="handleSuccess"></coursesModal>
   </div>
 </template>
 
 <script lang="ts" name="courses" setup>
   import { Switch } from 'ant-design-vue';
+  import { router } from '/@/router';
+
   import { ref, reactive, computed, unref } from 'vue';
   import { BasicTable, TableImg, TableAction } from '/@/components/Table';
   import { useModal } from '/@/components/Modal';
   import { useListPage } from '/@/hooks/system/useListPage';
-  import coursesModal from './components/coursesModal.vue';
   import { columns, searchFormSchema } from './courses.data';
   import { list, deleteOne } from './courses.api';
   import { useUserStore } from '/@/store/modules/user';
@@ -89,20 +93,10 @@
    * 编辑事件
    */
   function handleView(record: Recordable) {
-    router.push({ name: 'businessManagement-publishcourses', query: { type: 1, id: record.id } });
-  }
-  function handleEdit(record: Recordable) {
     router.push({ name: 'businessManagement-publishcourses', query: { type: 2, id: record.id } });
   }
-  /**
-   * 详情
-   */
-  function handleDetail(record: Recordable) {
-    openModal(true, {
-      record,
-      isUpdate: true,
-      showFooter: false,
-    });
+  function handleEdit(record: Recordable) {
+    router.push({ name: 'businessManagement-publishcourses', query: { type: 1, id: record.id } });
   }
   /**
    * 删除事件
@@ -139,10 +133,6 @@
    */
   function getDropDownAction(record) {
     return [
-      {
-        label: '详情',
-        onClick: handleDetail.bind(null, record),
-      },
       {
         label: '删除',
         popConfirm: {
@@ -156,11 +146,9 @@
   }
 </script>
 <script lang="ts">
-  import { router } from '/@/router';
-
   import { useMultipleTabStore } from '@/store/modules/multipleTab';
   import { storeToRefs } from 'pinia';
-  const typeList = ['发布课程', '查看课程'];
+  const typeList = ['发布课程', '编辑课程', '查看课程'];
   export default {
     async beforeRouteLeave(to, from, next) {
       to.meta.title = typeList[Number(to.query.type)];

+ 8 - 5
src/views/businessManagement/courses/publishcourses.vue

@@ -15,7 +15,7 @@
             :tableColumn="publishcoursesColums"
             v-model:value="model[field]"
             show-index
-            :show-action="Number(route.query.type) == 0"
+            :show-action="Number(route.query.type) != 2"
           ></ZtCustomTable>
         </template>
         <template #title3>
@@ -28,7 +28,7 @@
         </template>
 
         <template #formFooter>
-          <div class="w-full flex items-center justify-center my-3" v-if="Number(route.query.type) != 1">
+          <div class="w-full flex items-center justify-center my-3" v-if="Number(route.query.type) != 2">
             <a-button type="primary" @click="save" class="mr-2" :loading="isSubmit"> 保存 </a-button>
             <a-button type="error" @click="back" class="mr-2"> 关闭 </a-button>
           </div>
@@ -67,7 +67,7 @@
       await getDataList();
       nextTick(async () => {
         setProps({
-          disabled: Number(route.query.type) == 1,
+          disabled: Number(route.query.type) == 2,
         });
         await setFieldsValue({
           ...viewDataSport.value,
@@ -85,7 +85,8 @@
       return {
         name: it.name,
         time: [it.startTime, it.endTime],
-        id: dayjs().valueOf(),
+        id: it.id,
+        coursesId: it.coursesId,
       };
     });
     viewDataSport.value = res.courses;
@@ -100,7 +101,7 @@
     const form = await getFieldsValue();
     isSubmit.value = true;
     const newObj = {
-      courses: { ...form, priceRulesList: null },
+      courses: { ...form, priceRulesList: null, siteId: useShopInfoStore().currentId },
       priceRulesList: transformData(form.priceRulesList),
     };
     try {
@@ -118,6 +119,8 @@
         startTime: dayjs(it.editValueRefs.time[0]).format('YYYY-MM-DD hh:mm:ss'),
         endTime: dayjs(it.editValueRefs.time[1]).format('YYYY-MM-DD hh:mm:ss'),
         name: it.editValueRefs.name.value,
+        id: typeof it.id == 'string' ? it.id : null,
+        coursesId: it.coursesId,
       };
     });
   }

+ 46 - 0
src/views/businessManagement/gymnasiumBag/gymnasiumBag.api.ts

@@ -0,0 +1,46 @@
+import { defHttp } from '/@/utils/http/axios';
+
+enum Api {
+  list = '/app/appSitePlace/queryUnfixedPageList',
+  save = '/app/appSitePlace/savePack',
+  edit = '/app/appSitePlace/editPack',
+  deleteOne = '/app/appSitePlace/deleteUnfixed',
+  detaile = 'app/appCourese/queryById',
+  queryById = '/app/appSitePlace/queryPack',
+  Business = '/app/appSitePlace/queryByDeptId',
+}
+export const list = (params) => defHttp.get({ url: Api.list, params });
+
+/**
+ * 删除单个
+ */
+export const deleteOne = (params, handleSuccess) => {
+  return defHttp.delete({ url: Api.deleteOne, params }, { joinParamsToUrl: true }).then(() => {
+    handleSuccess();
+  });
+};
+
+/**
+ * 保存或者更新
+ * @param params
+ */
+export const saveOrUpdate = (params, isUpdate) => {
+  let url = isUpdate ? Api.edit : Api.save;
+  return defHttp.post({ url: url, params });
+};
+
+export const getDetaile = (params) => {
+  return defHttp.get({ url: Api.detaile, params });
+};
+
+export const queryById = (params) => {
+  return defHttp.get({ url: Api.queryById, params });
+};
+/**
+ * 营业名称
+ * @param params
+ * @returns
+ */
+export const Business = (params) => {
+  return defHttp.get({ url: `${Api.Business}/${params.currentId}` });
+};

+ 1 - 0
src/views/businessManagement/gymnasiumBag/gymnasiumBag.data.ts

@@ -12,6 +12,7 @@ export const ScheduleArrangementColums: BasicColumn[] = [
     editComponentProps: {
       placeholder: ['开始时间', '结束时间'],
       size: 'middle',
+      valueFormat: 'HH:mm:ss',
     },
     width: 350,
     editRow: true,

+ 180 - 64
src/views/businessManagement/gymnasiumBag/index.vue

@@ -14,10 +14,10 @@
           <div v-for="(item, idx) in getCustomerList(model['categoryId'])" :key="item.value">
             <Card class="my-4">
               <FormItem :label="`${item.label}场地`" required :labelCol="{ xl: 2, sm: 3 }">
-                <InputNumber addon-before="共有" addon-after="个球场" v-model:value="model['inventory' + idx]"></InputNumber>
+                <InputNumber addon-before="共有" addon-after="个球场" v-model:value="model['inventory' + idx]" :max="30" :min="1"></InputNumber>
               </FormItem>
               <FormItem label="时间段及费用" required :labelCol="{ xl: 2, sm: 3 }" :wrapper-col="{ span: 22 }">
-                <ZtCustomTable :tableColumn="ScheduleArrangementColums" v-model:value="model[item.value]"></ZtCustomTable>
+                <ZtCustomTable :tableColumn="ScheduleArrangementColums" v-model:value="model[item.value]"> </ZtCustomTable>
               </FormItem>
             </Card>
           </div>
@@ -44,12 +44,18 @@
   import { DemoOptionsItem } from '/@/api/demo/model/optionsModel';
   import ZtCustomTable from '/@/components/ZtCustomTable/index.vue';
   import { ScheduleArrangementColums, apiForm, priceRules } from './gymnasiumBag.data';
-  import { h, ref } from 'vue';
+  import { saveOrUpdate, queryById, Business } from './gymnasiumBag.api';
+  import { h, onUnmounted, ref, watch } from 'vue';
   import { OriginalItem } from '/#/utils';
   import { areAllItemsAllFieldsFilled } from '/@/utils';
   import dayjs from 'dayjs';
   import { useTabs } from '/@/hooks/web/useTabs';
+  import { useShopInfoStore } from '/@/store/modules/shopInfo';
+  import { storeToRefs } from 'pinia';
+  import { unref } from 'vue';
   const projectList = ref<DemoOptionsItem[]>([]);
+  useShopInfoStore().getCurrentDep();
+  const { deptList, currentId } = storeToRefs(useShopInfoStore());
   const { close: closeTab } = useTabs();
   const isSubmit = ref(false);
   const formSchema: FormSchema[] = [
@@ -57,15 +63,26 @@
       field: 'name',
       label: '营业名称',
       component: 'Select',
-      defaultValue: '111',
       required: true,
       labelWidth: 120,
+      componentProps: {
+        // api: Business,
+        // params: {
+        //   currentId,
+        // },
+        // immediate: false,
+        // valueField: 'id',
+        // labelField: 'name',
+        onChange(e) {
+          console.log(e, '请求新的');
+          getQueryData(e);
+        },
+      },
       colProps: {
         span: 14,
         xs: 24,
       },
     },
-
     {
       field: 'title1',
       colSlot: 'title1',
@@ -157,33 +174,87 @@
     schemas: formSchema,
     showActionButtonGroup: false,
   });
+  const isUpdate = ref(false);
   async function back() {
     await closeTab();
   }
-  async function save() {
-    const data = await getFieldsValue();
-    console.log(data);
-    return;
-    const form = await validate();
-
-    const cusotmValidate = await validateInventoryFields(form);
-    if (!cusotmValidate) {
-      return message.error('请填写必填项');
+  async function getQueryData(id) {
+    const res = await queryById({ siteId: id });
+    if (!res.siteCategoryRuleDTOS) {
+      isUpdate.value = false;
+    } else {
+      isUpdate.value = true;
+      console.log(res, '打印');
+      const categoryIdList = res.siteCategoryRuleDTOS.map((it) => it.categoryId);
+      const formData = res.siteCategoryRuleDTOS.map((it) => reverseTransformData(it.appSiteRuleDTOList));
+      const filed = {
+        categoryId: categoryIdList,
+        buyLimit: res.appSitePlaceCuDTO.buyLimit,
+        earlyRefundTime: res.appSitePlaceCuDTO.earlyRefundTime,
+        reminder: res.appSitePlaceCuDTO.reminder,
+      };
+      categoryIdList.forEach((it, idx) => {
+        filed[it] = formData[idx][it];
+        filed[`inventory${idx}`] = res.siteCategoryRuleDTOS[idx].count;
+      });
+      setFieldsValue(filed);
+      console.log(filed, 'filed');
     }
-    const newObj: apiForm = {
-      site: {
-        name: form.name,
-        categoryId: form.categoryId,
-        earlyRefundTime: form.earlyRefundTime,
+  }
+  async function getBusinessData() {
+    const res = await Business({ currentId: currentId.value });
+    updateSchema({ field: 'name', componentProps: { options: res.map((it) => ({ label: it.name, value: it.id })) } });
+    //如果商家端进入默认取第一个
+    setFieldsValue({
+      name: res[0].id,
+    });
+    getQueryData(res[0].id);
+  }
+  watch(
+    () => currentId.value,
+    () => {
+      if (currentId.value) getBusinessData();
+    },
+    { immediate: true }
+  );
+  async function save() {
+    const form = await getFieldsValue();
+    await validate();
+    console.log(validateInventoryFields(form));
+    if (!validateInventoryFields(form)) return message.error('请填写完整包场时间');
+    const categoryList = form.categoryId.split(',');
+    const newObj = {
+      appSitePlaceCuDTO: {
         buyLimit: form.buyLimit,
+        earlyRefundTime: form.earlyRefundTime,
         reminder: form.reminder,
-        type: 1, //0学校,1是体育馆
+        siteId: form.name,
       },
-      priceRulesList: getResultData(form),
+      siteCategoryRuleDTOS: categoryList.map((it, idx) => {
+        return {
+          categoryId: it,
+          count: form[`inventory${idx}`],
+          appSiteRuleDTOList: transformData(
+            form[it].map((items) => {
+              {
+                return {
+                  ...items,
+                  ...items.editValueRefs,
+                };
+              }
+            }),
+            it,
+            form.name
+          ),
+        };
+      }),
     };
+    console.log(newObj, '打印');
     isSubmit.value = true;
     try {
-      await siteAdd(newObj);
+      await saveOrUpdate(newObj, isUpdate.value);
+      isSubmit.value = false;
+
       await closeTab();
     } catch (error) {
       isSubmit.value = false;
@@ -229,21 +300,40 @@
       );
     });
   }
+  type TransformedItem = {
+    dayOfWeek: string;
+    sellingPrice: number | null;
+    startTime: string;
+    endTime: string;
+    categoryId: string;
+    sitePlaceId: string;
+    batchuuid?: string;
+  };
+  /**
+   * 将原始数据转换为目标格式
+   * @param data - 原始数据数组
+   * @returns 转换后的数据数组
+   */
+  function transformData(data: OriginalItem[], categoryId: string, sitePlaceId: string): TransformedItem[] {
+    const result: TransformedItem[] = [];
+    for (const item of data) {
+      const { time = ['1', '2'] } = item;
+      for (let i = 1; i <= 7; i++) {
+        const key = `day${i}` as keyof OriginalItem;
+        const sellingPrice: any = item[key];
+        result.push({
+          dayOfWeek: `${i}`,
+          sellingPrice: sellingPrice === undefined ? null : unref(sellingPrice),
+          startTime: unref(time)[0],
+          endTime: unref(time)[1],
+          categoryId,
+          sitePlaceId,
+          batchuuid: item[`dayId${i}`],
+        });
+      }
+    }
 
-  function getResultData(form: any): priceRules[] {
-    const priceRulesList: any = [];
-    form.categoryId.split(',').map((it, idx) => {
-      const data = transformData(form[it]?.obj).map((item) => {
-        return {
-          ...item,
-          categoryId: it,
-          inventory: form[`inventory${idx}`],
-          type: 1, //0学校 1体育馆包场 2体育馆不固定场
-        };
-      });
-      priceRulesList.push(...data);
-    });
-    return priceRulesList;
+    return result;
   }
 
   /**
@@ -258,8 +348,7 @@
       return false;
     }
 
-    const categoryIds = categoryId.split(',').map((id) => id.trim());
-
+    const categoryIds = categoryId.split(',');
     for (let i = 0; i < categoryIds.length; i++) {
       const inventoryField = `inventory${i}`;
       const inventoryValue = formValues[inventoryField];
@@ -267,39 +356,66 @@
       if (inventoryValue === undefined || inventoryValue === null || inventoryValue <= 0 || !priceRulesList) {
         return false;
       }
-      if (!areAllItemsAllFieldsFilled(priceRulesList.obj)) {
+      const priceRulesListIsValid = priceRulesList.map((it) => it.editValueRefs);
+      if (!areAllItemsAllFieldsFilled(priceRulesListIsValid)) {
         return false;
       }
     }
 
     return true;
   }
-  type TransformedItem = {
-    dayOfWeek: string;
-    sellingPrice: number | null;
-    startTime: string;
-    endTime: string;
-  };
-  /**
-   * 将原始数据转换为目标格式
-   * @param data - 原始数据数组
-   * @returns 转换后的数据数组
-   */
-  function transformData(data: OriginalItem[]): TransformedItem[] {
-    const result: TransformedItem[] = [];
-    for (const item of data) {
-      const { time = ['1', '2'] } = item;
-      for (let i = 1; i <= 7; i++) {
-        const key = `day${i}` as keyof OriginalItem;
-        const sellingPrice = item[key];
-        result.push({
-          dayOfWeek: `${i}`,
-          sellingPrice: sellingPrice === undefined ? null : sellingPrice,
-          startTime: dayjs(time[0]).format('YYYY-MM-DD hh:mm:ss'),
-          endTime: dayjs(time[1]).format('YYYY-MM-DD hh:mm:ss'),
-        });
+  onUnmounted(() => {
+    useShopInfoStore().isShowSelect = false;
+  });
+  function reverseTransformData(transformedData: TransformedItem[]): Record<string, OriginalItem[]> {
+    const groupedByTime: Record<string, TransformedItem[]> = {};
+
+    // 首先按时间分组数据
+    transformedData.forEach((item) => {
+      const timeKey = `${item.startTime}-${item.endTime}`;
+      if (!groupedByTime[timeKey]) {
+        groupedByTime[timeKey] = [];
       }
-    }
+      groupedByTime[timeKey].push(item);
+    });
+
+    // 构造结果对象
+    const result: Record<string, OriginalItem[]> = {};
+
+    Object.keys(groupedByTime).forEach((timeKey) => {
+      const items = groupedByTime[timeKey];
+      const [startTime, endTime] = timeKey.split('-');
+
+      // 按照 categoryId 分组
+      const groupedByCategory: Record<string, TransformedItem[]> = {};
+      items.forEach((item) => {
+        if (!groupedByCategory[item.categoryId]) {
+          groupedByCategory[item.categoryId] = [];
+        }
+        groupedByCategory[item.categoryId].push(item);
+      });
+
+      // 为每个 categoryId 创建原始数据项
+      Object.keys(groupedByCategory).forEach((categoryId) => {
+        if (!result[categoryId]) {
+          result[categoryId] = [];
+        }
+
+        const categoryItems = groupedByCategory[categoryId];
+        const originalItem: OriginalItem = {
+          time: [startTime, endTime],
+        };
+
+        // 添加每天的价格信息
+        categoryItems.forEach((item) => {
+          const dayKey = `day${item.dayOfWeek}` as keyof OriginalItem;
+          originalItem[dayKey] = item.sellingPrice;
+          originalItem[`dayId${item.dayOfWeek}`] = item.batchuuid;
+        });
+
+        result[categoryId].push(originalItem);
+      });
+    });
 
     return result;
   }

+ 12 - 6
src/views/businessManagement/gymnasiumNoFixed/DataRuleModal.vue

@@ -19,18 +19,20 @@
   </BasicModal>
 </template>
 <script lang="ts" setup>
-  import JImageUpload from '@/components/Form/src/jeecg/components/JImageUpload.vue';
   import { TypographyTitle, Divider } from 'ant-design-vue';
   import { ref, computed, unref } from 'vue';
   import { BasicModal, useModalInner } from '/@/components/Modal';
   import { BasicForm, useForm } from '/@/components/Form/index';
   import { dataRuleFormSchema } from './gymnasiumNoFixed.data';
+  import { saveOrUpdate } from './gymnasiumNoFixed.api';
+  import { useShopInfoStore } from '/@/store/modules/shopInfo';
+  import { useUserStore } from '/@/store/modules/user';
   // 声明Emits
   const emit = defineEmits(['success', 'register']);
-  const props = defineProps({ permissionId: String });
   const isUpdate = ref(true);
+  const isDetail = ref(false);
   //表单配置
-  const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
+  const [registerForm, { resetFields, setFieldsValue, validate, setProps }] = useForm({
     schemas: dataRuleFormSchema,
     showActionButtonGroup: false,
   });
@@ -38,12 +40,16 @@
   const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
     //重置表单
     await resetFields();
-    setModalProps({ confirmLoading: false });
+    setModalProps({ confirmLoading: false, showCancelBtn: !!data?.showFooter, showOkBtn: !!data?.showFooter });
     isUpdate.value = !!data?.isUpdate;
+    isDetail.value = !!data?.showFooter;
+
     if (unref(isUpdate)) {
       //表单赋值
+      setProps({ disabled: !data?.showFooter });
       await setFieldsValue({
         ...data.record,
+        downTime: data.record.downTime.split(','),
       });
     }
   });
@@ -55,10 +61,10 @@
   async function handleSubmit() {
     try {
       const values = await validate();
-      values.permissionId = props.permissionId;
+
       setModalProps({ confirmLoading: true });
       //提交表单
-      // await saveOrUpdateRule(values, isUpdate.value);
+      await saveOrUpdate({ ...values, orgCode: useUserStore().userInfo?.orgCode }, isUpdate.value);
       //关闭弹窗
       closeModal();
       //刷新列表

+ 32 - 0
src/views/businessManagement/gymnasiumNoFixed/gymnasiumNoFixed.api.ts

@@ -0,0 +1,32 @@
+import { defHttp } from '/@/utils/http/axios';
+
+enum Api {
+  list = '/app/appSitePlace/queryUnfixedPageList',
+  save = '/app/appSitePlace/addUnfixed',
+  edit = '/app/appSitePlace/updateUnfixed',
+  deleteOne = '/app/appSitePlace/deleteUnfixed',
+  detaile = 'app/appCourese/queryById',
+}
+export const list = (params) => defHttp.get({ url: Api.list, params });
+
+/**
+ * 删除单个
+ */
+export const deleteOne = (params, handleSuccess) => {
+  return defHttp.delete({ url: Api.deleteOne, params }, { joinParamsToUrl: true }).then(() => {
+    handleSuccess();
+  });
+};
+
+/**
+ * 保存或者更新
+ * @param params
+ */
+export const saveOrUpdate = (params, isUpdate) => {
+  let url = isUpdate ? Api.edit : Api.save;
+  return defHttp.post({ url: url, params });
+};
+
+export const getDetaile = (params) => {
+  return defHttp.get({ url: Api.detaile, params });
+};

+ 18 - 15
src/views/businessManagement/gymnasiumNoFixed/gymnasiumNoFixed.data.ts

@@ -1,10 +1,13 @@
-import { FormSchema } from '/@/components/Table';
+import { BasicColumn, FormSchema } from '/@/components/Table';
 
-export const columns = [
+export const columns: BasicColumn[] = [
   {
     title: '封面',
     dataIndex: 'cover',
     width: 100,
+    slots: {
+      customRender: 'img',
+    },
   },
   {
     title: '商品名称',
@@ -147,7 +150,7 @@ export const dataRuleFormSchema: FormSchema[] = [
     },
   },
   {
-    field: 'name',
+    field: 'indate',
     label: '',
     component: 'InputNumber',
     required: true,
@@ -165,7 +168,7 @@ export const dataRuleFormSchema: FormSchema[] = [
     },
   },
   {
-    field: 'name',
+    field: 'downTime',
     label: '除外日期(不可用)  ',
     component: 'CheckboxGroup',
     required: true,
@@ -173,18 +176,18 @@ export const dataRuleFormSchema: FormSchema[] = [
     labelWidth: 180,
     componentProps: {
       options: [
-        { label: '星期一', value: 1 },
-        { label: '星期二', value: 2 },
-        { label: '星期三', value: 3 },
-        { label: '星期四', value: 4 },
-        { label: '星期五', value: 5 },
-        { label: '星期六', value: 6 },
-        { label: '星期日', value: 7 },
+        { label: '星期一', value: '1' },
+        { label: '星期二', value: '2' },
+        { label: '星期三', value: '3' },
+        { label: '星期四', value: '4' },
+        { label: '星期五', value: '5' },
+        { label: '星期六', value: '6' },
+        { label: '星期日', value: '7' },
       ],
     },
   },
   {
-    field: 'name',
+    field: 'usableCount',
     label: '适用人数',
     component: 'InputNumber',
     required: true,
@@ -196,7 +199,7 @@ export const dataRuleFormSchema: FormSchema[] = [
     },
   },
   {
-    field: 'name',
+    field: 'reminder',
     label: '温馨提示',
     component: 'InputTextArea',
     required: true,
@@ -208,8 +211,8 @@ export const dataRuleFormSchema: FormSchema[] = [
         //最大显示行数
         maxRows: 6,
       },
-      defaultValue:
-        '场馆可能致电您,预约到馆时间,请保持手机畅通;\n如需购券发票,请您在消费时向商户咨询;\n 为了保障您的权益,建议使用平台线上支付,若使用了其他方式支付导致纠纷,\n平台不承担任何责任,感谢您的理解和支持。\n温馨提醒:您在到馆使用本商品/本服务期间,请关注场馆的安全提示内容,了解相关注意事项,做好安全防护措施,包含您的安全。',
     },
+    defaultValue:
+      '场馆可能致电您,预约到馆时间,请保持手机畅通;\n如需购券发票,请您在消费时向商户咨询;\n 为了保障您的权益,建议使用平台线上支付,若使用了其他方式支付导致纠纷,\n平台不承担任何责任,感谢您的理解和支持。\n温馨提醒:您在到馆使用本商品/本服务期间,请关注场馆的安全提示内容,了解相关注意事项,做好安全防护措施,包含您的安全。',
   },
 ];

+ 81 - 25
src/views/businessManagement/gymnasiumNoFixed/index.vue

@@ -1,50 +1,57 @@
 <template>
-  <div class="p-4"
-    ><BasicTable @register="registerTable">
-      <template #tableTitle>
-        <a-button type="primary" preIcon="ant-design:plus-outlined" @click="handleAdd"> 新增</a-button>
-        <a-dropdown>
-          <template #overlay>
-            <a-menu>
-              <a-menu-item key="1">
-                <Icon icon="ant-design:delete-outlined"></Icon>
-                删除
-              </a-menu-item>
-            </a-menu>
-          </template>
-        </a-dropdown>
-      </template>
-    </BasicTable></div
-  >
+  ><BasicTable @register="registerTable">
+    <template #tableTitle>
+      <a-button type="primary" preIcon="ant-design:plus-outlined" @click="handleAdd"> 新增</a-button>
+      <a-dropdown>
+        <template #overlay>
+          <a-menu>
+            <a-menu-item key="1">
+              <Icon icon="ant-design:delete-outlined"></Icon>
+              删除
+            </a-menu-item>
+          </a-menu>
+        </template>
+      </a-dropdown>
+    </template>
+    <template #action="{ record }">
+      <TableAction :actions="getTableAction(record)" />
+    </template>
+    <template #img="{ text }">
+      <TableImg :img-list="[text]" :size="60" simpleShow></TableImg>
+    </template>
+  </BasicTable>
   <dataRuleModal @register="registerModal" @success="ruleModal"></dataRuleModal>
 </template>
 
 <script setup lang="ts" name="businessManagement-gymnasiumNoFixed">
   import dataRuleModal from './DataRuleModal.vue';
-  import { BasicTable, TableAction } from '/@/components/Table';
+  import { BasicTable, TableImg, TableAction } from '/@/components/Table';
   import { useListPage } from '/@/hooks/system/useListPage';
-  import { getSiteList } from '@/api/common/api';
   import { useModal } from '/@/components/Modal';
 
   import { columns, searchFormSchema } from './gymnasiumNoFixed.data';
+  import { deleteOne, list } from './gymnasiumNoFixed.api';
+  import { useShopInfoStore } from '/@/store/modules/shopInfo';
+  import { reactive } from 'vue';
   const [registerModal, { openModal }] = useModal();
-
+  useShopInfoStore().getCurrentDep();
+  const queryParam = reactive<any>({});
   const { prefixCls, tableContext } = useListPage({
     designScope: 'competition-template',
     tableProps: {
       title: '赛场列表',
-      api: getSiteList,
+      api: list,
       columns: columns,
       formConfig: {
-        // update-begin--author:liaozhiyang---date:20230803---for:【QQYUN-5873】查询区域lablel默认居左
         labelWidth: 100,
-        rowProps: { gutter: 24 },
-        // update-end--author:liaozhiyang---date:20230803---for:【QQYUN-5873】查询区域lablel默认居左
         schemas: searchFormSchema,
       },
       actionColumn: {
         width: 120,
       },
+      beforeFetch: (params) => {
+        return Object.assign(params, queryParam);
+      },
       // rowSelection: null,
       //自定义默认排序
       defSort: {
@@ -54,12 +61,61 @@
     },
   });
   const [registerTable, { reload, expandAll, collapseAll }] = tableContext;
-  function ruleModal() {}
+  function ruleModal() {
+    reload();
+  }
   function handleAdd() {
     openModal(true, {
       isUpdate: false,
+      showFooter: true,
+    });
+  }
+  function handleView(record) {
+    openModal(true, {
+      isUpdate: true,
+      record,
+      showFooter: false,
+    });
+  }
+  function handleEdit(record) {
+    openModal(true, {
+      record,
+      isUpdate: true,
+      showFooter: true,
+    });
+  }
+  function handleDelete(record) {
+    deleteOne({ id: record.id }, () => {
+      reload();
     });
   }
+
+  /**
+   * 操作栏
+   */
+  function getTableAction(record) {
+    return [
+      {
+        label: '查看',
+        onClick: handleView.bind(null, record),
+        // auth: 'courses:nm_courses:edit',
+      },
+      {
+        label: '编辑',
+        onClick: handleEdit.bind(null, record),
+        // auth: 'courses:nm_courses:edit',
+      },
+      {
+        label: '删除',
+        popConfirm: {
+          title: '是否确认删除',
+          confirm: handleDelete.bind(null, record),
+          placement: 'topLeft',
+        },
+        // auth: 'courses:nm_courses:edit',
+      },
+    ];
+  }
 </script>
 
 <style scoped></style>

+ 3 - 5
src/views/demo/table/EditRowTable.vue

@@ -10,9 +10,7 @@
 <script lang="ts">
   import { defineComponent, ref } from 'vue';
   import { BasicTable, useTable, TableAction, BasicColumn, ActionItem, EditRecordRow } from '/@/components/Table';
-  import { optionsListApi } from '/@/api/demo/select';
-
-  import { demoListApi } from '/@/api/demo/table';
+  import { getUserList } from '/@/api/common/api';
   import { treeOptionsListApi } from '/@/api/demo/tree';
   import { cloneDeep } from 'lodash-es';
   import { useMessage } from '/@/hooks/web/useMessage';
@@ -91,7 +89,7 @@
       editRow: true,
       editComponent: 'ApiSelect',
       editComponentProps: {
-        api: optionsListApi,
+        api: getUserList,
         resultField: 'list',
         labelField: 'name',
         valueField: 'id',
@@ -162,7 +160,7 @@
       const [registerTable] = useTable({
         title: '可编辑行示例',
         titleHelpMessage: ['本例中修改[数字输入框]这一列时,同一行的[远程下拉]列的当前编辑数据也会同步发生改变'],
-        api: demoListApi,
+        api: getUserList,
         columns: columns,
         showIndexColumn: false,
         showTableSetting: true,

+ 3 - 0
src/views/system/staff/staff.api.ts

@@ -8,6 +8,7 @@ enum Api {
   edit = 'staff/staff/edit',
   queryId = '/staff/staff/queryById',
   del = 'staff/staff/delete',
+  test = '/sys/sysDepart/findByDepTree',
 }
 export const getList = (params) => defHttp.get({ url: Api.list, params });
 
@@ -24,3 +25,5 @@ export const saveOrUpdate = (params, isUpdate) => {
 export const queryById = (id) => defHttp.get({ url: Api.queryId, params: { id } });
 
 export const del = (params) => defHttp.delete({ url: Api.del, params }, { joinParamsToUrl: true });
+
+export const test = (params) => defHttp.get({ url: Api.test, params });

+ 1 - 0
src/views/system/staff/staffList.vue

@@ -49,6 +49,7 @@
     },
   });
   const [registerTable, { reload, expandAll, collapseAll }] = tableContext;
+
   function handleAdd() {
     openModal(true, {
       isUpdate: false,

+ 10 - 0
types/map.api.d.ts

@@ -0,0 +1,10 @@
+type options = {
+  key: string; // 高德地图的API Key
+  version?: string; // API版本,默认为最新版本
+  plugins?: string[]; // 需要加载的插件列表
+  AMapUI?: any;
+  Loca?: any;
+};
+declare namespace AMapLoader {
+  export function load<T>(settings: options): Promise<T>;
+}

+ 1 - 1
types/utils.d.ts

@@ -1,5 +1,4 @@
 import type { ComputedRef, Ref } from 'vue';
-import { TableColumnProps } from 'ant-design-vue';
 export type DynamicProps<T> = {
   [P in keyof T]: Ref<T[P]> | T[P] | ComputedRef<T[P]>;
 };
@@ -13,4 +12,5 @@ export type OriginalItem = {
   day6?: number | null;
   day7?: number | null;
   time?: string[];
+  id?: string;
 };