1
0

4 کامیت‌ها 1a16f72b30 ... dab5fa5044

نویسنده SHA1 پیام تاریخ
  hanxiaohui dab5fa5044 fix: 【公共】基础表格、基础表单修改 2 ماه پیش
  hanxiaohui b1dd742624 fix: BsUi-表格组件修改 2 ماه پیش
  hanxiaohui 6107e60da5 fix: 【公共】附件上传组件修改 2 ماه پیش
  hanxiaohui 3f751195f3 fix: BsUi-新增多文件附件上传组件 2 ماه پیش

+ 189 - 0
src/components/BsUi/FileUploadSubTable/index.vue

@@ -0,0 +1,189 @@
+<template>
+  <BsSubTableInput
+    :table-options="tableOptions"
+    :value="value"
+    :is-show-add-btn="false"
+    :is-show-batch-delete-btn="false"
+    :opt-btn-slots="batchTableOptBtnSlots"
+    :is-edit-row="false"
+    @sub-table-input-change="handleTableChange"
+    ref="subTableRef"
+  >
+    <template #custom1>
+      <div class="w-full flex items-center justify-center">
+        <FileUpload listType="text" :showUploadList="false" @change="handleChange" class="w-full file-btn">
+          <template #customUploadBtnSlot>
+            <a-button type="text" style="width: 100%">
+              <template #icon>
+                <upload-outlined />
+              </template>
+              <span>上传文件</span></a-button
+            >
+          </template>
+        </FileUpload>
+      </div>
+    </template>
+    <template #custom2>
+      <a-button type="text" style="width: 100%" :disabled="!isSelectedData" @click="handleDeleteBatch">
+        <template #icon> <DeleteOutlined /></template>
+        <span>批量删除</span>
+      </a-button>
+    </template>
+  </BsSubTableInput>
+</template>
+
+<script setup>
+  import { computed, h, ref, watch } from 'vue';
+  import { BsSubTableInput, useBsTable } from '/@/components/BsUi/index.js';
+  import { DISPLAY_STATE } from '/@/components/BsUi/constant.js';
+  import FileUpload from '/@/components/support/file-upload/index.vue';
+  import { cloneDeep, isEmpty, uniqBy } from 'lodash';
+  import { DeleteOutlined } from '@ant-design/icons-vue';
+
+  const props = defineProps({
+    value: {
+      type: Array,
+      default: () => [],
+    },
+  });
+
+  const subTableRef = ref(null);
+
+  const isSelectedData = computed(() => {
+    return isEmpty(subTableRef.value?.selectedData) ? false : subTableRef.value?.selectedData.length !== 0;
+  });
+
+  const rowKey = computed(() => {
+    return tableOptions.gridOptions?.rowKey || 'fileId';
+  });
+
+  const emits = defineEmits(['change', 'update:value']);
+
+  const batchTableOptBtnSlots = ref(['custom1', 'custom2']);
+
+  const { tableOptions, refreshTable } = useBsTable({
+    tableOptions: {
+      gridOptions: {
+        loading: false,
+        columns: [
+          {
+            field: 'fileId',
+            width: '80px',
+            align: 'center',
+            title: 'ID',
+          },
+          {
+            field: 'fileName',
+            title: '文件名称',
+          },
+          {
+            field: 'fileType',
+            width: '150px',
+            title: '文件类型',
+          },
+          {
+            field: 'fileSize',
+            width: '150px',
+            title: '文件大小(M)',
+            align: 'center',
+            slots: {
+              default: ({ row }) => (Number(row.fileSize) / (1024 * 1024)).toFixed(3),
+            },
+          },
+          {
+            fixed: 'right',
+            cellRender: {
+              name: 'CellOption',
+              extraProps: {
+                buttons: [
+                  {
+                    title: '预览',
+                    code: 'view',
+                    display: ({ row }) => {
+                      return DISPLAY_STATE.VISIBLE;
+                    },
+                    disabled({ row }) {
+                      return false;
+                    },
+                    onClick({ row }) {},
+                    extraProps: {},
+                  },
+                  {
+                    title: '删除',
+                    code: 'delete',
+                    display: ({ row }) => {
+                      return DISPLAY_STATE.VISIBLE;
+                    },
+                    disabled({ row }) {
+                      return false;
+                    },
+                    onClick({ row }) {
+                      handleDelete(row);
+                    },
+                    extraProps: {
+                      danger: true,
+                    },
+                  },
+                ],
+              },
+            },
+          },
+        ],
+        data: [],
+        rowKey: 'fileId',
+      },
+      request: loadTable,
+    },
+  });
+
+  const handleChange = (files) => {
+    const oldFiles = [...files, ...props.value];
+    const newFiles = cloneDeep(uniqBy(oldFiles, rowKey.value));
+    emits('update:value', newFiles);
+    emits('change', newFiles);
+  };
+
+  async function loadTable() {
+    return {
+      list: props.value,
+      total: props.value.length,
+    };
+  }
+
+  const handleDeleteBatch = () => {
+    subTableRef.value.deleteBatch();
+  };
+
+  function handleDelete(row) {
+    const delIndex = props.value.findIndex((v) => v[rowKey.value] === row[rowKey.value]);
+    subTableRef.value.deleteRow(delIndex);
+  }
+
+  function handleTableChange(value) {
+    emits('update:value', value);
+    emits('change', value);
+  }
+
+  watch(
+    () => props.value,
+    (newVal) => {
+      refreshTable();
+    },
+    {
+      immediate: true,
+    }
+  );
+</script>
+
+<style scoped lang="scss">
+  .file-btn {
+    :deep(.ant-upload-wrapper) {
+      display: block;
+      flex: 1;
+    }
+    :deep(.ant-upload) {
+      display: block;
+      flex: 1;
+    }
+  }
+</style>

