vue3组合式api + ts + Elementplus 高度封装增删改查列表

vue3组合式api + ts + Elementplus 高度封装增删改查列表,(搜索、表格列表、新增、编辑、查看)

先展示功能

在这里插入图片描述
在这里插入图片描述

使用代码展示

test/index.vue

<script setup lang="ts">
import {
  getTestList,
  addTest,
  updateTest,
  deleteTest,
  exportTest,
  getTestType
}from'@/api/test'
import { tSettings, tHeader, sFormData, fStructure, dConfig, fConfig, fModel, fItem } from './config'
import { getQueryVO } from '@/api/test/types';
import { ComponentInternalInstance } from "vue";

const { proxy } = getCurrentInstance() as ComponentInternalInstance

const searchFormData = ref({ ...sFormData });
const formStructure = ref<FormStructure[]>([...fStructure])
const tableData = ref<getQueryVO[]>([]);
const tableHeader = ref<TableHeader[]>([...tHeader]);
const tableSettings = ref<Settings>({ ...tSettings });
const dialogConfig = reactive({ ...dConfig })
const dialogVisible = ref(false)
const formModel = ref<FormModel>({ ...fModel })
const formConfig = reactive<FormConfig>({ ...fConfig })
const formItem = ref<FormItem[]>([...fItem])
// dialogForm确认按钮状态
const verifyLoading = ref(false);

const getList = async () => {
  tableSettings.value.isLoading = true;
  let params = {
    pageSize: tableSettings.value.pageSize,
    pageNum: tableSettings.value.pageNum,
    ...searchFormData.value
  };
  const res = await getTestList(params);
  tableData.value = res.rows;
  tableSettings.value.total = res.total;
  tableSettings.value.isLoading = false;
};

const exportFile = async () => {
  await exportTest({
    ...searchFormData.value
  }, 'test列表.xlsx');
};

const addData = () => {
  dialogConfig.dialogTitle = '新增test信息'
  formConfig.type = 'edit'
  formModel.value = { ...fModel }
  dialogConfig.dialogFooterSaveBtn = true
  dialogVisible.value = true
}

const editData = (row: getQueryVO) => {
  dialogConfig.dialogTitle = '编辑test信息'
  formConfig.type = 'edit'
  formModel.value = { ...row }
  dialogConfig.dialogFooterSaveBtn = true
  dialogVisible.value = true
}
const viewData = (row: getQueryVO) => {
  dialogConfig.dialogTitle = '查看test信息'
  formConfig.type = 'view'
  formModel.value = { ...row }
  dialogConfig.dialogFooterSaveBtn = false
  dialogVisible.value = true
}

const debouncedRefresh = async (ids: string) => {
  const res = await deleteTest(ids);
  proxy?.$modal.msgSuccess(res.msg);
  getList()
}

const submitForm = async (data: getQueryVO) => {
  try {
    verifyLoading.value = true
    const url = data.id ? updateTest : addTest
    const res = await url(data)
    proxy?.$modal.msgSuccess(res.msg);
    verifyLoading.value = false
    dialogVisible.value = false
    getList()
  } catch (error) {
    verifyLoading.value = false
  }
}

// 获取类型Opitons
const getUserList = async () => {
  const res = await getTestType()
  const list:Options[] = []
  res.data.forEach((e:any)=>{
    list.push({
      label: e.label,
      value: e.id
    })
  })
  formStructure.value[0].options = list
  formItem.value[0].options = list
}

