瀏覽代碼

feat(layout): 新增部门选择功能

- 在布局头部添加部门选择下拉菜单
- 实现部门数据的获取和展示
- 优化布局样式,调整部门选择的显示逻辑
- 业务模块联调(未完成)
zhangtao 2 周之前
父節點
當前提交
b4ce9ff08c
共有 53 個文件被更改,包括 3285 次插入232 次删除
  1. 2 0
      .env.development
  2. 0 7
      src/api/businessManagement/competition.ts
  3. 20 0
      src/api/common/api.ts
  4. 1 1
      src/api/sys/upload.ts
  5. 8 8
      src/components/Form/src/hooks/useForm.ts
  6. 1 1
      src/components/Form/src/jeecg/components/JAreaLinkage.vue
  7. 3 3
      src/components/Form/src/jeecg/components/JAreaSelect.vue
  8. 8 2
      src/components/Form/src/jeecg/components/JImageUpload.vue
  9. 3 1
      src/components/Table/src/componentMap.ts
  10. 2 0
      src/components/Table/src/components/TableImg.vue
  11. 7 7
      src/components/Table/src/components/editable/EditableCell.vue
  12. 3 1
      src/components/Table/src/components/editable/helper.ts
  13. 1 0
      src/components/Table/src/hooks/useCustomRow.ts
  14. 12 1
      src/components/Table/src/types/componentType.ts
  15. 60 51
      src/components/ZtCustomTable/index.vue
  16. 60 0
      src/components/uploadVideo/index.vue
  17. 1 1
      src/layouts/default/header/index.less
  18. 17 1
      src/layouts/default/header/index.vue
  19. 1 1
      src/store/index.ts
  20. 82 0
      src/store/modules/shopInfo.ts
  21. 17 17
      src/utils/areaData/pcaUtils.ts
  22. 1 1
      src/utils/http/axios/index.ts
  23. 31 17
      src/utils/index.ts
  24. 52 93
      src/views/businessManagement/competition/competition.data.ts
  25. 2 0
      src/views/businessManagement/competition/competitionCommon.vue
  26. 70 0
      src/views/businessManagement/courses/components/coursesForm.vue
  27. 78 0
      src/views/businessManagement/courses/components/coursesModal.vue
  28. 35 0
      src/views/businessManagement/courses/courses.api.ts
  29. 319 0
      src/views/businessManagement/courses/courses.data.ts
  30. 176 0
      src/views/businessManagement/courses/index.vue
  31. 122 0
      src/views/businessManagement/courses/publishcourses.vue
  32. 138 0
      src/views/businessManagement/gymnasiumBag/gymnasiumBag.data.ts
  33. 306 0
      src/views/businessManagement/gymnasiumBag/index.vue
  34. 70 0
      src/views/businessManagement/gymnasiumNoFixed/DataRuleModal.vue
  35. 215 0
      src/views/businessManagement/gymnasiumNoFixed/gymnasiumNoFixed.data.ts
  36. 65 0
      src/views/businessManagement/gymnasiumNoFixed/index.vue
  37. 5 2
      src/views/businessManagement/schoolOpen/index.vue
  38. 21 11
      src/views/businessManagement/schoolOpen/schoolOpen.data.ts
  39. 69 0
      src/views/informationManagement/projectList/components/projectForm.vue
  40. 79 0
      src/views/informationManagement/projectList/components/projectModal.vue
  41. 166 0
      src/views/informationManagement/projectList/index.vue
  42. 52 0
      src/views/informationManagement/projectList/project.api.ts
  43. 70 0
      src/views/informationManagement/projectList/project.data.ts
  44. 73 0
      src/views/informationManagement/shopInfo/index.vue
  45. 198 0
      src/views/informationManagement/shopInfo/shopInfo.data.ts
  46. 25 0
      src/views/informationManagement/teachorNoteach/index.vue
  47. 70 0
      src/views/operationManagement/feedback/components/feedbackForm.vue
  48. 90 0
      src/views/operationManagement/feedback/components/feedbackModal.vue
  49. 24 0
      src/views/operationManagement/feedback/feedback.api.ts
  50. 175 0
      src/views/operationManagement/feedback/feedback.data.ts
  51. 167 0
      src/views/operationManagement/feedback/index.vue
  52. 1 1
      tsconfig.json
  53. 11 4
      types/utils.d.ts

+ 2 - 0
.env.development

@@ -9,10 +9,12 @@ VITE_PUBLIC_PATH = /
 # 跨域代理,您可以配置多个 ,请注意,没有换行符
 # VITE_PROXY = [["/jeecgboot","http://192.168.1.34:8080/jeecg-boot"],["/upload","http://localhost:3300/upload"]]
 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_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  #张
 
 
 #后台接口父地址(必填)

+ 0 - 7
src/api/businessManagement/competition.ts

@@ -1,7 +0,0 @@
-import { defHttp } from '/@/utils/http/axios';
-enum Api {
-  competition = '',
-}
-export const getcompetition = (params) => {
-  return defHttp.get({ url: Api.competition, params });
-};

+ 20 - 0
src/api/common/api.ts

@@ -15,6 +15,9 @@ enum Api {
   getTableList = '/sys/user/queryUserComponentData',
   getCategoryData = '/sys/category/loadAllData',
   refreshDragCache = '/drag/page/refreshCache',
+  sprotProject = '/app/appCategory/list',
+  siteAdd = '/appSite/add',
+  siteList = '/appSite/list',
 }
 
 /**
@@ -154,3 +157,20 @@ export const uploadMyFile = (url, data) => {
  * @param params
  */
 export const refreshDragCache = () => defHttp.get({ url: Api.refreshDragCache }, { isTransformResponse: false });
+
+/**
+ * 通用获取项目列表
+ * @param params
+ * @returns
+ */
+export const getSprotProject = (params) => {
+  return defHttp.get({ url: Api.sprotProject, params });
+};
+
+export function siteAdd(params) {
+  return defHttp.post({ url: Api.siteAdd, params });
+}
+
+export function getSiteList(params) {
+  return defHttp.get({ url: Api.siteList, params });
+}

+ 1 - 1
src/api/sys/upload.ts