+ 25 - 17
src/components/BsUi/SubTableInput/index.vue

@@ -66,7 +66,7 @@
   });
 
   const rowKey = computed(() => {
-    return props.tableOptions.rowKey;
+    return props.tableOptions.gridOptions.rowKey;
   });
 
   const emits = defineEmits('change', 'update:value', 'add');
@@ -97,16 +97,7 @@
                   icon={h(DeleteOutlined)}
                   type='text'
                   onClick={() => {
-                    const tableData = props.value;
-                    const deepCloneData = cloneDeep(tableData);
-                    deepCloneData.splice(rowIndex, 1);
-                    emits('update:value', deepCloneData);
-                    emits('change', deepCloneData);
-                    emits('delete-row', {
-                      gridRef: getGridRef(),
-                      bsTable: bsTableBean,
-                      tableData: deepCloneData,
-                    });
+                    handleDeleteRow(rowIndex);
                   }}
                 ></a-button>
               </a-tooltip>
@@ -182,8 +173,7 @@
             item[column.field] = row[column.field];
           }
         });
-        emits('update:value', tableData);
-        emits('change', tableData);
+        updateTable(tableData);
       },
     };
     if (props.isEditRow) {
@@ -216,8 +206,7 @@
     newData.push({
       [ROW_KEY_FIELD]: getUUID(),
     });
-    emits('update:value', newData);
-    emits('change', newData);
+    updateTable(newData);
     emits('add', {
       gridRef: getGridRef(),
       bsTable: bsTableBean,
@@ -237,8 +226,7 @@
           }
         }) === -1
     );
-    emits('update:value', newTableData);
-    emits('change', newTableData);
+    updateTable(newTableData);
     selectedData.value = [];
     emits('delete-rows', {
       gridRef: getGridRef(),
@@ -247,6 +235,23 @@
     });
   };
 
+  function handleDeleteRow(index) {
+    const deepCloneData = cloneDeep(props.value);
+    deepCloneData.splice(index, 1);
+    updateTable(deepCloneData);
+    emits('delete-row', {
+      gridRef: getGridRef(),
+      bsTable: bsTableBean,
+      tableData: deepCloneData,
+    });
+  }
+
+  function updateTable(tableData) {
+    emits('update:value', tableData);
+    emits('change', tableData);
+    emits('sub-table-input-change', tableData);
+  }
+
   watch(
     () => props.value,
     (val) => {
@@ -268,6 +273,9 @@
         }
       });
     },
+    deleteBatch: handleBatchDelete,
+    deleteRow: handleDeleteRow,
+    selectedData,
   });
 </script>
 

+ 7 - 2
src/components/BsUi/Table/index.js