onMounted(() => {
  getUserList()
  getList();
});
</script>
<template>
  <div class="page-container">
    <div class="page-header">
      <form-search v-model:searchFormData="searchFormData" :formStructure="formStructure" @search="getList" />
    </div>
    <div class="page-body">
      <div class="page-table-header">
        <div class="page-table-title">test列表({{ tableSettings.total }}</div>
        <div>
          <el-button type="primary" @click="addData()" icon="CirclePlus"> 新增 </el-button>
          <el-button @click="exportFile()" icon="Download" v-hasPermi="['hw:warehouse:add']"> 导出 </el-button>
        </div>
      </div>
      <div style="flex: 1;">
        <custom-table :tableData="tableData" :tableHeader="tableHeader" v-model:settings="tableSettings" @pageChange="getList()">
          <template #controls="{ data }">
            <div class="table-controls">
              <div class="table-controls-item" @click="viewData(data)">详情</div>
              <div class="table-controls-item" @click="editData(data)">编辑</div>
              <div class="table-controls-item" style="color:#f25555">
                <el-popconfirm title="确认删除吗?" @confirm="debouncedRefresh(data.id + '')">
                  <template #reference> 删除 </template>
                </el-popconfirm>
              </div>
            </div>
          </template>
        </custom-table>
      </div>
    </div>
    <dialog-form
      v-model:dialogVisible="dialogVisible"
      :dialogConfig="dialogConfig"
      :verifyLoading="verifyLoading"
      :formModel="formModel"
      :formItem="formItem"
      :formConfig="formConfig"
      @submitForm="submitForm"
    />
  </div>
</template>
<style lang="scss" scoped></style>

test/config.ts


/**
 * 搜索组件初始值
 */
export const sFormData = {
  type: undefined,
  code: undefined,
  name: undefined
}

/**
 * @description 表单配置文件
 */
export const fStructure: FormStructure[] = [
  {
    type: 'select',
    label: 'test类型',
    prop: 'type',
    options: []
  },
  {
    type: 'input',
    label: 'test编号',
    prop: 'code',
  },
  {
    type: 'input',
    label: 'test名称',
    prop: 'name',
  },
]

/**
 * @description 表格配置文件
 */
export const tSettings: Settings = {
  isIndex: true,
  align: 'center',
  isPagination: true,
  total: 0,
  pageNum: 1,
  pageSize: 10,
  isLoading: true
}
/**
 * @description: 表头、数据配置
 */
export const tHeader: TableHeader[] = [
  {
    label: 'test类型',
    prop: 'type'
  },
  {
    label: 'test编号',
    prop: 'code'
  },
  {
    label: 'test名称',
    prop: 'name'
  },
  {
    label: 'test责任人',
    prop: 'people'
  },
  {
    label: '启用时间',
    prop: 'createdTime'
  },
  {
    label: 'test环境状态',
    prop: 'environmentalState'
  },
  {
    label: 'test状态',
    prop: 'warehouseStatus'
  },
  {
    label: 'test容量(吨)',
    prop: 'capacity'
  },
  {
    label: '操作',
    prop: 'controls',
    fixed: 'right',
    width: 250,
    isTemplate: true
  },
]

/* 弹窗配置 */
export const dConfig: DialogConfig = {
  dialogTitle: "",
  dialogWidth: "800px",
  dialogModal: true,
  dialogTop: '20px',
  dialogFullscreen: false,
  dialogClickModalClose: false,
  dialogESCModalClose: false,
  dialogDraggable: false,
  dialogFooterBtn: true,
  dialogFooterSaveBtn: true,
  dialogDestroyOnClose: false
}
/* 表单配置 */
export const fConfig: FormConfig = {
  type: 'edit',
  inline: true,
  labelWidth: '120px'
}

/**
 * @description: 表单模型
 */
export const fModel: FormModel = {
  id: undefined,
  type: undefined,
  code: undefined,
  name: undefined,
  createdTime: undefined,
  people: undefined,
  environmentalState: undefined,
  status: undefined,
  capacity: undefined
}

/**
 * @description: 表单配置项
 */
export const fItem: FormItem[] = [
  {
    componentType: 'select',
    label: 'test类型',
    prop: 'type',
    isRules: true,
    options: []
  },
  {
    componentType: 'input',
    type: 'text',
    label: 'test编号',
    prop: 'code',
    isRules: true
  },
  {
    componentType: 'input',
    type: 'text',
    label: 'test名称',
    prop: 'name',
    isRules: true,
    maxlength: 255
  },
  {
    componentType: 'input',
    type: 'text',
    label: 'test责任人',
    prop: 'people',
    isRules: true,
    maxlength: 10
  },
  {
    componentType: 'date',
    type: 'date',
    label: '启用时间',
    prop: 'createdTime',
    isRules: true,
  },
  {
    componentType: 'input',
    type: 'number',
    label: 'test总容量(吨)',
    prop: 'capacity',
    isRules: true,
    maxlength: 10
  },
  {
    componentType: 'input',
    type: 'text',
    label: 'test环境状态',
    prop: 'warehouseEnvironmentalStatus',
    isRules: true,
    maxlength: 10
  },
  {
    componentType: 'input',
    type: 'text',
    label: 'test状态',
    prop: 'warehouseStatus',
    isRules: true,
    maxlength: 10
  },

]

代码展示

1、所涉及的代码层级
├── src
│       └── api
│           └── test
│				└── index.ts
│				└── types.ts
│       └── components
│           └── CustomDialog
│				└── index.vue
│           └── CustomTable
│				└── index.vue
│           └── DialogForm
│				└── index.vue
│           └── FileUpload
│				└── index.vue
│           └── FormSearch
│				└── index.vue
│       └── types
│           └── global.d.ts
│       └── views
│           └── test
│				└── config.ts
│				└── index.vue
│       └── main.ts
├── .env.development
├── .eslintrc.js
├── .tsconfig.json
2、搜索区域封装
路径 /src/components/FormSearch/index.vue
<script setup lang="ts">
/**
  调用方式
  <form-search
    v-model:searchFormData="searchFormData"
    :formStructure="formStructure"
    @search="getList"
  />
*/
interface SearchFormData {
  [key: string]: any
}
interface SearchProps {
  formStructure?: FormStructure[],
  searchFormData: SearchFormData
}
const props = withDefaults(defineProps<SearchProps>(), {
  searchFormData: () => ({}),
  formStructure: () => ([])
})

const initialValue = ref(JSON.parse(JSON.stringify(props.searchFormData)))
const searchFormData = ref(JSON.parse(JSON.stringify(props.searchFormData)))
const formStructure = ref(JSON.parse(JSON.stringify(props.formStructure)))
const emit = defineEmits(['search', 'update:searchFormData'])

const search = () => {
  emit('update:searchFormData', searchFormData.value)
  emit('search')
}
const reset = () => {
  emit('update:searchFormData', initialValue.value)
  emit('search')
}

watch(() => props.searchFormData, (val) => {
  searchFormData.value = JSON.parse(JSON.stringify(val))
})
watch(() => props.formStructure, (val) => {
  formStructure.value = JSON.parse(JSON.stringify(val))
})
</script>
<template>
  <div style="overflow: auto;">
    <el-form
      :model="searchFormData"
      :inline="true"
    >
      <el-form-item
        v-for="(item, index) in formStructure"
        :key="index"
        :label="item.label"
        :prop="item.prop"
        :label-width="item.labelWidth ? item.labelWidth + 'px' : '120px'"
      >
        <el-input
          v-if="item.type === 'input'"
          v-model="searchFormData[item.prop]"
          :placeholder="item.placeholder || '请输入' + item.label"
          :maxlength="item.length || '99'"
          :clearable="item.clearable || true"
          :style="item.style || 'width: 250px'"
        />
        <el-select
          v-else-if="item.type === 'select'"
          :disabled="item.disabled"
          v-model="searchFormData[item.prop]"
          :style="item.style || 'width: 250px'"
          :clearable="item.clearable || true"
          :multiple="item.multiple"
          :filterable="item.filterable || true"
          :placeholder="item.placeholder || '请选择' + item.label"
        >
          <el-option
            v-for="(option, index2) in item.options || []"
            :key="index2"
            :label="option['label']"
            :value="option['value']"
          />
        </el-select>
        <el-date-picker
          v-else-if="item.type === 'date' || item.type === 'datetime'"
          v-model="item.value"
          :type="item.type"
          :placeholder="item.placeholder || '请选择' + item.label"
          :style="item.style || 'width: 250px'"
        />
      </el-form-item>
      <el-form-item
        prop=" "
        label-width="120px"
      >
        <el-input style="width: 170px;visibility: hidden;" />
      </el-form-item>
    </el-form>
    <div class="search-btn">
      <el-button
        type="primary"
        @click="search()"
        icon="search"
      >
        查询
      </el-button>
      <el-button
        @click="reset()"
        icon="RefreshLeft"
      >
        重置
      </el-button>
    </div>
  </div>
</template>
<style lang='scss' scoped>
.search-btn {
  float: right;
  height: 0px;
  height: 0;
  position: relative;
  top: -50px;
}
</style>
3、新增、编辑、查看Dialog封装
路径 /src/components/CustomDialog/index.vue
<script lang="ts" setup>
interface Props {
  dialogVisible: boolean,
  verifyLoading?: boolean,
  dialogConfig: DialogConfig
}
const props = withDefaults(defineProps<Props>(), {
  dialogVisible: false,
  verifyLoading: false,
  dialogConfig: () => {
    return {
      dialogTitle: "",
      dialogWidth: "800px",
      dialogModal: true,
      dialogTop: '5vh',
      dialogFullscreen: false,
      dialogClickModalClose: false,
      dialogESCModalClose: false,
      dialogDraggable: false,
      dialogFooterBtn: true,
      dialogFooterSaveBtn: true,
      dialogDestroyOnClose: false
    }
  }
})
const dialog = ref<boolean>(props.dialogVisible)
const emit = defineEmits(['verify', 'close'])

// 保存提交回调函数
const verifySubmit = () => {
  emit('verify') //emit方法供父级组件调用
}
const uploadVisible = () => {
  emit('close')
}

// watch监听
watch(() => props.dialogVisible, (newValue, oldValue) => {
  dialog.value = newValue
}, { deep: true, immediate: true })

</script>
<template>
  <el-dialog
    v-model="dialog"
    append-to-body
    :modal="props.dialogConfig.dialogModal"
    :close-on-click-modal="props.dialogConfig.dialogClickModalClose"
    :close-on-press-escape="props.dialogConfig.dialogESCModalClose"
    :draggable="props.dialogConfig.dialogDraggable"
    :show-close="false"
    :width="props.dialogConfig.dialogWidth"
    :top="props.dialogConfig.dialogTop"
    :destroy-on-close="props.dialogConfig.dialogDestroyOnClose"
    class="custom-dialog"
  >
    <template #header>
      <div class="custom-dialog-header">
        <div style="font-size: 20px;font-weight: 600;">{{ props.dialogConfig.dialogTitle }}</div>
        <div style="cursor: pointer;" @click="uploadVisible()">
          <el-icon>
            <CloseBold />
          </el-icon>
        </div>
      </div>
    </template>
    <div style="max-height: 540px;overflow: auto;">
      <slot></slot>
    </div>
    <template #footer v-if="props.dialogConfig.dialogFooterBtn">
      <div style="border-top: 1px solid #f0f0f0;padding: 12px;display: flex;justify-content: center;">
        <el-button
          v-if="props.dialogConfig.dialogFooterSaveBtn"
          type="primary"
          @click="verifySubmit"
          :disabled="props.verifyLoading"
          :icon="props.verifyLoading ? 'Loading' : ''"
          >确认</el-button
        >
        <el-button type="info" @click="uploadVisible()">取消</el-button>
      </div>
    </template>
  </el-dialog>
</template>
<style lang="scss" scoped>
.custom-dialog-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  height: 55px;
  border-bottom: 1px solid #f0f0f0;
  padding: 0 24px;
}
:deep(.el-dialog__footer) {
  padding: 0 !important;
}
</style>
路径 /src/components/DialogForm/index.vue
<script lang="ts" setup>
import type { FormRules } from 'element-plus'
import { parseTime } from '@/utils/ruoyi'
import FileUpload from '@/components/FileUpload/index.vue'

