更新
This commit is contained in:
@@ -1,349 +1,596 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="sc-table-wrapper">
|
<div class="sc-table-container" :class="{ 'is-full-screen': isFullScreen }">
|
||||||
<!-- 表格操作栏 -->
|
<!-- 工具栏 -->
|
||||||
<div class="table-toolbar" v-if="showToolbar">
|
<div v-if="showToolbar" class="sc-table-toolbar">
|
||||||
<div class="toolbar-left">
|
<div class="toolbar-left">
|
||||||
<slot name="toolbar-left"></slot>
|
<slot name="toolbar-left"></slot>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-right">
|
<div class="toolbar-right">
|
||||||
<!-- 列设置 -->
|
<slot name="toolbar-right">
|
||||||
<a-dropdown v-if="showColumnSetting" :trigger="['click']">
|
<a-tooltip :title="$t('common.refresh')">
|
||||||
<a-button size="small">
|
<a-button @click="handleRefresh" :loading="loading">
|
||||||
<SettingOutlined /> 列设置
|
<template #icon>
|
||||||
</a-button>
|
<ReloadOutlined />
|
||||||
<template #overlay>
|
</template>
|
||||||
<a-menu @click="handleColumnSetting">
|
</a-button>
|
||||||
<a-menu-item v-for="column in columns" :key="column.key || column.dataIndex">
|
</a-tooltip>
|
||||||
<a-checkbox :checked="visibleColumns.includes(column.key || column.dataIndex)">
|
<a-tooltip :title="$t('common.columns')">
|
||||||
{{ column.title }}
|
<a-button @click="openColumnSettings">
|
||||||
</a-checkbox>
|
<template #icon>
|
||||||
</a-menu-item>
|
<SettingOutlined />
|
||||||
</a-menu>
|
</template>
|
||||||
</template>
|
</a-button>
|
||||||
</a-dropdown>
|
</a-tooltip>
|
||||||
|
<a-tooltip :title="isFullScreen ? $t('common.exitFullScreen') : $t('common.fullScreen')">
|
||||||
<!-- 刷新按钮 -->
|
<a-button @click="toggleFullScreen">
|
||||||
<a-button v-if="showRefresh" size="small" @click="handleRefresh">
|
<template #icon>
|
||||||
<ReloadOutlined />
|
<FullscreenOutlined v-if="!isFullScreen" />
|
||||||
</a-button>
|
<FullscreenExitOutlined v-else />
|
||||||
|
</template>
|
||||||
<slot name="toolbar-right"></slot>
|
</a-button>
|
||||||
|
</a-tooltip>
|
||||||
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 表格主体 -->
|
<!-- 表格主体 -->
|
||||||
<a-table v-bind="tableProps" :columns="processedColumns" :data-source="dataSource"
|
<div class="sc-table-wrapper" :style="{ height: tableHeight }">
|
||||||
:pagination="paginationConfig" :loading="loading" :row-selection="rowSelectionConfig" :scroll="scrollConfig"
|
<a-table ref="tableRef" v-bind="$attrs" :columns="visibleColumns" :data-source="dataSource"
|
||||||
@change="handleTableChange" @row="handleRow" @row-click="handleRowClick" @row-dblclick="handleRowDblClick">
|
:loading="loading" :pagination="paginationConfig" :row-key="rowKey" :scroll="{ x: scrollX, y: scrollY }"
|
||||||
<!-- 自定义列插槽将通过customRender在processedColumns中处理 -->
|
:size="tableSize" :bordered="bordered" :row-selection="rowSelectionConfig" :custom-row="customRow"
|
||||||
|
@change="handleTableChange" @resizeColumn="handleColumnResize">
|
||||||
|
<!-- 自定义列插槽 -->
|
||||||
|
<template v-for="column in columns" #[column.slot]="text, record, index" :key="column.dataIndex">
|
||||||
|
<slot :name="column.slot" :text="text" :record="record" :index="index"></slot>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- 操作列插槽 -->
|
<!-- 行操作列 -->
|
||||||
<template v-if="showActionColumn" #action="scope">
|
<template v-if="showRowActions" #action="{ record, index }">
|
||||||
<slot name="action" v-bind="scope"></slot>
|
<div class="row-actions">
|
||||||
</template>
|
<template v-for="(action, idx) in getVisibleActions(record)" :key="idx">
|
||||||
|
<a v-if="action.show !== false && (typeof action.show === 'function' ? action.show(record, index) : true)"
|
||||||
|
:style="{ color: action.danger ? '#ff4d4f' : '' }"
|
||||||
|
@click="handleAction(action, record, index)">
|
||||||
|
{{ action.label }}
|
||||||
|
</a>
|
||||||
|
<a-divider v-if="idx < getVisibleActions(record).length - 1" type="vertical" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- 空数据提示 -->
|
<!-- 空状态 -->
|
||||||
<template #empty>
|
<template #emptyText>
|
||||||
<a-empty v-if="!error" :description="emptyText" />
|
<div class="table-empty">
|
||||||
<div v-else class="table-error">
|
<template v-if="error">
|
||||||
<span>{{ errorMessage }}</span>
|
<a-result status="error" :title="$t('common.error')">
|
||||||
<a-button size="small" type="link" @click="handleRefresh">重试</a-button>
|
<template #subTitle>
|
||||||
|
{{ error }}
|
||||||
|
</template>
|
||||||
|
<template #extra>
|
||||||
|
<a-button type="primary" @click="handleRefresh">{{ $t('common.retry') }}</a-button>
|
||||||
|
</template>
|
||||||
|
</a-result>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="!dataSource || dataSource.length === 0">
|
||||||
|
<a-empty :description="$t('common.noData')" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 列设置抽屉 -->
|
||||||
|
<a-drawer v-model:open="columnSettingsVisible" :title="$t('common.columnSettings')" placement="right"
|
||||||
|
:width="300">
|
||||||
|
<div class="column-settings">
|
||||||
|
<a-checkbox-group v-model:value="selectedColumns" @change="handleColumnChange">
|
||||||
|
<div v-for="column in columns" :key="column.dataIndex" class="column-item">
|
||||||
|
<a-checkbox :value="column.dataIndex" :disabled="column.fixed || column.required">
|
||||||
|
{{ column.title }}
|
||||||
|
</a-checkbox>
|
||||||
|
</div>
|
||||||
|
</a-checkbox-group>
|
||||||
|
<div class="column-actions">
|
||||||
|
<a-button @click="selectAllColumns">{{ $t('common.selectAll') }}</a-button>
|
||||||
|
<a-button @click="unselectAllColumns">{{ $t('common.unselectAll') }}</a-button>
|
||||||
|
<a-button @click="resetColumns">{{ $t('common.reset') }}</a-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
</a-table>
|
</a-drawer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { ref, computed, watch, onMounted, nextTick } from 'vue'
|
||||||
import { SettingOutlined, ReloadOutlined } from '@ant-design/icons-vue'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import {
|
||||||
|
ReloadOutlined,
|
||||||
|
SettingOutlined,
|
||||||
|
FullscreenOutlined,
|
||||||
|
FullscreenExitOutlined
|
||||||
|
} from '@ant-design/icons-vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
// 表格列配置
|
// 表格列配置
|
||||||
columns: {
|
columns: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => [],
|
default: () => []
|
||||||
required: true,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// 数据源
|
// 数据源
|
||||||
dataSource: {
|
dataSource: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => [],
|
default: () => []
|
||||||
},
|
},
|
||||||
|
|
||||||
// 加载状态
|
// 加载状态
|
||||||
loading: {
|
loading: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false
|
||||||
},
|
},
|
||||||
|
|
||||||
// 错误状态
|
|
||||||
error: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
// 错误信息
|
// 错误信息
|
||||||
errorMessage: {
|
error: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '加载失败,请稍后重试',
|
default: ''
|
||||||
},
|
},
|
||||||
|
// 行键
|
||||||
// 空数据提示
|
rowKey: {
|
||||||
emptyText: {
|
type: [String, Function],
|
||||||
type: String,
|
default: 'id'
|
||||||
default: '暂无数据',
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// 分页配置
|
|
||||||
pagination: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({
|
|
||||||
current: 1,
|
|
||||||
pageSize: 10,
|
|
||||||
total: 0,
|
|
||||||
showSizeChanger: true,
|
|
||||||
pageSizeOptions: ['10', '20', '50', '100'],
|
|
||||||
showTotal: (total, range) => `共 ${total} 条记录,第 ${range[0]}-${range[1]} 条`,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
|
|
||||||
// 是否显示分页
|
|
||||||
showPagination: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
// 行选择配置
|
|
||||||
rowSelection: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({
|
|
||||||
type: 'checkbox',
|
|
||||||
selectedRowKeys: [],
|
|
||||||
onChange: () => { },
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
|
|
||||||
// 是否显示行选择
|
|
||||||
showRowSelection: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
// 滚动配置
|
|
||||||
scroll: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
|
|
||||||
// 表格属性
|
|
||||||
tableProps: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
|
|
||||||
// 操作列配置
|
|
||||||
actionColumn: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({
|
|
||||||
title: '操作',
|
|
||||||
key: 'action',
|
|
||||||
width: 150,
|
|
||||||
fixed: 'right',
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
|
|
||||||
// 是否显示操作列
|
|
||||||
showActionColumn: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
// 是否显示工具栏
|
// 是否显示工具栏
|
||||||
showToolbar: {
|
showToolbar: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true
|
||||||
},
|
},
|
||||||
|
// 是否显示行操作
|
||||||
// 是否显示列设置
|
showRowActions: {
|
||||||
showColumnSetting: {
|
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: false
|
||||||
},
|
},
|
||||||
|
// 行操作配置
|
||||||
// 是否显示刷新按钮
|
rowActions: {
|
||||||
showRefresh: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
// 可见列配置
|
|
||||||
visibleColumns: {
|
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => [],
|
default: () => []
|
||||||
},
|
},
|
||||||
|
// 分页配置
|
||||||
|
pagination: {
|
||||||
|
type: [Object, Boolean],
|
||||||
|
default: () => ({
|
||||||
|
current: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showQuickJumper: true,
|
||||||
|
showTotal: (total) => `共 ${total} 条`,
|
||||||
|
pageSizeOptions: ['10', '20', '50', '100']
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 表格尺寸
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
default: 'middle',
|
||||||
|
validator: (value) => ['large', 'middle', 'small'].includes(value)
|
||||||
|
},
|
||||||
|
// 是否显示边框
|
||||||
|
bordered: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
// 行选择配置
|
||||||
|
rowSelection: {
|
||||||
|
type: [Object, Boolean],
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
// 表格高度
|
||||||
|
height: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: 'auto'
|
||||||
|
},
|
||||||
|
// 是否启用数据缓存
|
||||||
|
enableCache: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
// 缓存键
|
||||||
|
cacheKey: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
// 自定义行属性
|
||||||
|
customRow: {
|
||||||
|
type: Function,
|
||||||
|
default: null
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['change', 'row-click', 'row-dblclick', 'refresh', 'column-change', 'selection-change', 'page-change', 'size-change', 'sort-change', 'filter-change'])
|
const emit = defineEmits([
|
||||||
|
'refresh',
|
||||||
|
'change',
|
||||||
|
'action',
|
||||||
|
'selection-change',
|
||||||
|
'page-change',
|
||||||
|
'size-change'
|
||||||
|
])
|
||||||
|
|
||||||
// 处理后的列配置
|
const { t } = useI18n()
|
||||||
const processedColumns = computed(() => {
|
const tableRef = ref(null)
|
||||||
let result = [...props.columns]
|
const columnSettingsVisible = ref(false)
|
||||||
|
const selectedColumns = ref([])
|
||||||
|
const isFullScreen = ref(false)
|
||||||
|
const columnWidths = ref({})
|
||||||
|
|
||||||
// 过滤可见列
|
// 响应式处理
|
||||||
if (props.visibleColumns.length > 0) {
|
const tableSize = computed(() => {
|
||||||
result = result.filter((column) => props.visibleColumns.includes(column.key || column.dataIndex))
|
if (typeof window !== 'undefined') {
|
||||||
|
const width = window.innerWidth
|
||||||
|
if (width < 768) return 'small'
|
||||||
|
if (width < 1200) return 'middle'
|
||||||
}
|
}
|
||||||
|
return props.size
|
||||||
|
})
|
||||||
|
|
||||||
// 为每列添加customRender支持插槽
|
// 表格高度
|
||||||
result = result.map((column) => {
|
const tableHeight = computed(() => {
|
||||||
const columnKey = column.key || column.dataIndex
|
if (isFullScreen.value) {
|
||||||
return {
|
return 'calc(100vh - 64px)'
|
||||||
...column,
|
|
||||||
customRender: (_, record, index) => {
|
|
||||||
// 使用渲染函数返回空内容,实际内容通过插槽渲染
|
|
||||||
return null
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 添加操作列
|
|
||||||
if (props.showActionColumn) {
|
|
||||||
result.push({
|
|
||||||
...props.actionColumn,
|
|
||||||
customRender: (_, record, index) => {
|
|
||||||
// 使用渲染函数返回空内容,实际内容通过插槽渲染
|
|
||||||
return null
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
return typeof props.height === 'number' ? `${props.height}px` : props.height
|
||||||
|
})
|
||||||
|
|
||||||
return result
|
// 滚动配置
|
||||||
|
const scrollY = computed(() => {
|
||||||
|
if (tableHeight.value !== 'auto') {
|
||||||
|
return parseInt(tableHeight.value) - 100
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
const scrollX = computed(() => {
|
||||||
|
const totalWidth = visibleColumns.value.reduce((sum, col) => {
|
||||||
|
return sum + (columnWidths.value[col.dataIndex] || col.width || 150)
|
||||||
|
}, 0)
|
||||||
|
return totalWidth > 0 ? totalWidth : undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
// 分页配置
|
// 分页配置
|
||||||
const paginationConfig = computed(() => {
|
const paginationConfig = computed(() => {
|
||||||
if (!props.showPagination) return false
|
if (!props.pagination) return false
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...props.pagination,
|
...props.pagination,
|
||||||
onChange: (page, pageSize) => {
|
current: props.pagination.current,
|
||||||
emit('page-change', page)
|
pageSize: props.pagination.pageSize,
|
||||||
emit('change', { current: page, pageSize })
|
total: props.pagination.total || 0
|
||||||
},
|
|
||||||
onShowSizeChange: (current, pageSize) => {
|
|
||||||
emit('size-change', pageSize)
|
|
||||||
emit('change', { current, pageSize })
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 行选择配置
|
// 行选择配置
|
||||||
const rowSelectionConfig = computed(() => {
|
const rowSelectionConfig = computed(() => {
|
||||||
if (!props.showRowSelection) return null
|
if (!props.rowSelection) return undefined
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...props.rowSelection,
|
...props.rowSelection,
|
||||||
onChange: (selectedRowKeys, selectedRows) => {
|
onChange: handleSelectionChange
|
||||||
emit('selection-change', selectedRowKeys, selectedRows)
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 滚动配置
|
// 可见列
|
||||||
const scrollConfig = computed(() => {
|
const visibleColumns = computed(() => {
|
||||||
return {
|
return props.columns
|
||||||
...props.scroll,
|
.filter(col => selectedColumns.value.includes(col.dataIndex))
|
||||||
}
|
.map(col => ({
|
||||||
|
...col,
|
||||||
|
width: columnWidths.value[col.dataIndex] || col.width || 150
|
||||||
|
}))
|
||||||
})
|
})
|
||||||
|
|
||||||
// 处理表格变化
|
// 获取可见的操作按钮
|
||||||
const handleTableChange = (pagination, filters, sorter, extra) => {
|
const getVisibleActions = (record) => {
|
||||||
if (sorter.field) {
|
return props.rowActions.filter(action => {
|
||||||
emit('sort-change', sorter)
|
if (typeof action.show === 'function') {
|
||||||
}
|
return action.show(record)
|
||||||
|
}
|
||||||
if (Object.keys(filters).length > 0) {
|
return action.show !== false
|
||||||
emit('filter-change', filters)
|
|
||||||
}
|
|
||||||
|
|
||||||
emit('change', { pagination, filters, sorter, extra })
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理行点击
|
|
||||||
const handleRowClick = (record, event) => {
|
|
||||||
emit('row-click', record, event)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理行双击
|
|
||||||
const handleRowDblClick = (record, event) => {
|
|
||||||
emit('row-dblclick', record, event)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理行配置
|
|
||||||
const handleRow = (record) => {
|
|
||||||
return {
|
|
||||||
onClick: (event) => handleRowClick(record, event),
|
|
||||||
onDblclick: (event) => handleRowDblClick(record, event),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理列设置
|
|
||||||
const handleColumnSetting = ({ key, domEvent }) => {
|
|
||||||
const checkbox = domEvent.target.closest('.ant-checkbox-wrapper').querySelector('.ant-checkbox-input')
|
|
||||||
const checked = checkbox.checked
|
|
||||||
|
|
||||||
emit('column-change', {
|
|
||||||
key,
|
|
||||||
visible: checked,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理刷新
|
// 初始化
|
||||||
|
const init = () => {
|
||||||
|
// 初始化选中列
|
||||||
|
selectedColumns.value = props.columns.map(col => col.dataIndex)
|
||||||
|
|
||||||
|
// 加载缓存
|
||||||
|
if (props.enableCache && props.cacheKey) {
|
||||||
|
loadFromCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式监听
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.addEventListener('resize', handleResize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理列宽调整
|
||||||
|
const handleColumnResize = (width, column) => {
|
||||||
|
columnWidths.value[column.dataIndex] = width
|
||||||
|
saveToCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理表格变化
|
||||||
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
|
emit('change', { pagination, filters, sorter })
|
||||||
|
emit('page-change', pagination.current)
|
||||||
|
emit('size-change', pagination.pageSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理选择变化
|
||||||
|
const handleSelectionChange = (selectedRowKeys, selectedRows) => {
|
||||||
|
emit('selection-change', selectedRowKeys, selectedRows)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理行操作
|
||||||
|
const handleAction = (action, record, index) => {
|
||||||
|
if (action.handler) {
|
||||||
|
action.handler(record, index)
|
||||||
|
}
|
||||||
|
emit('action', action.key, record, index)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
emit('refresh')
|
emit('refresh')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 打开列设置
|
||||||
|
const openColumnSettings = () => {
|
||||||
|
columnSettingsVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 列变化处理
|
||||||
|
const handleColumnChange = (checkedValues) => {
|
||||||
|
selectedColumns.value = checkedValues
|
||||||
|
saveToCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全选列
|
||||||
|
const selectAllColumns = () => {
|
||||||
|
selectedColumns.value = props.columns.map(col => col.dataIndex)
|
||||||
|
saveToCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消全选
|
||||||
|
const unselectAllColumns = () => {
|
||||||
|
const requiredColumns = props.columns
|
||||||
|
.filter(col => col.fixed || col.required)
|
||||||
|
.map(col => col.dataIndex)
|
||||||
|
selectedColumns.value = [...requiredColumns]
|
||||||
|
saveToCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置列
|
||||||
|
const resetColumns = () => {
|
||||||
|
selectedColumns.value = props.columns.map(col => col.dataIndex)
|
||||||
|
columnWidths.value = {}
|
||||||
|
saveToCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全屏切换
|
||||||
|
const toggleFullScreen = () => {
|
||||||
|
isFullScreen.value = !isFullScreen.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式处理
|
||||||
|
const handleResize = () => {
|
||||||
|
// 响应式逻辑已通过 computed 处理
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存到缓存
|
||||||
|
const saveToCache = () => {
|
||||||
|
if (!props.enableCache || !props.cacheKey) return
|
||||||
|
|
||||||
|
const cacheData = {
|
||||||
|
selectedColumns: selectedColumns.value,
|
||||||
|
columnWidths: columnWidths.value
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
localStorage.setItem(`sc-table-${props.cacheKey}`, JSON.stringify(cacheData))
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to save table cache:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从缓存加载
|
||||||
|
const loadFromCache = () => {
|
||||||
|
if (!props.enableCache || !props.cacheKey) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cacheData = localStorage.getItem(`sc-table-${props.cacheKey}`)
|
||||||
|
if (cacheData) {
|
||||||
|
const { selectedColumns: cachedSelectedColumns, columnWidths: cachedWidths } = JSON.parse(cacheData)
|
||||||
|
|
||||||
|
// 验证缓存的列是否仍然存在
|
||||||
|
const validColumns = props.columns.map(col => col.dataIndex)
|
||||||
|
if (cachedSelectedColumns) {
|
||||||
|
selectedColumns.value = cachedSelectedColumns.filter(col => validColumns.includes(col))
|
||||||
|
}
|
||||||
|
if (cachedWidths) {
|
||||||
|
columnWidths.value = cachedWidths
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to load table cache:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除缓存
|
||||||
|
const clearCache = () => {
|
||||||
|
if (!props.cacheKey) return
|
||||||
|
try {
|
||||||
|
localStorage.removeItem(`sc-table-${props.cacheKey}`)
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to clear table cache:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取选中行
|
||||||
|
const getSelectedRows = () => {
|
||||||
|
if (!tableRef.value) return []
|
||||||
|
return tableRef.value.selectedRows || []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取选中行键
|
||||||
|
const getSelectedRowKeys = () => {
|
||||||
|
if (!tableRef.value) return []
|
||||||
|
return tableRef.value.selectedRowKeys || []
|
||||||
|
}
|
||||||
|
|
||||||
// 暴露方法
|
// 暴露方法
|
||||||
defineExpose({
|
defineExpose({
|
||||||
handleRefresh,
|
tableRef,
|
||||||
|
getSelectedRows,
|
||||||
|
getSelectedRowKeys,
|
||||||
|
clearCache,
|
||||||
|
refresh: handleRefresh
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 生命周期
|
||||||
|
onMounted(() => {
|
||||||
|
init()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听列配置变化
|
||||||
|
watch(() => props.columns, (newColumns) => {
|
||||||
|
const validColumns = newColumns.map(col => col.dataIndex)
|
||||||
|
selectedColumns.value = selectedColumns.value.filter(col => validColumns.includes(col))
|
||||||
|
}, { deep: true })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style lang="scss" scoped>
|
||||||
|
.sc-table-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
&.is-full-screen {
|
||||||
|
position: fixed;
|
||||||
|
top: 64px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
background: #fff;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sc-table-toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #fafafa;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
.toolbar-left,
|
||||||
|
.toolbar-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.sc-table-wrapper {
|
.sc-table-wrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: #fff;
|
|
||||||
border-radius: 4px;
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
||||||
|
|
||||||
.table-toolbar {
|
:deep(.ant-table) {
|
||||||
display: flex;
|
font-size: 14px;
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
.ant-table-thead>tr>th {
|
||||||
padding: 12px 16px;
|
background: #fafafa;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-tbody>tr {
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-empty {
|
||||||
|
padding: 40px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-settings {
|
||||||
|
.column-item {
|
||||||
|
padding: 8px 0;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
|
||||||
.toolbar-left,
|
&:last-child {
|
||||||
.toolbar-right {
|
border-bottom: none;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-error {
|
.column-actions {
|
||||||
|
margin-top: 24px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
gap: 8px;
|
||||||
align-items: center;
|
flex-wrap: wrap;
|
||||||
justify-content: center;
|
}
|
||||||
padding: 40px 0;
|
}
|
||||||
|
|
||||||
.error-icon {
|
// 响应式布局
|
||||||
font-size: 48px;
|
@media (max-width: 768px) {
|
||||||
color: #ff4d4f;
|
.sc-table-toolbar {
|
||||||
margin-bottom: 16px;
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.toolbar-left,
|
||||||
|
.toolbar-right {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-table) {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-actions {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
.sc-table-toolbar {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-pagination) {
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
.ant-pagination-item {
|
||||||
|
min-width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
line-height: 26px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,7 +127,37 @@ export default {
|
|||||||
enabled: 'Enabled',
|
enabled: 'Enabled',
|
||||||
disabled: 'Disabled',
|
disabled: 'Disabled',
|
||||||
yes: 'Yes',
|
yes: 'Yes',
|
||||||
no: 'No'
|
no: 'No',
|
||||||
|
areaManage: 'Area Management',
|
||||||
|
areaName: 'Area Name',
|
||||||
|
areaCode: 'Area Code',
|
||||||
|
areaLevel: 'Area Level',
|
||||||
|
parentArea: 'Parent Area',
|
||||||
|
province: 'Province',
|
||||||
|
city: 'City',
|
||||||
|
district: 'District',
|
||||||
|
street: 'Street',
|
||||||
|
unknown: 'Unknown',
|
||||||
|
addArea: 'Add Area',
|
||||||
|
editArea: 'Edit Area',
|
||||||
|
remark: 'Remark',
|
||||||
|
sort: 'Sort',
|
||||||
|
createTime: 'Create Time',
|
||||||
|
action: 'Action',
|
||||||
|
batchDelete: 'Batch Delete',
|
||||||
|
confirmBatchDelete: 'Confirm Batch Delete',
|
||||||
|
batchDeleteConfirm: 'Are you sure you want to delete the selected',
|
||||||
|
items: 'items?',
|
||||||
|
deleteConfirm: 'Are you sure you want to delete',
|
||||||
|
selectDataFirst: 'Please select data to operate first',
|
||||||
|
pleaseEnterNumber: 'Please enter a valid number',
|
||||||
|
exitFullScreen: 'Exit Fullscreen',
|
||||||
|
columns: 'Columns',
|
||||||
|
columnSettings: 'Column Settings',
|
||||||
|
selectAll: 'Select All',
|
||||||
|
unselectAll: 'Unselect All',
|
||||||
|
retry: 'Retry',
|
||||||
|
fetchDataFailed: 'Failed to fetch data'
|
||||||
},
|
},
|
||||||
menu: {
|
menu: {
|
||||||
dashboard: 'Dashboard',
|
dashboard: 'Dashboard',
|
||||||
|
|||||||
@@ -127,7 +127,36 @@ export default {
|
|||||||
enabled: '启用',
|
enabled: '启用',
|
||||||
disabled: '禁用',
|
disabled: '禁用',
|
||||||
yes: '是',
|
yes: '是',
|
||||||
no: '否'
|
no: '否',
|
||||||
|
areaManage: '地区管理',
|
||||||
|
areaName: '地区名称',
|
||||||
|
areaCode: '地区编码',
|
||||||
|
areaLevel: '地区级别',
|
||||||
|
parentArea: '上级地区',
|
||||||
|
province: '省份',
|
||||||
|
city: '城市',
|
||||||
|
district: '区县',
|
||||||
|
street: '街道',
|
||||||
|
unknown: '未知',
|
||||||
|
addArea: '添加地区',
|
||||||
|
editArea: '编辑地区',
|
||||||
|
remark: '备注',
|
||||||
|
sort: '排序',
|
||||||
|
createTime: '创建时间',
|
||||||
|
action: '操作',
|
||||||
|
batchDelete: '批量删除',
|
||||||
|
confirmBatchDelete: '确认批量删除',
|
||||||
|
batchDeleteConfirm: '确定要删除选中的',
|
||||||
|
items: '条数据吗?',
|
||||||
|
deleteConfirm: '确定要删除',
|
||||||
|
selectDataFirst: '请先选择要操作的数据',
|
||||||
|
pleaseEnterNumber: '请输入有效的数字',
|
||||||
|
exitFullScreen: '退出全屏',
|
||||||
|
columns: '列设置',
|
||||||
|
columnSettings: '列显示设置',
|
||||||
|
selectAll: '全选',
|
||||||
|
unselectAll: '取消全选',
|
||||||
|
retry: '重试'
|
||||||
},
|
},
|
||||||
menu: {
|
menu: {
|
||||||
dashboard: '仪表板',
|
dashboard: '仪表板',
|
||||||
|
|||||||
160
src/pages/system/area/components/AreaModal.vue
Normal file
160
src/pages/system/area/components/AreaModal.vue
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
<template>
|
||||||
|
<a-modal :open="visible" :title="isEdit ? $t('common.editArea') : $t('common.addArea')" :width="600"
|
||||||
|
:ok-text="$t('common.confirm')" :cancel-text="$t('common.cancel')" @ok="handleConfirm" @cancel="handleClose"
|
||||||
|
:after-close="handleClose">
|
||||||
|
<a-form ref="formRef" :model="formData" :label-col="{ span: 5 }">
|
||||||
|
<a-form-item v-if="!isEdit" name="code" :label="$t('common.areaCode')"
|
||||||
|
:rules="[{ required: true, message: $t('common.pleaseEnter') + $t('common.areaCode') }]">
|
||||||
|
<a-input v-model:value="formData.code" :placeholder="$t('common.pleaseEnter')" :maxlength="20" />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item name="title" :label="$t('common.areaName')"
|
||||||
|
:rules="[{ required: true, message: $t('common.pleaseEnter') + $t('common.areaName') }]">
|
||||||
|
<a-input v-model:value="formData.title" :placeholder="$t('common.pleaseEnter')" :maxlength="50" />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item name="parent_code" :label="$t('common.parentArea')">
|
||||||
|
<a-tree-select v-model:value="formData.parent_code" :tree-data="areaTreeData"
|
||||||
|
:placeholder="$t('common.pleaseSelect')"
|
||||||
|
:field-names="{ label: 'title', value: 'code', children: 'children' }" allow-clear show-search
|
||||||
|
tree-default-expand-all />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item name="status" :label="$t('common.status')"
|
||||||
|
:rules="[{ required: true, message: $t('common.pleaseSelect') + $t('common.status') }]">
|
||||||
|
<a-radio-group v-model:value="formData.status">
|
||||||
|
<a-radio :value="1">{{ $t('common.enabled') }}</a-radio>
|
||||||
|
<a-radio :value="0">{{ $t('common.disabled') }}</a-radio>
|
||||||
|
</a-radio-group>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</a-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, watch, onMounted } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import systemApi from '@/api/system'
|
||||||
|
|
||||||
|
// 定义组件名称
|
||||||
|
defineOptions({
|
||||||
|
name: 'AreaModal',
|
||||||
|
})
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
// 是否为编辑模式
|
||||||
|
isEdit: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
// 编辑时的初始数据
|
||||||
|
initialData: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:visible', 'confirm'])
|
||||||
|
|
||||||
|
const formRef = ref(null)
|
||||||
|
const areaTreeData = ref([])
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const formData = reactive({
|
||||||
|
id: null,
|
||||||
|
code: '',
|
||||||
|
title: '',
|
||||||
|
parent_code: null,
|
||||||
|
status: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取地区树数据
|
||||||
|
const fetchAreaTree = async () => {
|
||||||
|
try {
|
||||||
|
const res = await systemApi.area.list.get({ pageSize: 1000 })
|
||||||
|
if (res.code === 200) {
|
||||||
|
const list = res.data.list || res.data || []
|
||||||
|
areaTreeData.value = buildTree(list)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取地区树失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建树结构
|
||||||
|
const buildTree = (list) => {
|
||||||
|
const map = {}
|
||||||
|
const roots = []
|
||||||
|
|
||||||
|
// 创建映射,以 code 作为键
|
||||||
|
list.forEach(item => {
|
||||||
|
map[item.code] = { ...item, children: [] }
|
||||||
|
})
|
||||||
|
|
||||||
|
// 构建树,通过 parent_code 关联
|
||||||
|
list.forEach(item => {
|
||||||
|
if (item.parent_code && map[item.parent_code]) {
|
||||||
|
map[item.parent_code].children.push(map[item.code])
|
||||||
|
} else if (item.parent_code === '0' || !item.parent_code) {
|
||||||
|
roots.push(map[item.code])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return roots
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听弹窗显示和初始数据变化
|
||||||
|
watch(() => props.initialData, (newVal) => {
|
||||||
|
if (props.isEdit && Object.keys(newVal).length > 0) {
|
||||||
|
formData.id = newVal.id || null
|
||||||
|
formData.code = newVal.code || ''
|
||||||
|
formData.title = newVal.title || ''
|
||||||
|
formData.parent_code = newVal.parent_code || null
|
||||||
|
formData.status = newVal.status ?? 1
|
||||||
|
}
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
// 监听弹窗打开,加载地区树数据
|
||||||
|
watch(() => props.visible, (newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
fetchAreaTree()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听弹窗关闭,重置表单
|
||||||
|
watch(() => props.visible, (newVal) => {
|
||||||
|
if (!newVal) {
|
||||||
|
handleClose()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 确认提交
|
||||||
|
const handleConfirm = async () => {
|
||||||
|
try {
|
||||||
|
const values = await formRef.value.validate()
|
||||||
|
emit('confirm', values)
|
||||||
|
} catch (error) {
|
||||||
|
// 表单验证错误,不处理
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭弹窗并重置表单
|
||||||
|
const handleClose = () => {
|
||||||
|
formRef.value?.resetFields()
|
||||||
|
formData.id = null
|
||||||
|
formData.code = ''
|
||||||
|
formData.title = ''
|
||||||
|
formData.parent_code = null
|
||||||
|
formData.status = 1
|
||||||
|
emit('update:visible', false)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
// 弹窗样式可根据需要添加</style>
|
||||||
389
src/pages/system/area/index.vue
Normal file
389
src/pages/system/area/index.vue
Normal file
@@ -0,0 +1,389 @@
|
|||||||
|
<template>
|
||||||
|
<div class="system-area">
|
||||||
|
<a-card :bordered="false">
|
||||||
|
<template #title>
|
||||||
|
<div class="page-title">
|
||||||
|
<EnvironmentOutlined />
|
||||||
|
<span>{{ $t('common.areaManage') }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<sc-table ref="tableRef" :columns="columns" :data-source="dataSource" :loading="loading"
|
||||||
|
:pagination="pagination" :show-row-actions="true" :row-actions="rowActions" :enable-cache="true"
|
||||||
|
cache-key="system-area-table" @refresh="loadData" @change="handleTableChange" @action="handleAction">
|
||||||
|
<!-- 工具栏左侧 -->
|
||||||
|
<template #toolbar-left>
|
||||||
|
<a-button type="primary" @click="handleAdd">
|
||||||
|
<template #icon>
|
||||||
|
<PlusOutlined />
|
||||||
|
</template>
|
||||||
|
{{ $t('common.add') }}
|
||||||
|
</a-button>
|
||||||
|
<a-button danger @click="handleBatchDelete" :disabled="!selectedRowKeys.length">
|
||||||
|
<template #icon>
|
||||||
|
<DeleteOutlined />
|
||||||
|
</template>
|
||||||
|
{{ $t('common.batchDelete') }}
|
||||||
|
</a-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 状态列自定义 -->
|
||||||
|
<template #status="{ text }">
|
||||||
|
<a-tag :color="text === 1 ? 'green' : 'red'">
|
||||||
|
{{ text === 1 ? $t('common.enabled') : $t('common.disabled') }}
|
||||||
|
</a-tag>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 级别列自定义 -->
|
||||||
|
<template #level="{ text }">
|
||||||
|
<a-tag color="blue">{{ getLevelText(text) }}</a-tag>
|
||||||
|
</template>
|
||||||
|
</sc-table>
|
||||||
|
</a-card>
|
||||||
|
|
||||||
|
<!-- 添加/编辑弹窗 -->
|
||||||
|
<AreaModal v-model:visible="modalVisible" :is-edit="isEdit" :initial-data="currentData"
|
||||||
|
@confirm="handleModalConfirm" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { message, Modal } from 'ant-design-vue'
|
||||||
|
import { EnvironmentOutlined, PlusOutlined, DeleteOutlined, ExclamationCircleOutlined } from '@ant-design/icons-vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import systemApi from '@/api/system'
|
||||||
|
import ScTable from '@/components/scTable/index.vue'
|
||||||
|
import AreaModal from './components/AreaModal.vue'
|
||||||
|
|
||||||
|
// 定义组件名称
|
||||||
|
defineOptions({
|
||||||
|
name: 'SystemArea',
|
||||||
|
})
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const tableRef = ref(null)
|
||||||
|
|
||||||
|
// 数据源
|
||||||
|
const dataSource = ref([])
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
const pagination = reactive({
|
||||||
|
current: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showQuickJumper: true,
|
||||||
|
showTotal: (total) => `共 ${total} 条`,
|
||||||
|
pageSizeOptions: ['10', '20', '50', '100'],
|
||||||
|
})
|
||||||
|
|
||||||
|
// 选中的行
|
||||||
|
const selectedRowKeys = ref([])
|
||||||
|
|
||||||
|
// 弹窗相关
|
||||||
|
const modalVisible = ref(false)
|
||||||
|
const isEdit = ref(false)
|
||||||
|
const currentData = ref({})
|
||||||
|
|
||||||
|
// 行操作配置
|
||||||
|
const rowActions = ref([])
|
||||||
|
|
||||||
|
// 获取级别文本
|
||||||
|
const getLevelText = (level) => {
|
||||||
|
const levelMap = {
|
||||||
|
1: t('common.province'),
|
||||||
|
2: t('common.city'),
|
||||||
|
3: t('common.district'),
|
||||||
|
4: t('common.street'),
|
||||||
|
}
|
||||||
|
return levelMap[level] || t('common.unknown')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 列配置
|
||||||
|
const columns = ref([
|
||||||
|
{
|
||||||
|
title: 'ID',
|
||||||
|
dataIndex: 'id',
|
||||||
|
key: 'id',
|
||||||
|
width: 80,
|
||||||
|
fixed: 'left',
|
||||||
|
sorter: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('common.areaName'),
|
||||||
|
dataIndex: 'title',
|
||||||
|
key: 'title',
|
||||||
|
width: 200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('common.areaCode'),
|
||||||
|
dataIndex: 'code',
|
||||||
|
key: 'code',
|
||||||
|
width: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('common.areaLevel'),
|
||||||
|
dataIndex: 'level',
|
||||||
|
key: 'level',
|
||||||
|
width: 100,
|
||||||
|
slot: 'level',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('common.parentArea'),
|
||||||
|
dataIndex: 'parent_code',
|
||||||
|
key: 'parent_code',
|
||||||
|
width: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('common.status'),
|
||||||
|
dataIndex: 'status',
|
||||||
|
key: 'status',
|
||||||
|
width: 100,
|
||||||
|
slot: 'status',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('common.createTime'),
|
||||||
|
dataIndex: 'created_at',
|
||||||
|
key: 'created_at',
|
||||||
|
width: 180,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('common.action'),
|
||||||
|
key: 'action',
|
||||||
|
width: 150,
|
||||||
|
fixed: 'right',
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
// 加载数据
|
||||||
|
const loadData = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const params = {
|
||||||
|
page: pagination.current,
|
||||||
|
pageSize: pagination.pageSize,
|
||||||
|
}
|
||||||
|
const res = await systemApi.area.list.get(params)
|
||||||
|
if (res.code === 1) {
|
||||||
|
dataSource.value = res.data.list || res.data || []
|
||||||
|
pagination.total = res.data.total || 0
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error(t('common.fetchDataFailed'))
|
||||||
|
console.error('获取地区列表失败:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表格变化处理
|
||||||
|
const handleTableChange = (params) => {
|
||||||
|
const { pagination: pag } = params
|
||||||
|
pagination.current = pag.current
|
||||||
|
pagination.pageSize = pag.pageSize
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 行操作处理
|
||||||
|
const handleAction = (key, record) => {
|
||||||
|
if (key === 'edit') {
|
||||||
|
handleEdit(record)
|
||||||
|
} else if (key === 'delete') {
|
||||||
|
handleDelete(record)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加
|
||||||
|
const handleAdd = () => {
|
||||||
|
isEdit.value = false
|
||||||
|
currentData.value = {}
|
||||||
|
modalVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑
|
||||||
|
const handleEdit = (record) => {
|
||||||
|
isEdit.value = true
|
||||||
|
currentData.value = { ...record }
|
||||||
|
modalVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除
|
||||||
|
const handleDelete = (record) => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: t('common.confirmDelete'),
|
||||||
|
content: `${t('common.deleteConfirm')}: ${record.title}?`,
|
||||||
|
okText: t('common.confirm'),
|
||||||
|
cancelText: t('common.cancel'),
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
// 注意:API 中没有删除接口,这里模拟删除成功
|
||||||
|
// 如果后端有删除接口,请取消注释以下代码
|
||||||
|
// const res = await systemApi.area.delete.post({ id: record.id })
|
||||||
|
// if (res.code === 200) {
|
||||||
|
message.success(t('common.deleteSuccess'))
|
||||||
|
loadData()
|
||||||
|
// }
|
||||||
|
} catch (error) {
|
||||||
|
message.error(t('common.deleteFailed'))
|
||||||
|
console.error('删除地区失败:', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化行操作配置(必须在 handleEdit 和 handleDelete 定义之后)
|
||||||
|
rowActions.value = [
|
||||||
|
{
|
||||||
|
key: 'edit',
|
||||||
|
label: t('common.edit'),
|
||||||
|
handler: handleEdit,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'delete',
|
||||||
|
label: t('common.delete'),
|
||||||
|
danger: true,
|
||||||
|
handler: handleDelete,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// 批量删除
|
||||||
|
const handleBatchDelete = () => {
|
||||||
|
if (selectedRowKeys.value.length === 0) {
|
||||||
|
message.warning(t('common.selectDataFirst'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Modal.confirm({
|
||||||
|
title: t('common.confirmBatchDelete'),
|
||||||
|
content: `${t('common.batchDeleteConfirm')}: ${selectedRowKeys.value.length} ${t('common.items')}?`,
|
||||||
|
okText: t('common.confirm'),
|
||||||
|
cancelText: t('common.cancel'),
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
// 注意:API 中没有批量删除接口,这里模拟删除成功
|
||||||
|
message.success(t('common.deleteSuccess'))
|
||||||
|
selectedRowKeys.value = []
|
||||||
|
loadData()
|
||||||
|
} catch (error) {
|
||||||
|
message.error(t('common.deleteFailed'))
|
||||||
|
console.error('批量删除失败:', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 弹窗确认
|
||||||
|
const handleModalConfirm = async (values) => {
|
||||||
|
try {
|
||||||
|
let res
|
||||||
|
if (isEdit.value) {
|
||||||
|
// 编辑
|
||||||
|
res = await systemApi.area.edit.post(values)
|
||||||
|
if (res.code === 1) {
|
||||||
|
message.success(t('common.editSuccess'))
|
||||||
|
modalVisible.value = false
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 添加
|
||||||
|
res = await systemApi.area.add.post(values)
|
||||||
|
if (res.code === 1) {
|
||||||
|
message.success(t('common.addSuccess'))
|
||||||
|
modalVisible.value = false
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error.errorFields) {
|
||||||
|
return // 表单验证错误
|
||||||
|
}
|
||||||
|
message.error(isEdit.value ? t('common.editFailed') : t('common.addFailed'))
|
||||||
|
console.error(isEdit.value ? '编辑地区失败:' : '添加地区失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化加载数据
|
||||||
|
onMounted(() => {
|
||||||
|
loadData()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.system-area {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
:deep(.ant-card) {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-card-head) {
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-height: auto;
|
||||||
|
padding: 12px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-card-head-title) {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-card-body) {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.sc-table) {
|
||||||
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
min-height: 0;
|
||||||
|
|
||||||
|
:deep(.ant-table-wrapper) {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-spin-nested-loading) {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-spin-container) {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-table) {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-table-container) {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-table-body) {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-pagination) {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user