浏览代码

新版发布,支持低代码模块: online表单、online报表

zhangdaiscott 2 年之前
父节点
当前提交
870892c346

+ 2 - 0
.gitignore

@@ -29,3 +29,5 @@ pnpm-debug.log*
 *.sln
 *.sw?
 /os_del.cmd
+/.vscode/
+/.history/

+ 12 - 0
docs/Vue3升级脚本_mysql.sql

@@ -259,3 +259,15 @@ INSERT INTO `sys_permission` VALUES ('d7d6e2e4e2934f2c9385a623fd98c6f3', '', '
 delete from sys_permission where id = '1449995470942593026';
 
 -- 角色授权vue3的菜单
+
+
+-- online低代码菜单(online表单、online报表)
+INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1455100420297859074', '', '在线开发', '/online', 'layouts/default/index', 1, NULL, '/online/cgform', 0, NULL, '0', 2.00, 0, 'ant-design:cloud-outlined', 0, 0, 0, 0, NULL, 'admin', '2021-11-01 17:12:29', 'admin', '2022-05-11 16:38:26', 0, 0, NULL, 0);
+INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1455101470794850305', '1455100420297859074', 'Online表单开发', '/online/cgform', 'super/online/cgform/index', 1, NULL, NULL, 1, NULL, '0', 1.00, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2021-11-01 17:16:40', 'admin', '2022-04-04 18:36:25', 0, 0, NULL, 0);
+INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1457678003102879745', '1455100420297859074', '系统编码规则', '/system/fillrule', 'system/fillRule/index', 1, NULL, NULL, 1, NULL, '0', 9.00, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2021-11-08 19:54:53', 'admin', '2021-11-18 10:49:40', 0, 0, NULL, 0);
+INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1458353686530232321', '1455100420297859074', '系统校验规则', '/system/checkrule', 'system/checkRule/index', 1, NULL, NULL, 1, NULL, '0', 15.00, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2021-11-10 16:39:48', 'admin', '2021-11-18 10:49:48', 0, 0, NULL, 0);
+INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1460888189937176577', '1455100420297859074', 'Online报表配置', '/online/cgreport', 'super/online/cgreport/index', 1, NULL, NULL, 1, NULL, '0', 2.00, 0, NULL, 1, 0, 0, 0, NULL, 'admin', '2021-11-17 16:31:01', 'admin', '2021-12-08 10:55:32', 0, 0, NULL, 0);
+INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1461270075543347202', '1455100420297859074', 'Online表单视图', '/online/copyform/:code', 'super/online/cgform/CgformCopyList', 1, NULL, NULL, 1, NULL, '0', 99.00, 0, NULL, 1, 0, 1, 0, NULL, 'admin', '2021-11-18 17:48:30', NULL, NULL, 0, 0, NULL, 0);
+INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1461291438825558017', '1455100420297859074', 'AUTO在线报表', '/online/cgreport/:id', 'super/online/cgreport/auto/OnlCgReportList', 1, NULL, NULL, 1, NULL, '0', 2.00, 0, NULL, 1, 0, 1, 0, NULL, 'admin', '2021-11-18 19:13:23', 'admin', '2021-11-19 20:16:13', 0, 0, NULL, 0);
+INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1465686870713782273', '1455100420297859074', 'AUTO在线表单', '/online/cgformList/:id', 'super/online/cgform/auto/default/OnlineAutoList', 1, NULL, NULL, 1, NULL, '0', 5.00, 0, NULL, 1, 0, 1, 0, NULL, 'admin', '2021-11-30 22:19:16', NULL, NULL, 0, 0, NULL, 0);
+INSERT INTO `sys_permission` (`id`, `parent_id`, `name`, `url`, `component`, `is_route`, `component_name`, `redirect`, `menu_type`, `perms`, `perms_type`, `sort_no`, `always_show`, `icon`, `is_leaf`, `keep_alive`, `hidden`, `hide_tab`, `description`, `create_by`, `create_time`, `update_by`, `update_time`, `del_flag`, `rule_flag`, `status`, `internal_or_external`) VALUES ('1509417558230999041', '1455100420297859074', 'AUTO树表单列表', '/online/cgformTreeList/:id', 'super/online/cgform/auto/tree/OnlineAutoTreeList', 1, NULL, NULL, 1, NULL, '0', 5.00, 0, NULL, 1, 0, 1, 0, NULL, 'admin', '2022-03-31 14:29:24', NULL, NULL, 0, 0, NULL, 0);

+ 3 - 1
package.json

@@ -10,6 +10,7 @@
     "bootstrap": "yarn install",
     "serve": "npm run dev",
     "dev": "vite",
+    "clean:cache": "rimraf node_modules/.cache/ && rimraf node_modules/.vite",
     "build": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=4096 vite build && esno ./build/script/postBuild.ts",
     "build:test": "cross-env vite build --mode test && esno ./build/script/postBuild.ts",
     "build:no-cache": "yarn clean:cache && npm run build",
@@ -18,7 +19,6 @@
     "preview": "npm run build && vite preview",
     "preview:dist": "vite preview",
     "log": "conventional-changelog -p angular -i CHANGELOG.md -s",
-    "clean:cache": "rimraf node_modules/.cache/ && rimraf node_modules/.vite",
     "clean:lib": "rimraf node_modules",
     "lint:eslint": "eslint --cache --max-warnings 0  \"{src,mock}/**/*.{vue,ts,tsx}\" --fix",
     "lint:prettier": "prettier --write  \"src/**/*.{js,json,tsx,css,less,scss,vue,html,md}\"",