interface Props {
  dialogVisible: boolean,
  verifyLoading: boolean,
  dialogConfig: DialogConfig,
  formModel: FormModel,
  formItem: FormItem[],
  formConfig: FormConfig
}
const props = withDefaults(defineProps<Props>(), {})
const formRules = ref<FormRules>({})
const verifyLoading = ref(props.verifyLoading)
const formModel = ref({ ...props.formModel })
const formRef = ref<any>(null)
const emit = defineEmits(['update:dialogVisible', 'submitForm'])
const messageStr = (type: ComponentType) => {
  switch (type) {
    case 'input':
      return { message: '请输入', trigger: 'blur' }
    case 'select':
      return { message: '请选择', trigger: 'change' }
    case 'date':
      return { message: '请选择', trigger: 'change' }
    case 'datetime':
      return { message: '请选择', trigger: 'change' }
    case 'radio':
      return { message: '请选择', trigger: 'change' }
    case 'checkbox':
      return { message: '请选择', trigger: 'change' }
    case 'upload':
      return { message: '请上传', trigger: 'change' }
    default:
      return { message: '请输入', trigger: 'blur' }
  }
}
props.formItem.forEach(e => {
  if (e.isRules) {
    formRules.value[e.prop] = [
      {
        required: true,
        message: `${messageStr(e.componentType).message}${e.label}`,
        trigger: messageStr(e.componentType).trigger
      }
    ]
  }
})

