|
|
@@ -1,83 +1,64 @@
|
|
|
<template>
|
|
|
- <view class="table-container">
|
|
|
- <!-- 表格 -->
|
|
|
- <uni-table :type="selectionType" :border="false" @selection-change="handleSelectionChange">
|
|
|
- <uni-tr class="tabel-header">
|
|
|
- <uni-th v-for="column in columns" :key="column.prop" :align="column.align || 'center'"
|
|
|
- :width="column.width || '50'" :class="[
|
|
|
- headerClass
|
|
|
- ]" :style="getHeaderStyle(column)">
|
|
|
- <view class="header-content">
|
|
|
- {{ column.label }}
|
|
|
- <view v-if="column.sortable" class="sort-icon">
|
|
|
- <uni-icons :type="getSortIcon(column)" :size="14" color="#999" />
|
|
|
- </view>
|
|
|
- </view>
|
|
|
- </uni-th>
|
|
|
- <uni-th v-if="showOperation" align="center" :width="operationWidth" :style="getOperationHeaderStyle()">
|
|
|
- {{ t('common.operation') }}
|
|
|
- </uni-th>
|
|
|
- </uni-tr>
|
|
|
- <template v-for="(row, rowIndex) in tableData" :key="rowIndex">
|
|
|
-
|
|
|
- <uni-tr>
|
|
|
- <uni-td v-for="column in columns" :key="column.prop" :align="column.align || 'center'"
|
|
|
- :class="getCellClass(column, row)" :style="getCellStyle(column, row)">
|
|
|
- <!-- 自定义渲染插槽 -->
|
|
|
- <slot v-if="column.slot" :name="column.slot" :row="row" :column="column" :index="rowIndex">
|
|
|
- {{ row[column.prop] }}
|
|
|
- </slot>
|
|
|
-
|
|
|
- <!-- 标签类型渲染 -->
|
|
|
- <uni-tag v-else-if="column.type === 'tag'" :text="formatTagText(row[column.prop], column)"
|
|
|
- :type="formatTagType(row[column.prop], column)" size="small" />
|
|
|
-
|
|
|
- <!-- 文件类型渲染 -->
|
|
|
- <view v-else-if="column.type === 'file'">
|
|
|
- <cwg-file :path="row[column.prop]" />
|
|
|
+ <view>
|
|
|
+ <!-- 统一表格容器,根据屏幕宽度自适应 -->
|
|
|
+ <view class="table-container" :class="{ 'mobile-table': isMobile }">
|
|
|
+ <uni-table :type="selectionType" :border="false" @selection-change="handleSelectionChange" emptyText="">
|
|
|
+ <!-- 表头:根据设备类型显示不同的列 -->
|
|
|
+ <uni-tr class="table-header">
|
|
|
+ <uni-th v-for="column in displayColumns" :key="column.prop" :align="column.align || 'center'"
|
|
|
+ :width="column.width || '200'" :class="[headerClass, { sortable: column.sortable }]"
|
|
|
+ :style="getHeaderStyle(column)" @click="column.sortable && handleSort(column)">
|
|
|
+ <view class="header-content">
|
|
|
+ {{ column.label }}
|
|
|
+ <view v-if="column.sortable" class="sort-icon">
|
|
|
+ <uni-icons :type="getSortIcon(column)" :size="14" color="#999" />
|
|
|
+ </view>
|
|
|
</view>
|
|
|
+ </uni-th>
|
|
|
+ </uni-tr>
|
|
|
|
|
|
- <!-- 默认文本渲染 -->
|
|
|
- <template v-else>{{ formatCellValue(row[column.prop], column, row) }}</template>
|
|
|
- </uni-td>
|
|
|
-
|
|
|
- <!-- 操作按钮 -->
|
|
|
- <uni-td v-if="showOperation" align="center">
|
|
|
- <view class="operation-btns">
|
|
|
- <template v-for="(action, index) in operationActions" :key="index">
|
|
|
- <button v-if="showAction(action, row)" class="btn" size="mini"
|
|
|
- :type="action.type || 'default'" @click="handleAction(action, row)">
|
|
|
- <cwg-icon :name="action.icon" :size="16" color="#fff" v-if="action.icon" />
|
|
|
- {{ action.text }}
|
|
|
- </button>
|
|
|
+ <!-- 表格主体 -->
|
|
|
+ <template v-for="(row, rowIndex) in tableData" :key="rowIndex">
|
|
|
+ <uni-tr>
|
|
|
+ <!-- 数据列:根据设备类型动态渲染 -->
|
|
|
+ <uni-td v-for="column in displayColumns" :key="column.prop" :align="column.align || 'center'"
|
|
|
+ :class="getCellClass(column, row)" :style="getCellStyle(column, row)"
|
|
|
+ @click="openRowDetail(row)">
|
|
|
+ <template v-if="column.slot">
|
|
|
+ <slot :name="column.slot" :row="row" :column="column" :index="rowIndex">
|
|
|
+ {{ row[column.prop] }}
|
|
|
+ </slot>
|
|
|
</template>
|
|
|
- </view>
|
|
|
- </uni-td>
|
|
|
- </uni-tr>
|
|
|
- <!-- 展开行 - 这里是您要放置 expand 插槽的地方 -->
|
|
|
- <uni-tr v-if="expandedRows[rowIndex]" class="expand-row">
|
|
|
- <uni-td :colspan="columnSpan" class="expand-cell">
|
|
|
- <slot name="expand" :row="row" :rowIndex="rowIndex">
|
|
|
- <view class="default-expand-content">
|
|
|
- <text class="no-content">暂无展开内容</text>
|
|
|
+ <uni-tag v-else-if="column.type === 'tag'" :text="formatTagText(row[column.prop], column)"
|
|
|
+ :type="formatTagType(row[column.prop], column)" size="small" />
|
|
|
+ <view v-else-if="column.type === 'file'">
|
|
|
+ <cwg-file :path="row[column.prop]" />
|
|
|
</view>
|
|
|
- </slot>
|
|
|
- </uni-td>
|
|
|
- </uni-tr>
|
|
|
- </template>
|
|
|
-
|
|
|
- <!-- 空数据提示 -->
|
|
|
- <uni-tr v-if="tableData.length === 0">
|
|
|
- <uni-td :colspan="columnSpan" align="center">
|
|
|
- <view class="empty-data">
|
|
|
- <uni-icons type="info" size="30" color="#999"></uni-icons>
|
|
|
- <text class="empty-text">暂无数据</text>
|
|
|
- </view>
|
|
|
- </uni-td>
|
|
|
- </uni-tr>
|
|
|
- </uni-table>
|
|
|
-
|
|
|
- <!-- 分页组件 -->
|
|
|
+ <cwg-icon v-else-if="column.type === 'more'" name="crm-chevron-down"
|
|
|
+ class="crm-chevron-down" :size="16" color="#007" @click="toggleRowExpand(rowIndex)" />
|
|
|
+ <template v-else>
|
|
|
+ {{ formatCellValue(row[column.prop], column, row) }}
|
|
|
+ </template>
|
|
|
+ </uni-td>
|
|
|
+ </uni-tr>
|
|
|
+ <!-- 桌面端展开行(仅当非移动端且行展开时显示) -->
|
|
|
+ <uni-tr v-if="!isMobile && expandedRows[rowIndex]" class="expand-row">
|
|
|
+ <uni-td :colspan="columnSpan" class="expand-cell">
|
|
|
+ <slot name="expand" :row="row" :rowIndex="rowIndex">
|
|
|
+ <view class="default-expand-content">
|
|
|
+ <text class="no-content">暂无展开内容</text>
|
|
|
+ </view>
|
|
|
+ </slot>
|
|
|
+ </uni-td>
|
|
|
+ </uni-tr>
|
|
|
+ </template>
|
|
|
+ </uni-table>
|
|
|
+ </view>
|
|
|
+ <!-- 空状态 -->
|
|
|
+ <view v-if="tableData.length === 0">
|
|
|
+ <cwg-empty-state />
|
|
|
+ </view>
|
|
|
+ <!-- 分页 -->
|
|
|
<view class="pagination-container" v-if="showPagination && tableData.length > 0">
|
|
|
<view class="pagination-info">
|
|
|
<text>共 {{ pagination.total }} 条记录</text>
|
|
|
@@ -91,7 +72,7 @@
|
|
|
<view class="pagination">
|
|
|
<view class="page-item prev" :class="{ disabled: pagination.current === 1 }"
|
|
|
@click="handlePageChange('prev')">
|
|
|
- <uni-icons type="arrowleft" size="16" color="#666"></uni-icons>
|
|
|
+ <uni-icons type="arrowleft" size="16" color="#666" />
|
|
|
<text>上一页</text>
|
|
|
</view>
|
|
|
|
|
|
@@ -101,189 +82,145 @@
|
|
|
{{ page }}
|
|
|
</view>
|
|
|
</view>
|
|
|
+
|
|
|
<view class="page-item next" :class="{ disabled: pagination.current === pagination.pages }"
|
|
|
@click="handlePageChange('next')">
|
|
|
<text>下一页</text>
|
|
|
- <uni-icons type="arrowright" size="16" color="#666"></uni-icons>
|
|
|
+ <uni-icons type="arrowright" size="16" color="#666" />
|
|
|
</view>
|
|
|
</view>
|
|
|
</view>
|
|
|
+ <!-- 移动端详情弹窗 -->
|
|
|
+ <cwg-detail-popup v-model:visible="detailVisible" title="详情" :items="detailItems" />
|
|
|
</view>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { ref, computed, watch, onMounted } from 'vue'
|
|
|
+import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
|
|
|
|
|
|
const props = defineProps({
|
|
|
- // 表格列配置
|
|
|
- columns: {
|
|
|
- type: Array,
|
|
|
- required: true,
|
|
|
- default: () => []
|
|
|
- },
|
|
|
+ /**
|
|
|
+ * 表格列配置(columns 数组中每一项是一个列配置对象)
|
|
|
+ * - prop: 字段名,对应 row[prop],用于取值/排序/详情展示
|
|
|
+ * - label: 表头显示文本(也会作为详情弹窗的 label)
|
|
|
+ * - align: 对齐方式('left' | 'center' | 'right')
|
|
|
+ * - width: 列宽(设置后该列固定宽度;未设置则自适应
|
|
|
+ * - slot: 自定义渲染插槽名(<template #xxx="{ row, column, index }">),优先级最高
|
|
|
+ * - formatter: ({ value, row }) => string | number,自定义格式化显示值(无 slot 时生效)
|
|
|
+ * - sortable: 是否可排序(点击表头切换 asc/desc)
|
|
|
+ * - headerStyle: 表头样式(对象),会合并到表头 style
|
|
|
+ * - cellStyle: 单元格样式(对象或函数({ row, column }) => style)
|
|
|
+ * - cellClass: 单元格 class(string 或函数({ row, column }) => string | string[])
|
|
|
+ * - isTabel: false 时不在表格中显示该列,但仍会在“详情弹窗”中显示
|
|
|
+ * - type: 内置渲染类型
|
|
|
+ * - 'tag': 使用 uni-tag 渲染,搭配 tagMap/tagTypeMap
|
|
|
+ * - 'file': 使用 cwg-file 渲染
|
|
|
+ * - 'more': 展示“更多”图标(用于移动端点击查看详情等)
|
|
|
+ * - 'date': 日期格式化(搭配 dateFormat)
|
|
|
+ * - tagMap: 标签映射对象
|
|
|
+ * - tagTypeMap: 标签类型映射对象
|
|
|
+ * - dateFormat: 日期格式
|
|
|
+ * */
|
|
|
+ columns: { type: Array, required: true, default: () => [] },
|
|
|
+ // 移动端配置
|
|
|
+ mobilePrimaryCount: { type: Number, default: 3 },
|
|
|
+ // 参数如columns
|
|
|
+ mobilePrimaryFields: { type: Array, default: () => [] },
|
|
|
// API 请求函数
|
|
|
- api: {
|
|
|
- type: Function,
|
|
|
- required: true
|
|
|
- },
|
|
|
+ api: { type: Function, required: true },
|
|
|
// 查询参数
|
|
|
- queryParams: {
|
|
|
- type: Object,
|
|
|
- default: () => ({})
|
|
|
- },
|
|
|
+ queryParams: { type: Object, default: () => ({}) },
|
|
|
// 是否立即加载
|
|
|
- immediate: {
|
|
|
- type: Boolean,
|
|
|
- default: true
|
|
|
- },
|
|
|
+ immediate: { type: Boolean, default: true },
|
|
|
// 选择类型:'selection' | null
|
|
|
- selectionType: {
|
|
|
- type: String,
|
|
|
- default: null
|
|
|
- },
|
|
|
- // 是否显示操作列
|
|
|
- showOperation: {
|
|
|
- type: Boolean,
|
|
|
- default: false
|
|
|
- },
|
|
|
- // 操作按钮配置
|
|
|
- operationActions: {
|
|
|
- type: Array,
|
|
|
- default: () => [
|
|
|
- { text: '编辑', type: 'primary', action: 'edit', icon: 'crm-puls' },
|
|
|
- { text: '删除', type: 'warn', action: 'delete', icon: 'crm-delete' },
|
|
|
- { text: '查看', type: 'default', action: 'view', icon: 'view' }
|
|
|
- ]
|
|
|
- },
|
|
|
- // 是否显示底部操作栏
|
|
|
- showBottomActions: {
|
|
|
- type: Boolean,
|
|
|
- default: false
|
|
|
- },
|
|
|
- // 底部操作按钮
|
|
|
- bottomActions: {
|
|
|
- type: Array,
|
|
|
- default: () => []
|
|
|
- },
|
|
|
+ selectionType: { type: String, default: null },
|
|
|
// 是否显示每页条数选择
|
|
|
- showPageSize: {
|
|
|
- type: Boolean,
|
|
|
- default: true
|
|
|
- },
|
|
|
+ showPageSize: { type: Boolean, default: true },
|
|
|
// 是否显示分页
|
|
|
- showPagination: {
|
|
|
- type: Boolean,
|
|
|
- default: true
|
|
|
- },
|
|
|
+ showPagination: { type: Boolean, default: true },
|
|
|
+ isViewDetail: { type: Boolean, default: true },
|
|
|
// 每页条数选项
|
|
|
- pageSizes: {
|
|
|
- type: Array,
|
|
|
- default: () => [10, 20, 30, 50, 100]
|
|
|
- },
|
|
|
+ pageSizes: { type: Array, default: () => [10, 20, 30, 50, 100] },
|
|
|
// 默认每页条数
|
|
|
- defaultPageSize: {
|
|
|
- type: Number,
|
|
|
- default: 10
|
|
|
- },
|
|
|
- // ========== 表头样式自定义 ==========
|
|
|
- // 表头背景色
|
|
|
- headerBackground: {
|
|
|
- type: String,
|
|
|
- default: 'var(--color-slate-200)'
|
|
|
- },
|
|
|
- // 表头文字颜色
|
|
|
- headerColor: {
|
|
|
- type: String,
|
|
|
- default: 'var(--color-slate-800)'
|
|
|
- },
|
|
|
- // 表头字体大小
|
|
|
- headerFontSize: {
|
|
|
- type: [String, Number],
|
|
|
- default: '14rpx'
|
|
|
- },
|
|
|
- // 表头字体粗细
|
|
|
- headerFontWeight: {
|
|
|
- type: [String, Number],
|
|
|
- default: 600
|
|
|
- },
|
|
|
- // 表头高度
|
|
|
- headerHeight: {
|
|
|
- type: [String, Number],
|
|
|
- default: '40rpx'
|
|
|
- },
|
|
|
- // 表头自定义类名
|
|
|
- headerClass: {
|
|
|
- type: [String, Array],
|
|
|
- default: ''
|
|
|
- },
|
|
|
- // 表头自定义样式对象
|
|
|
- headerStyle: {
|
|
|
- type: Object,
|
|
|
- default: () => ({})
|
|
|
- },
|
|
|
- // 是否固定表头
|
|
|
- stickyHeader: {
|
|
|
- type: Boolean,
|
|
|
- default: true
|
|
|
- },
|
|
|
- // 表头固定时的偏移量
|
|
|
- stickyOffset: {
|
|
|
- type: [String, Number],
|
|
|
- default: '0'
|
|
|
- }
|
|
|
+ defaultPageSize: { type: Number, default: 10 },
|
|
|
+ // 表头样式自定义
|
|
|
+ headerBackground: { type: String, default: '#fff' },
|
|
|
+ headerColor: { type: String, default: 'var(--color-slate-800)' },
|
|
|
+ headerFontSize: { type: [String, Number], default: '14rpx' },
|
|
|
+ headerFontWeight: { type: [String, Number], default: 600 },
|
|
|
+ headerHeight: { type: [String, Number], default: '40rpx' },
|
|
|
+ headerClass: { type: [String, Array], default: '' },
|
|
|
+ headerStyle: { type: Object, default: () => ({}) },
|
|
|
+ stickyHeader: { type: Boolean, default: true },
|
|
|
+ stickyOffset: { type: [String, Number], default: '0' },
|
|
|
})
|
|
|
|
|
|
const emit = defineEmits([
|
|
|
'selection-change',
|
|
|
'action-click',
|
|
|
- 'bottom-action-click',
|
|
|
'page-change',
|
|
|
'load-success',
|
|
|
- 'load-error'
|
|
|
+ 'load-error',
|
|
|
+ 'sort-change'
|
|
|
])
|
|
|
|
|
|
-// 展开行状态
|
|
|
-const expandedRows = ref({})
|
|
|
-// 切换行展开状态
|
|
|
-const toggleRowExpand = (rowIndex) => {
|
|
|
- const key = rowIndex
|
|
|
- if (expandedRows.value[key]) {
|
|
|
- expandedRows.value[key] = false
|
|
|
- } else {
|
|
|
- expandedRows.value = {}
|
|
|
- expandedRows.value[key] = true
|
|
|
- }
|
|
|
- emit('expand-change', { rowIndex, expanded: expandedRows.value[key] })
|
|
|
-}
|
|
|
-
|
|
|
-// 表格数据
|
|
|
+// ========== 响应式状态 ==========
|
|
|
const tableData = ref([])
|
|
|
const selectedItems = ref([])
|
|
|
-
|
|
|
-// 分页参数
|
|
|
+const detailVisible = ref(false)
|
|
|
+const detailItems = ref([])
|
|
|
const pagination = ref({
|
|
|
current: 1,
|
|
|
pageSize: props.defaultPageSize,
|
|
|
total: 0,
|
|
|
pages: 0
|
|
|
})
|
|
|
-
|
|
|
-// 加载状态
|
|
|
const loading = ref(false)
|
|
|
+const expandedRows = ref({})
|
|
|
+
|
|
|
+// 移动端检测
|
|
|
+const isMobile = ref(false)
|
|
|
+// 排序状态
|
|
|
+const sortState = ref({ prop: '', order: '' }) // order: 'asc' | 'desc' | ''
|
|
|
+
|
|
|
+// ========== 计算属性 ==========
|
|
|
+// 显示的列(根据设备类型)
|
|
|
+const displayColumns = computed(() => {
|
|
|
+ const filterForTable = (cols) => (cols || []).filter((c) => c && c.isTabel !== false)
|
|
|
+
|
|
|
+ const normalizeColumns = (cols) => {
|
|
|
+ if (!Array.isArray(cols)) return []
|
|
|
+ if (cols.length === 0) return []
|
|
|
+ const first = cols[0]
|
|
|
+ if (typeof first === 'string') {
|
|
|
+ return cols
|
|
|
+ .map((prop) => props.columns.find((c) => c && c.prop === prop))
|
|
|
+ .filter(Boolean)
|
|
|
+ }
|
|
|
+ return cols
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!isMobile.value) return filterForTable(normalizeColumns(props.columns))
|
|
|
+
|
|
|
+ if (props.mobilePrimaryFields && props.mobilePrimaryFields.length) {
|
|
|
+ return filterForTable(normalizeColumns(props.mobilePrimaryFields))
|
|
|
+ }
|
|
|
+ return filterForTable(normalizeColumns(props.columns.slice(0, props.mobilePrimaryCount)))
|
|
|
+})
|
|
|
|
|
|
-// 计算列跨度
|
|
|
+// 列跨度(用于展开行)
|
|
|
const columnSpan = computed(() => {
|
|
|
- let span = props.columns.length
|
|
|
+ let span = displayColumns.value.length
|
|
|
if (props.showOperation) span += 1
|
|
|
return span
|
|
|
})
|
|
|
|
|
|
-// 计算每页条数索引
|
|
|
+// 每页条数索引
|
|
|
const pageSizeIndex = computed(() => {
|
|
|
return props.pageSizes.indexOf(pagination.value.pageSize)
|
|
|
})
|
|
|
|
|
|
-// 计算显示的页码
|
|
|
+// 可见页码
|
|
|
const visiblePages = computed(() => {
|
|
|
const maxVisible = 5
|
|
|
const half = Math.floor(maxVisible / 2)
|
|
|
@@ -295,13 +232,7 @@ const visiblePages = computed(() => {
|
|
|
}
|
|
|
return Array.from({ length: end - start + 1 }, (_, i) => start + i)
|
|
|
})
|
|
|
-
|
|
|
-// 监听查询参数变化
|
|
|
-watch(() => props.queryParams, () => {
|
|
|
- refreshTable()
|
|
|
-}, { deep: true })
|
|
|
-
|
|
|
-// ========== 表头样式计算 ==========
|
|
|
+// ========== 工具函数 ==========
|
|
|
// 获取表头样式
|
|
|
const getHeaderStyle = (column) => {
|
|
|
const baseStyle = {
|
|
|
@@ -313,40 +244,35 @@ const getHeaderStyle = (column) => {
|
|
|
lineHeight: typeof props.headerHeight === 'number' ? props.headerHeight + 'rpx' : props.headerHeight,
|
|
|
...props.headerStyle
|
|
|
}
|
|
|
-
|
|
|
- // 固定表头
|
|
|
if (props.stickyHeader) {
|
|
|
baseStyle.position = 'sticky'
|
|
|
baseStyle.top = props.stickyOffset
|
|
|
baseStyle.zIndex = 100
|
|
|
}
|
|
|
- // 列自定义样式
|
|
|
+ if (column.width) {
|
|
|
+ const w = typeof column.width === 'number' ? column.width + 'px' : column.width
|
|
|
+ baseStyle.width = w
|
|
|
+ baseStyle.minWidth = w
|
|
|
+ baseStyle.maxWidth = w
|
|
|
+ }
|
|
|
if (column.headerStyle) {
|
|
|
Object.assign(baseStyle, column.headerStyle)
|
|
|
}
|
|
|
-
|
|
|
return baseStyle
|
|
|
}
|
|
|
|
|
|
-// 获取操作列表头样式
|
|
|
-const getOperationHeaderStyle = () => {
|
|
|
- return {
|
|
|
- backgroundColor: props.headerBackground,
|
|
|
- color: props.headerColor,
|
|
|
- fontSize: typeof props.headerFontSize === 'number' ? props.headerFontSize + 'px' : props.headerFontSize,
|
|
|
- fontWeight: props.headerFontWeight,
|
|
|
- height: typeof props.headerHeight === 'number' ? props.headerHeight + 'px' : props.headerHeight,
|
|
|
- lineHeight: typeof props.headerHeight === 'number' ? props.headerHeight + 'px' : props.headerHeight,
|
|
|
- position: props.stickyHeader ? 'sticky' : 'static',
|
|
|
- top: props.stickyHeader ? props.stickyOffset : 'auto',
|
|
|
- zIndex: props.stickyHeader ? 100 : 'auto'
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
// 获取单元格样式
|
|
|
const getCellStyle = (column, row) => {
|
|
|
const style = {}
|
|
|
-
|
|
|
+ if (column.width) {
|
|
|
+ const w = typeof column.width === 'number' ? column.width + 'px' : column.width
|
|
|
+ style.width = w
|
|
|
+ style.minWidth = w
|
|
|
+ style.maxWidth = w
|
|
|
+ style.whiteSpace = 'nowrap'
|
|
|
+ style.overflow = 'hidden'
|
|
|
+ style.textOverflow = 'ellipsis'
|
|
|
+ }
|
|
|
if (column.cellStyle) {
|
|
|
if (typeof column.cellStyle === 'function') {
|
|
|
Object.assign(style, column.cellStyle({ row, column }))
|
|
|
@@ -354,7 +280,6 @@ const getCellStyle = (column, row) => {
|
|
|
Object.assign(style, column.cellStyle)
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
return style
|
|
|
}
|
|
|
|
|
|
@@ -377,10 +302,103 @@ const getCellClass = (column, row) => {
|
|
|
}
|
|
|
return classes.join(' ')
|
|
|
}
|
|
|
-// 加载数据
|
|
|
+
|
|
|
+// 格式化单元格值
|
|
|
+const formatCellValue = (value, column, row) => {
|
|
|
+ if (column.formatter) {
|
|
|
+ return column.formatter({ value, row })
|
|
|
+ }
|
|
|
+ if (column.type === 'date' && value) {
|
|
|
+ return formatDate(value, column.dateFormat || 'YYYY-MM-DD HH:mm:ss')
|
|
|
+ }
|
|
|
+ if (value === null || value === undefined) {
|
|
|
+ return '-'
|
|
|
+ }
|
|
|
+ return value
|
|
|
+}
|
|
|
+
|
|
|
+// 格式化标签文本
|
|
|
+const formatTagText = (value, column) => {
|
|
|
+ if (column.tagMap && column.tagMap[value]) {
|
|
|
+ return column.tagMap[value]
|
|
|
+ }
|
|
|
+ return value || '-'
|
|
|
+}
|
|
|
+
|
|
|
+// 格式化标签类型
|
|
|
+const formatTagType = (value, column) => {
|
|
|
+ if (column.tagTypeMap && column.tagTypeMap[value]) {
|
|
|
+ return column.tagTypeMap[value]
|
|
|
+ }
|
|
|
+ return 'default'
|
|
|
+}
|
|
|
+
|
|
|
+// 格式化日期
|
|
|
+const formatDate = (date, format) => {
|
|
|
+ if (!date) return '-'
|
|
|
+ const d = new Date(date)
|
|
|
+ const year = d.getFullYear()
|
|
|
+ const month = String(d.getMonth() + 1).padStart(2, '0')
|
|
|
+ const day = String(d.getDate()).padStart(2, '0')
|
|
|
+ const hour = String(d.getHours()).padStart(2, '0')
|
|
|
+ const minute = String(d.getMinutes()).padStart(2, '0')
|
|
|
+ const second = String(d.getSeconds()).padStart(2, '0')
|
|
|
+
|
|
|
+ return format
|
|
|
+ .replace('YYYY', year)
|
|
|
+ .replace('MM', month)
|
|
|
+ .replace('DD', day)
|
|
|
+ .replace('HH', hour)
|
|
|
+ .replace('mm', minute)
|
|
|
+ .replace('ss', second)
|
|
|
+}
|
|
|
+
|
|
|
+// 排序图标
|
|
|
+const getSortIcon = (column) => {
|
|
|
+ if (sortState.value.prop !== column.prop) return 'arrow-up'
|
|
|
+ return sortState.value.order === 'asc' ? 'arrow-up' : 'arrow-down'
|
|
|
+}
|
|
|
+
|
|
|
+// 处理排序
|
|
|
+const handleSort = (column) => {
|
|
|
+ if (!column.sortable) return
|
|
|
+ let newOrder = ''
|
|
|
+ if (sortState.value.prop === column.prop) {
|
|
|
+ if (sortState.value.order === '') newOrder = 'asc'
|
|
|
+ else if (sortState.value.order === 'asc') newOrder = 'desc'
|
|
|
+ else newOrder = ''
|
|
|
+ } else {
|
|
|
+ newOrder = 'asc'
|
|
|
+ }
|
|
|
+ sortState.value = { prop: newOrder ? column.prop : '', order: newOrder }
|
|
|
+ emit('sort-change', { prop: sortState.value.prop, order: sortState.value.order })
|
|
|
+ refreshTable()
|
|
|
+}
|
|
|
+// 展开行切换
|
|
|
+const toggleRowExpand = (rowIndex) => {
|
|
|
+ const key = rowIndex
|
|
|
+ if (expandedRows.value[key]) {
|
|
|
+ expandedRows.value[key] = false
|
|
|
+ } else {
|
|
|
+ expandedRows.value = {}
|
|
|
+ expandedRows.value[key] = true
|
|
|
+ }
|
|
|
+ emit('expand-change', { rowIndex, expanded: expandedRows.value[key] })
|
|
|
+}
|
|
|
+// 打开详情弹窗(移动端使用)
|
|
|
+const openRowDetail = (row) => {
|
|
|
+ detailItems.value = props.columns
|
|
|
+ .filter((column) => column && column.prop && column.label)
|
|
|
+ .map((column) => ({
|
|
|
+ label: column.label,
|
|
|
+ value: String(formatCellValue(row[column.prop], column, row))
|
|
|
+ }))
|
|
|
+ detailVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+// ========== 数据加载 ==========
|
|
|
const loadData = async () => {
|
|
|
if (loading.value) return
|
|
|
-
|
|
|
loading.value = true
|
|
|
try {
|
|
|
const params = {
|
|
|
@@ -390,14 +408,13 @@ const loadData = async () => {
|
|
|
},
|
|
|
...props.queryParams
|
|
|
}
|
|
|
-
|
|
|
+ // 添加排序参数
|
|
|
+ if (sortState.value.prop && sortState.value.order) {
|
|
|
+ params.sort = { field: sortState.value.prop, order: sortState.value.order }
|
|
|
+ }
|
|
|
const res = await props.api(params)
|
|
|
-
|
|
|
- // 处理不同的API返回格式
|
|
|
if (res.code === 200 || res.code === 0) {
|
|
|
const data = res.data || res
|
|
|
-
|
|
|
- // 支持不同的分页数据结构
|
|
|
if (Array.isArray(data)) {
|
|
|
tableData.value = data
|
|
|
pagination.value.total = data.length
|
|
|
@@ -409,7 +426,6 @@ const loadData = async () => {
|
|
|
} else {
|
|
|
tableData.value = []
|
|
|
}
|
|
|
-
|
|
|
emit('load-success', res)
|
|
|
} else {
|
|
|
throw new Error(res.message || '加载失败')
|
|
|
@@ -425,40 +441,28 @@ const loadData = async () => {
|
|
|
loading.value = false
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
// 刷新表格
|
|
|
const refreshTable = () => {
|
|
|
pagination.value.current = 1
|
|
|
loadData()
|
|
|
}
|
|
|
-
|
|
|
-// 重新加载
|
|
|
const reload = () => {
|
|
|
loadData()
|
|
|
}
|
|
|
-
|
|
|
// 分页变化
|
|
|
const handlePageChange = (page) => {
|
|
|
if (page === 'prev') {
|
|
|
- if (pagination.value.current > 1) {
|
|
|
- pagination.value.current--
|
|
|
- } else {
|
|
|
- return
|
|
|
- }
|
|
|
+ if (pagination.value.current > 1) pagination.value.current--
|
|
|
+ else return
|
|
|
} else if (page === 'next') {
|
|
|
- if (pagination.value.current < pagination.value.pages) {
|
|
|
- pagination.value.current++
|
|
|
- } else {
|
|
|
- return
|
|
|
- }
|
|
|
+ if (pagination.value.current < pagination.value.pages) pagination.value.current++
|
|
|
+ else return
|
|
|
} else {
|
|
|
pagination.value.current = page
|
|
|
}
|
|
|
-
|
|
|
loadData()
|
|
|
emit('page-change', pagination.value)
|
|
|
}
|
|
|
-
|
|
|
// 每页条数变化
|
|
|
const handlePageSizeChange = (e) => {
|
|
|
const size = props.pageSizes[e.detail.value]
|
|
|
@@ -466,102 +470,52 @@ const handlePageSizeChange = (e) => {
|
|
|
pagination.value.current = 1
|
|
|
loadData()
|
|
|
}
|
|
|
-
|
|
|
// 选择变化
|
|
|
const handleSelectionChange = (e) => {
|
|
|
selectedItems.value = e.detail.value
|
|
|
emit('selection-change', selectedItems.value)
|
|
|
}
|
|
|
-
|
|
|
-// 操作按钮点击
|
|
|
-const handleAction = (action, row) => {
|
|
|
- emit('action-click', { action: action.action, row, original: action })
|
|
|
-}
|
|
|
-
|
|
|
-// 底部操作点击
|
|
|
-const handleBottomAction = (action) => {
|
|
|
- emit('bottom-action-click', action)
|
|
|
-}
|
|
|
-
|
|
|
-// 判断是否显示操作按钮
|
|
|
-const showAction = (action, row) => {
|
|
|
- if (action.show) {
|
|
|
- return action.show(row)
|
|
|
- }
|
|
|
- return true
|
|
|
-}
|
|
|
-
|
|
|
-// 格式化单元格值
|
|
|
-const formatCellValue = (value, column, row) => {
|
|
|
- if (column.formatter) {
|
|
|
- return column.formatter({ value, row })
|
|
|
- }
|
|
|
-
|
|
|
- if (column.type === 'date' && value) {
|
|
|
- return formatDate(value, column.dateFormat || 'YYYY-MM-DD HH:mm:ss')
|
|
|
- }
|
|
|
-
|
|
|
- if (value === null || value === undefined) {
|
|
|
- return '-'
|
|
|
- }
|
|
|
-
|
|
|
- return value
|
|
|
-}
|
|
|
-
|
|
|
-// 格式化标签文本
|
|
|
-const formatTagText = (value, column) => {
|
|
|
- if (column.tagMap && column.tagMap[value]) {
|
|
|
- return column.tagMap[value]
|
|
|
- }
|
|
|
- return value || '-'
|
|
|
-}
|
|
|
-
|
|
|
-// 格式化标签类型
|
|
|
-const formatTagType = (value, column) => {
|
|
|
- if (column.tagTypeMap && column.tagTypeMap[value]) {
|
|
|
- return column.tagTypeMap[value]
|
|
|
- }
|
|
|
- return 'default'
|
|
|
-}
|
|
|
-
|
|
|
-// 格式化日期
|
|
|
-const formatDate = (date, format) => {
|
|
|
- if (!date) return '-'
|
|
|
- const d = new Date(date)
|
|
|
- const year = d.getFullYear()
|
|
|
- const month = String(d.getMonth() + 1).padStart(2, '0')
|
|
|
- const day = String(d.getDate()).padStart(2, '0')
|
|
|
- const hour = String(d.getHours()).padStart(2, '0')
|
|
|
- const minute = String(d.getMinutes()).padStart(2, '0')
|
|
|
- const second = String(d.getSeconds()).padStart(2, '0')
|
|
|
-
|
|
|
- return format
|
|
|
- .replace('YYYY', year)
|
|
|
- .replace('MM', month)
|
|
|
- .replace('DD', day)
|
|
|
- .replace('HH', hour)
|
|
|
- .replace('mm', minute)
|
|
|
- .replace('ss', second)
|
|
|
-}
|
|
|
-
|
|
|
-// 预览图片
|
|
|
-const handlePreviewImage = (url) => {
|
|
|
- uni.previewImage({
|
|
|
- urls: [url]
|
|
|
- })
|
|
|
-}
|
|
|
-
|
|
|
// 获取选中项
|
|
|
-const getSelectedItems = () => {
|
|
|
- return selectedItems.value
|
|
|
+const getSelectedItems = () => selectedItems.value
|
|
|
+const clearSelection = () => { selectedItems.value = [] }
|
|
|
+// ========== 移动端检测 ==========
|
|
|
+const checkIsMobile = () => {
|
|
|
+ // 适配 uni-app 环境
|
|
|
+ // #ifdef H5
|
|
|
+ const width = window.innerWidth
|
|
|
+ isMobile.value = width < 991
|
|
|
+ // #endif
|
|
|
+ // #ifndef H5
|
|
|
+ const systemInfo = uni.getSystemInfoSync()
|
|
|
+ isMobile.value = systemInfo.windowWidth < 991
|
|
|
+ // #endif
|
|
|
}
|
|
|
-
|
|
|
-// 清空选中
|
|
|
-const clearSelection = () => {
|
|
|
- selectedItems.value = []
|
|
|
+// 监听窗口大小变化(仅 H5)
|
|
|
+// #ifdef H5
|
|
|
+const handleResize = () => {
|
|
|
+ checkIsMobile()
|
|
|
}
|
|
|
-
|
|
|
-// 暴露方法给父组件
|
|
|
+// #endif
|
|
|
+// ========== 监听参数变化 ==========
|
|
|
+watch(() => props.queryParams, () => {
|
|
|
+ refreshTable()
|
|
|
+}, { deep: true })
|
|
|
+// ========== 生命周期 ==========
|
|
|
+onMounted(() => {
|
|
|
+ checkIsMobile()
|
|
|
+ // #ifdef H5
|
|
|
+ window.addEventListener('resize', handleResize)
|
|
|
+ // #endif
|
|
|
+ if (props.immediate) {
|
|
|
+ loadData()
|
|
|
+ }
|
|
|
+})
|
|
|
+onUnmounted(() => {
|
|
|
+ // #ifdef H5
|
|
|
+ window.removeEventListener('resize', handleResize)
|
|
|
+ // #endif
|
|
|
+})
|
|
|
+// 暴露方法
|
|
|
defineExpose({
|
|
|
refreshTable,
|
|
|
reload,
|
|
|
@@ -572,13 +526,6 @@ defineExpose({
|
|
|
toggleRowExpand,
|
|
|
pagination
|
|
|
})
|
|
|
-
|
|
|
-// 初始化加载
|
|
|
-onMounted(() => {
|
|
|
- if (props.immediate) {
|
|
|
- loadData()
|
|
|
- }
|
|
|
-})
|
|
|
</script>
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
@@ -586,18 +533,15 @@ onMounted(() => {
|
|
|
|
|
|
.table-container {
|
|
|
width: 100%;
|
|
|
- overflow-x: auto;
|
|
|
- /* 关键:横向滚动 */
|
|
|
- -webkit-overflow-scrolling: touch;
|
|
|
- /* 流畅滚动 */
|
|
|
+ // overflow-x: auto;
|
|
|
+ // -webkit-overflow-scrolling: touch;
|
|
|
max-height: calc(100vh - 209px);
|
|
|
- overflow: hidden;
|
|
|
color: var(--color-slate-800);
|
|
|
|
|
|
:deep(.uni-table-scroll) {
|
|
|
width: 100%;
|
|
|
max-height: calc(100vh - 375px);
|
|
|
- overflow: auto;
|
|
|
+ // overflow: auto;
|
|
|
}
|
|
|
|
|
|
:deep(.uni-table) {
|
|
|
@@ -618,20 +562,14 @@ onMounted(() => {
|
|
|
.sort-icon {
|
|
|
display: inline-flex;
|
|
|
cursor: pointer;
|
|
|
-
|
|
|
- .uni-icons {
|
|
|
- transition: all 0.3s;
|
|
|
- }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
&.sortable {
|
|
|
cursor: pointer;
|
|
|
|
|
|
- &:hover {
|
|
|
- .sort-icon .uni-icons {
|
|
|
- color: var(--color-primary) !important;
|
|
|
- }
|
|
|
+ &:hover .sort-icon .uni-icons {
|
|
|
+ color: var(--color-primary) !important;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -641,89 +579,42 @@ onMounted(() => {
|
|
|
}
|
|
|
|
|
|
.uni-table-tr {
|
|
|
- border-bottom: 5px solid var(--color-slate-200) !important;
|
|
|
-
|
|
|
&:last-child {
|
|
|
border-bottom: none !important;
|
|
|
}
|
|
|
+
|
|
|
+ .uni-table-th {
|
|
|
+ border-bottom: 1px solid #141d22 !important;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
.uni-table-th,
|
|
|
.uni-table-td {
|
|
|
- padding: px2rpx(12) px2rpx(20);
|
|
|
+ padding: px2rpx(16) px2rpx(5);
|
|
|
color: var(--color-slate-800);
|
|
|
box-sizing: border-box;
|
|
|
}
|
|
|
|
|
|
- .uni-table-td {
|
|
|
- word-break: break-word;
|
|
|
- }
|
|
|
-
|
|
|
.expand-cell {
|
|
|
padding: 0 !important;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-.table-image {
|
|
|
- width: px2rpx(30);
|
|
|
- height: px2rpx(30);
|
|
|
- border-radius: px2rpx(8);
|
|
|
-}
|
|
|
-
|
|
|
-.operation-btns {
|
|
|
- display: flex;
|
|
|
- gap: px2rpx(10);
|
|
|
- justify-content: center;
|
|
|
- flex-wrap: wrap;
|
|
|
-}
|
|
|
-
|
|
|
-.btn {
|
|
|
- margin: 0 px2rpx(4);
|
|
|
- font-size: px2rpx(16);
|
|
|
- padding: 0 px2rpx(20);
|
|
|
- height: px2rpx(36);
|
|
|
- border-radius: none;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
+.crm-chevron-down {
|
|
|
+ transform: rotate(-90deg);
|
|
|
cursor: pointer;
|
|
|
- transition: all 0.3s;
|
|
|
-
|
|
|
- &:hover {
|
|
|
- opacity: 0.8;
|
|
|
- }
|
|
|
-
|
|
|
- &:active {
|
|
|
- transform: scale(0.95);
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-.empty-data {
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- padding: px2rpx(30) 0;
|
|
|
}
|
|
|
|
|
|
-.empty-text {
|
|
|
- margin-top: px2rpx(20);
|
|
|
- font-size: px2rpx(16);
|
|
|
- color: #999;
|
|
|
-}
|
|
|
-
|
|
|
-.bottom-actions {
|
|
|
- margin: px2rpx(30) px2rpx(20);
|
|
|
- display: flex;
|
|
|
- justify-content: space-between;
|
|
|
- align-items: center;
|
|
|
+.mobile-table {
|
|
|
+ :deep(.uni-table) {
|
|
|
+ min-width: 0 !important;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-.left-actions,
|
|
|
-.right-actions {
|
|
|
- display: flex;
|
|
|
- gap: px2rpx(20);
|
|
|
+.detail-btn-mobile {
|
|
|
+ background-color: #f0f0f0;
|
|
|
+ color: #333;
|
|
|
}
|
|
|
|
|
|
/* 分页样式 */
|
|
|
@@ -846,4 +737,4 @@ onMounted(() => {
|
|
|
gap: px2rpx(10);
|
|
|
}
|
|
|
}
|
|
|
-</style>
|
|
|
+</style>
|