@@ -257,8 +257,8 @@ export const useBsTable = (options, tableRef) => {
   };
 
   const refreshTable = async () => {
-    setPageNum(tableOptions.pagerConfig.pageNum || PAGE_NUM);
-    setPageSize(tableOptions.pagerConfig.pageSize || PAGE_SIZE);
+    setPageNum(tableOptions.pagerConfig?.pageNum || PAGE_NUM);
+    setPageSize(tableOptions.pagerConfig?.pageSize || PAGE_SIZE);
     await getTableData();
   };
 
@@ -268,6 +268,10 @@ export const useBsTable = (options, tableRef) => {
     await getTableData(searchParams);
   };
 
+  const getSelectedTableData = () => {
+    return [];
+  };
+
   return {
     tableOptions,
     setTablePropsValue,
@@ -276,6 +280,7 @@ export const useBsTable = (options, tableRef) => {
     getGridRef,
     refreshTable,
     fetchTableData,
+    getSelectedTableData,
   };
 };
 export default BsTable;

+ 17 - 14
src/components/BsUi/index.js

@@ -7,17 +7,18 @@ import BsTable, { useBsTable } from './Table/index.js';
 import BsDrawer, { useBsDrawer } from './Drawer/index.js';
 import BsForm, { useBsForm } from './Form/index.js';
 import BsModalTableSelector from './ModalTableSelector/index.vue';