const closeDialog = () => {
  emit('update:dialogVisible', false)
}
const dialogVerify = async () => {
  await formRef.value?.validate((valid: boolean) => {
    if (valid) {
      const formData = { ...formModel.value }
      // 日期自动格式化提交
      props.formItem.forEach(e => {
        if (e.type == 'date') {
          formData[e.prop] = parseTime(formData[e.prop], '{y}-{m}-{d}')
        }
        if (e.type == 'datetime') {
          formData[e.prop] = parseTime(formData[e.prop], '{y}-{m}-{d} {h}:{i}:{s}')
        }
      })
      emit('submitForm', formData)
    }
  })
}

watch(() => props.dialogVisible, (newValue, oldValue) => {
  if (newValue === true && formRef.value) {
    formRef.value.resetFields()
  }
  if (newValue === false) {
    closeDialog()
  }
}, { deep: true, immediate: true })
watch(() => props.verifyLoading, (newValue, oldValue) => {
  verifyLoading.value = newValue
}, { deep: true, immediate: true })
watch(() => props.formModel, (newValue, oldValue) => {
  formModel.value = newValue
}, { deep: true, immediate: true })
</script>
<template>
  <custom-dialog
    :dialogVisible="props.dialogVisible"
    :dialogConfig="props.dialogConfig"
    :verifyLoading="verifyLoading"
    @verify="dialogVerify"
    @close="closeDialog"
  >
    <el-form :model="formModel" :rules="formRules" :inline="formConfig.inline" :label-width="formConfig.labelWidth || '100px'" ref="formRef">
      <el-form-item v-for="(item, index) in formItem" :key="index" :label="item.label" :prop="item.prop">
        <!-- 默认输入框 -->
        <el-input
          v-if="item.componentType === 'input'"
          v-model="formModel[item.prop]"
          :type="item.type ?? 'text'"
          :placeholder="item.placeholder || `请输入${item.label}`"
          :readonly="formConfig.type === 'view' ? true : item.disabled ? true : false"
          :clearable="item.clearable"
          :maxlength="item.maxlength !== undefined ? item.maxlength : 100"
          :style="{ width: item.width ? item.width : '214px' }"
        />
        <!-- 下拉框  -->
        <el-select
          v-else-if="item.componentType === 'select'"
          v-model="formModel[item.prop]"
          :placeholder="item.placeholder || `请选择${item.label}`"
          :disabled="formConfig.type === 'view' ? true : item.disabled ? true : false"
          :clearable="item.clearable"
          :style="{ width: item.width ? item.width : '214px' }"
        >
          <el-option v-for="s in item.options" :key="s.value" :label="s.label" :value="s.value" />
        </el-select>
        <!-- 日期选择  -->
        <el-date-picker
          v-else-if="item.componentType === 'date'"
          v-model="formModel[item.prop]"
          type="date"
          :placeholder="item.placeholder || `请选择${item.label}`"
          :disabled="formConfig.type === 'view' ? true : item.disabled ? true : false"
          :style="{ width: item.width ? item.width : '214px' }"
        />
        <!-- 日期时间选择选择  -->
        <el-date-picker
          v-else-if="item.componentType === 'datetime'"
          v-model="formModel[item.prop]"
          type="datetime"
          :placeholder="item.placeholder || `请选择${item.label}`"
          :disabled="formConfig.type === 'view' ? true : item.disabled ? true : false"
          :style="{ width: item.width ? item.width : '214px' }"
        />
        <!-- 单选框  -->
        <el-radio-group
          v-else-if="item.componentType === 'radio'"
          v-model="formModel[item.prop]"
          :disabled="formConfig.type === 'view' ? true : item.disabled ? true : false"
          :placeholder="item.placeholder || `请选择${item.label}`"
          :style="{ width: item.width ? item.width : '214px' }"
        >
          <el-radio-group size="large" v-for="radio in item.options" :key="radio.value" :label="radio.label">
            {{ radio.label }}.value
          </el-radio-group>
        </el-radio-group>

        <!-- 复选框 -->
        <el-checkbox-group
          v-else-if="item.componentType === 'checkbox'"
          v-model="formModel[item.prop]"
          :disabled="formConfig.type === 'view' ? true : item.disabled ? true : false"
          :placeholder="item.placeholder || `请选择${item.label}`"
          :style="{ width: item.width ? item.width : '214px' }"
        >
          <el-checkbox v-for="checkbox in item.options" :key="checkbox.value" :label="checkbox.label">{{ checkbox.label }}</el-checkbox>
        </el-checkbox-group>
        <file-upload
          v-if="item.componentType === 'upload'"
          v-model:modelValue="formModel[item.prop]"
          :limit="item.limit ? item.limit : 5"
          :fileSize="item.fileSize ? item.fileSize : 5"
          :fileType="item.fileType ? item.fileType : undefined"
          :disabled="formConfig.type === 'view' ? true : item.disabled ? true : false"
        />
      </el-form-item>
    </el-form>
  </custom-dialog>
</template>
<style lang="scss" scoped></style>
路径 /src/components/FileUpload/index.vue
<script setup lang="ts">
import { getToken } from "@/utils/auth";
import { ComponentInternalInstance } from "vue";
import { ElUpload, UploadFile } from "element-plus";