@@ -11,7 +11,7 @@ const { uploadUrl = '' } = useGlobSetting();
 export function uploadApi(params: UploadFileParams, onUploadProgress: (progressEvent: ProgressEvent) => void) {
   return defHttp.uploadFile<UploadApiResult>(
     {
-      url: uploadUrl,
+      url: `${uploadUrl}/upload`,
       onUploadProgress,
     },
     params

+ 8 - 8
src/components/Form/src/hooks/useForm.ts

@@ -6,9 +6,9 @@ import { ref, onUnmounted, unref, nextTick, watch } from 'vue';
 import { isProdMode } from '/@/utils/env';
 import { error } from '/@/utils/log';
 import { getDynamicProps, getValueType, getValueTypeBySchema } from '/@/utils';
-import { add } from "/@/components/Form/src/componentMap";
+import { add } from '/@/components/Form/src/componentMap';
 //集成online专用控件
-import { OnlineSelectCascade, LinkTableCard, LinkTableSelect } from  '@jeecg/online';
+import { OnlineSelectCascade, LinkTableCard, LinkTableSelect } from '@jeecg/online';
 
 export declare type ValidateFields = (nameList?: NamePath[], options?: ValidateOptions) => Promise<Recordable>;
 
@@ -19,10 +19,10 @@ export function useForm(props?: Props): UseFormReturnType {
   const loadedRef = ref<Nullable<boolean>>(false);
 
   //集成online专用控件
-  add("OnlineSelectCascade", OnlineSelectCascade)
-  add("LinkTableCard", LinkTableCard)
-  add("LinkTableSelect", LinkTableSelect)
-  
+  add('OnlineSelectCascade', OnlineSelectCascade);
+  add('LinkTableCard', LinkTableCard);
+  add('LinkTableSelect', LinkTableSelect);
+
   async function getForm() {
     const form = unref(formRef);
     if (!form) {
@@ -94,8 +94,8 @@ export function useForm(props?: Props): UseFormReturnType {
     getFieldsValue: <T>() => {
       //update-begin-author:taoyan date:2022-7-5 for: VUEN-1341【流程】编码方式 流程节点编辑表单时,填写数据报错 包括用户组件、部门组件、省市区
       let values = unref(formRef)?.getFieldsValue() as T;
-      if(values){
-        Object.keys(values).map(key=>{
+      if (values) {
+        Object.keys(values).map((key) => {
           if (values[key] instanceof Array) {
             // update-begin-author:sunjianlei date:20221205 for: 【issues/4330】判断如果是对象数组,则不拼接
             let isObject = typeof (values[key][0] || '') === 'object';

+ 1 - 1
src/components/Form/src/jeecg/components/JAreaLinkage.vue

@@ -136,7 +136,7 @@
         // state.value = result;
         //update-end-author:taoyan date:2022-6-27 for: VUEN-1424【vue3】树表、单表、jvxe、erp 、内嵌子表省市县 选择不上
       }
-      
+
       return {
         cascaderValue,
         attrs,

+ 3 - 3
src/components/Form/src/jeecg/components/JAreaSelect.vue

@@ -47,7 +47,7 @@
         default: (node) => node?.parentNode,
       },
     },
-    emits: ['change', 'update:value','update:area','update:city','update:province'],
+    emits: ['change', 'update:value', 'update:area', 'update:city', 'update:province'],
     setup(props, { emit, refs }) {
       const emitData = ref<any[]>([]);
       //下拉框的选择值
@@ -154,10 +154,10 @@
     width: 100%;
 
     /* update-begin-author:taoyan date:2023-2-18 for: QQYUN-4292【online表单】高级查询 2.省市县样式问题 */
-   /* display: flex;*/
+    /* display: flex;*/
 
     .ant-select {
-      width: calc(33.3% - 7px)
+      width: calc(33.3% - 7px);
     }
     /* update-end-author:taoyan date:2023-2-18 for:  QQYUN-4292【online表单】高级查询 2.省市县样式问题 */
 

+ 8 - 2
src/components/Form/src/jeecg/components/JImageUpload.vue

@@ -28,6 +28,7 @@
     <a-modal :width="previewWidth" :open="previewVisible" :footer="null" @cancel="handleCancel()">
       <img alt="example" style="width: 100%" :src="previewImage" />
     </a-modal>
+    <span v-if="tipText" class="text-gray">{{ tipText }} </span>
   </div>
 </template>
 <script lang="ts">
@@ -88,6 +89,11 @@
         required: false,
         default: 520,
       },
+      tipText: {
+        type: String,
+        required: false,
+        default: '',
+      },
     },
     emits: ['options-change', 'change', 'update:value'],
     setup(props, { emit, refs }) {
@@ -134,8 +140,8 @@
       watch(
         () => props.value,
         (val, prevCount) => {
-         //update-begin---author:liusq ---date:20230601  for:【issues/556】JImageUpload组件value赋初始值没显示图片------------
-            if (val && val instanceof Array) {
+          //update-begin---author:liusq ---date:20230601  for:【issues/556】JImageUpload组件value赋初始值没显示图片------------
+          if (val && val instanceof Array) {
             val = val.join(',');
           }
           if (initTag.value == true) {

+ 3 - 1
src/components/Table/src/componentMap.ts

@@ -1,5 +1,5 @@
 import type { Component } from 'vue';
-import { Input, Select, Checkbox, InputNumber, Switch, DatePicker, TimePicker } from 'ant-design-vue';
+import { Input, Select, Checkbox, InputNumber, Switch, DatePicker, TimePicker, TimeRangePicker, RangePicker } from 'ant-design-vue';
 import type { ComponentType } from './types/componentType';
 import { ApiSelect, ApiTreeSelect } from '/@/components/Form';
 
@@ -14,6 +14,8 @@ componentMap.set('Switch', Switch);
 componentMap.set('Checkbox', Checkbox);
 componentMap.set('DatePicker', DatePicker);
 componentMap.set('TimePicker', TimePicker);
+componentMap.set('TimeRangePicker', TimeRangePicker);
+componentMap.set('RangePicker', RangePicker);
 
 export function add(compName: ComponentType, component: Component) {
   componentMap.set(compName, component);

+ 2 - 0
src/components/Table/src/components/TableImg.vue

@@ -9,6 +9,7 @@
               :style="{
                 display: index === 0 ? '' : 'none !important',
               }"
+              :class="[round ? 'rounded-3' : '']"
               :src="srcPrefix + img"
             />
           </template>
@@ -43,6 +44,7 @@
       margin: propTypes.number.def(4),
       // src前缀,将会附加在imgList中每一项之前
       srcPrefix: propTypes.string.def(''),
+      round: propTypes.bool.def(true),
     },
     setup(props) {
       const getWrapStyle = computed((): CSSProperties => {

+ 7 - 7
src/components/Table/src/components/editable/EditableCell.vue

@@ -1,10 +1,10 @@
 <template>
   <div :class="prefixCls">
     <div v-show="!isEdit" :class="{ [`${prefixCls}__normal`]: true, 'ellipsis-cell': column.ellipsis }" @click="handleEdit">
-      <div class="cell-content" :title="column.ellipsis ? getValues ?? '' : ''">
+      <div class="cell-content" :title="column.ellipsis ? (getValues ?? '') : ''">
         <!-- update-begin--author:liaozhiyang---date:20240731---for:【issues/6957】editableCell组件值长度为0,无法编辑 -->
         <!-- update-begin--author:liaozhiyang---date:20240709---for:【issues/6851】editableCell组件值为0时不展示 -->
-        {{ typeof getValues === 'string' && getValues.length === 0 ? '&nbsp;' : getValues ?? '&nbsp;' }}
+        {{ typeof getValues === 'string' && getValues.length === 0 ? '&nbsp;' : (getValues ?? '&nbsp;') }}
         <!-- update-end--author:liaozhiyang---date:20240709---for:【issues/6851】editableCell组件值为0时不展示 -->
         <!-- update-end--author:liaozhiyang---date:20240731---for:【issues/6957】editableCell组件值长度为0,无法编辑 -->
       </div>
@@ -26,10 +26,10 @@
           @options-change="handleOptionsChange"
           @pressEnter="handleEnter"
         />
-        <div :class="`${prefixCls}__action`" v-if="!getRowEditable">
+        <!-- <div :class="`${prefixCls}__action`" v-if="!getRowEditable">
           <CheckOutlined :class="[`${prefixCls}__icon`, 'mx-2']" @click="handleSubmitClick" />
           <CloseOutlined :class="`${prefixCls}__icon `" @click="handleCancel" />
-        </div>
+        </div> -->
       </div>
     </a-spin>
   </div>
@@ -114,8 +114,8 @@
 
         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){
+        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提示,如果表格有滚动条,会不跟着单元格滚动---
@@ -381,7 +381,7 @@
               const [fn] = Object.values(item);
               fn();
             });
-           // update-end--author:liaozhiyang---date:20240424---for:【issues/1165】解决canResize为true时第一行校验不过
+          // update-end--author:liaozhiyang---date:20240424---for:【issues/1165】解决canResize为true时第一行校验不过
         };
         /* eslint-disable */
         props.record.onSubmitEdit = async () => {

+ 3 - 1
src/components/Table/src/components/editable/helper.ts

@@ -20,7 +20,9 @@ export function createPlaceholderMessage(component: ComponentType) {
     component.includes('Radio') ||
     component.includes('Switch') ||
     component.includes('DatePicker') ||
-    component.includes('TimePicker')
+    component.includes('TimePicker') ||
+    component.includes('TimeRangePicker') ||
+    component.includes('RangePicker')
   ) {
     return t('common.chooseText');
   }

+ 1 - 0
src/components/Table/src/hooks/useCustomRow.ts

@@ -85,6 +85,7 @@ export function useCustomRow(
           }
         }
         handleClick();
+
         emit('row-click', record, index, e);
       },
       onDblclick: (event: Event) => {

+ 12 - 1
src/components/Table/src/types/componentType.ts

@@ -1 +1,12 @@
-export type ComponentType = 'Input' | 'InputNumber' | 'Select' | 'ApiSelect' | 'ApiTreeSelect' | 'Checkbox' | 'Switch' | 'DatePicker' | 'TimePicker';
+export type ComponentType =
+  | 'Input'
+  | 'InputNumber'
+  | 'Select'
+  | 'ApiSelect'
+  | 'ApiTreeSelect'
+  | 'Checkbox'
+  | 'Switch'
+  | 'DatePicker'
+  | 'TimePicker'
+  | 'TimeRangePicker'
+  | 'RangePicker';

+ 60 - 51
src/components/ZtCustomTable/index.vue

@@ -1,81 +1,90 @@
 <template>
-  <Form :model="modelValue" class="customForm">
-    <Table :dataSource="modelValue" :columns="tableColumn" :pagination="false">
-      <template #bodyCell="{ column, index }">
-        <template v-if="column.dataIndex == 'operation'">
-          <FormItem>
-            <div class="text-18px cursor-pointer text-#eb5050" @click="handleDelete(index)">
-              <minus-circle-outlined />
-            </div>
-          </FormItem>
-        </template>
-        <template v-else>
-          <FormItem>
-            <component :is="getDom(String(column.key))" v-model:value="modelValue[index][String(column.dataIndex)]" v-bind="column.props"></component>
-          </FormItem>
-        </template>
+  <BasicTable @register="registerTable" @edit-change="onEditChange" :pagination="false" :data-source="modelValue" rowKey="id">
+    <template #headerCell="{ column }">
+      <template v-if="column.dataIndex == 'operation' && showAction">
+        <div class="text-18px cursor-pointer" @click="handleAdd">
+          <PlusCircleOutlined></PlusCircleOutlined>
+        </div>
       </template>
-      <template #headerCell="{ title }">
-        <template v-if="title == 'operation'">
-          <FormItem>
-            <div class="text-18px cursor-pointer" @click="handleAdd">
-              <PlusCircleOutlined></PlusCircleOutlined>
-            </div>
-          </FormItem>
-        </template>
-      </template>
-    </Table>
-  </Form>
+      <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">
+        <minus-circle-outlined />
+      </div>
+    </template>
+  </BasicTable>
 </template>
 <script lang="ts" setup>
-  import { Table, Input, InputNumber, Select, RangePicker, Form, FormItem } from 'ant-design-vue';
+  import { ref, computed, withDefaults } from 'vue';
   import { PlusCircleOutlined, MinusCircleOutlined } from '@ant-design/icons-vue';
-  import { ref, computed } from 'vue';
-  import { ZtTableColumnProps } from '/#/utils';
-  const props = defineProps<{ tableColumn: ZtTableColumnProps[]; value: any[] }>();
+  import { BasicTable, BasicColumn } from '/@/components/Table';
+  import { useListPage } from '/@/hooks/system/useListPage';
+  import _ from 'lodash-es';
+  import dayjs from 'dayjs';
+  interface Props {
+    tableColumn: BasicColumn[];
+    value: any;
+    showAction?: boolean;
+    showIndex?: boolean;
+  }
+  const props = withDefaults(defineProps<Props>(), {
+    tableColumn: () => [],
+    value: () => [],
+    showAction: () => true,
+    showIndex: () => false,
+  });
   const emit = defineEmits(['update:value']);
   const modelValue = computed({
     get() {
-      console.log(props.value, 'asdas');
+      console.log(props.value, 'props.value');
 
-      return props.value || [];
+      return props.value;
     },
     set(val) {
+      console.log(val, '重新设置');
+
       emit('update:value', val);
     },
   });
-  const componentMap = {
-    Input,
-    InputNumber,
-    Select,
-    RangePicker,
-  };
+  const { tableContext } = useListPage({
+    designScope: 'basic-table-demo',
+    tableProps: {
+      title: '',
+      columns: props.tableColumn,
+      showIndexColumn: props.showIndex,
+      showTableSetting: false,
+      showActionColumn: false,
+    },
+  });
+
+  //BasicTable绑定注册
+  const [registerTable] = tableContext;
+
+  function onEditChange({ column, value, record }) {
+    if (column.dataIndex == 'time') {
+      record.editValueRefs.time.value = value;
+      return;
+    }
+    record.editValueRefs[column.dataIndex].value = `${value}`;
+  }
   type DataRow = Record<string, any> & {
     [key: string]: any;
   };
-  const addEmptyRow = (columns: ZtTableColumnProps[]): DataRow => {
+  const addEmptyRow = (columns: BasicColumn[]): DataRow => {
     const inputFields = columns.filter((col) => col.dataIndex && col.dataIndex != 'operation').map((col) => col.dataIndex);
     const newRow: DataRow = {};
     inputFields.forEach((field) => {
       newRow[String(field)] = null;
+      newRow['id'] = dayjs().valueOf();
     });
     return newRow;
   };
   function handleAdd() {
     modelValue.value.push(addEmptyRow(props.tableColumn));
+    modelValue.value = [].concat(modelValue.value);
   }
-  function handleDelete(index) {
+  function handleRemove(index) {
     modelValue.value.splice(index, 1);
   }
-  function getDom(type: string) {
-    return componentMap[type];
-  }
 </script>
-
-<style lang="less">
-  .customForm {
-    .ant-form-item {
-      margin-bottom: 0 !important;
-    }
-  }
-</style>

+ 60 - 0
src/components/uploadVideo/index.vue

@@ -0,0 +1,60 @@
+<template>
+  <div class="video-upload">
+    <Upload v-if="!videoUrl" :beforeUpload="beforeUpload" :fileList="[]" :showUploadList="false" accept="video/*">
+      <Button type="primary">上传视频</Button>
+    </Upload>
+
+    <div v-else class="flex items-center">
+      <video :src="videoUrl" controls class="w-200px h-200px mb-3"></video>
+      <div class="actions">
+        <Button type="link" @click="removeVideo">删除</Button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ref } from 'vue';
+  import { Upload, Button, message } from 'ant-design-vue';
+  import { uploadApi } from '@/api/sys/upload';
+  const props = defineProps<{
+    modelValue: File | null;
+  }>();
+
+  const emit = defineEmits<{
+    (e: 'update:modelValue', file: File | null): void;
+  }>();
+
+  const videoUrl = ref<string | null>(null);
+
+  const beforeUpload = async (file: File) => {
+    // 验证文件类型
+    const isValidType = file.type.startsWith('video/');
+    if (!isValidType) {
+      message.error('只能上传视频文件');
+      return false;
+    }
+
+    // 限制大小为100MB
+    const isValidSize = file.size <= 100 * 1024 * 1024;
+    if (!isValidSize) {
+      message.error('视频大小不能超过100MB');
+      return false;
+    }
+    const res = await uploadApi(file);
+    console.log(res, '上传文件');
+
+    // 创建本地预览链接
+    videoUrl.value = URL.createObjectURL(file);
+    emit('update:modelValue', file);
+
+    return false; // 禁止自动上传
+  };
+
+  const removeVideo = () => {
+    videoUrl.value = null;
+    emit('update:modelValue', null);
+  };
+</script>
+
+<style scoped></style>

+ 1 - 1
src/layouts/default/header/index.less

@@ -105,7 +105,7 @@
 
   &-action {
     display: flex;
-    min-width: 180px;
+    // min-width: 180px;
     // padding-right: 12px;
     align-items: center;
 

+ 17 - 1
src/layouts/default/header/index.vue

@@ -28,6 +28,13 @@
 
     <!-- action  -->
     <div :class="`${prefixCls}-action`">
+      <Row>
+        <Col :span="24">
+          <div class="mr-3" v-if="isShowSelect">
+            <Select :options="deptList" :fieldNames="{ label: 'departName', value: 'id' }" v-model:value="currentId"></Select> </div
+        ></Col>
+      </Row>
+
       <AppSearch :class="`${prefixCls}-action__item `" v-if="getShowSearch" />
 
       <ErrorAction v-if="getUseErrorHandle" :class="`${prefixCls}-action__item error-action`" />
@@ -50,7 +57,7 @@
   import { useGlobSetting } from '/@/hooks/setting';
   import { propTypes } from '/@/utils/propTypes';
 
-  import { Layout } from 'ant-design-vue';
+  import { Layout, Select, Row, Col } from 'ant-design-vue';
   import { AppLogo } from '/@/components/Application';
   import LayoutMenu from '../menu/index.vue';
   import LayoutTrigger from '../trigger/index.vue';
@@ -75,6 +82,8 @@
   import { useUserStore } from '/@/store/modules/user';
   import { useI18n } from '/@/hooks/web/useI18n';
   const { t } = useI18n();
+  import { useShopInfoStore } from '/@/store/modules/shopInfo';
+  import { storeToRefs } from 'pinia';
 
   export default defineComponent({
     name: 'LayoutHeader',
@@ -91,6 +100,9 @@
       ErrorAction,
       LockScreen,
       LoginSelect,
+      Select,
+      Row,
+      Col,
       SettingDrawer: createAsyncComponent(() => import('/@/layouts/default/setting/index.vue'), {
         loading: true,
       }),
@@ -104,6 +116,7 @@
       const { getShowTopMenu, getShowHeaderTrigger, getSplit, getIsMixMode, getMenuWidth, getIsMixSidebar } = useMenuSetting();
       const { getUseErrorHandle, getShowSettingButton, getSettingButtonPosition, getAiIconShow } = useRootSetting();
       const { title } = useGlobSetting();
+      const { isShowSelect, deptList, currentId } = storeToRefs(useShopInfoStore());
 
       const {
         getHeaderTheme,
@@ -214,6 +227,9 @@
         title,
         t,
         getAiIconShow,
+        isShowSelect,
+        deptList,
+        currentId,
       };
     },
   });

+ 1 - 1
src/store/index.ts

@@ -21,4 +21,4 @@ export function destroyStore() {
 // 获取app实例
 export const getAppContext = () => app?._context;
 
-export {app, store};
+export { app, store };

+ 82 - 0
src/store/modules/shopInfo.ts

@@ -0,0 +1,82 @@
+import { defineStore } from 'pinia';
+import { getUserTenants } from '/@/views/system/tenant/tenant.api';
+import { getUserDeparts } from '/@/views/system/depart/depart.api';
+type dep = {
+  address: string;
+  createBy: string;
+  createTime?: string;
+  delFlag: string;
+  departName: string;
+  departNameAbbr: string;
+  departNameEn: string;
+  departOrder: number;
+  description: string;
+  dingIdentifier: string;
+  directorUserIds: string;
+  fax: string;
+  id: string;
+  izLeaf: number;
+  memo: string;
+  mobile: string;
+  oldDirectorUserIds: string;
+  orgCategory: string;
+  orgCode: string;
+  orgType: string;
+  parentId: string;
+  qywxIdentifier: string;
+  status: string;
+  tenantId: number;
+  updateBy?: string;
+  updateTime?: string;
+};
+interface useShopInfoState {
+  currentDep: dep;
+  isShowSelect: boolean;
+  deptList: dep[];
+  currentId: string;
+}
+export const useShopInfoStore = defineStore({
+  id: 'app-ShopInfo',
+  state: (): useShopInfoState => ({
+    currentDep: {
+      address: '',
+      createBy: '',
+      delFlag: '',
+      departName: '',
+      departNameAbbr: '',
+      departNameEn: '',
+      departOrder: 0,
+      description: '',
+      dingIdentifier: '',
+      directorUserIds: '',
+      fax: '',
+      id: '',
+      izLeaf: 0,
+      memo: '',
+      mobile: '',
+      oldDirectorUserIds: '',
+      orgCategory: '',
+      orgCode: '',
+      orgType: '',
+      parentId: '',
+      qywxIdentifier: '',
+      status: '',
+      tenantId: 0,
+    },
+    isShowSelect: false,
+    deptList: [],
+    currentId: '',
+  }),
+  getters: {},
+  actions: {
+    async getCurrentDep() {
+      const result = await getUserDeparts();
+      console.log(result, 'asdas');
+
+      this.currentDep = result.list[0];
+      this.deptList = result.list;
+      this.currentId = this.currentDep.id;
+      this.isShowSelect = true;
+    },
+  },
+});

+ 17 - 17
src/utils/areaData/pcaUtils.ts

@@ -1,38 +1,38 @@
-import {areaList} from '@vant/area-data'
-import {freezeDeep} from "@/utils/common/compUtils";
+import { areaList } from '@vant/area-data';
+import { freezeDeep } from '@/utils/common/compUtils';
 
 // 扁平化的省市区数据
-export const pcaa = freezeDeep(usePlatPcaaData())
+export const pcaa = freezeDeep(usePlatPcaaData());
 
 /**
  * 获取扁平化的省市区数据
  */
 function usePlatPcaaData() {
-  const {city_list: city, county_list: county, province_list: province} = areaList;
-  const dataMap = new Map<string, Recordable>()
-  const flatData: Recordable = {'86': province}
+  const { city_list: city, county_list: county, province_list: province } = areaList;
+  const dataMap = new Map<string, Recordable>();
+  const flatData: Recordable = { '86': province };
   // 省
   Object.keys(province).forEach((code) => {
-    flatData[code] = {}
-    dataMap.set(code.slice(0, 2), flatData[code])
-  })
+    flatData[code] = {};
+    dataMap.set(code.slice(0, 2), flatData[code]);
+  });
   // 市区
   Object.keys(city).forEach((code) => {
-    flatData[code] = {}
-    dataMap.set(code.slice(0, 4), flatData[code])
+    flatData[code] = {};
+    dataMap.set(code.slice(0, 4), flatData[code]);
     // 填充上一级
-    const getProvince = dataMap.get(code.slice(0, 2))
+    const getProvince = dataMap.get(code.slice(0, 2));
     if (getProvince) {
-      getProvince[code] = city[code]
+      getProvince[code] = city[code];
     }
   });
   // 县
   Object.keys(county).forEach((code) => {
     // 填充上一级
-    const getCity = dataMap.get(code.slice(0, 4))
+    const getCity = dataMap.get(code.slice(0, 4));
     if (getCity) {
-      getCity[code] = county[code]
+      getCity[code] = county[code];
     }
   });
-  return flatData
-}
+  return flatData;
+}

+ 1 - 1
src/utils/http/axios/index.ts

@@ -275,7 +275,7 @@ function createAxios(opt?: Partial<CreateAxiosOptions>) {
         // authenticationScheme: 'Bearer',
         authenticationScheme: '',
         //接口超时设置
-        timeout: 10 * 1000,
+        timeout: 10 * 10000,
         // 基础接口地址
         // baseURL: globSetting.apiUrl,
         headers: { 'Content-Type': ContentTypeEnum.JSON },

+ 31 - 17
src/utils/index.ts

@@ -1,6 +1,6 @@
 import type { RouteLocationNormalized, RouteRecordNormalized } from 'vue-router';
 import type { App, Plugin } from 'vue';
-import type { FormSchema } from "@/components/Form";
+import type { FormSchema } from '@/components/Form';
 
 import { unref } from 'vue';
 import { isObject, isFunction, isString } from '/@/utils/is';
@@ -224,7 +224,7 @@ export function getQueryVariable(url) {
     r,
     i = url.split('?')[1],
     s = {};
-  (t = i.split('&')), (r = null), (n = null);
+  ((t = i.split('&')), (r = null), (n = null));
   for (var o in t) {
     var u = t[o].indexOf('=');
     u !== -1 && ((r = t[o].substr(0, u)), (n = t[o].substr(u + 1)), (s[r] = n));
@@ -337,7 +337,6 @@ export function importViewsFile(path): Promise<any> {
 }
 //update-end-author:taoyan date:2022-6-8 for:解决老的vue2动态导入文件语法 vite不支持的问题
 
-
 /**
  * 跳转至积木报表的 预览页面
  * @param url
@@ -347,23 +346,22 @@ export function importViewsFile(path): Promise<any> {
 export function goJmReportViewPage(url, id, token) {
   // update-begin--author:liaozhiyang---date:20230904---for:【QQYUN-6390】eval替换成new Function,解决build警告
   // URL支持{{ window.xxx }}占位符变量
-  url = url.replace(/{{([^}]+)?}}/g, (_s1, s2) => _eval(s2))
+  url = url.replace(/{{([^}]+)?}}/g, (_s1, s2) => _eval(s2));
   // update-end--author:liaozhiyang---date:20230904---for:【QQYUN-6390】eval替换成new Function,解决build警告
   if (url.includes('?')) {
-    url += '&'
+    url += '&';
   } else {
-    url += '?'
+    url += '?';
   }
-  url += `id=${id}`
-  url += `&token=${token}`
-  window.open(url)
+  url += `id=${id}`;
+  url += `&token=${token}`;
+  window.open(url);
 }
 
 /**
  * 获取随机颜色
  */
 export function getRandomColor(index?) {
-
   const colors = [
     'rgb(100, 181, 246)',
     'rgb(77, 182, 172)',
@@ -386,7 +384,7 @@ export function getRandomColor(index?) {
     'rgb(254, 161, 172)',
     'rgb(194, 163, 205)',
   ];
-  return index && index < 19 ? colors[index] : colors[Math.floor((Math.random()*(colors.length-1)))];
+  return index && index < 19 ? colors[index] : colors[Math.floor(Math.random() * (colors.length - 1))];
 }
 
 export function getRefPromise(componentRef) {
@@ -410,7 +408,7 @@ export function getRefPromise(componentRef) {
  * 用new Function替换eval
  */
 export function _eval(str: string) {
- return new Function(`return ${str}`)();
+  return new Function(`return ${str}`)();
 }
 
 /**
@@ -455,7 +453,7 @@ export const setPopContainer = (node, selector) => {
       const retrospect = (node, elems) => {
         let ele = node.parentNode;
         while (ele) {
-          const findParentNode = elems.find(item => item === ele);
+          const findParentNode = elems.find((item) => item === ele);
           if (findParentNode) {
             ele = null;
             return findParentNode;
@@ -486,12 +484,11 @@ export const setPopContainer = (node, selector) => {
  * label、value通用,title、val给权限管理用的
  */
 export function useConditionFilter() {
-
   // 通用条件
   const commonConditionOptions = [
-    {label: '为空', value: 'empty', val: 'EMPTY'},
-    {label: '不为空', value: 'not_empty', val: 'NOT_EMPTY'},
-  ]
+    { label: '为空', value: 'empty', val: 'EMPTY' },
+    { label: '不为空', value: 'not_empty', val: 'NOT_EMPTY' },
+  ];
 
   // 数值、日期
   const numberConditionOptions = [
@@ -632,3 +629,20 @@ export const split = (str) => {
   }
   return str;
 };
+/**
+ * 判断一个对象的所有字段是否有值(非 null 和 undefined)
+ * @param obj 要检查的对象
+ * @returns 所有字段有值返回 true,否则 false
+ */
+function areAllFieldsFilled(obj: Record<string, any>): boolean {
+  return Object.values(obj).every((value) => value !== null && value !== undefined);
+}
+
+/**
+ * 判断数组中每个对象是否所有字段都有值
+ * @param arr 要检查的对象数组
+ * @returns 所有对象字段都有值返回 true,否则 false
+ */
+export function areAllItemsAllFieldsFilled(arr: Array<Record<string, any>>): boolean {
+  return arr.every((item) => areAllFieldsFilled(item));
+}

+ 52 - 93
src/views/businessManagement/competition/competition.data.ts

@@ -1,5 +1,5 @@
-import { ZtTableColumnProps } from '/#/utils';
 import { FormSchema } from '/@/components/Table';
+import { BasicColumn } from '/@/components/Table';
 
 /**
  * 列表columns
@@ -55,7 +55,7 @@ export const searchFormSchema: FormSchema[] = [
 export const formSchema: FormSchema[] = [
   {
     field: 'title1',
-    slot: 'title1',
+    colSlot: 'title1',
     label: '',
     component: 'Input',
     labelWidth: 0,
@@ -65,55 +65,40 @@ export const formSchema: FormSchema[] = [
     label: '赛事名称',
     component: 'Input',
     required: true,
-    labelWidth: 120,
-    colProps: {
-      span: 14,
-      xs: 24,
-    },
   },
   {
     field: 'file',
     label: '封面',
     component: 'JImageUpload',
     required: true,
-    labelWidth: 120,
-    colProps: {
-      span: 18,
-      xs: 24,
+    componentProps: {
+      tipText: '单张图片,比例 9:16,支持格式:.jpg .png .svg .gif ,单个文件不能超过5MB ',
     },
   },
   {
     field: 'fileimg',
     label: '背景图',
     component: 'JImageUpload',
-    required: true,
-    labelWidth: 120,
-    colProps: {
-      span: 18,
-      xs: 24,
+    componentProps: {
+      tipText: '单张图片,比例 5:4,支持格式:.jpg .png .svg .gif ,单个文件不能超过5MB。',
     },
+    required: true,
   },
   {
     field: 'name',
     label: '主办单位',
     component: 'Input',
     required: true,
-    labelWidth: 120,
-    colProps: {
-      span: 14,
-      xs: 24,
-    },
+    labelWidth: 210,
+    colProps: { lg: 14 },
   },
   {
     field: 'time',
     label: '报名结束时间',
     component: 'DatePicker',
     required: true,
-    labelWidth: 120,
-    colProps: {
-      span: 14,
-      xs: 24,
-    },
+    labelWidth: 210,
+    colProps: { lg: 14 },
     componentProps: {
       format: 'YYYY-MM-DD hh:mm:ss',
       showTime: true,
@@ -125,21 +110,14 @@ export const formSchema: FormSchema[] = [
     label: '',
     component: 'RangePicker',
     labelWidth: 0,
-    colProps: {
-      span: 28,
-      xs: 28,
-    },
   },
   {
     field: 'RangePicker',
     label: '比赛时间',
     component: 'RangePicker',
     required: true,
-    labelWidth: 120,
-    colProps: {
-      span: 14,
-      xs: 24,
-    },
+    labelWidth: 210,
+    colProps: { lg: 14 },
   },
   {
     field: 'ScheduleData1',
@@ -148,22 +126,13 @@ export const formSchema: FormSchema[] = [
     slot: 'ZtCustomTable1',
     defaultValue: [],
     required: true,
-    labelWidth: 120,
-    colProps: {
-      span: 18,
-      xs: 24,
-    },
   },
   {
     field: 'title3',
-    slot: 'title3',
+    colSlot: 'title3',
     label: '',
     component: 'RangePicker',
     labelWidth: 0,
-    colProps: {
-      span: 28,
-      xs: 28,
-    },
   },
   {
     field: 'ScheduleData2',
@@ -172,17 +141,11 @@ export const formSchema: FormSchema[] = [
     slot: 'ZtCustomTable2',
     defaultValue: [],
     required: true,
-    labelWidth: 120,
-    colProps: {
-      span: 14,
-      xs: 24,
-    },
   },
   {
     field: 'address',
     label: '比赛地点',
     component: 'RadioGroup',
-    labelWidth: 120,
     required: true,
     componentProps: {
       options: [
@@ -190,16 +153,11 @@ export const formSchema: FormSchema[] = [
         { label: '其他场地', value: 0 },
       ],
     },
-    colProps: {
-      span: 24,
-      xs: 24,
-    },
   },
   {
     field: 'address',
     label: '平台场地',
     component: 'Select',
-    labelWidth: 120,
     required: true,
     componentProps: {
       options: [
@@ -207,38 +165,24 @@ export const formSchema: FormSchema[] = [
         { label: '其他场地', value: 0 },
       ],
     },
-    colProps: {
-      span: 16,
-      xs: 24,
-    },
   },
   {
     field: 'address',
     label: '图文说明',
     component: 'JEditor',
-    labelWidth: 120,
     required: true,
-    colProps: {
-      span: 16,
-      xs: 24,
-    },
   },
   {
     field: 'title4',
-    slot: 'title4',
+    colSlot: 'title4',
     label: '',
     component: 'RangePicker',
     labelWidth: 0,
-    colProps: {
-      span: 28,
-      xs: 28,
-    },
   },
   {
     field: 'baoxain',
     label: '配套保险',
     component: 'RadioGroup',
-    labelWidth: 120,
     required: true,
     componentProps: {
       options: [
@@ -246,40 +190,47 @@ export const formSchema: FormSchema[] = [
         { label: '其他场地', value: 0 },
       ],
     },
-    colProps: {
-      span: 24,
-      xs: 24,
-    },
   },
 ];
 
-export const ScheduleArrangementColums: ZtTableColumnProps[] = [
+export const ScheduleArrangementColums: BasicColumn[] = [
   {
     title: '比赛项目',
-    key: 'Select',
     dataIndex: 'project',
     width: 220,
-    props: {
-      placeholder: '请选择比赛项目',
+    editComponent: 'Select',
+    editRule: true,
+    editRow: true,
+    editable: true,
+    editComponentProps: {
+      size: 'middle',
     },
   },
   {
     title: '时间段',
-    key: 'RangePicker',
     dataIndex: 'time',
     width: 450,
-    props: {
-      format: 'YYYY-MM-DD hh:mm:ss',
+    editComponent: 'RangePicker',
+    editRule: true,
+    editComponentProps: {
       showTime: true,
+      size: 'middle',
+      placeholder: ['开始时间', '结束时间'],
+      valueFormat: 'YYYY-MM-DD HH:mm:ss',
     },
+    editRow: true,
+    editable: true,
   },
   {
     title: '安排',
-    key: 'Input',
     dataIndex: 'Schedule',
     width: 220,
-    props: {
-      placeholder: '请输入',
+    editComponent: 'Input',
+    editRule: true,
+    editRow: true,
+    editable: true,
+    editComponentProps: {
+      size: 'middle',
     },
   },
   {
@@ -287,25 +238,32 @@ export const ScheduleArrangementColums: ZtTableColumnProps[] = [
     dataIndex: 'operation',
     title: 'operation',
     fixed: 'right',
+    width: 80,
   },
 ];
-export const SchedulePricesColums: ZtTableColumnProps[] = [
+export const SchedulePricesColums: BasicColumn[] = [
   {
     title: '比赛项目',
-    key: 'Select',
     dataIndex: 'project',
-    width: 220,
-    props: {
-      placeholder: '请选择比赛项目',
+    editComponent: 'Select',
+    editRule: true,
+    editRow: true,
+    editable: true,
+    editComponentProps: {
+      size: 'middle',
     },
   },
   {
     title: '价格',
-    key: 'InputNumber',
     dataIndex: 'price',
-    width: 250,
-    props: {
+    editComponent: 'InputNumber',
+    editRule: true,
+    editRow: true,
+    editable: true,
+    editComponentProps: {
+      size: 'middle',
       step: 0.01,
+      precision: 2,
     },
   },
   {
@@ -313,5 +271,6 @@ export const SchedulePricesColums: ZtTableColumnProps[] = [
     dataIndex: 'operation',
     title: 'operation',
     fixed: 'right',
+    width: 80,
   },
 ];

+ 2 - 0
src/views/businessManagement/competition/competitionCommon.vue

@@ -43,6 +43,8 @@
   const [registerForm, { setProps, resetFields, setFieldsValue, updateSchema, validate, clearValidate }] = useForm({
     schemas: formSchema,
     showActionButtonGroup: false,
+    labelCol: { span: 4 },
+    wrapperCol: { span: 18 },
   });
   function back() {}
   function save() {}

+ 70 - 0
src/views/businessManagement/courses/components/coursesForm.vue

@@ -0,0 +1,70 @@
+<template>
+  <div style="min-height: 400px">
+    <BasicForm @register="registerForm"></BasicForm>
+    <div style="width: 100%; text-align: center" v-if="!formDisabled">
+      <a-button @click="submitForm" pre-icon="ant-design:check" type="primary">提 交</a-button>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+  import { BasicForm, useForm } from '/@/components/Form/index';
+  import { computed, defineComponent } from 'vue';
+  import { defHttp } from '/@/utils/http/axios';
+  import { propTypes } from '/@/utils/propTypes';
+  import { getBpmFormSchema } from '../courses.data';
+  import { saveOrUpdate } from '../courses.api';
+
+  export default defineComponent({
+    name: 'coursesForm',
+    components: {
+      BasicForm,
+    },
+    props: {
+      formData: propTypes.object.def({}),
+      formBpm: propTypes.bool.def(true),
+    },
+    setup(props) {
+      const [registerForm, { setFieldsValue, setProps, getFieldsValue }] = useForm({
+        labelWidth: 150,
+        schemas: getBpmFormSchema(props.formData),
+        showActionButtonGroup: false,
+        baseColProps: { span: 24 },
+      });
+
+      const formDisabled = computed(() => {
+        if (props.formData.disabled === false) {
+          return false;
+        }
+        return true;
+      });
+
+      let formData = {};
+      const queryByIdUrl = '/courses/courses/queryById';
+      async function initFormData() {
+        let params = { id: props.formData.dataId };
+        const data = await defHttp.get({ url: queryByIdUrl, params });
+        formData = { ...data };
+        //设置表单的值
+        await setFieldsValue(formData);
+        //默认是禁用
+        await setProps({ disabled: formDisabled.value });
+      }
+
+      async function submitForm() {
+        let data = getFieldsValue();
+        let params = Object.assign({}, formData, data);
+        console.log('表单数据', params);
+        await saveOrUpdate(params, true);
+      }
+
+      initFormData();
+
+      return {
+        registerForm,
+        formDisabled,
+        submitForm,
+      };
+    },
+  });
+</script>

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

@@ -0,0 +1,78 @@
+<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>

+ 35 - 0
src/views/businessManagement/courses/courses.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/appCourese/list',
+  save = '/app/appCourese/add',
+  edit = '/app/appCourese/edit',
+  deleteOne = '/app/appCourese/delete',
+  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 });
+};

+ 319 - 0
src/views/businessManagement/courses/courses.data.ts

@@ -0,0 +1,319 @@
+import { BasicColumn } from '/@/components/Table';
+import { FormSchema } from '/@/components/Table';
+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 { ZtTableColumnProps } from '/#/utils';
+import { ref } from 'vue';
+
+//列表数据
+export const columns: BasicColumn[] = [
+  {
+    title: '课程名称',
+    align: 'center',
+    dataIndex: 'name',
+  },
+  {
+    title: '课程封面',
+    align: 'center',
+    dataIndex: 'cover',
+    slots: {
+      customRender: 'img',
+    },
+  },
+  {
+    title: '课时节数及时间',
+    align: 'center',
+    dataIndex: 'tenantId',
+  },
+  {
+    title: '上课地点',
+    align: 'center',
+    dataIndex: 'siteId',
+  },
+  {
+    title: '销售价/原价(元)',
+    align: 'center',
+    dataIndex: 'categoryId',
+    slots: { customRender: 'price' },
+  },
+  {
+    title: '上下架状态',
+    align: 'center',
+    dataIndex: 'priceType',
+    slots: { customRender: 'priceType' },
+  },
+  {
+    title: '课程类型',
+    align: 'center',
+    dataIndex: 'siteId',
+  },
+];
+//查询数据
+export const searchFormSchema: FormSchema[] = [
+  {
+    field: 'name',
+    label: '课程名称',
+    component: 'Input',
+    colProps: { span: 6 },
+  },
+  {
+    field: '上下架状态',
+    label: '上下架状态',
+    component: 'Select',
+    colProps: { span: 6 },
+    componentProps: {
+      options: [
+        { label: '上架', value: 0 },
+        { label: '下架', value: 1 },
+      ],
+    },
+  },
+  {
+    field: 'coursesStatus',
+    label: '课程进展',
+    component: 'Select',
+    colProps: { span: 6 },
+    componentProps: {
+      options: [
+        { label: '全部', value: '' },
+        // 全部、未开始、正常上课中、休息中、补课中、已结束
+        { label: '未开始', value: 0 },
+        { label: '正常上课中', value: 1 },
+        { label: '休息中', value: 2 },
+        { label: '补课中', value: 3 },
+        { label: '已结束', value: 4 },
+      ],
+    },
+  },
+  {
+    field: 'categoryId',
+    label: '课程类型',
+    component: 'ApiSelect',
+    colProps: { span: 6 },
+    componentProps: {
+      api: () => defHttp.get({ url: '/app/appCategory/list' }),
+      labelField: 'name',
+      valueField: 'id',
+      resultField: 'records',
+    },
+  },
+];
+//表单数据
+export const formSchema: FormSchema[] = [
+  {
+    label: 'title1',
+    field: 'orgCode',
+    colSlot: 'title1',
+    component: 'Input',
+  },
+  {
+    label: '课程类型',
+    field: 'categoryId',
+    component: 'CheckboxGroup',
+    required: true,
+
+    componentProps: {
+      options: sportList.records.map((it) => {
+        return {
+          label: it.name,
+          value: it.id,
+        };
+      }),
+    },
+  },
+  {
+    label: '课程名称',
+    required: true,
+    field: 'name',
+    component: 'Input',
+  },
+  {
+    label: '课程封面',
+    field: 'cover',
+    component: 'JImageUpload',
+    componentProps: {
+      tipText: '支持单张上传;宽高比为5:4,且宽高均大于480px,大小5M以内。',
+    },
+    required: true,
+  },
+  {
+    label: '上课地点',
+    field: 'siteId',
+    component: 'Select',
+    required: true,
+    defaultValue: '福利中心',
+  },
+  {
+    label: '教练',
+    field: 'userId',
+    component: 'Select',
+    required: true,
+    defaultValue: 1,
+  },
+  {
+    label: 'title2',
+    field: 'title2',
+    component: 'Input',
+    colSlot: 'title2',
+  },
+  {
+    label: '原价',
+    field: 'originalPrice',
+    component: 'InputNumber',
+  },
+  {
+    label: '销售价',
+    field: 'sellingPrice',
+    component: 'InputNumber',
+    required: true,
+  },
+  {
+    label: '限购',
+    field: 'limitNum',
+    required: true,
+    component: 'InputNumber',
+    componentProps: {
+      addonBefore: '每人限制购买',
+      addonAfter: '张',
+    },
+  },
+  {
+    label: '正常课表',
+    field: 'priceRulesList',
+    component: 'InputNumber',
+    slot: 'ZtCustomTable1',
+
+    required: true,
+  },
+  {
+    label: 'title3',
+    field: 'sellingPrice',
+    component: 'InputNumber',
+    colSlot: 'title3',
+  },
+  {
+    label: '视频',
+    field: 'video',
+    component: 'JUpload',
+    slot: 'VideoUpload',
+    componentProps: {
+      fileType: 'video',
+      bizPath: 'video',
+    },
+  },
+  {
+    label: '图片',
+    field: 'backgroundImage',
+    component: 'JImageUpload',
+    componentProps: {
+      tipText: '支持单张上传;宽高比为5:4,且宽高均大于480px,大小5M以内。',
+    },
+    required: true,
+  },
+  {
+    label: '使用须知',
+    field: 'reminder',
+    component: 'InputTextArea',
+    required: true,
+  },
+  {
+    label: '配套保险',
+    field: 'insureIds',
+    component: 'RadioGroup',
+    required: true,
+    componentProps: {
+      options: [
+        {
+          label: '意外伤害险',
+          value: 0,
+        },
+        {
+          label: '大额意外伤害险',
+          value: 1,
+        },
+      ],
+    },
+  },
+  {
+    label: '图文详情',
+    field: 'details',
+    component: 'JEditor',
+    required: true,
+  },
+
+  {
+    label: '上架状态',
+    field: 'rackingStatus',
+    component: 'Switch',
+    required: true,
+    componentProps: {
+      //非选中时的内容
+      unCheckedChildren: '下架',
+      //非选中时的值
+      unCheckedValue: 1,
+      //选中时的内容
+      checkedChildren: '上架',
+      //选中时的值
+      checkedValue: 0,
+    },
+    defaultValue: 0,
+  },
+  // TODO 主键隐藏字段,目前写死为ID
+  {
+    label: '',
+    field: 'id',
+    component: 'Input',
+    show: false,
+  },
+];
+
+/**
+ * 流程表单调用这个方法获取formSchema
+ * @param param
+ */
+export function getBpmFormSchema(_formData): FormSchema[] {
+  // 默认和原始表单保持一致 如果流程中配置了权限数据,这里需要单独处理formSchema
+  return formSchema;
+}
+
+export const publishcoursesColums: BasicColumn[] = [
+  {
+    title: '时间段',
+    dataIndex: 'time',
+    editComponent: 'RangePicker',
+    editRule: true,
+    editComponentProps: {
+      showTime: true,
+      size: 'middle',
+      placeholder: ['开始时间', '结束时间'],
+      valueFormat: 'YYYY-MM-DD HH:mm:ss',
+    },
+    width: 450,
+    editRow: true,
+    editable: true,
+  },
+  {
+    title: '课时名称',
+    dataIndex: 'name',
+    editComponent: 'Input',
+    editRule: true,
+    editRow: true,
+    editable: true,
+    width: 500,
+    editComponentProps: {
+      size: 'middle',
+    },
+  },
+  {
+    key: 'op',
+    dataIndex: 'operation',
+    title: 'operation',
+    fixed: 'right',
+    ellipsis: true,
+    width: 80,
+  },
+];

+ 176 - 0
src/views/businessManagement/courses/index.vue

@@ -0,0 +1,176 @@
+<template>
+  <div>
+    <!--引用表格-->
+    <BasicTable @register="registerTable" :rowSelection="rowSelection">
+      <!--插槽:table标题-->
+      <template #tableTitle>
+        <a-button
+          type="primary"
+          @click="router.push({ name: 'businessManagement-publishcourses', query: { type: 0 } })"
+          preIcon="ant-design:plus-outlined"
+        >
+          新增</a-button
+        >
+        <a-dropdown v-if="selectedRowKeys.length > 0">
+          <a-button v-auth="'courses:nm_courses:deleteBatch'"
+            >批量操作
+            <Icon icon="mdi:chevron-down"></Icon>
+          </a-button>
+        </a-dropdown>
+      </template>
+      <!--操作栏-->
+      <template #action="{ record }">
+        <TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)" />
+      </template>
+      <!--字段回显插槽-->
+      <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>
+      </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 { 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';
+  import { useMessage } from '/@/hooks/web/useMessage';
+  const queryParam = reactive<any>({});
+  const checkedKeys = ref<Array<string | number>>([]);
+  const userStore = useUserStore();
+  const { createMessage } = useMessage();
+  //注册model
+  const [registerModal, { openModal }] = useModal();
+  //注册table数据
+  const { prefixCls, tableContext } = useListPage({
+    tableProps: {
+      title: 'courses',
+      api: list,
+      columns,
+      canResize: false,
+      formConfig: {
+        //labelWidth: 120,
+        schemas: searchFormSchema,
+        autoSubmitOnEnter: true,
+        showAdvancedButton: true,
+        fieldMapToNumber: [],
+        fieldMapToTime: [],
+        autoAdvancedCol: 4,
+      },
+
+      actionColumn: {
+        width: 180,
+        fixed: 'right',
+      },
+      beforeFetch: (params) => {
+        return Object.assign(params, queryParam);
+      },
+    },
+  });
+
+  const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
+  /**
+   * 编辑事件
+   */
+  function handleView(record: Recordable) {
+    router.push({ name: 'businessManagement-publishcourses', query: { type: 1, id: record.id } });
+  }
+  /**
+   * 详情
+   */
+  function handleDetail(record: Recordable) {
+    openModal(true, {
+      record,
+      isUpdate: true,
+      showFooter: false,
+    });
+  }
+  /**
+   * 删除事件
+   */
+  async function handleDelete(record) {
+    await deleteOne({ id: record.id }, handleSuccess);
+  }
+
+  /**
+   * 成功回调
+   */
+  function handleSuccess() {
+    (selectedRowKeys.value = []) && reload();
+  }
+  /**
+   * 操作栏
+   */
+  function getTableAction(record) {
+    return [
+      {
+        label: '查看',
+        onClick: handleView.bind(null, record),
+        // auth: 'courses:nm_courses:edit',
+      },
+      {
+        label: '编辑',
+        onClick: handleView.bind(null, record),
+        // auth: 'courses:nm_courses:edit',
+      },
+    ];
+  }
+  /**
+   * 下拉操作栏
+   */
+  function getDropDownAction(record) {
+    return [
+      {
+        label: '详情',
+        onClick: handleDetail.bind(null, record),
+      },
+      {
+        label: '删除',
+        popConfirm: {
+          title: '是否确认删除',
+          confirm: handleDelete.bind(null, record),
+          placement: 'topLeft',
+        },
+        // auth: 'courses:nm_courses:delete',
+      },
+    ];
+  }
+</script>
+<script lang="ts">
+  import { router } from '/@/router';
+
+  import { useMultipleTabStore } from '@/store/modules/multipleTab';
+  import { storeToRefs } from 'pinia';
+  const typeList = ['发布课程', '查看课程'];
+  export default {
+    async beforeRouteLeave(to, from, next) {
+      to.meta.title = typeList[Number(to.query.type)];
+      const { getTabList } = storeToRefs(useMultipleTabStore());
+      const closeTab = getTabList.value.filter((it) => it.name == to.name).filter((it) => it.fullPath != to.fullPath);
+      if (closeTab.length) {
+        useMultipleTabStore().closeTabByKey(closeTab[0].fullPath, router);
+      }
+      await useMultipleTabStore().updateCacheTab();
+      next();
+    },
+  };
+</script>
+<style lang="less" scoped>
+  :deep(.ant-picker),
+  :deep(.ant-input-number) {
+    width: 100%;
+  }
+</style>

+ 122 - 0
src/views/businessManagement/courses/publishcourses.vue

@@ -0,0 +1,122 @@
+<template>
+  <div class="p-8px bg-white">
+    <div class="px-4">
+      <BasicForm @register="registerForm">
+        <template #title1>
+          <TypographyTitle :level="4">基础信息</TypographyTitle>
+          <Divider></Divider>
+        </template>
+        <template #title2>
+          <TypographyTitle :level="4">价格、限购、正常课表</TypographyTitle>
+          <Divider></Divider>
+        </template>
+        <template #ZtCustomTable1="{ model, field }">
+          <ZtCustomTable
+            :tableColumn="publishcoursesColums"
+            v-model:value="model[field]"
+            show-index
+            :show-action="Number(route.query.type) == 0"
+          ></ZtCustomTable>
+        </template>
+        <template #title3>
+          <TypographyTitle :level="4">课程介绍</TypographyTitle>
+          <Divider></Divider>
+        </template>
+        <template #VideoUpload="{ model, field }">
+          <uploadVideo v-model:model-value="model[field]"></uploadVideo>
+          <span class="text-gray">单个视频;时长10s ~ 5分钟以内;宽高比为5:4。单个文件不超过100M;</span>
+        </template>
+
+        <template #formFooter>
+          <div class="w-full flex items-center justify-center my-3" v-if="Number(route.query.type) == 0">
+            <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>
+      </BasicForm>
+      <div class="h-20px"></div>
+    </div>
+  </div>
+</template>
+<script lang="ts" setup name="business-management-publishcourses">
+  import uploadVideo from '@/components/uploadVideo/index.vue';
+
+  import { TypographyTitle, Divider } from 'ant-design-vue';
+  import { BasicForm, useForm } from '/@/components/Form/index';
+  import ZtCustomTable from '/@/components/ZtCustomTable/index.vue';
+  import { formSchema, publishcoursesColums } from './courses.data';
+  import dayjs from 'dayjs';
+  import { useTabs } from '/@/hooks/web/useTabs';
+  import { saveOrUpdate, getDetaile } from './courses.api';
+  import { ref } from 'vue';
+  import { useRoute } from 'vue-router';
+  import { nextTick } from 'vue';
+
+  const { close: closeTab } = useTabs();
+  const route = useRoute();
+  const [registerForm, { setProps, resetFields, setFieldsValue, updateSchema, validate, clearValidate, getFieldsValue }] = useForm({
+    schemas: formSchema,
+    showActionButtonGroup: false,
+    labelCol: { span: 4 },
+  });
+  const viewDataSport = ref<any>({});
+  const customTableData = ref({});
+  async function isView() {
+    if (Number(route.query.type) == 1) {
+      await getDataList();
+      nextTick(async () => {
+        setProps({
+          disabled: true,
+        });
+        console.log('viewDataSportList', customTableData.value);
+
+        await setFieldsValue({
+          ...viewDataSport.value,
+          categoryId: viewDataSport.value.categoryId.split(','),
+          priceRulesList: customTableData.value,
+        });
+      });
+    }
+  }
+  isView();
+  async function getDataList() {
+    const res = await getDetaile({ id: route.query.id });
+    // viewDataSportList.value = res;\
+    customTableData.value = res.priceRulesList.map((it) => {
+      return {
+        name: it.name,
+        time: [it.startTime, it.endTime],
+        id: dayjs().valueOf(),
+      };
+    });
+    viewDataSport.value = res.courses;
+  }
+
+  const isSubmit = ref(false);
+  async function back() {
+    await closeTab();
+  }
+  async function save() {
+    const form = await validate();
+    const newObj = {
+      courses: { ...form },
+      priceRulesList: transformData(form.priceRulesList.obj),
+    };
+    try {
+      await saveOrUpdate(newObj, false);
+      await closeTab();
+    } catch (error) {
+      console.log(error);
+    }
+    console.log(newObj);
+  }
+  function transformData(data) {
+    return data.map((it) => {
+      return {
+        startTime: dayjs(it.time[0]).format('YYYY-MM-DD hh:mm:ss'),
+        endTime: dayjs(it.time[1]).format('YYYY-MM-DD hh:mm:ss'),
+        name: it.name,
+      };
+    });
+  }
+</script>

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

@@ -0,0 +1,138 @@
+import { h } from 'vue';
+import { BasicColumn } from '/@/components/Table';
+import { PlusCircleOutlined, MinusCircleOutlined } from '@ant-design/icons-vue';
+import { render } from 'nprogress';
+
+export const ScheduleArrangementColums: BasicColumn[] = [
+  {
+    title: '时间段',
+    dataIndex: 'time',
+    editComponent: 'TimeRangePicker',
+    editRule: true,
+    editComponentProps: {
+      placeholder: ['开始时间', '结束时间'],
+      size: 'middle',
+    },
+    width: 350,
+    editRow: true,
+    editable: true,
+  },
+  {
+    title: '周一',
+    dataIndex: 'day1',
+    editComponent: 'InputNumber',
+    editRow: true,
+    editable: true,
+    editRule: true,
+    editComponentProps: {
+      min: 1,
+      step: 0.01,
+      size: 'middle',
+      precision: 2,
+    },
+  },
+  {
+    title: '周二',
+    dataIndex: 'day2',
+    editComponent: 'InputNumber',
+    editRow: true,
+    editable: true,
+    editComponentProps: {
+      min: 1,
+      step: 0.01,
+      size: 'middle',
+      precision: 2,
+    },
+  },
+
+  {
+    title: '周三',
+    dataIndex: 'day3',
+    editComponent: 'InputNumber',
+    editRow: true,
+    editable: true,
+    editComponentProps: {
+      min: 1,
+      step: 0.01,
+      size: 'middle',
+      precision: 2,
+    },
+  },
+  {
+    title: '周四',
+    dataIndex: 'day4',
+    editComponent: 'InputNumber',
+    editRow: true,
+    editable: true,
+    editComponentProps: {
+      min: 1,
+      size: 'middle',
+      step: 0.01,
+      precision: 2,
+    },
+  },
+  {
+    title: '周五',
+    dataIndex: 'day5',
+    editComponent: 'InputNumber',
+    editRow: true,
+    editable: true,
+    editComponentProps: {
+      min: 1,
+      step: 0.01,
+      size: 'middle',
+      precision: 2,
+    },
+  },
+  {
+    title: '周六',
+    dataIndex: 'day6',
+    editComponent: 'InputNumber',
+    editRow: true,
+    editable: true,
+    editComponentProps: {
+      min: 1,
+      step: 0.01,
+      size: 'middle',
+      precision: 2,
+    },
+  },
+  {
+    title: '周日',
+    dataIndex: 'day7',
+    editComponent: 'InputNumber',
+    editRow: true,
+    editable: true,
+    editComponentProps: {
+      size: 'middle',
+      min: 1,
+      step: 0.01,
+      precision: 2,
+    },
+  },
+  {
+    dataIndex: 'operation',
+    title: 'operation',
+    fixed: 'right',
+  },
+];
+
+export interface priceRules {
+  categoryId: string;
+  startTime: string;
+  endTime: string;
+  dayOfWeek: string;
+  originalPrice: string;
+  inventory: string;
+}
+export interface apiForm {
+  priceRulesList: priceRules[];
+  site: {
+    name: string;
+    categoryId: string;
+    earlyRefundTime: string;
+    buyLimit: string;
+    reminder: string;
+    type: number;
+  };
+}

+ 306 - 0
src/views/businessManagement/gymnasiumBag/index.vue

@@ -0,0 +1,306 @@
+<template>
+  <div class="p-8px bg-white">
+    <div class="px-4">
+      <BasicForm @register="registerForm">
+        <template #title1>
+          <TypographyTitle :level="4">包场时间</TypographyTitle>
+          <Divider></Divider>
+        </template>
+        <template #title2>
+          <TypographyTitle :level="4">封面、配套保险、使用须知</TypographyTitle>
+          <Divider></Divider>
+        </template>
+        <template #customer="{ model, field }">
+          <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>
+              </FormItem>
+              <FormItem label="时间段及费用" required :labelCol="{ xl: 2, sm: 3 }" :wrapper-col="{ span: 22 }">
+                <ZtCustomTable :tableColumn="ScheduleArrangementColums" v-model:value="model[item.value]"></ZtCustomTable>
+              </FormItem>
+            </Card>
+          </div>
+        </template>
+        <template #title3>
+          <TypographyTitle :level="4">退款、预订限制</TypographyTitle>
+          <Divider></Divider>
+        </template>
+        <template #formFooter>
+          <div style="margin: 0 auto" class="w-full flex items-center justify-center">
+            <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>
+      </BasicForm>
+      <div class="h-20px"></div>
+    </div>
+  </div>
+</template>
+<script lang="ts" setup name="business-management-gymnasiumbag">
+  import { TypographyTitle, Divider, InputNumber, FormItem, Card, message } from 'ant-design-vue';
+  import { BasicForm, FormSchema, useForm } from '/@/components/Form/index';
+  import { getSprotProject, siteAdd } from '/@/api/common/api';
+  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 { OriginalItem } from '/#/utils';
+  import { areAllItemsAllFieldsFilled } from '/@/utils';
+  import dayjs from 'dayjs';
+  import { useTabs } from '/@/hooks/web/useTabs';
+  const projectList = ref<DemoOptionsItem[]>([]);
+  const { close: closeTab } = useTabs();
+  const isSubmit = ref(false);
+  const formSchema: FormSchema[] = [
+    {
+      field: 'name',
+      label: '营业名称',
+      component: 'Select',
+      defaultValue: '111',
+      required: true,
+      labelWidth: 120,
+      colProps: {
+        span: 14,
+        xs: 24,
+      },
+    },
+
+    {
+      field: 'title1',
+      colSlot: 'title1',
+      label: '',
+      component: 'Input',
+      labelWidth: 0,
+    },
+    {
+      field: 'categoryId',
+      label: '球类项目',
+      component: 'CheckboxGroup',
+      defaultValue: [],
+      componentProps: {
+        options: projectList,
+      },
+      required: true,
+      labelWidth: 120,
+      colProps: {
+        span: 24,
+        xs: 24,
+      },
+    },
+    {
+      field: 'customerInfo',
+      label: '球类项目',
+      component: 'CheckboxGroup',
+      colSlot: 'customer',
+      required: true,
+      labelWidth: 120,
+      colProps: {
+        span: 24,
+        xs: 24,
+      },
+    },
+    {
+      field: 'title3',
+      colSlot: 'title3',
+      label: '',
+      component: 'Input',
+      labelWidth: 0,
+    },
+    {
+      field: 'earlyRefundTime',
+      label: '退款',
+      component: 'InputNumber',
+      required: true,
+      componentProps: {
+        addonBefore: '开场前',
+        addonAfter: '分钟可退',
+      },
+      labelWidth: 120,
+      colProps: {
+        span: 24,
+      },
+    },
+    {
+      field: 'buyLimit',
+      label: '预定限制',
+      component: 'InputNumber',
+      required: true,
+      componentProps: {
+        addonBefore: '每笔订单最多可选择',
+        addonAfter: '场',
+      },
+      labelWidth: 120,
+      colProps: {
+        span: 24,
+      },
+    },
+    {
+      field: 'reminder',
+      label: '温馨提示',
+      component: 'InputTextArea',
+      required: true,
+      componentProps: {
+        autosize: {
+          minRows: 3,
+          maxRows: 6,
+        },
+      },
+      defaultValue: '场地仅限个人使用,培训、比赛或者其他商业活动,可能需要额外付费。如有需求,请提前联系商家,咨询收费标准。',
+      labelWidth: 120,
+      colProps: {
+        span: 18,
+      },
+    },
+  ];
+  const [registerForm, { setProps, resetFields, setFieldsValue, updateSchema, validate, getFieldsValue, appendSchemaByField }] = useForm({
+    schemas: formSchema,
+    showActionButtonGroup: 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('请填写必填项');
+    }
+    const newObj: apiForm = {
+      site: {
+        name: form.name,
+        categoryId: form.categoryId,
+        earlyRefundTime: form.earlyRefundTime,
+        buyLimit: form.buyLimit,
+        reminder: form.reminder,
+        type: 1, //0学校,1是体育馆
+      },
+      priceRulesList: getResultData(form),
+    };
+    isSubmit.value = true;
+    try {
+      await siteAdd(newObj);
+      await closeTab();
+    } catch (error) {
+      isSubmit.value = false;
+    }
+  }
+  async function getProjectData() {
+    const res = await getSprotProject({ pageSize: 20 });
+    projectList.value = res.records.map((it) => {
+      return { value: it.id, label: it.name };
+    });
+    renderTable();
+  }
+  getProjectData();
+
+  function getCustomerList(list: string[]) {
+    const arr = projectList.value.filter((it) => {
+      return list.includes(it.value);
+    });
+    return arr;
+  }
+
+  async function renderTable() {
+    projectList.value.forEach(async (it, idx) => {
+      await appendSchemaByField(
+        {
+          field: it.value,
+          label: ``,
+          component: 'Select',
+          show: false,
+          defaultValue: [],
+        },
+
+        ''
+      );
+      await appendSchemaByField(
+        {
+          field: `inventory${idx}`,
+          label: ``,
+          component: 'Select',
+          show: false,
+        },
+        ''
+      );
+    });
+  }
+
+  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;
+  }
+
+  /**
+   * 校验 inventory 是否存在且 > 0
+   * @param formValues 表单数据
+   * @returns boolean 校验是否通过
+   */
+  function validateInventoryFields(formValues: any): boolean {
+    const { categoryId } = formValues;
+
+    if (!categoryId) {
+      return false;
+    }
+
+    const categoryIds = categoryId.split(',').map((id) => id.trim());
+
+    for (let i = 0; i < categoryIds.length; i++) {
+      const inventoryField = `inventory${i}`;
+      const inventoryValue = formValues[inventoryField];
+      const priceRulesList = formValues[categoryIds[i]];
+      if (inventoryValue === undefined || inventoryValue === null || inventoryValue <= 0 || !priceRulesList) {
+        return false;
+      }
+      if (!areAllItemsAllFieldsFilled(priceRulesList.obj)) {
+        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'),
+        });
+      }
+    }
+
+    return result;
+  }
+</script>

+ 70 - 0
src/views/businessManagement/gymnasiumNoFixed/DataRuleModal.vue

@@ -0,0 +1,70 @@
+<template>
+  <BasicModal v-bind="$attrs" @register="registerModal" :title="getTitle" @ok="handleSubmit" width="900px" destroyOnClose>
+    <div class="px-10">
+      <BasicForm @register="registerForm">
+        <template #title1>
+          <TypographyTitle :level="4">基础信息</TypographyTitle>
+          <Divider></Divider>
+        </template>
+        <template #title2>
+          <TypographyTitle :level="4">服务保障</TypographyTitle>
+          <Divider></Divider>
+        </template>
+        <template #title3>
+          <TypographyTitle :level="4">购买须知</TypographyTitle>
+          <Divider></Divider>
+        </template>
+      </BasicForm>
+    </div>
+  </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';
+  // 声明Emits
+  const emit = defineEmits(['success', 'register']);
+  const props = defineProps({ permissionId: String });
+  const isUpdate = ref(true);
+  //表单配置
+  const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
+    schemas: dataRuleFormSchema,
+    showActionButtonGroup: false,
+  });
+  //表单赋值
+  const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
+    //重置表单
+    await resetFields();
+    setModalProps({ confirmLoading: false });
+    isUpdate.value = !!data?.isUpdate;
+    if (unref(isUpdate)) {
+      //表单赋值
+      await setFieldsValue({
+        ...data.record,
+      });
+    }
+  });
+
+  //设置标题
+  const getTitle = computed(() => (!unref(isUpdate) ? '添加商品' : '编辑商品'));
+
+  //表单提交事件
+  async function handleSubmit() {
+    try {
+      const values = await validate();
+      values.permissionId = props.permissionId;
+      setModalProps({ confirmLoading: true });
+      //提交表单
+      // await saveOrUpdateRule(values, isUpdate.value);
+      //关闭弹窗
+      closeModal();
+      //刷新列表
+      emit('success');
+    } finally {
+      setModalProps({ confirmLoading: false });
+    }
+  }
+</script>

+ 215 - 0
src/views/businessManagement/gymnasiumNoFixed/gymnasiumNoFixed.data.ts

@@ -0,0 +1,215 @@
+import { FormSchema } from '/@/components/Table';
+
+export const columns = [
+  {
+    title: '封面',
+    dataIndex: 'cover',
+    width: 100,
+  },
+  {
+    title: '商品名称',
+    dataIndex: 'name',
+    width: 100,
+  },
+  {
+    title: '提前预约',
+    dataIndex: 'advanceTime',
+    width: 100,
+  },
+  {
+    title: '销售价',
+    dataIndex: 'sellingPrice',
+    width: 100,
+  },
+  {
+    title: '原价',
+    dataIndex: 'originalPrice',
+    width: 100,
+  },
+  {
+    title: '服务保障',
+    dataIndex: 'refundType',
+    width: 100,
+  },
+];
+export const searchFormSchema: FormSchema[] = [
+  {
+    field: 'name',
+    label: '商品名称',
+    component: 'Input',
+    colProps: { span: 6 },
+  },
+];
+
+export const dataRuleFormSchema: FormSchema[] = [
+  {
+    field: 'id',
+    label: 'id',
+    component: 'Input',
+    show: false,
+  },
+  {
+    field: 'title1',
+    component: 'Input',
+    colSlot: 'title1',
+    label: '',
+  },
+  {
+    field: 'name',
+    label: '商品名称',
+    component: 'Input',
+    required: true,
+  },
+  {
+    field: 'cover',
+    label: '封面',
+    component: 'JImageUpload',
+    required: true,
+    componentProps: {
+      tipText: '支持单张上传;宽高比为5:4,且宽高均大于480px,大小5M以内。',
+    },
+  },
+  {
+    field: 'sellingPrice',
+    label: '销售价',
+    component: 'InputNumber',
+    required: true,
+    span: 6,
+    componentProps: {
+      min: 1,
+      addonAfter: '元',
+    },
+  },
+  {
+    field: 'originalPrice',
+    label: '原价',
+    component: 'InputNumber',
+    required: true,
+    span: 8,
+    componentProps: {
+      min: 1,
+      addonAfter: '元',
+    },
+  },
+  {
+    field: 'title2',
+    component: 'Input',
+    colSlot: 'title2',
+    label: '',
+  },
+  {
+    field: 'refundType',
+    label: '退款',
+    component: 'RadioGroup',
+    required: true,
+    span: 8,
+    componentProps: {
+      options: [
+        { label: '随时退·过期自动退', value: 1 },
+        { label: '不支持退款', value: 0 },
+      ],
+    },
+  },
+  {
+    field: 'title3',
+    component: 'Input',
+    colSlot: 'title3',
+    label: '',
+  },
+  {
+    field: 'indate',
+    label: '有效期',
+    component: 'InputNumber',
+    required: true,
+    span: 8,
+    componentProps: {
+      min: 1,
+      addonBefore: '购买后',
+      addonAfter: '天内有效',
+    },
+  },
+  {
+    field: 'advanceTime',
+    label: '提前预约  ',
+    component: 'RadioGroup',
+    required: true,
+    span: 4,
+    defaultValue: 1,
+    labelWidth: 135,
+    colProps: {
+      sm: 12,
+    },
+    componentProps: {
+      options: [
+        { label: '免预约', value: 1 },
+        { label: '需提前预约', value: 0 },
+      ],
+    },
+  },
+  {
+    field: 'name',
+    label: '',
+    component: 'InputNumber',
+    required: true,
+    span: 4,
+    colProps: {
+      sm: 12,
+    },
+    ifShow: (schema) => {
+      return schema.model.advanceTime == 0;
+    },
+    componentProps: {
+      min: 1,
+      addonBefore: '提前',
+      addonAfter: '小时',
+    },
+  },
+  {
+    field: 'name',
+    label: '除外日期(不可用)  ',
+    component: 'CheckboxGroup',
+    required: true,
+    span: 8,
+    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 },
+      ],
+    },
+  },
+  {
+    field: 'name',
+    label: '适用人数',
+    component: 'InputNumber',
+    required: true,
+    span: 8,
+    componentProps: {
+      min: 1,
+      addonBefore: '每张券最多',
+      addonAfter: '人使用',
+    },
+  },
+  {
+    field: 'name',
+    label: '温馨提示',
+    component: 'InputTextArea',
+    required: true,
+
+    componentProps: {
+      autoSize: {
+        //最小显示行数
+        minRows: 6,
+        //最大显示行数
+        maxRows: 6,
+      },
+      defaultValue:
+        '场馆可能致电您,预约到馆时间,请保持手机畅通;\n如需购券发票,请您在消费时向商户咨询;\n 为了保障您的权益,建议使用平台线上支付,若使用了其他方式支付导致纠纷,\n平台不承担任何责任,感谢您的理解和支持。\n温馨提醒:您在到馆使用本商品/本服务期间,请关注场馆的安全提示内容,了解相关注意事项,做好安全防护措施,包含您的安全。',
+    },
+  },
+];

+ 65 - 0
src/views/businessManagement/gymnasiumNoFixed/index.vue

@@ -0,0 +1,65 @@
+<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
+  >
+  <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 { useListPage } from '/@/hooks/system/useListPage';
+  import { getSiteList } from '@/api/common/api';
+  import { useModal } from '/@/components/Modal';
+
+  import { columns, searchFormSchema } from './gymnasiumNoFixed.data';
+  const [registerModal, { openModal }] = useModal();
+
+  const { prefixCls, tableContext } = useListPage({
+    designScope: 'competition-template',
+    tableProps: {
+      title: '赛场列表',
+      api: getSiteList,
+      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,
+      },
+      // rowSelection: null,
+      //自定义默认排序
+      defSort: {
+        column: 'id',
+        order: 'desc',
+      },
+    },
+  });
+  const [registerTable, { reload, expandAll, collapseAll }] = tableContext;
+  function ruleModal() {}
+  function handleAdd() {
+    openModal(true, {
+      isUpdate: false,
+    });
+  }
+</script>
+
+<style scoped></style>

+ 5 - 2
src/views/businessManagement/schoolOpen/index.vue

@@ -32,10 +32,13 @@
   import { BasicForm, useForm } from '/@/components/Form/index';
   import ZtCustomTable from '/@/components/ZtCustomTable/index.vue';
   import { formSchema, ScheduleArrangementColums, SchedulePricesColums } from './schoolOpen.data';
-  const [registerForm, { setProps, resetFields, setFieldsValue, updateSchema, validate, clearValidate }] = useForm({
+  const [registerForm, { setProps, resetFields, setFieldsValue, updateSchema, validate, clearValidate, getFieldsValue }] = useForm({
     schemas: formSchema,
     showActionButtonGroup: false,
   });
   function back() {}
-  function save() {}
+  async function save() {
+    const form = await getFieldsValue();
+    console.log(form);
+  }
 </script>

+ 21 - 11
src/views/businessManagement/schoolOpen/schoolOpen.data.ts

@@ -1,5 +1,5 @@
 import { ZtTableColumnProps } from '/#/utils';
-import { FormSchema } from '/@/components/Table';
+import { BasicColumn, FormSchema } from '/@/components/Table';
 
 export const formSchema: FormSchema[] = [
   {
@@ -15,7 +15,7 @@ export const formSchema: FormSchema[] = [
   },
   {
     field: 'title1',
-    slot: 'title1',
+    colSlot: 'title1',
     label: '',
     component: 'Input',
     labelWidth: 0,
@@ -29,7 +29,7 @@ export const formSchema: FormSchema[] = [
     required: true,
     labelWidth: 120,
     colProps: {
-      span: 18,
+      span: 20,
       xs: 24,
     },
   },
@@ -42,13 +42,13 @@ export const formSchema: FormSchema[] = [
     required: true,
     labelWidth: 120,
     colProps: {
-      span: 18,
+      span: 20,
       xs: 24,
     },
   },
   {
     field: 'title2',
-    slot: 'title2',
+    colSlot: 'title2',
     label: '',
     component: 'Input',
     labelWidth: 0,
@@ -94,22 +94,32 @@ export const formSchema: FormSchema[] = [
   },
 ];
 
-export const ScheduleArrangementColums: ZtTableColumnProps[] = [
+export const ScheduleArrangementColums: BasicColumn[] = [
   {
     title: '时间段',
-    key: 'RangePicker',
     dataIndex: 'time',
-    width: 450,
-    props: {
-      format: 'YYYY-MM-DD hh:mm:ss',
-      showTime: true,
+    editComponent: 'RangePicker',
+    editRule: true,
+    editComponentProps: {
+      placeholder: ['开始时间', '结束时间'],
+      size: 'middle',
     },
+    width: 350,
+    editRow: true,
+    editable: true,
   },
   {
     title: '总票数',
     key: 'InputNumber',
     dataIndex: 'price',
     width: 250,
+    editComponent: 'InputNumber',
+    editRule: true,
+    editComponentProps: {
+      size: 'middle',
+    },
+    editRow: true,
+    editable: true,
   },
   {
     key: 'op',

+ 69 - 0
src/views/informationManagement/projectList/components/projectForm.vue

@@ -0,0 +1,69 @@
+<template>
+  <div style="min-height: 400px">
+    <BasicForm @register="registerForm"> </BasicForm>
+    <div style="width: 100%; text-align: center" v-if="!formDisabled">
+      <a-button @click="submitForm" pre-icon="ant-design:check" type="primary">提 交</a-button>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+  import { BasicForm, useForm } from '/@/components/Form/index';
+  import { computed, defineComponent } from 'vue';
+  import { defHttp } from '/@/utils/http/axios';
+  import { propTypes } from '/@/utils/propTypes';
+  import { getBpmFormSchema } from '../project.data';
+  import { saveOrUpdate } from '../project.api';
+
+  export default defineComponent({
+    name: 'ProjectForm',
+    components: {
+      BasicForm,
+    },
+    props: {
+      formData: propTypes.object.def({}),
+      formBpm: propTypes.bool.def(true),
+    },
+    setup(props) {
+      const [registerForm, { setFieldsValue, setProps, getFieldsValue }] = useForm({
+        labelWidth: 150,
+        schemas: getBpmFormSchema(props.formData),
+        showActionButtonGroup: false,
+        baseColProps: { span: 24 },
+      });
+
+      const formDisabled = computed(() => {
+        if (props.formData.disabled === false) {
+          return false;
+        }
+        return true;
+      });
+
+      let formData = {};
+      const queryByIdUrl = '/app/appCategory/queryById';
+      async function initFormData() {
+        let params = { id: props.formData.dataId };
+        const data = await defHttp.get({ url: queryByIdUrl, params });
+        formData = { ...data };
+        //设置表单的值
+        await setFieldsValue(formData);
+        //默认是禁用
+        await setProps({ disabled: formDisabled.value });
+      }
+
+      async function submitForm() {
+        let data = getFieldsValue();
+        let params = Object.assign({}, formData, data);
+        console.log('表单数据', params);
+        await saveOrUpdate(params, true);
+      }
+
+      initFormData();
+      return {
+        registerForm,
+        formDisabled,
+        submitForm,
+      };
+    },
+  });
+</script>

+ 79 - 0
src/views/informationManagement/projectList/components/projectModal.vue

@@ -0,0 +1,79 @@
+<template>
+  <BasicModal v-bind="$attrs" @register="registerModal" destroyOnClose :title="title" :width="800" @ok="handleSubmit">
+    <BasicForm @register="registerForm" name="ProjectForm"> </BasicForm>
+  </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 '../project.data';
+  import { saveOrUpdate } from '../project.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>

+ 166 - 0
src/views/informationManagement/projectList/index.vue

@@ -0,0 +1,166 @@
+<template>
+  <div>
+    <!--引用表格-->
+    <BasicTable @register="registerTable" :rowSelection="rowSelection">
+      <!--插槽:table标题-->
+      <template #tableTitle>
+        <a-button type="primary" @click="handleAdd" preIcon="ant-design:plus-outlined"> 新增</a-button>
+        <a-dropdown v-if="selectedRowKeys.length > 0">
+          <template #overlay>
+            <a-menu>
+              <a-menu-item key="1" @click="batchHandleDelete">
+                <Icon icon="ant-design:delete-outlined"></Icon>
+                删除
+              </a-menu-item>
+            </a-menu>
+          </template>
+          <a-button v-auth="'category:nm_category:deleteBatch'"
+            >批量操作
+            <Icon icon="mdi:chevron-down"></Icon>
+          </a-button>
+        </a-dropdown>
+      </template>
+      <!--操作栏-->
+      <template #action="{ record }">
+        <TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)" />
+      </template>
+      <!--字段回显插槽-->
+      <template v-slot:bodyCell="{ column, record, index, text }"> </template>
+    </BasicTable>
+    <!-- 表单区域 -->
+    <projectModal @register="registerModal" @success="handleSuccess"></projectModal>
+  </div>
+</template>
+
+<script lang="ts" name="project" setup>
+  import { ref, reactive, computed, unref } from 'vue';
+  import { BasicTable, useTable, TableAction } from '/@/components/Table';
+  import { useModal } from '/@/components/Modal';
+  import { useListPage } from '/@/hooks/system/useListPage';
+  import projectModal from './components/projectModal.vue';
+  import { columns, searchFormSchema } from './project.data';
+  import { list, deleteOne, batchDelete } from './project.api';
+  import { useUserStore } from '/@/store/modules/user';
+  import { useMessage } from '/@/hooks/web/useMessage';
+  const queryParam = reactive<any>({});
+  const checkedKeys = ref<Array<string | number>>([]);
+  const userStore = useUserStore();
+  const { createMessage } = useMessage();
+  //注册model
+  const [registerModal, { openModal }] = useModal();
+  //注册table数据
+  const { tableContext } = useListPage({
+    tableProps: {
+      title: 'project',
+      api: list,
+      columns,
+      canResize: false,
+      formConfig: {
+        //labelWidth: 120,
+        schemas: searchFormSchema,
+        autoSubmitOnEnter: true,
+        showAdvancedButton: true,
+        fieldMapToNumber: [],
+        fieldMapToTime: [],
+      },
+      actionColumn: {
+        width: 120,
+        fixed: 'right',
+      },
+      beforeFetch: (params) => {
+        return Object.assign(params, queryParam);
+      },
+    },
+  });
+
+  const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
+
+  /**
+   * 新增事件
+   */
+  function handleAdd() {
+    openModal(true, {
+      isUpdate: false,
+      showFooter: true,
+    });
+  }
+  /**
+   * 编辑事件
+   */
+  function handleEdit(record: Recordable) {
+    console.log('record....', record);
+
+    openModal(true, {
+      record,
+      isUpdate: true,
+      showFooter: true,
+    });
+  }
+  /**
+   * 详情
+   */
+  function handleDetail(record: Recordable) {
+    openModal(true, {
+      record,
+      isUpdate: true,
+      showFooter: false,
+    });
+  }
+  /**
+   * 删除事件
+   */
+  async function handleDelete(record) {
+    await deleteOne({ id: record.id }, handleSuccess);
+  }
+  /**
+   * 批量删除事件
+   */
+  async function batchHandleDelete() {
+    await batchDelete({ ids: selectedRowKeys.value }, handleSuccess);
+  }
+  /**
+   * 成功回调
+   */
+  function handleSuccess() {
+    (selectedRowKeys.value = []) && reload();
+  }
+  /**
+   * 操作栏
+   */
+  function getTableAction(record) {
+    return [
+      {
+        label: '编辑',
+        onClick: handleEdit.bind(null, record),
+        // auth: 'category:nm_category:edit',
+      },
+    ];
+  }
+  /**
+   * 下拉操作栏
+   */
+  function getDropDownAction(record) {
+    return [
+      {
+        label: '详情',
+        onClick: handleDetail.bind(null, record),
+      },
+      {
+        label: '删除',
+        popConfirm: {
+          title: '是否确认删除',
+          confirm: handleDelete.bind(null, record),
+          placement: 'topLeft',
+        },
+        // auth: 'category:nm_category:delete',
+      },
+    ];
+  }
+</script>
+
+<style lang="less" scoped>
+  :deep(.ant-picker),
+  :deep(.ant-input-number) {
+    width: 100%;
+  }
+</style>

+ 52 - 0
src/views/informationManagement/projectList/project.api.ts

@@ -0,0 +1,52 @@
+import { defHttp } from '/@/utils/http/axios';
+import { useMessage } from '/@/hooks/web/useMessage';
+
+const { createConfirm } = useMessage();
+
+enum Api {
+  list = '/app/appCategory/list',
+  save = '/app/appCategory/add',
+  edit = '/app/appCategory/edit',
+  deleteOne = '/app/appCategory/delete',
+  deleteBatch = '/app/appCategory/deleteBatch',
+}
+/**
+ * 列表接口
+ * @param params
+ */
+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 batchDelete = (params, handleSuccess) => {
+  createConfirm({
+    iconType: 'warning',
+    title: '确认删除',
+    content: '是否删除选中数据',
+    okText: '确认',
+    cancelText: '取消',
+    onOk: () => {
+      return defHttp.delete({ url: Api.deleteBatch, data: 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 });
+};

+ 70 - 0
src/views/informationManagement/projectList/project.data.ts

@@ -0,0 +1,70 @@
+import { BasicColumn } from '/@/components/Table';
+import { FormSchema } from '/@/components/Table';
+import { rules } from '/@/utils/helper/validator';
+import { render } from '/@/utils/common/renderUtils';
+import { getWeekMonthQuarterYear } from '/@/utils';
+import { InputNumber } from 'ant-design-vue';
+import { h } from 'vue';
+//列表数据
+export const columns: BasicColumn[] = [
+  {
+    title: '项目名称',
+    align: 'center',
+    dataIndex: 'name',
+  },
+  {
+    title: '显示顺序',
+    align: 'center',
+    dataIndex: 'sort',
+  },
+  {
+    title: '创建时间',
+    align: 'center',
+    dataIndex: 'createTime',
+  },
+];
+//查询数据
+export const searchFormSchema: FormSchema[] = [];
+//表单数据
+export const formSchema: FormSchema[] = [
+  {
+    label: '',
+    field: 'id',
+    component: 'Input',
+    show: false,
+  },
+  {
+    label: '项目名称',
+    field: 'name',
+    component: 'Input',
+    required: true,
+  },
+  {
+    label: '显示顺序',
+    field: 'sort',
+    component: 'InputNumber',
+    required: true,
+    render: ({ model, field }) => {
+      return h('div', {}, [
+        h(InputNumber, {
+          placeholder: '请输入',
+          value: model[field],
+          onChange: (e) => {
+            model[field] = e;
+          },
+        }),
+        h('span', { class: 'text-gray' }, '越小越靠前,非负整数,不能重复'),
+      ]);
+    },
+  },
+  // TODO 主键隐藏字段,目前写死为ID
+];
+
+/**
+ * 流程表单调用这个方法获取formSchema
+ * @param param
+ */
+export function getBpmFormSchema(_formData): FormSchema[] {
+  // 默认和原始表单保持一致 如果流程中配置了权限数据,这里需要单独处理formSchema
+  return formSchema;
+}

+ 73 - 0
src/views/informationManagement/shopInfo/index.vue

@@ -0,0 +1,73 @@
+<template>
+  <div class="p-8px bg-white">
+    <div class="px-4">
+      <BasicForm @register="registerForm">
+        <template #title1>
+          <TypographyTitle :level="4">基础信息</TypographyTitle>
+          <Divider></Divider>
+        </template>
+        <template #address="{ model, field }">
+          <FormItem label="营业地址" required>
+            <JAreaSelect v-model:province="model['province']" v-model:city="model['city']" v-model:area="model['area']"></JAreaSelect>
+            <div class="mt-4">
+              <Textarea v-model:value="model[field]"></Textarea>
+            </div>
+          </FormItem>
+        </template>
+        <template #phone="{ model, field }">
+          <Input v-model:value="model[field]"></Input>
+          <span class="text-gray">用户拨打商家电话时,拨打的号码为该号码。</span>
+        </template>
+
+        <template #title3>
+          <TypographyTitle :level="4">视频/图片</TypographyTitle>
+          <Divider></Divider>
+        </template>
+        <template #VideoUpload="{ model, field }">
+          <uploadVideo v-model:model-value="model[field]"></uploadVideo>
+          <span class="text-gray">单个视频;时长10s ~ 5分钟以内;宽高比为5:4。单个文件不超过100M;</span>
+        </template>
+
+        <template #title4>
+          <div class="flex items-center">
+            <TypographyTitle :level="4">VR实景 </TypographyTitle>
+            <span class="ml-8 text-gray">找一个能呈现体育馆特点的站点,拍摄前、后,左、右,上、下6张图片。 </span>
+          </div>
+          <Divider></Divider>
+        </template>
+
+        <template #formFooter>
+          <div style="margin: 0 auto">
+            <a-button type="primary" @click="save" class="mr-2"> 保存 </a-button>
+            <a-button type="error" @click="back" class="mr-2"> 关闭 </a-button>
+          </div>
+        </template>
+      </BasicForm>
+      <div class="h-20px"></div>
+    </div>
+  </div>
+</template>
+<script lang="ts" setup>
+  import { TypographyTitle, Divider, FormItem, Textarea, Input } from 'ant-design-vue';
+  import { BasicForm, useForm, JAreaSelect } from '/@/components/Form/index';
+  import uploadVideo from '@/components/uploadVideo/index.vue';
+  import { onUnmounted } from 'vue';
+  import { useShopInfoStore } from '/@/store/modules/shopInfo';
+  const hopInfoStore = useShopInfoStore();
+  import { formSchema } from './shopInfo.data';
+  const [registerForm, { setProps, resetFields, setFieldsValue, updateSchema, validate, clearValidate, getSchemaByField }] = useForm({
+    schemas: formSchema,
+    showActionButtonGroup: false,
+    labelCol: { span: 4 },
+    wrapperCol: { span: 18 },
+  });
+  function back() {}
+  async function save() {
+    const form = await validate();
+    console.log(form, '撒大苏打');
+  }
+  hopInfoStore.getCurrentDep();
+  onUnmounted(() => {
+    hopInfoStore.isShowSelect = false;
+  });
+</script>

+ 198 - 0
src/views/informationManagement/shopInfo/shopInfo.data.ts

@@ -0,0 +1,198 @@
+import { FormSchema } from '/@/components/Table';
+import { getSprotProject } from '/@/api/common/api';
+const sportList = await getSprotProject({ pageSize: 20 });
+export const formSchema: FormSchema[] = [
+  {
+    field: 'name',
+    label: '营业名称',
+    component: 'Input',
+    required: true,
+  },
+  {
+    field: 'address',
+    label: '营业地址',
+    component: 'JAreaSelect',
+    required: true,
+    colSlot: 'address',
+  },
+  {
+    field: 'province',
+    label: '',
+    component: 'JAreaSelect',
+    show: false,
+  },
+  {
+    field: 'city',
+    label: '',
+    component: 'JAreaSelect',
+    show: false,
+  },
+  {
+    field: 'area',
+    label: '',
+    component: 'JAreaSelect',
+    show: false,
+  },
+  {
+    field: 'type',
+    label: '业务类型',
+    component: 'Select',
+    componentProps: {
+      options: [
+        { label: '学校', value: 0 },
+        { label: '体育馆', value: 1 },
+      ],
+    },
+    required: true,
+  },
+  {
+    field: 'title1',
+    colSlot: 'title1',
+    label: '',
+    component: 'RangePicker',
+    labelWidth: 0,
+  },
+  {
+    field: 'runStatus',
+    label: '经营状态',
+    component: 'RadioGroup',
+    required: true,
+    componentProps: {
+      options: [
+        { label: '营业', value: 0 },
+        { label: '休息', value: 1 },
+      ],
+    },
+  },
+  {
+    field: 'fileimg',
+    label: '营业时间',
+    component: 'RadioGroup',
+    required: true,
+    componentProps: {
+      options: [
+        { label: '全天', value: 1 },
+        { label: '自定义', value: 0 },
+      ],
+    },
+  },
+  {
+    field: 'phone',
+    label: '客服电话',
+    component: 'Input',
+    required: true,
+    slot: 'phone',
+  },
+  {
+    field: 'facility',
+    label: '设施/服务',
+    component: 'CheckboxGroup',
+    required: true,
+    componentProps: {
+      options: [
+        { label: '卫生间', value: 1 },
+        { label: '无烟环境', value: 0 },
+        { label: '淋浴间', value: 0 },
+        { label: '免费茶水', value: 0 },
+        { label: '停车方便', value: 0 },
+      ],
+    },
+  },
+  {
+    field: 'categoryId',
+    label: '运动项目',
+    component: 'CheckboxGroup',
+    required: true,
+    componentProps: {
+      options: sportList.records.map((it) => {
+        return {
+          label: it.name,
+          value: it.id,
+        };
+      }),
+    },
+  },
+  {
+    field: 'title3',
+    colSlot: 'title3',
+    label: '',
+    component: 'RangePicker',
+    labelWidth: 0,
+  },
+  {
+    field: 'video',
+    label: '视频',
+    component: 'Input',
+    slot: 'VideoUpload',
+  },
+  {
+    field: 'address',
+    label: '图片',
+    component: 'JImageUpload',
+    required: true,
+    componentProps: {
+      tipText: '支持单张上传;宽高比为5:4,且宽高均大于480px,大小5M以内。',
+    },
+  },
+  {
+    field: 'title4',
+    colSlot: 'title4',
+    label: '',
+    component: 'RangePicker',
+    labelWidth: 0,
+  },
+  {
+    field: 'baoxain',
+    label: '图片(前)',
+    component: 'JImageUpload',
+    colProps: { xl: 12 },
+    componentProps: {
+      tipText: '支持单张上传;宽高比为5:4,且宽高均大于480px,大小5M以内。',
+    },
+  },
+  {
+    field: 'baoxain',
+    label: '图片(后)',
+    component: 'JImageUpload',
+    colProps: { xl: 12 },
+    componentProps: {
+      tipText: '支持单张上传;宽高比为5:4,且宽高均大于480px,大小5M以内。',
+    },
+  },
+  {
+    field: 'baoxain',
+    label: '图片(左)',
+    component: 'JImageUpload',
+    colProps: { xl: 12 },
+    componentProps: {
+      tipText: '支持单张上传;宽高比为5:4,且宽高均大于480px,大小5M以内。',
+    },
+  },
+  {
+    field: 'baoxain',
+    label: '图片(右)',
+    component: 'JImageUpload',
+    componentProps: {
+      tipText: '支持单张上传;宽高比为5:4,且宽高均大于480px,大小5M以内。',
+    },
+    colProps: { xl: 12 },
+  },
+  {
+    field: 'baoxain',
+    label: '图片(上)',
+    component: 'JImageUpload',
+    componentProps: {
+      tipText: '支持单张上传;宽高比为5:4,且宽高均大于480px,大小5M以内。',
+    },
+    colProps: { xl: 12 },
+  },
+  {
+    field: 'baoxain',
+    label: '图片(下)',
+    component: 'JImageUpload',
+    componentProps: {
+      tipText: '支持单张上传;宽高比为5:4,且宽高均大于480px,大小5M以内。',
+    },
+    colProps: { xl: 12 },
+  },
+];

+ 25 - 0
src/views/informationManagement/teachorNoteach/index.vue

@@ -0,0 +1,25 @@
+<template>
+  <div class="w-full bg-white p-8 customer flex justify-center items-center">
+    <div class="w-700px">
+      <Calendar :fullscreen="false">
+        <!-- <template #dateFullCellRender="{ current: time }">
+          {{ dayjs(time).date() }}
+        </template> -->
+        <template #headerRender="{ value: current, type, onChange, onTypeChange }"> </template>
+      </Calendar>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { Calendar } from 'ant-design-vue';
+  import dayjs from 'dayjs';
+</script>
+
+<!-- <style scoped lang="less">
+  .customer {
+    :deep(.ant-picker-content thead tr th) {
+      font-size: 28px;
+    }
+  }
+</style> -->

+ 70 - 0
src/views/operationManagement/feedback/components/feedbackForm.vue

@@ -0,0 +1,70 @@
+<template>
+  <div style="min-height: 400px">
+    <BasicForm @register="registerForm"></BasicForm>
+    <div style="width: 100%; text-align: center" v-if="!formDisabled">
+      <a-button @click="submitForm" pre-icon="ant-design:check" type="primary">提 交</a-button>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+  import { BasicForm, useForm } from '/@/components/Form/index';
+  import { computed, defineComponent } from 'vue';
+  import { defHttp } from '/@/utils/http/axios';
+  import { propTypes } from '/@/utils/propTypes';
+  import { getBpmFormSchema } from '../feedback.data';
+  import { saveOrUpdate } from '../feedback.api';
+
+  export default defineComponent({
+    name: 'feedbackForm',
+    components: {
+      BasicForm,
+    },
+    props: {
+      formData: propTypes.object.def({}),
+      formBpm: propTypes.bool.def(true),
+    },
+    setup(props) {
+      const [registerForm, { setFieldsValue, setProps, getFieldsValue }] = useForm({
+        labelWidth: 150,
+        schemas: getBpmFormSchema(props.formData),
+        showActionButtonGroup: false,
+        baseColProps: { span: 24 },
+      });
+
+      const formDisabled = computed(() => {
+        if (props.formData.disabled === false) {
+          return false;
+        }
+        return true;
+      });
+
+      let formData = {};
+      const queryByIdUrl = '/feedback/feedback/queryById';
+      async function initFormData() {
+        let params = { id: props.formData.dataId };
+        const data = await defHttp.get({ url: queryByIdUrl, params });
+        formData = { ...data };
+        //设置表单的值
+        await setFieldsValue(formData);
+        //默认是禁用
+        await setProps({ disabled: formDisabled.value });
+      }
+
+      async function submitForm() {
+        let data = getFieldsValue();
+        let params = Object.assign({}, formData, data);
+        console.log('表单数据', params);
+        await saveOrUpdate(params, true);
+      }
+
+      initFormData();
+
+      return {
+        registerForm,
+        formDisabled,
+        submitForm,
+      };
+    },
+  });
+</script>

+ 90 - 0
src/views/operationManagement/feedback/components/feedbackModal.vue

@@ -0,0 +1,90 @@
+<template>
+  <BasicModal v-bind="$attrs" @register="registerModal" destroyOnClose :title="title" :width="800" @ok="handleSubmit">
+    <div class="px-4">
+      <BasicForm @register="registerForm" name="feedbackForm">
+        <template #title1>
+          <TypographyTitle :level="4">反馈信息</TypographyTitle>
+          <Divider></Divider>
+        </template>
+        <template #title2>
+          <TypographyTitle :level="4">回复信息</TypographyTitle>
+          <Divider></Divider>
+        </template>
+      </BasicForm>
+    </div>
+  </BasicModal>
+</template>
+
+<script lang="ts" setup>
+  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 { formSchema } from '../feedback.data';
+  import { saveOrUpdate } from '../feedback.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>

+ 24 - 0
src/views/operationManagement/feedback/feedback.api.ts

@@ -0,0 +1,24 @@
+import { defHttp } from '/@/utils/http/axios';
+import { useMessage } from '/@/hooks/web/useMessage';
+
+const { createConfirm } = useMessage();
+
+enum Api {
+  list = '/appFeedback/list',
+  save = '/appFeedback/add',
+  edit = '/appFeedback/edit',
+}
+/**
+ * 列表接口
+ * @param params
+ */
+export const list = (params) => defHttp.get({ url: Api.list, params });
+
+/**
+ * 保存或者更新
+ * @param params
+ */
+export const saveOrUpdate = (params, isUpdate) => {
+  let url = isUpdate ? Api.edit : Api.save;
+  return defHttp.post({ url: url, params });
+};

+ 175 - 0
src/views/operationManagement/feedback/feedback.data.ts

@@ -0,0 +1,175 @@
+import { BasicColumn } from '/@/components/Table';
+import { FormSchema } from '/@/components/Table';
+import { rules } from '/@/utils/helper/validator';
+import { render } from '/@/utils/common/renderUtils';
+import { getWeekMonthQuarterYear } from '/@/utils';
+import { h } from 'vue';
+import { Image } from 'ant-design-vue';
+//列表数据
+// 反馈类型0-投诉吐槽;1-功能异常;2-用户体验;3-功能建议;4-其他
+export const feedbackTypeOptions = ['投诉吐槽', '功能异常', '用户体验', '功能建议', '其他'];
+export const columns: BasicColumn[] = [
+  {
+    title: '昵称',
+    align: 'center',
+    dataIndex: 'userName',
+  },
+  {
+    title: '手机号码',
+    align: 'center',
+    dataIndex: 'phone',
+  },
+  {
+    title: '问题类型',
+    align: 'center',
+    dataIndex: 'feedbackType',
+    slots: { customRender: 'type' },
+  },
+  {
+    title: '问题描述',
+    align: 'center',
+    dataIndex: 'feedbackDescribed',
+  },
+  {
+    title: '图片',
+    align: 'center',
+    dataIndex: 'feedbackImgList',
+    slots: { customRender: 'img' },
+  },
+  {
+    title: '反馈时间',
+    align: 'center',
+    dataIndex: 'createTime',
+  },
+  {
+    title: '回复',
+    align: 'center',
+    dataIndex: 'replyContent',
+  },
+];
+//查询数据
+export const searchFormSchema: FormSchema[] = [];
+//表单数据
+export const formSchema: FormSchema[] = [
+  {
+    label: '',
+    field: 'userId',
+    component: 'Input',
+    colSlot: 'title1',
+    labelWidth: 0,
+    colProps: {
+      span: 28,
+      xs: 28,
+    },
+  },
+  // TODO 主键隐藏字段,目前写死为ID
+  {
+    label: '',
+    field: 'id',
+    component: 'Input',
+    show: false,
+  },
+  {
+    label: '昵称',
+    field: 'userName',
+    required: true,
+
+    component: 'Input',
+    span: 12,
+    componentProps: {
+      readonly: true,
+    },
+  },
+  {
+    label: '手机号码',
+    field: 'phone',
+    component: 'Input',
+    required: true,
+
+    span: 12,
+    componentProps: {
+      readonly: true,
+    },
+  },
+  {
+    label: '问题类型',
+    field: 'feedbackType',
+    component: 'Input',
+    required: true,
+
+    span: 12,
+    componentProps: {
+      readonly: true,
+    },
+  },
+  {
+    label: '问题描述',
+    field: 'feedbackDescribed',
+    component: 'Input',
+    span: 12,
+    required: true,
+
+    componentProps: {
+      readonly: true,
+    },
+  },
+  {
+    label: '图片',
+    field: 'feedbackImgList',
+    component: 'Input',
+    span: 12,
+    required: true,
+
+    componentProps: {
+      readonly: true,
+    },
+    render: ({ model, field }) => {
+      console.log(typeof model[field] == 'object', field);
+      return h(
+        'div',
+        { class: 'flex item-center justify-around' }, // 外层容器
+        model[field]?.map((img, idx) =>
+          h(Image, {
+            key: idx, // 重要:为循环项添加唯一 key
+            src: img,
+            width: 80,
+            rootClassName: 'mr-4',
+          })
+        )
+      );
+    },
+  },
+  {
+    label: '反馈时间',
+    field: 'createTime',
+    component: 'Input',
+    span: 12,
+    required: true,
+    componentProps: {
+      readonly: true,
+    },
+  },
+  {
+    label: '',
+    field: '',
+    component: 'Input',
+    colSlot: 'title2',
+    labelWidth: 0,
+  },
+  {
+    label: '回复',
+    field: 'replyContent',
+    component: 'InputTextArea',
+    span: 12,
+    required: true,
+  },
+];
+
+/**
+ * 流程表单调用这个方法获取formSchema
+ * @param param
+ */
+export function getBpmFormSchema(_formData): FormSchema[] {
+  // 默认和原始表单保持一致 如果流程中配置了权限数据,这里需要单独处理formSchema
+  return formSchema;
+}

+ 167 - 0
src/views/operationManagement/feedback/index.vue

@@ -0,0 +1,167 @@
+<template>
+  <div>
+    <!--引用表格-->
+    <BasicTable @register="registerTable" :rowSelection="rowSelection">
+      <!--插槽:table标题-->
+      <template #tableTitle>
+        <a-button type="primary" v-auth="'feedback:nm_feedback:add'" @click="handleAdd" preIcon="ant-design:plus-outlined"> 新增</a-button>
+        <a-dropdown v-if="selectedRowKeys.length > 0">
+          <!-- <template #overlay>
+            <a-menu>
+              <a-menu-item key="1" @click="batchHandleDelete">
+                <Icon icon="ant-design:delete-outlined"></Icon>
+                删除
+              </a-menu-item>
+            </a-menu>
+          </template> -->
+          <a-button v-auth="'feedback:nm_feedback:deleteBatch'"
+            >批量操作
+            <Icon icon="mdi:chevron-down"></Icon>
+          </a-button>
+        </a-dropdown>
+      </template>
+      <!--操作栏-->
+      <template #action="{ record }">
+        <TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)" />
+      </template>
+      <template #img="{ text }">
+        <TableImg :img-list="text" :size="120"></TableImg>
+      </template>
+
+      <!--字段回显插槽-->
+      <template v-slot:bodyCell="{ column, record, index, text }"> </template>
+
+      <template #type="{ text }">
+        {{ feedbackTypeOptions[text] }}
+      </template>
+    </BasicTable>
+    <!-- 表单区域 -->
+    <feedbackModal @register="registerModal" @success="handleSuccess"></feedbackModal>
+  </div>
+</template>
+
+<script lang="ts" name="feedback" setup>
+  import { ref, reactive, computed, unref } from 'vue';
+  import { BasicTable, useTable, TableAction, TableImg } from '/@/components/Table';
+  import { useModal } from '/@/components/Modal';
+  import { useListPage } from '/@/hooks/system/useListPage';
+  import feedbackModal from './components/feedbackModal.vue';
+  import { columns, searchFormSchema, feedbackTypeOptions } from './feedback.data';
+  import { list } from './feedback.api';
+  import { useUserStore } from '/@/store/modules/user';
+  import { useMessage } from '/@/hooks/web/useMessage';
+  const queryParam = reactive<any>({});
+  const checkedKeys = ref<Array<string | number>>([]);
+  const userStore = useUserStore();
+  const { createMessage } = useMessage();
+  //注册model
+  const [registerModal, { openModal }] = useModal();
+  //注册table数据
+  const { prefixCls, tableContext } = useListPage({
+    tableProps: {
+      title: 'feedback',
+      api: list,
+      columns,
+      canResize: false,
+      formConfig: {
+        //labelWidth: 120,
+        schemas: searchFormSchema,
+        autoSubmitOnEnter: true,
+        showAdvancedButton: true,
+        fieldMapToNumber: [],
+        fieldMapToTime: [],
+      },
+
+      actionColumn: {
+        width: 120,
+        fixed: 'right',
+      },
+      beforeFetch: (params) => {
+        return Object.assign(params, queryParam);
+      },
+    },
+  });
+
+  const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
+
+  /**
+   * 新增事件
+   */
+  function handleAdd() {
+    openModal(true, {
+      isUpdate: false,
+      showFooter: true,
+    });
+  }
+  /**
+   * 编辑事件
+   */
+  function handleEdit(record: Recordable) {
+    openModal(true, {
+      record,
+      isUpdate: true,
+      showFooter: true,
+    });
+  }
+  /**
+   * 详情
+   */
+  function handleDetail(record: Recordable) {
+    openModal(true, {
+      record,
+      isUpdate: true,
+      showFooter: false,
+    });
+  }
+  // /**
+  //  * 删除事件
+  //  */
+  // async function handleDelete(record) {
+  //   await deleteOne({ id: record.id }, handleSuccess);
+  // }
+  /**
+   * 成功回调
+   */
+  function handleSuccess() {
+    (selectedRowKeys.value = []) && reload();
+  }
+  /**
+   * 操作栏
+   */
+  function getTableAction(record) {
+    return [
+      {
+        label: '回复',
+        onClick: handleEdit.bind(null, record),
+        // auth: 'feedback:nm_feedback:edit',
+      },
+    ];
+  }
+  /**
+   * 下拉操作栏
+   */
+  function getDropDownAction(record) {
+    return [
+      {
+        label: '详情',
+        onClick: handleDetail.bind(null, record),
+      },
+      {
+        label: '删除',
+        popConfirm: {
+          title: '是否确认删除',
+          confirm: handleDelete.bind(null, record),
+          placement: 'topLeft',
+        },
+        // auth: 'feedback:nm_feedback:delete',
+      },
+    ];
+  }
+</script>
+
+<style lang="less" scoped>
+  :deep(.ant-picker),
+  :deep(.ant-input-number) {
+    width: 100%;
+  }
+</style>

+ 1 - 1
tsconfig.json

@@ -40,6 +40,6 @@
     "build/**/*.d.ts",
     "mock/**/*.ts",
     "vite.config.ts"
-  ],
+, "src/views/informationManagement/shopInfo/shopInfo/.data.ts"  ],
   "exclude": ["node_modules", "tests/server/**/*.ts", "dist", "**/*.js"]
 }

+ 11 - 4
types/utils.d.ts

@@ -1,9 +1,16 @@
 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]>;
 };
-export interface ZtTableColumnProps extends TableColumnProps {
-  props?: { placeholder?: string; format?: string; showTime?: boolean; step?: number };
-}
+
+export type OriginalItem = {
+  day1?: number | null;
+  day2?: number | null;
+  day3?: number | null;
+  day4?: number | null;
+  day5?: number | null;
+  day6?: number | null;
+  day7?: number | null;
+  time?: string[];
+};