@@ -34,6 +34,7 @@
     "gen:icon": "esno ./build/generate/icon/index.ts"
   },
   "dependencies": {
+    "@jeecg/online": "1.0.1",
     "@iconify/iconify": "^2.0.4",
     "@fullcalendar/core": "^5.10.1",
     "@fullcalendar/daygrid": "^5.10.1",
@@ -191,6 +192,7 @@
         "@fullcalendar/interaction",
         "@fullcalendar/timegrid",
         "@fullcalendar/vue3",
+        "@jeecg/online",
         "@vueuse/core",
         "@vueuse/shared",
         "@zxcvbn-ts/core",

+ 2 - 0
src/components/Form/src/componentMap.ts

@@ -58,6 +58,7 @@ import JSearchSelect from './jeecg/components/JSearchSelect.vue'
 import JAddInput from './jeecg/components/JAddInput.vue'
 import {Time} from '/@/components/Time';
 import JOnlineSelectCascade from './jeecg/components/JOnlineSelectCascade.vue'
+import JRangeNumber from './jeecg/components/JRangeNumber.vue'
 
 const componentMap = new Map<ComponentType, Component>();
 
@@ -125,6 +126,7 @@ componentMap.set('JUpload', JUpload);
 componentMap.set('JSearchSelect', JSearchSelect);
 componentMap.set('JAddInput', JAddInput);
 componentMap.set('JOnlineSelectCascade', JOnlineSelectCascade)
+componentMap.set('JRangeNumber', JRangeNumber)
 
 export function add(compName: ComponentType, component: Component) {
   componentMap.set(compName, component);

+ 69 - 0
src/components/Form/src/jeecg/components/JRangeNumber.vue

@@ -0,0 +1,69 @@
+<template>
+    <a-input-group>
+        <a-input :value="beginValue" style="width: calc( 50% - 15px )" placeholder="请输入最小值" @change="handleChangeBegin"/>
+        <a-input style="width: 30px; border-left: 0; pointer-events: none; background-color: #fff" placeholder="~" disabled/>
+        <a-input :value="endValue" style="width: calc( 50% - 15px );border-left: 0" placeholder="请输入最大值" @change="handleChangeEnd"/>
+    </a-input-group>
+</template>
+
+<script>
+
+  /**
+   * 查询条件用-数值范围查询
+   */
+  import {ref, watch} from 'vue'
+  export default {
+    name: "JRangeNumber",
+    props:{
+      value: {
+        type: Array,
+        default: ['','']
+      }
+    },
+    emits: ['change'],
+    setup(props, {emit}){
+      const beginValue = ref('')
+      const endValue = ref('')
+      
+      function handleChangeBegin(e) {
+        beginValue.value = e.target.value
+        emitArray()
+      }
+
+      function handleChangeEnd(e) {
+        endValue.value = e.target.value
+        emitArray()
+      }
+      
+      function emitArray(){
+        let arr = []
+        let begin = beginValue.value || ''
+        let end = endValue.value || ''
+        arr.push(begin)
+        arr.push(end)
+        emit('change', arr)
+      }
+      
+      watch(()=>props.value, (val)=>{
+        if(val.length==2){
+          beginValue.value = val[0]
+          endValue.value = val[1]
+        }else{
+          beginValue.value = ''
+          endValue.value = ''
+        }
+      });
+      
+      return {
+        beginValue,
+        endValue,
+        handleChangeBegin,
+        handleChangeEnd
+      }
+    }
+  }
+</script>
+
+<style scoped>
+
+</style>

+ 1 - 0
src/components/Form/src/types/index.ts

@@ -142,4 +142,5 @@ export type ComponentType =
     | 'JAddInput'
     | 'Time'
     | 'JOnlineSelectCascade'
+    | 'JRangeNumber'
   ;

+ 218 - 0
src/components/jeecg/OnLine/JPopupOnlReport.vue

@@ -0,0 +1,218 @@
+<template>
+  <div>
+    <div class="jeecg-basic-table-form-container" v-if="showSearchFlag">
+      <a-form ref="formRef" :model="queryParam" :label-col="labelCol" :wrapper-col="wrapperCol" @keyup.enter.native="searchQuery">
+        <a-row :gutter="24">
+          <template v-for="(item,index) in queryInfo">
+            <template v-if=" item.hidden==='1' ">
+              <a-col :md="8" :sm="24" :key=" 'query'+index " v-show="toggleSearchStatus">
+                <SearchFormItem :formElRef="formRef" :queryParam="queryParam" :item="item" :dictOptions="dictOptions"></SearchFormItem>
+              </a-col>
+            </template>
+            <template v-else>
+              <a-col :md="8" :sm="24" :key=" 'query'+index ">
+                <SearchFormItem :formElRef="formRef" :queryParam="queryParam" :item="item" :dictOptions="dictOptions"></SearchFormItem>
+              </a-col>
+            </template>
+          </template>
+
+          <a-col :md="8" :sm="8">
+                           <span style="float: left;overflow: hidden;" class="table-page-search-submitButtons">
+                                <a-col :lg="6">
+                                  <a-button type="primary" preIcon="ant-design:reload-outlined" @click="searchReset">重置</a-button>
+                                  <a-button type="primary" preIcon="ant-design:search-outlined" @click="searchQuery" style="margin-left: 8px">查询</a-button>
+                                  <a @click="handleToggleSearch" style="margin-left: 8px">
+                                    {{ toggleSearchStatus ? '收起' : '展开' }}
+                                    <Icon :icon="toggleSearchStatus ? 'ant-design:up-outlined' : 'ant-design:down-outlined'"/>
+                                  </a>
+                                </a-col>
+                           </span>
+          </a-col>
+
+        </a-row>
+      </a-form>
+    </div>
+    <BasicTable
+            ref="tableRef"
+            :canResize="true"
+            :bordered="true"
+            :loading="loading"
+            :rowKey="combineRowKey"
+            :columns="columns"
+            :showIndexColumn="false"
+            :dataSource="dataSource"
+            :pagination="pagination"
+            :rowSelection="rowSelection"
+            @row-click="clickThenCheck"
+            @change="handleChangeInTable"
+    >
+      <template #tableTitle>
+        <a-button type="primary" preIcon="ant-design:export-outlined" @click="onExportXls"> 导出</a-button>
+      </template>
+    </BasicTable>
+
+    <!-- 跳转Href的动态组件方式 -->
+    <a-modal v-bind="hrefComponent.model" v-on="hrefComponent.on">
+      <component :is="hrefComponent.is" v-bind="hrefComponent.params"/>
+    </a-modal>
+
+  </div>
+</template>
+
+<script lang="ts">
+  import { defineComponent, unref, ref, watch, watchEffect, reactive, computed } from 'vue';
+  import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
+  import { useAttrs } from '/@/hooks/core/useAttrs';
+  import { usePopBiz } from '/@/components/jeecg/OnLine/hooks/usePopBiz';
+  import { useMessage } from '/@/hooks/web/useMessage';
+
+  export default defineComponent({
+    name: 'JPopupOnlReport',
+    components: {
+      SearchFormItem: createAsyncComponent(() => import('/@/components/jeecg/OnLine/SearchFormItem.vue'), { loading: true }),
+      BasicTable: createAsyncComponent(() => import('/@/components/Table/src/BasicTable.vue'), { loading: true }),
+    },
+    props: ['multi', 'code','id', 'sorter', 'groupId', 'param', 'clickToRowSelect'],
+    emits: ['ok', 'register'],
+    setup(props, { emit, refs }) {
+      const { createMessage } = useMessage();
+      const labelCol = reactive({
+        xs: { span: 24 },
+        sm: { span: 6 },
+      });
+      const wrapperCol = reactive({
+        xs: { span: 24 },
+        sm: { span: 18 },
+      });
+      const formRef = ref();
+      const tableRef = ref();
+      const toggleSearchStatus = ref(false);
+      const attrs = useAttrs();
+      const tableScroll = ref({ x: true });
+      const getBindValue = Object.assign({}, unref(props), unref(attrs));
+      const [{ visibleChange, loadColumnsInfo, dynamicParamHandler, loadData,loadColumnsAndData, handleChangeInTable, combineRowKey, clickThenCheck, filterUnuseSelect, handleExport },
+        { hrefComponent,visible, rowSelection, checkedKeys, selectRows, pagination, dataSource, columns, loading, title, iSorter, queryInfo, queryParam, dictOptions }] = usePopBiz(getBindValue);
+
+      const showSearchFlag = computed(() => unref(queryInfo) && unref(queryInfo).length > 0);
+      /**
+       *监听code
+       */
+      watch(
+        () => props.code,
+        () => {
+          loadColumnsAndData();
+        },
+        {immediate: true}
+      );
+      /**
+       *监听popup动态参数 支持系统变量语法
+       */
+      watch(
+        () => props.param,
+        () => {
+          if (visible) {
+            dynamicParamHandler();
+            //loadData();
+          }
+        },
+      );
+      /**
+       *监听sorter排序字段
+       */
+      watchEffect(() => {
+        if (props.sorter) {
+          let arr = props.sorter.split('=');
+          if (arr.length === 2 && ['asc', 'desc'].includes(arr[1].toLowerCase())) {
+            iSorter.value = { column: arr[0], order: arr[1].toLowerCase() };
+            // 排序字段受控
+            unref(columns).forEach(col => {
+              if (col.dataIndex === unref(iSorter).column) {
+                col['sortOrder'] = unref(iSorter).order === 'asc' ? 'ascend' : 'descend';
+              } else {
+                col['sortOrder'] = false;
+              }
+            });
+          } else {
+            console.warn('【JPopup】sorter参数不合法');
+          }
+        }
+      });
+
+      function handleToggleSearch() {
+        toggleSearchStatus.value = !unref(toggleSearchStatus);
+      }
+
+
+      /**
+       * 导出excel
+       */
+      function onExportXls() {
+        handleExport!()
+      }
+
+      /**
+       * 查询
+       */
+      function searchQuery() {
+        loadData(1);
+      }
+
+      /**
+       * 重置
+       */
+      function searchReset() {
+        queryParam.value = {};
+        loadData(1);
+      }
+
+      return {
+        attrs,
+
+        tableScroll,
+        dataSource,
+        pagination,
+        columns,
+        rowSelection,
+        checkedKeys,
+        loading,
+        title,
+        hrefComponent,
+
+        clickThenCheck,
+        loadData,
+        combineRowKey,
+        handleChangeInTable,
+        visibleChange,
+        queryInfo,
+        queryParam,
+        tableRef,
+        formRef,
+        labelCol,
+        wrapperCol,
+        dictOptions,
+        showSearchFlag,
+        toggleSearchStatus,
+        handleToggleSearch,
+        searchQuery,
+        searchReset,
+        onExportXls
+      };
+    },
+  });
+</script>
+
+<style lang="less" scoped>
+  .jeecg-basic-table-form-container {
+    padding: 0px;
+
+    .table-page-search-submitButtons {
+      display: block;
+      margin-bottom: 0;
+      white-space: nowrap;
+    }
+  }
+
+  :deep .jeecg-basic-table .ant-table-wrapper .ant-table-title {
+    min-height: 0;
+  }
+</style>

+ 289 - 0
src/components/jeecg/OnLine/SearchFormItem.vue

@@ -0,0 +1,289 @@
+<template>
+    <a-form-item v-if="item.view===DateTypeEnum.Date" :labelCol="labelCol" :class="'jeecg-online-search'" >
+        <template #label>
+            <span :title="item.label" class="label-text">{{ item.label }}</span>
+        </template>
+        <template v-if="single_mode===item.mode">
+            <a-date-picker :showTime="false" valueFormat="YYYY-MM-DD" :placeholder=" '请选择'+item.label " v-model:value="queryParam[item.field]"></a-date-picker>
+        </template>
+        <template v-else>
+            <a-date-picker :showTime="false" valueFormat="YYYY-MM-DD" placeholder="开始日期" v-model:value="queryParam[item.field+'_begin']" style="width: calc(50% - 15px);"></a-date-picker>
+            <span class="group-query-strig">~</span>
+            <a-date-picker :showTime="false" valueFormat="YYYY-MM-DD" placeholder="结束日期" v-model:value="queryParam[item.field+'_end']" style="width: calc(50% - 15px);"></a-date-picker>
+        </template>
+    </a-form-item>
+
+    <a-form-item v-else-if="item.view===DateTypeEnum.Datetime" :labelCol="labelCol" :class="'jeecg-online-search'">
+        <template #label :title="item.label">
+            <span :title="item.label" class="label-text">{{ item.label }}</span>
+        </template>
+        <template v-if="single_mode===item.mode">
+            <a-date-picker :placeholder=" '请选择'+item.label " :show-time="true" valueFormat="YYYY-MM-DD HH:mm:ss" v-model:value="queryParam[item.field]"></a-date-picker>
+        </template>
+        <template v-else>
+            <a-date-picker placeholder="1选择开始时间" :show-time="true" valueFormat="YYYY-MM-DD HH:mm:ss" v-model:value="queryParam[item.field+'_begin']" style="width: calc(50% - 9px);min-width: 60px;"></a-date-picker>
+            <span class="group-query-strig" style="width: auto;padding: 0 4px;">~</span>
+            <a-date-picker placeholder="2选择结束时间" :show-time="true" valueFormat="YYYY-MM-DD HH:mm:ss" v-model:value="queryParam[item.field+'_end']" style="width: calc(50% - 9px);min-width: 60px;"></a-date-picker>
+        </template>
+    </a-form-item>
+
+    <a-form-item v-else-if="item.view===DateTypeEnum.Time" :labelCol="labelCol" :class="'jeecg-online-search'">
+        <template #label>
+            <span :title="item.label" class="label-text">{{ item.label }}</span>
+        </template>
+        <template v-if="single_mode===item.mode">
+            <a-date-picker :placeholder=" '请选择'+item.label " mode="time" valueFormat="HH:mm:ss" v-model:value="queryParam[item.field]"></a-date-picker>
+        </template>
+        <template v-else>
+            <a-date-picker placeholder="请选择开始时间" mode="time" valueFormat="HH:mm:ss" v-model:value="queryParam[item.field+'_begin']" style="width: calc(50% - 15px);"></a-date-picker>
+            <span class="group-query-strig">~</span>
+            <a-date-picker placeholder="请选择结束时间" mode="time" valueFormat="HH:mm:ss" v-model:value="queryParam[item.field+'_end']" style="width: calc(50% - 15px);"></a-date-picker>
+        </template>
+    </a-form-item>
+
+    <a-form-item v-else-if=" item.view===CompTypeEnum.List || item.view===CompTypeEnum.Radio || item.view===CompTypeEnum.Switch" :labelCol="labelCol" :class="'jeecg-online-search'">
+        <template #label>
+            <span :title="item.label" class="label-text">{{ item.label }}</span>
+        </template>
+        <JDictSelectTag
+                v-if="item.config==='1'"
+                :placeholder=" '请选择'+item.label "
+                v-model="queryParam[item.field]"
+                :dictCode="getDictCode(item)">
+        </JDictSelectTag>
+        <a-select v-else :placeholder=" '请选择'+item.label " v-model:value="queryParam[item.field]">
+            <template v-for="(obj,index) in dictOptions[getDictOptionKey(item)]" :key="index" >
+                <a-select-option :value="obj.value"> {{ obj.text }}</a-select-option>
+            </template>
+        </a-select>
+    </a-form-item>
+
+    <a-form-item v-else-if=" item.view===CompTypeEnum.SelTree " :labelCol="labelCol" :class="'jeecg-online-search'">
+        <template #label>
+            <span :title="item.label" class="label-text">{{ item.label }}</span>
+        </template>
+        <JTreeSelect
+                :placeholder=" '请选择'+item.label "
+                v-model:value="queryParam[item.field]"
+                :dict="item.dict"
+                :pidField="item.pidField"
+                :pidValue="item.pidValue"
+                :hasChildField="item.hasChildField"
+                load-triggle-change>
+        </JTreeSelect>
+    </a-form-item>
+
+    <a-form-item v-else-if=" item.view===CompTypeEnum.CatTree " :labelCol="labelCol" :class="'jeecg-online-search'">
+        <template #label>
+            <span :title="item.label" class="label-text">{{ item.label }}</span>
+        </template>
+        <JCategorySelect :pcode="item.pcode" v-model:value="queryParam[item.field]" :placeholder=" '请选择'+item.label "/>
+    </a-form-item>
+
+    <a-form-item v-else-if=" item.view===CompTypeEnum.SelSearch" :labelCol="labelCol" :class="'jeecg-online-search'">
+        <template #label>
+            <span :title="item.label" class="label-text">{{ item.label }}</span>
+        </template>
+        <JDictSelectTag
+                v-if="item.config==='1'"
+                v-model:value="queryParam[item.field]"
+                :placeholder=" '请选择'+item.label "
+                :dict="getDictCode(item)">
+        </JDictSelectTag>
+        <!--TODO 新需要的组件-->
+       <!-- <j-online-search-select
+                v-else
+                :ref="item.field+'_search'"
+                v-model="queryParam[item.field]"
+                :placeholder=" '请选择'+item.label "
+                :sql="getSqlByDictCode(item)">
+        </j-online-search-select>-->
+
+    </a-form-item>
+
+    <a-form-item v-else-if=" item.view===CompTypeEnum.SelUser" :labelCol="labelCol" :class="'jeecg-online-search'">
+        <template #label>
+            <span :title="item.label" class="label-text">{{ item.label }}</span>
+        </template>
+        <JSelectUserByDept v-model:value="queryParam[item.field]" :placeholder=" '请选择'+item.label " ></JSelectUserByDept>
+    </a-form-item>
+
+    <a-form-item v-else-if=" item.view==CompTypeEnum.SelDepart" :labelCol="labelCol" :class="'jeecg-online-search'">
+        <template #label>
+            <span :title="item.label" class="label-text">{{ item.label }}</span>
+        </template>
+        <JSelectDept v-model:value="queryParam[item.field]" :placeholder=" '请选择'+item.label "/>
+    </a-form-item>
+
+    <a-form-item v-else-if=" item.view===CompTypeEnum.Popup" :labelCol="labelCol" :class="'jeecg-online-search'">
+        <template #label>
+            <span :title="item.label" class="label-text">{{ item.label }}</span>
+        </template>
+        <JPopup
+            :placeholder=" '请选择'+item.label "
+            v-model:value="queryParam[item.field]"
+            :formElRef="formElRef"
+            :code="item.dictTable"
+            :field-config="item.dictCode"
+            :multi="true"/>
+    </a-form-item>
+
+    <a-form-item v-else-if=" item.view=== CompTypeEnum.Pca" :labelCol="labelCol" :class="'jeecg-online-search'">
+        <template #label>
+            <span :title="item.label" class="label-text">{{ item.label }}</span>
+        </template>
+        <JAreaLinkage :placeholder=" '请选择'+item.label " v-model:value="queryParam[item.field]"/>
+    </a-form-item>
+    <!--TODO 缺少的组件-->
+    <a-form-item v-else-if=" item.view===CompTypeEnum.Checkbox || item.view===CompTypeEnum.ListMulti" :labelCol="labelCol" :label="item.label" :class="'jeecg-online-search'">
+       <!-- <j-select-multiple
+                v-if="item.config==='1'"
+                :placeholder=" '请选择'+item.label "
+                v-model="queryParam[item.field]"
+                :dictCode="getDictCode(item)">
+        </j-select-multiple>
+        <j-select-multiple
+                v-else
+                :placeholder=" '请选择'+item.label "
+                :options="dictOptions[item.dbField]"
+                v-model="queryParam[item.field]">
+        </j-select-multiple>-->
+    </a-form-item>
+
+    <a-form-item v-else :labelCol="labelCol" :class="'jeecg-online-search'">
+        <template #label>
+            <span :title="item.label" class="label-text">{{ item.label }}</span>
+        </template>
+        <template v-if="single_mode===item.mode">
+            <a-input :placeholder=" '请输入'+item.label " v-model:value="queryParam[item.field]"></a-input>
+        </template>
+        <template v-else>
+            <a-input :placeholder=" '请输入开始'+item.label " v-model:value="queryParam[item.field+'_begin']" style="width: calc(50% - 15px);"></a-input>
+            <span class="group-query-strig">~</span>
+            <a-input :placeholder=" '请输入结束'+item.label " v-model:value="queryParam[item.field+'_end']" style="width: calc(50% - 15px);"></a-input>
+        </template>
+    </a-form-item>
+
+</template>
+
+<script lang="ts">
+    //import JOnlineSearchSelect from '@/components/online/autoform/comp/JOnlineSearchSelect'
+    import {defineComponent, ref} from 'vue';
+    import {DateTypeEnum} from '/@/enums/DateTypeEnum.ts';
+    import {CompTypeEnum} from '/@/enums/CompTypeEnum.ts';
+    import {
+        JDictSelectTag,
+        JTreeSelect,
+        JCategorySelect,
+        JSelectUserByDept,
+        JSelectDept,
+        JPopup,
+        JAreaLinkage
+    } from '/@/components/Form'
+    export default defineComponent({
+        name: 'JPopupOnlReport',
+        components: {
+            //JOnlineSearchSelect
+            JDictSelectTag,
+            JTreeSelect,
+            JCategorySelect,
+            JSelectUserByDept,
+            JSelectDept,
+            JPopup,
+            JAreaLinkage
+        },
+        props: {
+            formElRef: {
+                type: Object,
+                default: () => {
+                },
+            },
+            item: {
+                type: Object,
+                default: () => {
+                },
+                required: true
+            },
+            dictOptions: {
+                type: Object,
+                default: () => {
+                },
+                required: true
+            },
+            queryParam: {
+                type: Object,
+                default: () => {
+                },
+                required: true
+            }
+        },
+        setup(props) {
+            const single_mode = ref("single");
+            console.log("dictOptions===>",props.dictOptions)
+            function getDictCode(item) {
+                if (item.dictTable && item.dictTable.length > 0) {
+                    return item.dictTable + ',' + item.dictText + ',' + item.dictCode
+                } else {
+                    return item.dictCode
+                }
+            }
+
+            function getSqlByDictCode(item) {
+                let {dictTable, dictCode, dictText} = item;
+                let temp = dictTable.toLowerCase();
+                let arr = temp.split('where');
+                let condition = '';
+                if (arr.length > 1) {
+                    condition = ' where' + arr[1]
+                }
+                let sql = "select " + dictCode + " as 'value', " + dictText + " as 'text' from " + arr[0] + condition;
+                console.log('sql', sql);
+                return sql;
+            }
+
+            function getDictOptionKey(item) {
+                if(item.dbField){
+                  return item.dbField
+                }else{
+                  return item.field
+                }
+            }
+
+            // 定义查询条件 文本label的最大宽度 比起单纯的控制字体个数更好
+            const labelTextMaxWidth = '120px'
+            const labelCol = {
+              style:{
+                'max-width': labelTextMaxWidth
+              }
+            }
+            return {labelTextMaxWidth, labelCol, single_mode,getDictOptionKey,getDictCode, getSqlByDictCode,DateTypeEnum,CompTypeEnum}
+        }
+    })
+
+</script>
+
+<style lang="less" scoped>
+    .group-query-strig {
+        width: 30px;
+        text-align: center;
+        display: inline-block;
+    }
+
+    /* 查询条件左对齐样式设置 */
+    .jeecg-online-search ::v-deep .ant-form-item-label{
+        flex: 0 0 auto !important;
+        width: auto;
+    }
+    .jeecg-online-search ::v-deep .ant-form-item-control{
+        max-width: 100%;
+    }
+
+    /* label显示宽度 超出显示... */
+    .jeecg-online-search ::v-deep .label-text{
+        max-width: v-bind(labelTextMaxWidth);
+        overflow: hidden;
+        white-space: nowrap;
+        text-overflow: ellipsis;
+        overflow-wrap: break-word;
+    }
+</style>

+ 746 - 0
src/components/jeecg/OnLine/hooks/usePopBiz.ts

@@ -0,0 +1,746 @@
+import {reactive, ref, unref, defineAsyncComponent } from "vue";
+import {httpGroupRequest} from "/@/components/Form/src/utils/GroupRequest";
+import {defHttp} from '/@/utils/http/axios';
+import {filterMultiDictText} from '/@/utils/dict/JDictSelectUtil.js';
+import {useMessage} from '/@/hooks/web/useMessage';
+import { OnlineColumn } from '/@/components/jeecg/OnLine/types/onlineConfig'
+import { h } from 'vue'
+import { useRouter } from 'vue-router';
+import {useMethods} from '/@/hooks/system/useMethods';
+
+export function usePopBiz(props,tableRef?) {
+
+    const {createMessage} = useMessage();
+    //弹窗可视状态
+    const visible = ref(false);
+    //表格加载
+    const loading = ref(false);
+    //cgRpConfigId
+    const cgRpConfigId = ref('');
+    //标题
+    const title = ref('列表');
+    // 排序字段,默认无排序
+    const iSorter = ref<any>('');
+    // 查询对象
+    const queryInfo = ref([]);
+    // 查询参数
+    const queryParam = ref<any>({});
+    // 动态参数
+    const dynamicParam = ref<any>({});
+    //字典配置项
+    const dictOptions = ref({});
+    //数据集
+    const dataSource = ref<Array<object>>([]);
+    //定义表格信息
+    const columns = ref<Array<object>>([]);
+    //定义请求url信息
+    const configUrl = reactive({
+        //列表页加载column和data
+        getColumnsAndData: '/online/cgreport/api/getColumnsAndData/',
+        getColumns: '/online/cgreport/api/getRpColumns/',
+        getData: '/online/cgreport/api/getData/',
+        getQueryInfo: '/online/cgreport/api/getQueryInfo/',
+        export: '/online/cgreport/api/exportXls/'
+    });
+    //已选择的值
+    const checkedKeys = ref<Array<string | number>>([]);
+    //选择的行记录
+    const selectRows = ref<Array<any>>([]);
+    // 点击单元格选中行 popup需要 但是报表预览不需要
+    let clickThenCheckFlag = true
+    if(props.clickToRowSelect===false){
+      clickThenCheckFlag = false
+    }
+
+    /**
+     * 选择列配置
+     */
+    const rowSelection = {
+        fixed: true,
+        selectedRowKeys: checkedKeys,
+        selectionRows: selectRows,
+        onChange: onSelectChange
+    }
+
+    /**
+     * 序号列配置
+     */
+    const indexColumnProps = {
+        dataIndex: 'index',
+        width: '15px',
+    };
+    /**
+     * 分页配置
+     */
+    const pagination = reactive({
+        current: 1,
+        pageSize: 10,
+        pageSizeOptions: ['10', '20', '30'],
+        // showTotal: (total, range) => {
+        //     return range[0] + '-' + range[1] + ' 共' + total + '条'
+        // },
+        showQuickJumper: true,
+        showSizeChanger: true,
+        total: 0,
+        // 合计逻辑 [待优化 3.0]
+        showTotal: (total) => onShowTotal(total),
+        realPageSize: 10,
+        realTotal: 0,
+        // 是否有合计列,默认为"",在第一次获取到数据之后会设计为ture或者false
+        isTotal: <string | boolean> '',
+        onShowSizeChange: (current, pageSize) => onSizeChange(current, pageSize)
+    });
+
+    /**
+     * 表格选择事件
+     * @param selectedRowKeys
+     * @param selectRow
+     */
+    function onSelectChange(selectedRowKeys: (string | number)[]) {
+        if (!selectedRowKeys || selectedRowKeys.length == 0) {
+            selectRows.value = []
+        } else {
+            for (let i = 0; i < selectedRowKeys.length; i++) {
+                let combineKey = combineRowKey(getRowByKey(selectedRowKeys[i]));
+                let keys = unref(checkedKeys);
+                if (combineKey && keys.indexOf(combineKey) < 0) {
+                    let row = getRowByKey(selectedRowKeys[i]);
+                    row && selectRows.value.push(row)
+                }
+            }
+        }
+        checkedKeys.value = selectedRowKeys;
+    }
+
+    /**
+     * 过滤没用选项
+     * @param selectedRowKeys
+     */
+    function filterUnuseSelect() {
+        selectRows.value = unref(selectRows).filter(item=>{
+            let combineKey = combineRowKey(item);
+            return unref(checkedKeys).indexOf(combineKey)>=0
+        })
+    }
+
+    /**
+     * 根据key获取row信息
+     * @param key
+     */
+    function getRowByKey(key) {
+        let row = unref(dataSource).filter(record => combineRowKey(record) === key);
+        return row&&row.length>0?row[0]:'';
+    }
+
+    /**
+     * 加载rowKey
+     */
+    function combineRowKey(record) {
+        let res = record?.id || '';
+        Object.keys(record).forEach(key => {
+            res = (key =='rowIndex') ? (record[key] + res) : (res + record[key]);
+        });
+        res = res.length > 50 ? res.substring(0, 50) : res;
+        return res
+    }
+
+    /**
+     * 加载列信息
+     */
+    function loadColumnsInfo() {
+        let url = `${configUrl.getColumns}${props.code}`;
+        //缓存key
+        let groupIdKey = props.groupId ? `${props.groupId}${url}` : '';
+        httpGroupRequest(() => defHttp.get({url}, {isTransformResponse: false, successMessageMode: 'none'}), groupIdKey).then(res => {
+            if (res.success) {
+                initDictOptionData(res.result.dictOptions);
+                cgRpConfigId.value = res.result.cgRpConfigId;
+                title.value = res.result.cgRpConfigName;
+                let currColumns = res.result.columns;
+                for (let a = 0; a < currColumns.length; a++) {
+                    if (currColumns[a].customRender) {
+                        let dictCode = currColumns[a].customRender;
+                        currColumns[a].customRender = ({text}) => {
+                            return filterMultiDictText(unref(dictOptions)[dictCode], text + "");
+                        }
+                    }
+                    // 排序字段受控
+                    if (unref(iSorter) && currColumns[a].dataIndex === unref(iSorter).column) {
+                        currColumns[a].sortOrder = unref(iSorter).order === 'asc' ? 'ascend' : 'descend'
+                    }
+                }
+                if (currColumns[0].key !== 'rowIndex') {
+                  currColumns.unshift({
+                      title: '序号',
+                      dataIndex: 'rowIndex',
+                      key:'rowIndex',
+                      width:60,
+                      align:"center",
+                      customRender:function ({text}) {
+                          return parseInt(text)+1;
+                      }
+                  });
+                }
+                columns.value = [...currColumns];
+                initQueryInfo(null)
+            }
+        })
+    }
+
+
+  /**
+   * 加载列和数据[列表专用]
+   */
+  function loadColumnsAndData() {
+    // 第一次加载 置空isTotal 在这里调用确保 该方法只是进入页面后 加载一次 其余查询不走该方法
+    pagination.isTotal = ''
+    let url = `${configUrl.getColumnsAndData}${props.id}`;
+    //缓存key
+    let groupIdKey = props.groupId ? `${props.groupId}${url}` : '';
+    httpGroupRequest(() => defHttp.get({url}, {isTransformResponse: false, successMessageMode: 'none'}), groupIdKey).then(res => {
+      if (res.success) {
+        initDictOptionData(res.result.dictOptions);
+        cgRpConfigId.value = props.id;
+        let { columns: metaColumnList, cgreportHeadName, fieldHrefSlots, isGroupTitle } = res.result;
+        title.value = cgreportHeadName;
+        // href 跳转
+        const fieldHrefSlotKeysMap = {}
+        fieldHrefSlots.forEach(item => fieldHrefSlotKeysMap[item.slotName] = item)
+        let currColumns = handleColumnHrefAndDict(metaColumnList, fieldHrefSlotKeysMap)
+
+        // popup需要序号, 普通列表不需要
+        if(clickThenCheckFlag===true){
+          currColumns.unshift({
+            title: '序号',
+            dataIndex: 'rowIndex',
+            key:'rowIndex',
+            width:60,
+            align:"center",
+            customRender:function ({text}) {
+              return parseInt(text)+1;
+            }
+          });
+        }
+
+        // 合并表头
+        if(isGroupTitle === true){
+          currColumns = handleGroupTitle(currColumns);
+        }
+        columns.value = [...currColumns];
+        initQueryInfo(res.result.data)
+      }else{
+        //update-begin-author:taoyan date:20220401 for: VUEN-583【vue3】JeecgBootException: sql黑名单校验不通过,请联系管理员!,前台无提示
+        createMessage.warning(res.message)
+        //update-end-author:taoyan date:20220401 for: VUEN-583【vue3】JeecgBootException: sql黑名单校验不通过,请联系管理员!,前台无提示
+      }
+    })
+  }
+
+  /**
+   * 处理求和的列 合计逻辑 [待优化 3.0]
+   */
+  function handleSumColumn(metaColumnList: OnlineColumn[], dataTotal: number):void {
+    // 获取需要合计列的dataIndex
+    let sumColumnList = getNeedSumColumns(metaColumnList);
+    // 判断是否为第一次获取数据,如果是的话,则需要重新设置pageSize
+    if (pagination.isTotal == '') {
+      if (sumColumnList.length > 0) {
+        pagination.isTotal = true
+        // 有合计字段时,每次最多查询原pageSize-1条记录,另外需要第一次时将查询的10条中删除最后一条
+        // 删除最后一条数据 如果第一次得到的数据长度等于pageSize的话,则删除最后一条
+        if (dataSource.value.length == pagination.pageSize) {
+          let remove_data = dataSource.value.pop()
+        }
+        pagination.realPageSize = pagination.pageSize - 1
+      } else {
+        pagination.isTotal = false
+      }
+    }
+    // 需要添加合计字段
+    if (pagination.isTotal) {
+      let totalRow = { }
+      sumColumnList.forEach(dataIndex => {
+        let count = 0
+        dataSource.value.forEach(row => {
+          // 统计去除null及空数据
+          if(row[dataIndex] != null && row[dataIndex] != ''){
+            count += parseFloat(row[dataIndex])
+          }
+        })
+        totalRow[dataIndex] = isNaN(count) ? '包含非数字内容' : count.toFixed(2)
+
+        // 长整形时合计不显示.00后缀
+        let v = metaColumnList.find(v=>v.dataIndex==dataIndex);
+        if(v && v.fieldType == 'Long'){
+          totalRow[dataIndex] =  parseInt(totalRow[dataIndex]);
+        }
+      })
+      dataSource.value.push(totalRow)
+      pagination.realTotal = dataTotal
+      pagination.total = Number(dataTotal) + Number(Math.floor(dataTotal/pagination.realPageSize))
+    }
+  }
+
+  /**
+   * 获取需要求和的列 dataIndex
+   * @param columns
+   */
+  function getNeedSumColumns(columns: OnlineColumn[]):string[]{
+    let arr:string[] = []
+    for(let column of columns){
+      if (column.isTotal === '1') {
+        arr.push(column.dataIndex!)
+        if(column.children && column.children.length>0){
+          let subArray = getNeedSumColumns(column.children)
+          if(subArray.length>0){
+            arr.push(...subArray)
+          }
+        }
+      }
+    }
+    return arr;
+  }
+
+  /**
+   * 处理列的href和字典翻译
+   */
+  function handleColumnHrefAndDict(columns: OnlineColumn[], fieldHrefSlotKeysMap:{}):OnlineColumn[] {
+    for(let column of columns){
+      let { customRender, hrefSlotName, fieldType } = column
+      // online 报表中类型配置为日期(yyyy-MM-dd ),但是实际展示为日期时间格式(yyyy-MM-dd HH:mm:ss) issues/3042
+      if(fieldType=='Date'){
+        column.customRender = ({text}) => {
+          if(!text){
+            return ''
+          }
+          if(text.length>10){
+            return text.substring(0, 10)
+          }
+          return text;
+        }
+      }else{
+        if (!hrefSlotName && (column.scopedSlots && column.scopedSlots.customRender)) {
+          //【Online报表】字典和href互斥 这里通过fieldHrefSlotKeysMap 先找到是href的列
+          if (fieldHrefSlotKeysMap.hasOwnProperty(column.scopedSlots.customRender)) {
+            hrefSlotName = column.scopedSlots.customRender
+          }
+        }
+        // 如果 customRender 有值则代表使用了字典
+        // 如果 hrefSlotName 有值则代表使用了href跳转
+        // 两者可以兼容。兼容的具体思路为:先获取到字典替换的值,再添加href链接跳转
+        if (customRender || hrefSlotName) {
+          let dictCode = customRender as string
+          let replaceFlag = '_replace_text_'
+          column.customRender = ({text, record}) => {
+            let value = text
+            // 如果 dictCode 有值,就进行字典转换
+            if (dictCode) {
+              if (dictCode.startsWith(replaceFlag)) {
+                let textFieldName = dictCode.replace(replaceFlag, '')
+                value = record[textFieldName]
+              } else {
+                value = filterMultiDictText(unref(dictOptions)[dictCode], text + "");
+              }
+            }
+            // 扩展参数设置列的内容长度
+            if(column.showLength){
+              if(value && value.length>column.showLength){
+                value = value.substr(0, column.showLength)+'...'
+              }
+            }
+            // 如果 hrefSlotName 有值,就生成一个 a 标签,包裹住字典替换后(或原生)的值
+            if (hrefSlotName) {
+              let field = fieldHrefSlotKeysMap[hrefSlotName]
+              if (field) {
+                return h('a', {
+                  onClick: ()=>handleClickFieldHref(field, record)
+                }, value)
+              }
+            }
+            return value
+          }
+        }
+      }
+    }
+    return columns;
+  }
+
+  /**
+   * 处理合并表头
+   * @param columns
+   */
+  function handleGroupTitle(columns: OnlineColumn[]):OnlineColumn[]{
+    let newColumns:OnlineColumn[] = []
+    for(let column of columns){
+      //排序字段受控  ---- 此逻辑为新增逻辑 待
+      if (unref(iSorter) && column.dataIndex === unref(iSorter).column) {
+        column.sortOrder = unref(iSorter).order === 'asc' ? 'ascend' : 'descend'
+      }
+      //判断字段是否需要合并表头
+      if (column.groupTitle) {
+        let clIndex = newColumns.findIndex(im => im.title === column.groupTitle)
+        if (clIndex !== -1) {
+          //表头已存在直接push children
+          newColumns[clIndex].children!.push(column)
+        } else {
+          //表头不存在组装表头信息
+          let clGroup:OnlineColumn = {},child:OnlineColumn[] = []
+          child.push(column)
+          clGroup.title = column.groupTitle
+          clGroup.align = 'center'
+          clGroup.children = child
+          newColumns.push(clGroup)
+        }
+      } else {
+        newColumns.push(column)
+      }
+    }
+    return newColumns;
+  }
+
+  // 获取路由器对象 href跳转用到
+  let router = useRouter();
+  /**
+   * href 点击事件
+   * @param field
+   * @param record
+   */
+  function handleClickFieldHref(field, record) {
+    let href = field.href
+    let urlPattern = /(ht|f)tp(s?)\:\/\/[0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*(:(0-9)*)*(\/?)([a-zA-Z0-9\-\.\?\,\'\/\\\+&amp;%\$#_]*)?/
+    let compPattern = /\.vue(\?.*)?$/
+    let jsPattern =  /{{([^}]+)}}/g // {{ xxx }}
+    if (typeof href === 'string') {
+      href = href.trim().replace(/\${([^}]+)?}/g, (s1, s2) => record[s2])
+      // 执行 {{...}} JS增强语句
+      if (jsPattern.test(href)) {
+        href = href.replace(jsPattern, function (text, s0) {
+          try {
+            return eval(s0)
+          } catch (e) {
+            console.error(e)
+            return text
+          }
+        })
+      }
+      if (urlPattern.test(href)) {
+        window.open(href, '_blank')
+      } else if (compPattern.test(href)) {
+        // 处理弹框
+        openHrefCompModal(href)
+      } else {
+        router.push(href)
+      }
+    }
+  }
+
+  /**
+   * 导出
+   */
+  function handleExport() {
+    const {handleExportXls} = useMethods();
+    let url = `${configUrl.export}${cgRpConfigId.value}`;
+    let params = getQueryParams();//查询条件
+    handleExportXls(title.value , url, params)
+  }
+
+
+  /**
+   * 合计逻辑 [待优化 3.0]
+   * 分页 大小改变事件
+   * @param _current
+   * @param size
+   */
+  function onSizeChange(_current, size){
+    pagination.isTotal = '';
+    pagination.pageSize = size;
+    if(pagination.isTotal){
+      pagination.realPageSize = size - 1;
+    }else{
+      pagination.realPageSize = size
+    }
+    pagination.current = 1;
+  }
+
+  /**
+   *  合计逻辑 [待优化 3.0]
+   * 显示总条数
+   * @param total
+   */
+  function onShowTotal(total) {
+    // 重新根据是否有合计计算每页显示的数据
+    let start = (pagination.current - 1) * pagination.realPageSize + 1
+    let end = start + (pagination.isTotal ? dataSource.value.length - 1 : dataSource.value.length) - 1
+    let realTotal = pagination.isTotal ? pagination.realTotal : total
+    return start + '-' + end + ' 共' + realTotal + '条'
+  }
+
+    /**
+     * 弹出框显示隐藏触发事件
+     */
+    async function visibleChange($event) {
+        visible.value = $event;
+        $event && loadColumnsInfo();
+    }
+
+   /**
+   * 初始化查询条件
+   * @param data 数据结果集
+   */
+    function initQueryInfo(data) {
+        let url = `${configUrl.getQueryInfo}${unref(cgRpConfigId)}`
+        //缓存key
+        let groupIdKey = props.groupId ? `${props.groupId}${url}` : '';
+        httpGroupRequest(() => defHttp.get({url}, {isTransformResponse: false, successMessageMode: 'none'}), groupIdKey).then((res) => {
+            // console.log("获取查询条件", res);
+            if (res.success) {
+                dynamicParamHandler(res.result);
+                queryInfo.value = res.result;
+                console.log("queryInfo==>",queryInfo.value)
+                //查询条件加载后再请求数据
+                if(data){
+                   setDataSource(data)
+                }else{
+                   //没有传递data时查询数据
+                   loadData(1);
+                }
+
+            } else {
+                createMessage.warning(res.message)
+            }
+        })
+    }
+
+    /**
+     * 加载表格数据
+     * @param arg
+     */
+    function loadData(arg?) {
+        if (arg == 1) {
+            pagination.current = 1
+        }
+        let params = getQueryParams();//查询条件
+        loading.value = true;
+        let url = `${configUrl.getData}${unref(cgRpConfigId)}`
+        //缓存key
+        let groupIdKey = props.groupId ? `${props.groupId}${url}${JSON.stringify(params)}` : '';
+        httpGroupRequest(() => defHttp.get({url, params}, {isTransformResponse: false, successMessageMode: 'none'}), groupIdKey).then(res => {
+            loading.value = false;
+            let data = res.result;
+            console.log("表格信息:", data)
+            setDataSource(data);
+        })
+    }
+
+   /**
+   * 设置dataSource
+   */
+    function setDataSource(data) {
+      if (data) {
+        pagination.total = Number(data.total)
+        let currentPage = pagination?.current??1;
+        for (let a = 0; a < data.records.length; a++) {
+          if (!data.records[a].rowIndex) {
+            data.records[a].rowIndex = a+(currentPage-1)*10;
+          }
+        }
+        dataSource.value = data.records
+      } else {
+        pagination.total = 0;
+        dataSource.value = [];
+      }
+      // 合计逻辑 [待优化 3.0]
+      handleSumColumn(columns.value, pagination.total)
+    }
+
+    /**
+     * 获取查询参数
+     */
+    function getQueryParams() {
+        let paramTarget = {};
+        if (unref(dynamicParam)) {
+            //处理自定义参数
+            Object.keys(unref(dynamicParam)).map(key => {
+                paramTarget['self_' + key] = unref(dynamicParam)[key]
+            })
+        }
+        let param = Object.assign(paramTarget, unref(queryParam), unref(iSorter));
+        param.pageNo = pagination.current;
+        // 合计逻辑 [待优化 3.0]
+        //  实际查询时不使用table组件的pageSize,而使用自定义的realPageSize,realPageSize会在第一次获取到数据后变化
+        param.pageSize = pagination.realPageSize;
+        return filterObj(param);
+    }
+
+    /**
+     * 处理动态参数
+     */
+    function dynamicParamHandler(arr?) {
+        if (arr && arr.length > 0) {
+            //第一次加载查询条件前 初始化queryParam为空对象
+            let queryTemp = {};
+            for (let item of arr) {
+                if (item.mode === 'single') {
+                    queryTemp[item.field] = ''
+                }
+            }
+            queryParam.value = {...queryTemp}
+        }
+        let dynamicTemp = {};
+        if (props.param) {
+            Object.keys(props.param).map(key => {
+                let str = props.param[key];
+                if (key in queryParam) {
+                    if (str && str.startsWith("'") && str.endsWith("'")) {
+                        str = str.substring(1, str.length - 1)
+                    }
+                    //如果查询条件包含参数 设置值
+                    unref(queryParam)[key] = str
+                }
+                dynamicTemp[key] = props.param[key]
+            })
+        }
+        dynamicParam.value = {...dynamicTemp}
+    }
+
+    /**
+     * 分页
+     * @param page
+     * @param filters
+     * @param sorter
+     */
+    function handleChangeInTable(page, filters, sorter) {
+        console.log(page, filters, sorter);
+        //分页、排序、筛选变化时触发
+        if (Object.keys(sorter).length > 0) {
+            iSorter.value = {
+                column: sorter.field,
+                order: 'ascend' === sorter.order ? 'asc' : 'desc'
+            }
+            // 排序字段受控
+            unref(columns).forEach(col => {
+                if (col['dataIndex'] === sorter.field) {
+                    col['sortOrder'] = sorter.order
+                }
+            })
+        }
+        pagination.current = page.current;
+        pagination.pageSize = page.pageSize;
+        loadData()
+    }
+
+    /**
+     * 行点击事件
+     * @param record
+     */
+    function clickThenCheck(record) {
+        if(clickThenCheckFlag===true){
+            let rowKey = combineRowKey(record);
+            if (!unref(checkedKeys) || unref(checkedKeys).length == 0) {
+                let arr1: any[] = [], arr2: any[] = [];
+                arr1.push(record);
+                arr2.push(rowKey);
+                checkedKeys.value = arr2;
+                selectRows.value = arr1
+            } else {
+                if (unref(checkedKeys).indexOf(rowKey) < 0) {
+                    //不存在就选中
+                    checkedKeys.value.push(rowKey);
+                    selectRows.value.push(record)
+                } else {
+                    //已选中就取消
+                    let rowKey_index = unref(checkedKeys).indexOf(rowKey);
+                    checkedKeys.value.splice(rowKey_index, 1);
+                    selectRows.value.splice(rowKey_index, 1);
+                }
+            }
+        }
+    }
+
+    //防止字典中有垃圾数据
+    function initDictOptionData(arr) {
+        let obj = {};
+        Object.keys(arr).map(k => {
+            obj[k] = arr[k].filter(item => {
+                return item != null
+            });
+        });
+        dictOptions.value = obj
+    }
+
+    /**
+     * 过滤对象中为空的属性
+     * @param obj
+     * @returns {*}
+     */
+    function filterObj(obj) {
+        if (!(typeof obj == 'object')) {
+            return;
+        }
+
+        for (let key in obj) {
+            if (obj.hasOwnProperty(key)
+                && (obj[key] == null || obj[key] == undefined || obj[key] === '')) {
+                delete obj[key];
+            }
+        }
+        return obj;
+    }
+
+  // 样式
+  const dialogStyle = {
+    top: 0,
+    left: 0,
+    height: '100%',
+    margin: 0,
+    padding: 0,
+  }
+
+  // 弹窗属性配置
+  const hrefComponent = ref({
+    model: {
+      title: '',
+      okText: '关闭',
+      width: '100%',
+      visible: false,
+      destroyOnClose: true,
+      style: dialogStyle,
+     // dialogStyle: dialogStyle,
+      bodyStyle: { padding: '8px', height: 'calc(100vh - 108px)', overflow: 'auto', overflowX: 'hidden' },
+      // 隐藏掉取消按钮
+      cancelButtonProps: { style: { display: 'none' } }
+    },
+    on: {
+      ok: () => hrefComponent.value.model.visible = false,
+      cancel: () => hrefComponent.value.model.visible = false,
+    },
+    is: <any> null,
+    params: {},
+  });
+
+  // 超链点击事件--> 打开一个modal窗口
+  function openHrefCompModal(href) {
+    // 解析 href 参数
+    let index = href.indexOf('?')
+    let path = href
+    if (index !== -1) {
+      path = href.substring(0, index)
+      let paramString = href.substring(index + 1, href.length)
+      let paramArray = paramString.split('&')
+      let params = {}
+      paramArray.forEach(paramObject => {
+        let paramItem = paramObject.split('=')
+        params[paramItem[0]] = paramItem[1]
+      })
+      hrefComponent.value.params = params
+    } else {
+      hrefComponent.value.params = {}
+    }
+    hrefComponent.value.model.visible = true
+    hrefComponent.value.model.title = '操作'
+    hrefComponent.value.is = defineAsyncComponent(() => import(/* @vite-ignore */'/@/views/' + (path.startsWith('/') ? path.slice(1) : path)))
+  }
+
+    return [{visibleChange, loadColumnsInfo,loadColumnsAndData, dynamicParamHandler, loadData, handleChangeInTable, combineRowKey, clickThenCheck,filterUnuseSelect, handleExport},
+        {hrefComponent, visible, rowSelection, checkedKeys, selectRows, pagination, dataSource, columns, indexColumnProps, loading, title,iSorter,queryInfo,queryParam,dictOptions}];
+}

+ 40 - 0
src/components/jeecg/OnLine/types/onlineConfig.ts

@@ -0,0 +1,40 @@
+interface ScopedSlots{
+  customRender: string
+}
+
+interface HrefSlots{
+  // 链接地址
+  href: string
+  // fieldHref_字段名
+  slotName: string
+}
+
+interface OnlineColumn {
+  dataIndex?: string;
+  title?: string;
+  key?: string;
+  fieldType?: string;
+  width?: number | string;
+  align?: string;
+  sorter?: string | boolean;
+  isTotal?: string | number | boolean;
+  groupTitle?: string;
+  // 超链的时候 和HrefSlots中的slotName匹配
+  scopedSlots? : ScopedSlots;
+  // 一般用于字典 字典传过来的是字典编码字符串 后转函数
+  customRender?: string | Function;
+  // 这个类型不知道有什么用
+  hrefSlotName?: string;
+  showLength?: number | string;
+  children?: OnlineColumn[];
+  sortOrder?: string;
+  // 插槽对应控件类型(列表)
+  slots?:ScopedSlots,
+  //超过宽度将自动省略,暂不支持和排序筛选一起使用。
+  ellipsis?: boolean
+}
+
+export{
+  OnlineColumn,
+  HrefSlots
+}

+ 5 - 0
src/main.ts

@@ -18,6 +18,8 @@ import {setupI18n} from '/@/locales/setupI18n';
 import {registerGlobComp} from '/@/components/registerGlobComp';
 import {registerThirdComp} from '/@/settings/registerThirdComp';
 import {useSso} from '/@/hooks/web/useSso';
+import {registerPackages} from '/@/utils/monorepo/registerPackages';
+
 // 在本地开发中引入的,以提高浏览器响应速度
 if (import.meta.env.DEV) {
     import('ant-design-vue/dist/antd.less');
@@ -35,6 +37,9 @@ async function bootstrap() {
     // 初始化内部系统配置
     initAppConfigStore();
 
+    // 注册外部模块路由
+    registerPackages(app);
+  
     // 注册全局组件
     registerGlobComp(app);
 

+ 6 - 1
src/router/helper/routeHelper.ts

@@ -7,6 +7,7 @@ import { warn } from '/@/utils/log';
 import { createRouter, createWebHashHistory } from 'vue-router';
 import { getToken } from '/@/utils/auth';
 import {URL_HASH_TAB} from '/@/utils'
+import { packageViews } from '/@/utils/monorepo/dynamicRouter';
 
 export type LayoutMapKey = 'LAYOUT';
 const IFRAME = () => import('/@/views/sys/iframe/FrameBlank.vue');
@@ -23,7 +24,11 @@ let dynamicViewsModules: Record<string, () => Promise<Recordable>>;
 
 // Dynamic introduction
 function asyncImportRoute(routes: AppRouteRecordRaw[] | undefined) {
-  dynamicViewsModules = dynamicViewsModules || import.meta.glob('../../views/**/*.{vue,tsx}');
+  if (!dynamicViewsModules) {
+    dynamicViewsModules = import.meta.glob('../../views/**/*.{vue,tsx}');
+    // 跟模块views合并
+    dynamicViewsModules = Object.assign({}, dynamicViewsModules, packageViews);
+  }
   if (!routes) return;
   routes.forEach((item) => {
     // update-begin--author:sunjianlei---date:20210918---for:适配旧版路由选项 --------

+ 19 - 0
src/utils/monorepo/dynamicRouter.ts

@@ -0,0 +1,19 @@
+export type DynamicViewsRecord = Record<string, () => Promise<Recordable>>
+
+/** 已注册模块的动态页面 */
+export const packageViews: DynamicViewsRecord = {};
+
+/**
+ * 注册动态路由页面
+ * @param getViews 获取该模块下所有页面的方法
+ */
+export function registerDynamicRouter(getViews: () => DynamicViewsRecord) {
+  if (typeof getViews === 'function') {
+    let dynamicViews = getViews();
+    Object.keys(dynamicViews).forEach((key) => {
+      // 处理动态页面的key,使其可以让路由识别
+      let newKey = key.replace('./src/views', '../../views');
+      packageViews[newKey] = dynamicViews[key];
+    });
+  }
+}

+ 50 - 0
src/utils/monorepo/registerPackages.ts

@@ -0,0 +1,50 @@
+import type { App } from 'vue';
+import { warn } from '/@/utils/log';
+import { registerDynamicRouter } from '/@/utils/monorepo/dynamicRouter';
+// 引入模块
+import PACKAGE_TEST_JEECG_ONLINE from '@jeecg/online';
+
+export function registerPackages(app: App) {
+  use(app, PACKAGE_TEST_JEECG_ONLINE);
+}
+
+// noinspection JSUnusedGlobalSymbols
+const installOptions = {
+  baseImport,
+};
+
+/** 注册模块 */
+function use(app: App, pkg) {
+  app.use(pkg, installOptions);
+  registerDynamicRouter(pkg.getViews);
+}
+
+// 模块里可使用的import
+const importGlobs = [
+  import.meta.glob('../../utils/**/*.{ts,js,tsx}'),
+  import.meta.glob('../../hooks/**/*.{ts,js,tsx}'),
+];
+
+/**
+ * 基础项目导包
+ * 目前支持导入如下
+ * /@/utils/**
+ * /@/hooks/**
+ *
+ * @param path 文件路径,ts无需输入后缀名。如:/@/utils/common/compUtils
+ */
+async function baseImport(path: string) {
+  if (path) {
+    // 将 /@/ 替换成 ../../
+    path = path.replace(/^\/@\//, '../../');
+    for (const glob of importGlobs) {
+      for (const key of Object.keys(glob)) {
+        if (path === key || `${path}.ts` === key || `${path}.tsx` === key) {
+          return glob[key]();
+        }
+      }
+    }
+    warn(`引入失败:${path} 不存在`);
+  }
+  return null;
+}

+ 4 - 0
vite.config.ts

@@ -62,6 +62,7 @@ export default ({ command, mode }: ConfigEnv): UserConfig => {
     },
     build: {
       target: 'es2015',
+      cssTarget: 'chrome80',
       outDir: OUTPUT_DIR,
       terserOptions: {
         compress: {
@@ -93,6 +94,9 @@ export default ({ command, mode }: ConfigEnv): UserConfig => {
     plugins: createVitePlugins(viteEnv, isBuild),
 
     optimizeDeps: {
+      esbuildOptions: {
+        target: 'es2020',
+      },
       // @iconify/iconify: The dependency is dynamically and virtually loaded by @purge-icons/generated, so it needs to be specified explicitly
       include: [
         '@iconify/iconify',