const VITE_APP_BASE_API = ref(import.meta.env.VITE_APP_BASE_API)
const props = defineProps({
  modelValue: [String, Object, Array],
  // 数量限制
  limit: {
    type: Number,
    default: 10,
  },
  // 大小限制(MB)
  fileSize: {
    type: Number,
    default: 10,
  },
  // 文件类型, 例如['png', 'jpg', 'jpeg']
  fileType: {
    type: Array,
    default: () => ['jpeg', 'jpg', 'png', 'doc', 'docx', 'xls', 'xlsx', 'pdf'],
  },
  // 是否显示提示
  isShowTip: {
    type: Boolean,
    default: true
  },
  disabled: {
    type: Boolean,
    default: false
  }
});

const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const emit = defineEmits(['update:modelValue', 'uploadSuccess']);
const number = ref(0);
const uploadList = ref<any[]>([]);

const baseUrl = import.meta.env.VITE_APP_BASE_API;
const uploadFileUrl = ref(baseUrl + "/common/upload"); // 上传文件服务器地址
const headers = ref({ Authorization: "Bearer " + getToken() });

const fileList = ref<any[]>([]);
const showTip = computed(
  () => props.isShowTip && (props.fileType || props.fileSize)
);

const fileUploadRef = ref(ElUpload);

watch(() => props.modelValue, async val => {
  if (val) {
    let temp = 1;
    // 首先将值转为数组
    let list = [];
    if (Array.isArray(val)) {
      list = val;
    } else {
      list = val.split(',');
    }
    // 然后将数组转为对象数组
    fileList.value = list.map((item: any) => {
      item = { name: item, url: item, ossId: item };
      item.uid = item.uid || new Date().getTime() + temp++;
      return item;
    });
  } else {
    fileList.value = [];
    return [];
  }
}, { deep: true, immediate: true });

// 上传前校检格式和大小
const handleBeforeUpload = (file: any) => {
  // 校检文件类型
  if (props.fileType.length) {
    const fileName = file.name.split('.');
    const fileExt = fileName[fileName.length - 1];
    const isTypeOk = props.fileType.indexOf(fileExt) >= 0;
    if (!isTypeOk) {
      proxy?.$modal.msgError(`文件格式不正确, 请上传${props.fileType.join("/")}格式文件!`);
      return false;
    }
  }
  // 校检文件大小
  if (props.fileSize) {
    const isLt = file.size / 1024 / 1024 < props.fileSize;
    if (!isLt) {
      proxy?.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`);
      return false;
    }
  }
  proxy?.$modal.loading("正在上传文件,请稍候...");
  number.value++;
  return true;
}

// 文件个数超出
const handleExceed = () => {
  proxy?.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`);
}

// 上传失败
const handleUploadError = () => {
  proxy?.$modal.msgError("上传文件失败");
  proxy?.$modal.closeLoading();
}

// 上传成功回调
const handleUploadSuccess = (res: any, file: UploadFile) => {
  if (res.code === 200) {
    uploadList.value.push({ name: res.originalFilename, url: res.fileName, ossId: res.fileName });
    emit('uploadSuccess', file, file, uploadList.value)
    uploadedSuccessfully();
  } else {
    number.value--;
    proxy?.$modal.closeLoading();
    proxy?.$modal.msgError(res.msg);
    fileUploadRef.value.handleRemove(file);
    uploadedSuccessfully();
  }
}

// 删除文件
const handleDelete = (index: number) => {
  let ossId = fileList.value[index].ossId;
  fileList.value.splice(index, 1);
  emit("update:modelValue", listToString(fileList.value));
}

// 上传结束处理
const uploadedSuccessfully = () => {
  if (number.value > 0 && uploadList.value.length === number.value) {
    fileList.value = fileList.value.filter(f => f.url !== undefined).concat(uploadList.value);
    uploadList.value = [];
    number.value = 0;
    emit("update:modelValue", listToString(fileList.value));
    proxy?.$modal.closeLoading();
  }
}

// 获取文件名称
const getFileName = (name: string) => {
  // 如果是url那么取最后的名字 如果不是直接返回
  if (name.lastIndexOf("/") > -1) {
    return name.slice(name.lastIndexOf("/") + 1);
  } else {
    return name;
  }
}

// 对象转成指定字符串分隔
const listToString = (list: any[], separator?: string) => {
  let strs = "";
  separator = separator || ",";
  list.forEach(item => {
    if (item.ossId) {
      strs += item.ossId + separator;
    }
  })
  return strs != "" ? strs.substring(0, strs.length - 1) : "";
}
</script>
<template>
  <div class="upload-file">
    <el-upload multiple :action="uploadFileUrl" :before-upload="handleBeforeUpload" :file-list="fileList" :limit="limit"
      :on-error="handleUploadError" :on-exceed="handleExceed" :on-success="handleUploadSuccess" :show-file-list="false"
      :headers="headers" :accept="fileType ? fileType.join(',') : ''" class="upload-file-uploader" ref="fileUploadRef"
      :disabled="disabled">
      <!-- 上传按钮 -->
      <el-button type="primary" :disabled="disabled">选取文件</el-button>
    </el-upload>
    <!-- 上传提示 -->
    <div class="el-upload__tip" style="line-height: 15px" v-if="showTip">
      请上传
      <template v-if="fileSize">
        大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
      </template>
      <template v-if="fileType">
        格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b>
      </template>
      的文件
    </div>
    <!-- 文件列表 -->
    <transition-group class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul">
      <li :key="file.uid" class="el-upload-list__item ele-upload-list__item-content" v-for="(file, index) in fileList">
        <el-link :href="`${VITE_APP_BASE_API + file.url}`" :underline="false" target="_blank"
          style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">
          <span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" class="el-icon-document"> {{
            getFileName(file.name) }} </span>
        </el-link>
        <div class="ele-upload-list__item-content-action" style="flex-shrink: 0;">
          <el-link :underline="false" @click="handleDelete(index)" type="danger">删除</el-link>
        </div>
      </li>
    </transition-group>
  </div>