-import BsDescriptions from "./Descriptions/index.vue"
-import BsContentsWrapper from "./ContentsWrapper/index.vue"
-import BsTabs from "./Tabs/index.vue"
-import BsEllipsisText from "./EllipsisText/index.vue"
-import BsDicTag from "./DicTag/index.vue"
-import BsLink from "./Link/index.vue"
-import BsCatalog from "./Catalog/index.vue"
-import BsSvgIcon from "./SvgIcon/index.vue"
-import BsTabBar from "/@/components/BsUi/TabBar/index.vue"
-import BsPageWrapper from "./PageWrapper/index.vue"
-import BsTableSelector from './TableSelector/index.vue'
+import BsDescriptions from './Descriptions/index.vue';
+import BsContentsWrapper from './ContentsWrapper/index.vue';
+import BsTabs from './Tabs/index.vue';
+import BsEllipsisText from './EllipsisText/index.vue';
+import BsDicTag from './DicTag/index.vue';
+import BsLink from './Link/index.vue';
+import BsCatalog from './Catalog/index.vue';
+import BsSvgIcon from './SvgIcon/index.vue';
+import BsTabBar from '/@/components/BsUi/TabBar/index.vue';
+import BsPageWrapper from './PageWrapper/index.vue';
+import BsTableSelector from './TableSelector/index.vue';
+import BsFileUploadSubTable from './FileUploadSubTable/index.vue';
 
 const BsUi = {
   install(app) {
@@ -28,8 +29,8 @@ const BsUi = {
     app.component('BsTable', BsTable);
     app.component('BsDrawer', BsDrawer);
     app.component('BsModalTableSelector', BsModalTableSelector);
-    app.component('BsDescriptions', BsDescriptions)
-    app.component('BsContentsWrapper', BsContentsWrapper)
+    app.component('BsDescriptions', BsDescriptions);
+    app.component('BsContentsWrapper', BsContentsWrapper);
     app.component('BsTabs', BsTabs);
     app.component('BsEllipsisText', BsEllipsisText);
     app.component('BsDicTag', BsDicTag);
@@ -39,6 +40,7 @@ const BsUi = {
     app.component('BsTabBar', BsTabBar);
     app.component('BsPageWrapper', BsPageWrapper);
     app.component('BsTableSelector', BsTableSelector);
+    app.component('BsFileUploadSubTable', BsFileUploadSubTable);
   },
 };
 
@@ -68,5 +70,6 @@ export {
   BsSvgIcon,
   BsTabBar,
   BsPageWrapper,
-  BsTableSelector
+  BsTableSelector,
+  BsFileUploadSubTable,
 };

+ 216 - 202
src/components/support/file-upload/index.vue

@@ -7,10 +7,21 @@
 -->
 <template>
   <div class="clearfix">
-    <a-upload multiple :accept="props.accept" :before-upload="beforeUpload" :customRequest="customRequest"
-      :file-list="fileList" :headers="{ 'x-access-token': useUserStore().getToken }" :list-type="listType"
-      @change="handleChange" @preview="handlePreview" @remove="handleRemove" :showUploadList="showUploadList" :disabled="disabled">
-      <div v-if="fileList.length < props.maxUploadSize">
+    <a-upload
+      multiple
+      :accept="props.accept"
+      :before-upload="beforeUpload"
+      :customRequest="customRequest"
+      :file-list="fileList"
+      :headers="{ 'x-access-token': useUserStore().getToken }"
+      :list-type="listType"
+      @change="handleChange"
+      @preview="handlePreview"
+      @remove="handleRemove"
+      :showUploadList="showUploadList"
+      :disabled="disabled"
+    >
+      <div v-if="fileList.length < props.maxUploadSize && !slot.customUploadBtnSlot">
         <template v-if="listType === 'picture-card'">
           <PlusOutlined />
           <div class="ant-upload-text">
@@ -19,11 +30,12 @@
         </template>
         <template v-if="listType === 'text'">
           <a-button :type="btnType">
-            <upload-outlined v-if="btnIcon"/>
+            <upload-outlined v-if="btnIcon" />
             {{ buttonText }}
           </a-button>
         </template>
       </div>
+      <slot name="customUploadBtnSlot"></slot>
     </a-upload>
     <a-modal :footer="null" :open="previewVisible" @cancel="handleCancel">
       <img :src="previewUrl" alt="example" style="width: 100%" />
@@ -31,218 +43,220 @@
   </div>
 </template>
 <script setup>
-import { computed, ref, watch } from 'vue';
-import { Modal } from 'ant-design-vue';
-import { fileApi } from '/src/api/support/file-api';
-import { useUserStore } from '/@/store/modules/system/user';
-import { SmartLoading } from '/@/components/framework/smart-loading';
-import { FILE_FOLDER_TYPE_ENUM } from '/@/constants/support/file-const';
-import { smartSentry } from '/@/lib/smart-sentry';
-const props = defineProps({
-  value: String,
-  buttonText: {
-    type: String,
-    default: '点击上传附件',
-  },
-  showUploadBtn: {
-    type: Boolean,
-    default: true,
-  },
-  defaultFileList: {
-    type: Array,
-    default: () => [],
-  },
-  multiple: {
-    type: Boolean,
-    default: false,
-  },
-  // 最多上传文件数量
-  maxUploadSize: {
-    type: Number,
-    default: 10,
-  },
-  maxSize: {
-    type: Number,
-    default: 500,
-  },
-  // 上传的文件类型
-  accept: {
-    type: String,
-    default: '',
-  },
-  // 文件上传类型
-  folder: {
-    type: Number,
-    default: FILE_FOLDER_TYPE_ENUM.COMMON.value,
-  },
-  // 上传列表的内建样式,支持三种基本样式 text, picture 和 picture-card
-  listType: {
-    type: String,
-    default: 'picture-card',
-  },
-  // 上传按钮样式
-  btnType: {
-    type: String,
-    default: 'default',
-  },
-  // 是否显示上传按钮图标
-  btnIcon: {
-    type: Boolean,
-    default: true,
-  },
-  //是否显示上传列表
-  showUploadList:{
-    type: Boolean,
-    default: true,
-  },
-  //是否上传后清空文件列表
-  uploadAterClear:{
-    type: Boolean,
-    default: false,
-  },
-  //是否禁用上传
-  disabled:{
-    type: Boolean,
-    default: false,
-  }
-});
-
-// 图片类型的后缀名
-const imgFileType = ['jpg', 'jpeg', 'png', 'gif'];
-
-// 重新修改图片展示字段
-const files = computed(() => {
-  let res = [];
-  if (props.defaultFileList && props.defaultFileList.length > 0) {
-    props.defaultFileList.forEach((element) => {
-      element.url = element.fileUrl;
-      element.name = element.fileName;
-      res.push(element);
-    });
+  import { computed, ref, useSlots, watch } from 'vue';
+  import { Modal } from 'ant-design-vue';
+  import { fileApi } from '/src/api/support/file-api';
+  import { useUserStore } from '/@/store/modules/system/user';
+  import { SmartLoading } from '/@/components/framework/smart-loading';
+  import { FILE_FOLDER_TYPE_ENUM } from '/@/constants/support/file-const';
+  import { smartSentry } from '/@/lib/smart-sentry';
+  const props = defineProps({
+    value: String,
+    buttonText: {
+      type: String,
+      default: '点击上传附件',
+    },
+    showUploadBtn: {
+      type: Boolean,
+      default: true,
+    },
+    defaultFileList: {
+      type: Array,
+      default: () => [],
+    },
+    multiple: {
+      type: Boolean,
+      default: false,
+    },
+    // 最多上传文件数量
+    maxUploadSize: {
+      type: Number,
+      default: 10,
+    },
+    maxSize: {
+      type: Number,
+      default: 500,
+    },
+    // 上传的文件类型
+    accept: {
+      type: String,
+      default: '',
+    },
+    // 文件上传类型
+    folder: {
+      type: Number,
+      default: FILE_FOLDER_TYPE_ENUM.COMMON.value,
+    },
+    // 上传列表的内建样式,支持三种基本样式 text, picture 和 picture-card
+    listType: {
+      type: String,
+      default: 'picture-card',
+    },
+    // 上传按钮样式
+    btnType: {
+      type: String,
+      default: 'default',
+    },
+    // 是否显示上传按钮图标
+    btnIcon: {
+      type: Boolean,
+      default: true,
+    },
+    //是否显示上传列表
+    showUploadList: {
+      type: Boolean,
+      default: true,
+    },
+    //是否上传后清空文件列表
+    uploadAterClear: {
+      type: Boolean,
+      default: false,
+    },
+    //是否禁用上传
+    disabled: {
+      type: Boolean,
+      default: false,
+    },
+  });
+
+  const slot = useSlots();
+
+  // 图片类型的后缀名
+  const imgFileType = ['jpg', 'jpeg', 'png', 'gif'];
+
+  // 重新修改图片展示字段
+  const files = computed(() => {
+    let res = [];
+    if (props.defaultFileList && props.defaultFileList.length > 0) {
+      props.defaultFileList.forEach((element) => {
+        element.url = element.fileUrl;
+        element.name = element.fileName;
+        res.push(element);
+      });
+      return res;
+    }
     return res;
-  }
-  return res;
-});
-// -------------------- 逻辑 --------------------
-
-const previewVisible = ref(false);
-const fileList = ref([]);
-const previewUrl = ref('');
-
-watch(
-  files,
-  (value) => {
-    fileList.value = value;
-  },
-  {
-    immediate: true,
-  }
-);
-
-const emit = defineEmits(['update:value', 'change']);
-const customRequest = async (options) => {
-  SmartLoading.show();
-  try {
-    // console.log(options);
-    const formData = new FormData();
-    formData.append('file', options.file);
-    // console.log(formData)
-    let res = await fileApi.uploadFile(formData, props.folder);
-    // console.log(res)
-    let file = res.data;
-    file.url = file.fileUrl;
-    file.name = file.fileName;
-    if (props.uploadAterClear) {
-      fileList.value = []
+  });
+  // -------------------- 逻辑 --------------------
+
+  const previewVisible = ref(false);
+  const fileList = ref([]);
+  const previewUrl = ref('');
+
+  watch(
+    files,
+    (value) => {
+      fileList.value = value;
+    },
+    {
+      immediate: true,
     }
-    fileList.value.push(file);
-    emit('change', fileList.value);
-  } catch (e) {
-    smartSentry.captureError(e);
-  } finally {
-    SmartLoading.hide();
-  }
-};
-
-function handleChange(info) {
-  // console.log(info)
-  let fileStatus = info.file.status;
-  let file = info.file;
-  if (fileStatus === 'removed') {
-    let index = fileList.value.findIndex((e) => e.fileId === file.fileId);
-    if (index !== -1) {
-      fileList.value.splice(index, 1);
+  );
+
+  const emit = defineEmits(['update:value', 'change']);
+  const customRequest = async (options) => {
+    SmartLoading.show();
+    try {
+      // console.log(options);
+      const formData = new FormData();
+      formData.append('file', options.file);
+      // console.log(formData)
+      let res = await fileApi.uploadFile(formData, props.folder);
+      // console.log(res)
+      let file = res.data;
+      file.url = file.fileUrl;
+      file.name = file.fileName;
+      if (props.uploadAterClear) {
+        fileList.value = [];
+      }
+      fileList.value.push(file);
       emit('change', fileList.value);
+    } catch (e) {
+      smartSentry.captureError(e);
+    } finally {
+      SmartLoading.hide();
     }
-  }
-}
+  };
 
-function handleRemove(file) {
-  // console.log(fileList.value);
-}
+  function handleChange(info) {
+    // console.log(info)
+    let fileStatus = info.file.status;
+    let file = info.file;
+    if (fileStatus === 'removed') {
+      let index = fileList.value.findIndex((e) => e.fileId === file.fileId);
+      if (index !== -1) {
+        fileList.value.splice(index, 1);
+        emit('change', fileList.value);
+      }
+    }
+  }
 
-function beforeUpload(file, files) {
-  if (fileList.value.length + files.length > props.maxUploadSize) {
-    showErrorMsgOnce(`最多支持上传 ${props.maxUploadSize} 个文件哦!`);
-    return false;
+  function handleRemove(file) {
+    // console.log(fileList.value);
   }
 
-  if (props.accept) {
-    const suffixIndex = file.name.lastIndexOf('.');
-    const fileSuffix = file.name.substring(suffixIndex <= -1 ? 0 : suffixIndex);
-    if (props.accept.indexOf(fileSuffix) === -1) {
-      showErrorMsgOnce(`只支持上传 ${props.accept.replaceAll(',', ' ')} 格式的文件`);
+  function beforeUpload(file, files) {
+    if (fileList.value.length + files.length > props.maxUploadSize) {
+      showErrorMsgOnce(`最多支持上传 ${props.maxUploadSize} 个文件哦!`);
       return false;
     }
-  }
 
-  const isLimitSize = file.size / 1024 / 1024 < props.maxSize;
-  if (!isLimitSize) {
-    showErrorMsgOnce(`单个文件大小必须小于 ${props.maxSize} Mb`);
-  }
-  return isLimitSize;
-}
-
-const showErrorModalFlag = ref(true);
-const showErrorMsgOnce = (content) => {
-  if (showErrorModalFlag.value) {
-    Modal.error({
-      title: '提示',
-      content: content,
-      okType: 'danger',
-      centered: true,
-      onOk() {
-        showErrorModalFlag.value = true;
-      },
-    });
-    showErrorModalFlag.value = false;
+    if (props.accept) {
+      const suffixIndex = file.name.lastIndexOf('.');
+      const fileSuffix = file.name.substring(suffixIndex <= -1 ? 0 : suffixIndex);
+      if (props.accept.indexOf(fileSuffix) === -1) {
+        showErrorMsgOnce(`只支持上传 ${props.accept.replaceAll(',', ' ')} 格式的文件`);
+        return false;
+      }
+    }
+
+    const isLimitSize = file.size / 1024 / 1024 < props.maxSize;
+    if (!isLimitSize) {
+      showErrorMsgOnce(`单个文件大小必须小于 ${props.maxSize} Mb`);
+    }
+    return isLimitSize;
   }
-};
-
-function handleCancel() {
-  previewVisible.value = false;
-}
-
-const handlePreview = async (file) => {
-  if (imgFileType.some((e) => e === file.fileType)) {
-    previewUrl.value = file.url || file.preview;
-    previewVisible.value = true;
-  } else {
-    fileApi.downLoadFile(file.fileKey);
+
+  const showErrorModalFlag = ref(true);
+  const showErrorMsgOnce = (content) => {
+    if (showErrorModalFlag.value) {
+      Modal.error({
+        title: '提示',
+        content: content,
+        okType: 'danger',
+        centered: true,
+        onOk() {
+          showErrorModalFlag.value = true;
+        },
+      });
+      showErrorModalFlag.value = false;
+    }
+  };
+
+  function handleCancel() {
+    previewVisible.value = false;
   }
-};
 
-// ------------------------ 清空 上传 ------------------------
-function clear() {
-  fileList.value = [];
-}
+  const handlePreview = async (file) => {
+    if (imgFileType.some((e) => e === file.fileType)) {
+      previewUrl.value = file.url || file.preview;
+      previewVisible.value = true;
+    } else {
+      fileApi.downLoadFile(file.fileKey);
+    }
+  };
+
+  // ------------------------ 清空 上传 ------------------------
+  function clear() {
+    fileList.value = [];
+  }
 
-defineExpose({
-  clear,
-});
+  defineExpose({
+    clear,
+  });
 </script>
 <style lang="less" scoped>
-:deep(.ant-upload-picture-card-wrapper) {
-  display: flex;
-}
+  :deep(.ant-upload-picture-card-wrapper) {
+    display: flex;
+  }
 </style>

+ 82 - 7
src/views/form-demo/index.vue

@@ -17,28 +17,71 @@
         <OrgUserSelector v-model:selected-data="orgSelectors" :multiple="SELECT_MULTIPLE.MORE" :scene-type="SCENE_TYPE.ORG" />
       </a-form-item>
 
-      <a-form-item label="子表" name="batchTable">
-        <BsSubTableInput :customColumns="columns" v-model:value="batchTable" :row-key="id" @add="handleAdd" />
+      <a-form-item label="通用子表" name="batchTable">
+        <BsSubTableInput v-model:value="batchTable" @add="handleAdd" :table-options="tableOptions" />
+      </a-form-item>
+
+      <a-form-item label="自定义子表" name="batchTable1">
+        <BsSubTableInput
+          :table-options="tableOptions"
+          v-model:value="batchTable1"
+          :is-show-add-btn="false"
+          :is-show-batch-delete-btn="false"
+          :opt-btn-slots="batchTableOptBtnSlots"
+          :is-edit-row="false"
+        >
+          <template #custom1>
+            <a-button type="text" style="width: 100%"> 自定义按钮1</a-button>
+          </template>
+          <template #custom2>
+            <a-button type="text" style="width: 100%"> 自定义按钮2 </a-button>
+          </template>
+        </BsSubTableInput>
+      </a-form-item>
+
+      <a-form-item label="多文件上传">
+        <BsFileUploadSubTable v-model:value="fileList" />
       </a-form-item>
     </a-form>
   </div>
 </template>
 
 <script setup>
-  import OrgUserSelector from '/@/components/BsUi/OrgUserSelector/index.vue';
   import { reactive, ref } from 'vue';
   import { SCENE_TYPE, SELECT_MULTIPLE } from '/@/components/BsUi/constant.js';
-  import { BsSubTableInput } from '/@/components/BsUi/index.js';
+  import { BsSubTableInput, BsFileUploadSubTable, BsOrgUserSelector as OrgUserSelector } from '/@/components/BsUi/index.js';
+  import useBsDict from '/@/utils/dict.js';
+
+  const fileList = ref([
+    {
+      fileId: 845,
+      fileName: '已处理@2x.png',
+      fileUrl: 'http://59.110.6.97:9000/upload/public/common/b952203ba2a24c69bdedf78f09153db2_20250908134830.png',
+      fileKey: 'public/common/b952203ba2a24c69bdedf78f09153db2_20250908134830.png',
+      fileSize: 11252,
+      fileType: 'png',
+    },
+    {
+      fileId: 846,
+      fileName: '已处理@2x (1).png',
+      fileUrl: 'http://59.110.6.97:9000/upload/public/common/87b7a3c555c549b8b81c6ed25259a17f_20250908134830.png',
+      fileKey: 'public/common/87b7a3c555c549b8b81c6ed25259a17f_20250908134830.png',
+      fileSize: 10647,
+      fileType: 'png',
+    },
+  ]);
+
+  // const fileList = ref([]);
 
   const batchTable = ref([
     {
       id: '1',
       name: '韩晓辉',
-      sex: '男',
+      sex: '1',
     },
   ]);
 
-  const columns = ref([
+  const columns1 = ref([
     {
       title: '姓名',
       field: 'name',
@@ -49,8 +92,40 @@
     },
   ]);
 
+  const tableOptions = ref({
+    gridOptions: {
+      columns: [
+        {
+          title: '姓名',
+          field: 'name',
+          editRender: { name: 'VxeInput' },
+        },
+        {
+          title: '性别',
+          field: 'sex',
+          editRender: {
+            name: 'VxeSelect',
+            props: {
+              multiple: false,
+              filterable: true,
+            },
+            options: useBsDict.getDictList('SYS_SEX'),
+          },
+        },
+      ],
+      editRules: {
+        name: [{ required: true, message: '必须填写' }],
+        sex: [{ required: true, message: '必须填写' }],
+      },
+    },
+  });
+
+  const batchTable1 = ref([]);
+
+  const batchTableOptBtnSlots = ref(['custom1', 'custom2']);
+
   const handleAdd = ({ tableData }) => {
-    batchTable.value = [...tableData, { name: '甘雨' + new Date().getTime(), sex: '女' + new Date().getTime(), id: new Date().getTime() }];
+    console.log('handleAdd', tableData);
   };
 
   const userSelectors = ref([

+ 113 - 8
src/views/table-demo/components/AddOrEditDrawer.vue

@@ -15,7 +15,7 @@
       :footer-render="() => null"
       :form-id="formOptions.formId"
       ref="bsFormRef"
-    ></bs-form>
+    />
   </bs-drawer>
 </template>
 
@@ -36,7 +36,7 @@
     getDrawerPropsValue: getDVal,
   } = useBsDrawer({
     drawerOptions: {
-      width: '500px',
+      width: '700px',
       title: '标题',
       visible: false,
       drawerExtraProps: {
@@ -72,7 +72,7 @@
         },
         {
           id: '2',
-          label: '表格选择器',
+          label: '表格选择器(多选)',
           component: 'bs-table-selector',
           componentProps: {
             multiple: '1',
@@ -108,7 +108,7 @@
               },
             },
             modalOptions: {
-              title: '表格选择器',
+              title: '表格选择器(多选)',
             },
           },
           field: 'name1',
@@ -126,6 +126,110 @@
             ],
           },
         },
+        {
+          id: '3',
+          label: '表格选择器(单选)',
+          component: 'bs-table-selector',
+          componentProps: {
+            multiple: '0',
+            placeholder: '请选择',
+            labelField: 'name',
+            valueField: 'id',
+            tableOptions: {
+              gridOptions: {
+                loading: false,
+                columns: [
+                  {
+                    field: 'id',
+                    title: 'ID',
+                  },
+                  {
+                    field: 'name',
+                    title: '名字',
+                  },
+                ],
+                rowKey: 'id',
+                data: [],
+              },
+              async request() {
+                return {
+                  list: Array.from({ length: 10 })
+                    .fill(0)
+                    .map((v, idx) => ({
+                      id: idx,
+                      name: 'name' + idx,
+                    })),
+                  total: 10,
+                };
+              },
+            },
+            modalOptions: {
+              title: '表格选择器单选',
+            },
+          },
+          field: 'name2',
+          sort: '2',
+          visible: '1',
+          required: '1',
+          formItemExtraProps: {
+            rules: [
+              {
+                validator: (_, value) => {
+                  // return Promise.reject(new Error('报错'));
+                  return Promise.resolve();
+                },
+              },
+            ],
+          },
+        },
+        {
+          id: '4',
+          label: '子表',
+          component: 'bs-sub-table-input',
+          componentProps: {
+            tableOptions: {
+              gridOptions: {
+                loading: false,
+                columns: [
+                  {
+                    field: 'id',
+                    title: 'ID',
+                    visible: false,
+                  },
+                  {
+                    field: 'name',
+                    title: '名字',
+                    editRender: { name: 'VxeInput' },
+                  },
+                  {
+                    field: 'phone',
+                    title: '电话',
+                    editRender: { name: 'VxeInput' },
+                  },
+                ],
+                rowKey: 'id',
+                editRules: {
+                  name: [{ required: true, message: '必须填写' }],
+                  phone: [{ required: true, message: '必须填写' }],
+                },
+              },
+            },
+          },
+          field: 'name3',
+          sort: '2',
+          visible: '1',
+          required: '1',
+          formItemExtraProps: {
+            rules: [
+              {
+                validator: (_, value) => {
+                  // return Promise.reject(new Error('报错'));
+                  return Promise.resolve();
+                },
+              },
+            ],
+          },
+        },
       ],
       formData: {
         name: '韩晓辉',
@@ -135,10 +239,11 @@
             name: 'name1',
           },
         ],
-        // name1: {
-        //   id: 1,
-        //   name: 'name1',
-        // },
+        name2: {
+          id: 1,
+          name: 'name1',
+        },
+        name3: [],
       },
       formId: 'formId',
       formExtraProps: {},