gaoluyang
2025-05-07 e0430e0b25d759f6505a4e4542562a69c93b1db5
客户档案页面开发
已修改2个文件
已添加3个文件
486 ■■■■■ 文件已修改
src/assets/styles/index.scss 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/PIMTable/PIMTable.vue 314 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/PIMTable/Pagination.vue 100 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main.js 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/customerFile/index.vue 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/styles/index.scss
@@ -125,7 +125,22 @@
.app-container {
  padding: 20px;
}
.search_form {
  display: flex;
  align-items: center;
  justify-content: space-between;
  .search_title {
    font-size: 14px;
    font-weight: 700;
    color: #333333;
  }
}
.table_list {
  height: calc(100vh - 11em);
  margin-top: 20px;
  background: #fff;
  padding: 18px
}
.components-container {
  margin: 30px 50px;
  position: relative;
src/components/PIMTable/PIMTable.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,314 @@
<template>
  <el-table ref="multipleTable" v-loading="tableLoading" :border="border" :data="tableData"
            :header-cell-style="{ background: '#F0F1F5', color: '#333333' }" :height="height"
            :highlight-current-row="highlightCurrentRow" :row-class-name="rowClassName" :row-style="rowStyle"
            :row-key="rowKey" :span-method="spanMethod" stripe style="width: 100%" tooltip-effect="dark" @row-click="rowClick"
            @current-change="currentChange" @selection-change="handleSelectionChange" class="lims-table">
    <el-table-column align="center" type="selection" width="55" v-if="isSelection" />
    <el-table-column align="center" label="序号" type="index" width="60" :index="indexMethod" />
    <el-table-column v-for="(item, index) in column" :key="index" :column-key="item.columnKey"
                     :filter-method="item.filterHandler" :filter-multiple="item.filterMultiple" :filtered-value="item.filteredValue"
                     :filters="item.filters" :fixed="item.fixed" :label="item.label" :prop="item.prop"
                     :show-overflow-tooltip="!(item.dataType === 'action' || item.dataType === 'slot')"
                     :min-width="item.dataType == 'action' ? btnWidth : item.width"
                     :sortable="!!item.sortable" :type="item.type" :width="item.dataType == 'action' ? btnWidth : item.width" align="center">
      <template v-if="item.hasOwnProperty('colunmTemplate')" #[item.colunmTemplate]="scope">
        <slot v-if="item.theadSlot" :name="item.theadSlot" :index="scope.$index" :row="scope.row" />
      </template>
      <template #default="scope">
        <!-- æ’æ§½ -->
        <div v-if="item.dataType == 'slot'">
          <slot v-if="item.slot" :index="scope.$index" :name="item.slot" :row="scope.row" />
        </div>
        <!-- è¿›åº¦æ¡ -->
        <div v-else-if="item.dataType == 'progress'">
          <el-progress :percentage="Number(scope.row[item.prop])" />
        </div>
        <!-- å›¾ç‰‡ -->
        <div v-else-if="item.dataType == 'image'">
          <img :src="javaApi + '/img/' + scope.row[item.prop]" alt="" style="width: 40px; height: 40px; margin-top: 10px" />
        </div>
        <!-- tag -->
        <div v-else-if="item.dataType == 'tag'">
          <el-tag v-if="typeof dataTypeFn(scope.row[item.prop], item.formatData) === 'string'"
                  :title="formatters(scope.row[item.prop], item.formatData)"
                  :type="formatType(scope.row[item.prop], item.formatType)">
            {{ formatters(scope.row[item.prop], item.formatData) }}
          </el-tag>
          <el-tag v-for="(tag, index) in dataTypeFn(scope.row[item.prop], item.formatData)"
                  v-else-if="typeof dataTypeFn(scope.row[item.prop], item.formatData) === 'object'"
                  :key="index" :title="formatters(scope.row[item.prop], item.formatData)" :type="formatType(tag, item.formatType)">
            {{ item.tagGroup ? tag[item.tagGroup.label] ?? tag : tag }}
          </el-tag>
          <el-tag v-else :title="formatters(scope.row[item.prop], item.formatData)" :type="formatType(scope.row[item.prop], item.formatType)">
            {{ formatters(scope.row[item.prop], item.formatData) }}
          </el-tag>
        </div>
        <!-- æŒ‰é’® -->
        <div v-else-if="item.dataType == 'action'"
             :style="`min-width:${getWidth(item.operation, scope.row)}`">
          <template v-for="(o, key) in item.operation" :key="key">
            <el-button v-show="o.type != 'upload'" size="small" v-if="o.showHide ? o.showHide(scope.row) : true"
                       :disabled="o.disabled ? o.disabled(scope.row) : false" :plain="o.plain"
                       :style="{ color: (o.name === '删除' || o.name === 'delete') ? '#f56c6c' : o.color }" :type="typeFn(o.type, scope.row)"
                       @click="o.clickFun(scope.row)" :key="key">
              {{ o.name }}
            </el-button>
            <el-upload :action="javaApi + o.url + '?id=' + (o.uploadIdFun ? o.uploadIdFun(scope.row) : scope.row.id)"
                       ref="uploadRef" size="small" :multiple="o.multiple ? o.multiple : false" :limit="1"
                       :disabled="o.disabled ? o.disabled(scope.row) : false"
                       :accept="o.accept ? o.accept : '.jpg,.jpeg,.png,.gif,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.pdf,.zip,.rar'"
                       v-if="o.type == 'upload'" style="display: inline-block; width: 50px"
                       v-show="o.showHide ? o.showHide(scope.row) : true" :headers="uploadHeader"
                       :before-upload="(file) => beforeUpload(file, scope.$index)"
                       :on-change="(file, fileList) => handleChange(file, fileList, scope.$index)"
                       :on-error="(error, file, fileList) => onError(error, file, fileList, scope.$index)"
                       :on-success="(response, file, fileList) => handleSuccessUp(response, file, fileList, scope.$index)"
                       :on-exceed="onExceed" :show-file-list="false">
              <el-button :size="o.size ? o.size : 'small'" type="text"
                         :disabled="o.disabled ? o.disabled(scope.row) : false">{{ o.name }}</el-button>
            </el-upload>
          </template>
        </div>
        <!-- å¯ç‚¹å‡»çš„æ–‡å­— -->
        <div v-else-if="item.dataType == 'link'" class="cell link" style="width: 100%"
             @click="goLink(scope.row, item.linkMethod)">
          <span v-if="!item.formatData">{{ scope.row[item.prop] }}</span>
        </div>
        <!-- é»˜è®¤çº¯å±•示数据 -->
        <div v-else class="cell" style="width: 100%">
          <span v-if="!item.formatData">{{ scope.row[item.prop] }}</span>
          <span v-else>{{ formatters(scope.row[item.prop], item.formatData) }}</span>
        </div>
      </template>
    </el-table-column>
  </el-table>
  <pagination v-show="page.total > 0" :total="page.total" :layout="page.layout" :page="page.current"
              :limit="page.size" @pagination="paginationSearch" />
</template>
<script setup>
import pagination from './Pagination.vue'
import { ref, inject, getCurrentInstance } from "vue"
import { ElMessage } from "element-plus"
// èŽ·å–å…¨å±€çš„ uploadHeader
const { proxy } = getCurrentInstance()
const uploadHeader = proxy.uploadHeader
const javaApi = proxy.javaApi
// Filters
const typeFn = (val, row) => {
  return typeof val === 'function' ? val(row) : val
}
const formatters = (val, format) => {
  return typeof format === 'function' ? format(val) : val
}
// Props(使用 defineProps çš„非 TS å½¢å¼ï¼‰
const props = defineProps({
  isSelection: {
    type: Boolean,
    default: false
  },
  height: {
    type: [String, null],
    default: null
  },
  tableLoading: {
    type: Boolean,
    default: false
  },
  handleSelectionChange: {
    type: Function,
    default: () => {}
  },
  rowClick: {
    type: Function,
    default: () => {}
  },
  currentChange: {
    type: Function,
    default: () => {}
  },
  border: {
    type: Boolean,
    default: true
  },
  highlightCurrentRow: {
    type: Boolean,
    default: false
  },
  headerCellStyle: {
    type: Object,
    default: () => ({})
  },
  column: {
    type: Array,
    default: () => []
  },
  rowClassName: {
    type: Function,
    default: () => ''
  },
  rowStyle: {
    type: [Object, Function],
    default: () => ({})
  },
  tableData: {
    type: Array,
    default: () => []
  },
  rowKey: {
    type: String,
    default: undefined
  },
  page: {
    type: Object,
    default: () => ({
      total: 0,
      current: 0,
      size: 10,
      layout: 'total, sizes, prev, pager, next, jumper'
    })
  }
})
// Data
const spanList = ref([])
const btnWidth = ref('120px')
const uploadRefs = ref([])
const currentFiles = ref({})
const uploadKeys = ref({})
// åˆå¹¶å•元格方法
const spanMethod = ({ row, column, rowIndex, columnIndex }) => {
  if (column.find((m) => m.mergeCol)) {
    let i = Number(rowIndex)
    const obj = spanList.value.find((item, index) => {
      i = index
      return item.index == columnIndex
    })
    if (obj) {
      const _row = spanList[i].arr[rowIndex]
      const _col = _row > 0 ? 1 : 0
      return {
        rowspan: _row,
        colspan: _col
      }
    }
  }
}
const indexMethod = (index) => {
  return (props.page.current - 1) * props.page.size + index + 1
}
const getWidth = (row, row0) => {
  let count = 0
  row.forEach((a) => {
    if (a.showHide !== undefined && a.showHide(row0)) {
      count += a.name.length
    } else if (!a.showHide) {
      count += a.name.length
    }
  })
  btnWidth.value = count * 15 + 60 + "px"
  return count * 15 + 60 + "px"
}
// ç‚¹å‡» link äº‹ä»¶
const goLink = (row, linkMethod) => {
  if (!linkMethod) {
    return ElMessage.warning("请配置 link äº‹ä»¶")
  }
  const parentMethod = getParentMethod(linkMethod)
  if (typeof parentMethod === 'function') {
    parentMethod(row)
  } else {
    console.warn(`父组件中未找到方法: ${linkMethod}`)
  }
}
// èŽ·å–çˆ¶ç»„ä»¶æ–¹æ³•ï¼ˆç¤ºä¾‹å®žçŽ°ï¼‰
const getParentMethod = (methodName) => {
  const parentMethods = inject('parentMethods', {})
  return parentMethods[methodName]
}
const dataTypeFn = (val, format) => {
  if (typeof format === "function") {
    return format(val)
  } else return val
}
const formatType = (val, format) => {
  if (typeof format === "function") {
    return format(val)
  } else return ""
}
// æ–‡ä»¶å˜åŒ–处理
const handleChange = (file, fileList, index) => {
  if (fileList.length > 1) {
    const earliestFile = fileList[0]
    uploadRefs.value[index]?.handleRemove(earliestFile)
  }
  currentFiles.value[index] = file
}
// æ–‡ä»¶ä¸Šä¼ å‰æ ¡éªŒ
const beforeUpload = (rawFile, index) => {
  currentFiles.value[index] = {}
  if (rawFile.size > 1024 * 1024 * 10) {
    ElMessage.error('上传文件不超过10M')
    return false
  }
  return true
}
// ä¸Šä¼ æˆåŠŸ
const handleSuccessUp = (response, file, fileList, index) => {
  if (response.code == 200) {
    if (uploadRefs[index]) {
      uploadRefs[index].clearFiles()
    }
    currentFiles[index] = file
    ElMessage.success("上传成功")
    resetUploadComponent(index)
  } else {
    ElMessage.error(response.message)
  }
}
const resetUploadComponent = (index) => {
  uploadKeys[index] = Date.now()
}
// ä¸Šä¼ å¤±è´¥
const onError = (error, file, fileList, index) => {
  ElMessage.error('文件上传失败,请重试')
  if (uploadRefs.value[index]) {
    uploadRefs.value[index].clearFiles()
  }
}
// æ–‡ä»¶æ•°é‡è¶…限提示
const onExceed = () => {
  ElMessage.warning('超出文件个数')
}
const paginationSearch = ({ page, limit }) => {
  emit("pagination", { page: page, limit: limit });
}
</script>
<style scoped lang="scss">
</style>
src/components/PIMTable/Pagination.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,100 @@
<template>
  <div :class="{ hidden }" class="pagination-container">
    <el-pagination
      :background="background"
      v-model:current-page="currentPage"
      v-model:page-size="pageSize"
      :layout="layout"
      :page-sizes="pageSizes"
      :pager-count="pagerCount"
      :total="total"
      v-bind="$attrs"
      @size-change="handleSizeChange"
      @current-change="handleCurrentChange"
    />
  </div>
</template>
<script setup>
import { computed } from 'vue'
import { scrollTo } from '@/utils/scroll-to'
const props = defineProps({
  total: {
    type: Number,
    required: true
  },
  page: {
    type: Number,
    default: 1
  },
  limit: {
    type: Number,
    default: 20
  },
  pageSizes: {
    type: Array,
    default: () => [10, 20, 30, 50, 100]
  },
  pagerCount: {
    type: Number,
    default: () => (document.body.clientWidth < 992 ? 5 : 7)
  },
  layout: {
    type: String,
    default: 'total, sizes, prev, pager, next, jumper'
  },
  background: {
    type: Boolean,
    default: true
  },
  autoScroll: {
    type: Boolean,
    default: true
  },
  hidden: {
    type: Boolean,
    default: false
  }
})
const emit = defineEmits(['update:page', 'update:limit', 'pagination'])
const currentPage = computed({
  get: () => props.page,
  set: (val) => emit('update:page', val)
})
const pageSize = computed({
  get: () => props.limit,
  set: (val) => emit('update:limit', val)
})
const handleSizeChange = (val) => {
  if (currentPage.value * val > props.total) {
    currentPage.value = 1
  }
  emit('pagination', { page: currentPage.value, limit: val })
  if (props.autoScroll) {
    scrollTo(0, 800)
  }
}
const handleCurrentChange = (val) => {
  emit('pagination', { page: val, limit: pageSize.value })
  if (props.autoScroll) {
    scrollTo(0, 800)
  }
}
</script>
<style scoped>
.pagination-container {
  background: #fff;
  padding: 28px 16px;
  margin-top: 10px;
}
.pagination-container.hidden {
  display: none;
}
</style>
src/main.js
@@ -42,6 +42,10 @@
import ImagePreview from "@/components/ImagePreview"
// å­—典标签组件
import DictTag from '@/components/DictTag'
// è¡¨æ ¼ç»„ä»¶
import PIMTable from "@/components/PIMTable/PIMTable.vue";
import { getToken } from "@/utils/auth";
const app = createApp(App)
@@ -54,6 +58,13 @@
app.config.globalProperties.addDateRange = addDateRange
app.config.globalProperties.selectDictLabel = selectDictLabel
app.config.globalProperties.selectDictLabels = selectDictLabels
app.config.globalProperties.javaApi = 'http://192.168.1.36:8080'
app.config.globalProperties.HaveJson = (val) => {
  return JSON.parse(JSON.stringify(val));
};
app.config.globalProperties.uploadHeader = {
  Authorization: "Bearer " + getToken(),
};
// å…¨å±€ç»„件挂载
app.component('DictTag', DictTag)
@@ -63,6 +74,7 @@
app.component('ImagePreview', ImagePreview)
app.component('RightToolbar', RightToolbar)
app.component('Editor', Editor)
app.component('PIMTable', PIMTable)
app.use(router)
app.use(store)
src/views/basicData/customerFile/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,43 @@
<template>
<div class="app-container">
  <div class="search_form">
    <div>
      <span class="search_title">客户名称:</span>
      <el-input
          v-model="input2"
          style="width: 240px"
          placeholder="请输入"
          :prefix-icon="Search"
      />
    </div>
    <div>
      <el-button type="primary">新增客户</el-button>
      <el-button>导出</el-button>
      <el-button type="danger" plain>删除</el-button>
    </div>
  </div>
  <div class="table_list">
    <PIMTable :column="tableColumn"></PIMTable>
  </div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import {Search} from "@element-plus/icons-vue";
const input2 = ref('')
const tableColumn = ref([
  {
    label: '批准内容',
    prop: 'ratifyRemark'
  }, {
    label: '批准人',
    prop: 'ratifyName',
  },
])
</script>
<style scoped lang="scss">
</style>