</template>
<style scoped lang="scss">
.upload-file-uploader {
  margin-bottom: 5px;
}

.upload-file-list .el-upload-list__item {
  border: 1px solid #e4e7ed;
  line-height: 2;
  margin-bottom: 10px;
  position: relative;
}

.upload-file-list .ele-upload-list__item-content {
  display: flex;
  justify-content: space-between;
  align-items: center;
  color: inherit;
}

.ele-upload-list__item-content-action .el-link {
  margin-right: 10px;
}

:deep(.el-link__inner) {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
</style>

4、表格区域封装
路径 /src/components/CustomTable/index.vue
<script setup lang="ts">
/*传值说明:
    settings:{          相关配置
        isLoading       加载数据时显示动效
        height          表格高度
        isSelection;    是否有多选
        isIndex         是否需要序列号,默认不需要
        isBorder:       是否加框线,默认添加,
        isPagination:   是否添加分页,默认false,
        pageSize        每页多少条
        paginationHeight分页高度
        total:          列表总条数
        pageNum     当前页数
        align           数据对齐方式menu[left,center,right]
    }
    tableData:          表格数据
    tableHeader:{       表头数据
        label           表头文本
        prop            tableData对应字段
        isTemplate      是否使用插槽
        type            对应列的类型。'default' | 'selection' | 'index' | 'expand'
        width
        fixed           列是否固定在左侧或者右侧。 true 表示固定在左侧
    }
事件说明:
    pageSize          pageSize发生变化
    pageNum       pageNum发生变化
    handleSelect      勾选发生变化

调用方式
 <custom-table
   :tableData="tableData"
   :tableHeader="tableHeader"
   v-model:settings="tableSettings"
   @pageChange="getList()"
 ></custom-table>
*/
export interface TableProps {
  tableHeader: TableHeader[],
  settings: Settings,
  tableData: any[]
}
const props = withDefaults(defineProps<TableProps>(), {
  tableHeader: () => { return [] },
  tableData: () => { return [] },
  settings: () => {
    return {
      height: 60,
      isBorder: true,
      isLoading: false,
      isIndex: false,
      isSelection: false,
      isPagination: false,
      paginationHeight: 75,
      pageNum: 1,
      pageSize: 10,
      pageSizes: [5, 10, 15],
      total: 0,
      align: 'center',
    }
  },
})
// pageChange
const emit = defineEmits(['pageChange', 'handleSelect', 'update:settings'])
const customTable = ref<any>(null)
const pageChange = (type: 'pageSize' | 'pageNum', num: number) => {
  const settingsData = {
    ...props.settings,
    [type]: num
  }
  emit('update:settings', settingsData)
  emit('pageChange')
}
const headerCellStyle = () => {
  return {
    'background-color': '#e9f0ee !important',
    'font-size': '14px',
    'font-weight': 400,
    'color': '#333333',
    'border': '1px solid #c2cccb',
    'border-right': '0px',
    'height': '64px !important'
  }
}
const cellStyle = () => {
  return {
    'font-weight': 400,
    'color': '#000000',
    'font-size': '14px',
    // 'border': '1px solid #d7e6e5',
    'height': '50px !important',
    'padding': '0 !important'
  }
}
const height = computed(() => {
  let height: number = 0
  if (customTable.value) {
    height = customTable.value.offsetHeight
    if (props.settings.isPagination) {
      height = height - (props.settings.paginationHeight as number ? props.settings.paginationHeight as number : 75)
    }
  }
  return height ? height : undefined
})
</script>
<template>
  <!-- 添加一层定位,方式flex布局时flex为1时宽度自动变宽  -->
  <div style="width: 100%;height: 100%;position: relative;">
    <div class="custom-table" ref="customTable">
      <el-table
        :height="height"
        v-loading="settings.isLoading || false"
        @selection-change="(e: any) => emit('handleSelect', e)"
        :data="tableData"
        style="width: 100%;border: 1px solid #f0f0f0;"
        row-key="id"
        :indent="0"
        :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
        stripe
        :header-cell-style="headerCellStyle"
        :cell-style="cellStyle"
        :default-expand-all="false"
        border
      >
        <el-table-column v-if="settings.isSelection" width="55" type="selection" fixed :align="settings.align"></el-table-column>
        <el-table-column
          v-if="settings.isIndex"
          type="index"
          :index="1"
          fixed
          label="序号"
          width="55"
          :align="settings.align"
          style="color: #007EFF;"
        ></el-table-column>
        <template v-for="item in tableHeader">
          <template v-if="!item.isTemplate">
            <el-table-column
              :key="item.prop"
              :type="item.type"
              :prop="item.prop"
              :label="item.label"
              :width="item.width ? item.width : tableHeader.length > 12 ? 130 : undefined"
              :fixed="item.fixed"
              :show-overflow-tooltip="true"
              :align="settings.align"
            ></el-table-column>
          </template>
          <!-- 使用插槽自定义 -->
          <template v-else>
            <el-table-column
              :key="item.prop"
              :type="item.type"
              :label="item.label"
              :width="item.width"
              :fixed="item.fixed"
              :show-overflow-tooltip="true"
              :align="settings.align"
            >
              <template v-slot="{ row }">
                <slot :name="item.prop" :data="row" />
              </template>
            </el-table-column>
          </template>
        </template>
        <slot name="action"></slot>
        <template #empty>
          <div class="table-null" v-if="!settings.isLoading">
            <img src="@/assets/images/public/zanwushuju.png" alt="" style="width: 150px;height: 150px;" />
            <div class="empty-text">暂无数据</div>
          </div>
        </template>
      </el-table>
      <div class="tab-footer-pagination" :style="'height:' + props.settings.paginationHeight + 'px'" v-if="settings.isPagination">
        <el-pagination
          background
          style="text-align:right;padding:25px 15px 25px 0;overflow-x: auto;overflow-y:hidden"
          @size-change="(e: any) => pageChange('pageSize', e)"
          @current-change="(e: any) => pageChange('pageNum', e)"
          :current-page="settings.pageNum"
          :page-sizes="settings.pageSizes"
          :page-size="settings.pageSize"
          layout=" prev, pager, next, sizes,jumper"
          :total="settings.total"
        ></el-pagination>
      </div>
    </div>
  </div>
</template>
<style lang="scss" scoped>
.custom-table {
  width: 100%;
  // flex: 1;
  height: 100%;
  box-sizing: border-box;
  box-sizing: border-box;
  background-color: #ffffff;
  position: absolute;
}

.empty-text {
  font-size: 18px;
  color: #999;
  font-weight: 400;
  text-align: center;
  height: 24px;
  line-height: 24px;
}

.el-table .el-table__expand-icon {
  display: none;
}

.el-table__row--level-1 {
  background-color: #F0EDFF;
}

.el-table__body tr.current-row>td {
  background-color: #fff !important;
}

.tab-footer-pagination {
  display: flex;
  justify-content: flex-end;
  margin-right: 27px;
  height: 75px;
  background-color: #ffffff;
}

.table-null {
  margin: auto;
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
}

/* // 表格斑马自定义颜色 */
:deep(.el-table__row.warning-row) {
  background: #f6fbfa;
}

/* // 修改高亮当前行颜色 */
:deep(.el-table tbody tr:hover>td) {
  background: #ccf1e9 !important;
}

/* 全局样式或组件的<style>标签中 */
//强制显示横向滚动条
:deep(.el-scrollbar__bar.is-horizontal) {
  display: block !important;
}

//滚动条颜色
:deep(.el-scrollbar__thumb) {
  background: #d4d8de !important;
  opacity: 1 !important;
}

//强制显示纵向滚动条
:deep(.el-scrollbar__bar.is-vertical) {
  display: block !important;
}
</style>
5、其他代码
路径 /src/types/global.d.ts
import { FormRules } from 'element-plus';
declare global {
  /**
   * 界面字段隐藏属性
   */
  interface FieldOption {
    key: number;
    label: string;
    visible: boolean;
  }

  /**
   * 弹窗属性
   */
  interface DialogOption {
    /**
     * 弹窗标题
     */
    title?: string;
    /**
     * 是否显示
     */
    visible: boolean;
  }

  interface UploadOption {
    /** 设置上传的请求头部 */
    headers: { [key: string]: any };

    /** 上传的地址 */
    url: string;
  }

  /**
   * 导入属性
   */
  interface ImportOption extends UploadOption {
    /** 是否显示弹出层 */
    open: boolean;
    /** 弹出层标题 */
    title: string;
    /** 是否禁用上传 */
    isUploading: boolean;

    /** 其他参数 */
    [key: string]: any;
  }
  /**
   * 字典数据  数据配置
   */
  interface DictDataOption {
    label: string;
    value: string;
    elTagType?: ElTagType;
    elTagClass?: string;
  }

  interface BaseEntity {
    createBy?: any;
    createTime?: string;
    updateBy?: any;
    updateTime?: any;
  }

  /**
   * 分页数据
   * T : 表单数据
   * D : 查询参数
   */
  interface PageData<T, D> {
    form: T;
    queryParams: D;
    rules: FormRules;
  }
  /**
   * 分页查询参数
   */
  interface PageQuery {
    pageNum: number;
    pageSize: number;
  }

  /**
   * 表单结构
   */
  interface FormStructure {
    type: 'input' | 'select' | 'date' | 'datetime',
    label: string,
    prop: string,
    placeholder?: string,
    length?: string,
    disabled?: boolean,
    style?: string,
    multiple?: boolean,
    filterable?: boolean,
    clearable?: boolean,
    options?: { label: string; value: string | number; }[];
    labelWidth?: string,
  }

  /**
   * 表格头部
   */
  interface TableHeader {
    label: string;
    prop: string;
    type?: string,
    isTemplate?: boolean;
    width?: string | number;
    fixed?: boolean | 'left' | 'right',
  }

  /**
   * 表格数据
   */
  interface Settings {
    height?: number | string;
    isBorder?: boolean;
    isLoading?: boolean;
    isIndex?: boolean;
    isSelection?: boolean;
    isPagination?: boolean;
    paginationHeight?: number;
    pageNum: number;
    pageSize: number;
    pageSizes?: number[];
    total?: number;
    align?: string;
  }

  /**
  * 模态框配置
  * @param dialogTitle: string; //模态框标题名称
  * @param dialogWidth: string; //模态框弹窗宽度
  * @param dialogModal: boolean; //是否需要模态框(遮罩层)
  * @param dialogTop: string, //模态框距离顶部距离
  * @param dialogFullscreen: boolean; //模态框是否为全屏
  * @param dialogClickModalClose: any; //是否可以通过点击遮罩层关闭Dialog
  * @param dialogESCModalClose: any; //是否可以通过按下ESC关闭Dialog
  * @param dialogDraggable: any; //是否开启模态框拖拽功能
  * @param dialogFooterBtn: any; //是否开启底部操作按钮
  * @param dialogFooterSaveBtn: any; //是否开启底部确认按钮
  * @param dialogDestroyOnClose: boolean; //是否在关闭时销毁
  */
  interface DialogConfig {
    dialogTitle: string; //模态框标题名称
    dialogWidth: string; //模态框弹窗宽度
    dialogModal: boolean; //是否需要模态框(遮罩层)
    dialogTop: string, //模态框距离顶部距离
    dialogFullscreen: boolean; //模态框是否为全屏
    dialogClickModalClose: any; //是否可以通过点击遮罩层关闭Dialog
    dialogESCModalClose: any; //是否可以通过按下ESC关闭Dialog
    dialogDraggable: any; //是否开启模态框拖拽功能
    dialogFooterBtn: any; //是否开启底部操作按钮
    dialogFooterSaveBtn: any; //是否开启底部确认按钮
    dialogDestroyOnClose: boolean; //是否在关闭时销毁
  }

  type ComponentType = 'input' | 'select' | 'date' | 'datetime' | 'radio' | 'checkbox' | 'upload'
  type Types = 'text' | 'date' | 'datetime' | 'textarea' | 'number' | 'password'
  /**
   * 表单配置
   * @param componentType 组件类型
   * @param type 字段类型
   * @param label 标签
   * @param prop 字段名
   * @param isRules 是否校验
   * @param maxlength 最大长度
   * @param options 选项
   * @param clearable 是否可清除
   * @param placeholder 占位符
   * @param width 宽度
   * @param limit 最大上传数量
   * @param fileType 文件类型
   * @param fileSize 文件大小
   */
  interface FormItem {
    componentType: ComponentType,
    type?: Types,
    label: string,
    prop: string,
    isRules: boolean,
    maxlength?: number,
    options?: { label: string, value: string }[],
    clearable?: boolean,
    placeholder?: string,
    width?: string,
    limit?: number,
    fileType?: string[],
    fileSize?: number,
    disabled?: boolean
  }
  interface FormConfig {
    type: 'edit' | 'view',
    inline: boolean,
    labelWidth: string | number
  }
  interface FormModel {
    [key: string]: any
  }

  interface Options {
    label: string,
    value: string
  }
mian.ts
...
// 全局组件
import CustomTable from '@/components/CustomTable/index.vue';
import FormSearch from '@/components/FormSearch/index.vue';
import CustomDialog from '@/components/CustomDialog/index.vue';
import DialogForm from '@/components/DialogForm/index.vue';
...
// 全局组件
app.component('custom-table', CustomTable)
app.component('form-search', FormSearch)
app.component('custom-dialog', CustomDialog)
app.component('dialog-form', DialogForm)
...
.env.development
...
# 开发环境
VITE_APP_BASE_API = '/dev-api'
.eslintrc.js
module.exports = {
...
  include: [...其他规则...,'src/types/**/*.d.ts'],
...
}
tsconfig.json
{
...
  "include": [...其他规则..., "src/types/**/*.d.ts"],
...
}
  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,我会尽力回答你的问题。 首先,Element Plus 是一个基于 Vue 3 的 UI 组件库,而 TypeScript 则是一种强类型的 JavaScript 扩展语言。在 Vue 3 中,我们可以通过 Composition API 来编写组件。 针对你的需求,我们可以封装一个 Tree 组件,代码如下: ```typescript <template> <el-tree :data="data" :props="props" :expand-on-click-node="false" @node-click="handleNodeClick"> <slot name="default" v-bind="{ node, data }" /> </el-tree> </template> <script lang="ts"> import { defineComponent, PropType } from 'vue' import { ElTree } from 'element-plus' interface TreeNode { label: string children?: TreeNode[] } interface Props { data: TreeNode[] props: Record<string, any> } export default defineComponent({ name: 'MyTree', components: { ElTree }, props: { data: { type: Array as PropType<TreeNode[]>, required: true }, props: { type: Object as PropType<Record<string, any>>, default: () => ({ label: 'label', children: 'children' }) }, }, setup(props: Props, { emit }) { const handleNodeClick = (data: any, node: any, instance: any) => { emit('node-click', data, node, instance) } return { handleNodeClick, } }, }) </script> ``` 在这个组件中,我们使用了 Element Plus 的 `el-tree` 组件,并使用了 `slot` 来插入自定义节点内容。通过 TypeScript 的类型定义,我们可以确保传入的 `data` 和 `props` 属性是正确的格式。同时,我们为 `node-click` 事件添加了一个自定义的处理函数。 使用这个组件的方式非常简单: ```vue <template> <my-tree :data="data" @node-click="handleNodeClick"> <template #default="{ node, data }"> <span>{{ node.label }}</span> </template> </my-tree> </template> <script lang="ts"> import { defineComponent } from 'vue' import MyTree from '@/components/MyTree.vue' interface TreeNode { label: string children?: TreeNode[] } export default defineComponent({ components: { MyTree }, setup() { const data: TreeNode[] = [ { label: '一级 1', children: [ { label: '二级 1-1', children: [ { label: '三级 1-1-1' }, { label: '三级 1-1-2' }, ], }, { label: '二级 1-2', children: [ { label: '三级 1-2-1' }, { label: '三级 1-2-2' }, ], }, ], }, { label: '一级 2', children: [ { label: '二级 2-1', children: [ { label: '三级 2-1-1' }, { label: '三级 2-1-2' }, ], }, { label: '二级 2-2', children: [ { label: '三级 2-2-1' }, { label: '三级 2-2-2' }, ], }, ], }, ] const handleNodeClick = (data: any, node: any, instance: any) => { console.log('node-click', data, node, instance) } return { data, handleNodeClick, } }, }) </script> ``` 在这个示例中,我们使用了自定义的 `MyTree` 组件,并在 `slot` 中渲染了节点的标签文本。 希望这个示例能够帮助到你。如果你还有其他问题,可以随时问我。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值