Files
one-pipe-system/src/components/core/views/ArtDataViewer.vue
sexygoat 222e5bb11a Initial commit: One Pipe System
完整的管理系统,包含账户管理、卡片管理、套餐管理、财务管理等功能模块。

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-22 16:35:33 +08:00

411 lines
10 KiB
Vue

<template>
<div class="art-data-viewer">
<!-- 视图切换头部 -->
<div class="viewer-header" v-if="showViewToggle">
<div class="header-left">
<slot name="header-left"></slot>
</div>
<div class="header-right">
<slot name="header-right"></slot>
</div>
</div>
<!-- 表格视图 -->
<div v-if="currentView === 'table'" class="table-view">
<ArtTable
ref="tableRef"
:row-key="rowKey"
:loading="loading"
:data="data"
:current-page="pagination.currentPage"
:page-size="pagination.pageSize"
:total="pagination.total"
:margin-top="10"
@selection-change="handleSelectionChange"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
>
<template #default>
<ElTableColumn v-for="col in tableColumns" :key="col.prop || col.type" v-bind="col" />
</template>
</ArtTable>
</div>
<!-- 描述列表视图 -->
<div v-else class="descriptions-view">
<div class="descriptions-grid">
<ElCard
v-for="(item, index) in data"
:key="getItemKey(item, index)"
shadow="hover"
class="description-card"
:class="{ selected: isCardSelected(item) }"
@click="handleCardClick(item)"
>
<!-- 卡片左上角选择器 -->
<div class="flex-row-sb">
<div v-if="showCardSelection" class="card-selector-top-left" @click.stop>
<ElCheckbox
:model-value="isCardSelected(item)"
@change="handleCardSelect(item, $event)"
/>
</div>
<!-- 卡片右上角操作按钮 -->
<div v-if="showCardActions" class="card-actions-top-right" @click.stop>
<slot name="card-actions" :item="item" :index="index"></slot>
</div>
</div>
<ElDescriptions
:column="descriptionsColumns"
:label-width="labelWidth"
border
class="mt-20"
>
<ElDescriptionsItem
v-for="field in visibleDescriptionsFields"
:key="field.prop"
:label="field.label"
:span="field.span || 1"
>
<template v-if="field.formatter">
<component :is="'div'" v-html="field.formatter(item)" />
</template>
<template v-else>
<div class="field-content">{{ getFieldValue(item, field.prop) }}</div>
</template>
</ElDescriptionsItem>
</ElDescriptions>
</ElCard>
</div>
<!-- 分页器 -->
<div v-if="showPagination" class="descriptions-pagination">
<ElPagination
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import {
ElCard,
ElDescriptions,
ElDescriptionsItem,
ElTableColumn,
ElPagination,
ElCheckbox
} from 'element-plus'
import ArtTable from '@/components/core/tables/ArtTable.vue'
// 定义组件Props类型
interface FieldConfig {
prop: string
label: string
span?: number
formatter?: (row: any) => string
}
interface TableColumn {
prop?: string
label?: string
type?: string
width?: number | string
minWidth?: number | string
formatter?: (row: any) => any
[key: string]: any
}
interface PaginationConfig {
currentPage: number
pageSize: number
total: number
}
const props = withDefaults(
defineProps<{
// 数据相关
data: any[]
loading?: boolean
rowKey?: string
// 表格配置
tableColumns: TableColumn[]
// 描述列表配置
descriptionsFields: FieldConfig[]
descriptionsColumns?: number
cardTitleField?: string
labelWidth?: string // 新增:描述列表标签宽度配置
// 字段列配置(用于控制显示哪些字段)
fieldColumns?: any[]
// 分页配置
pagination: PaginationConfig
showPagination?: boolean
// 视图配置
defaultView?: 'table' | 'descriptions'
showViewToggle?: boolean
showCardActions?: boolean
showCardSelection?: boolean
}>(),
{
loading: false,
rowKey: 'id',
descriptionsColumns: 2,
cardTitleField: 'name',
labelWidth: '120px', // 默认标签宽度
fieldColumns: () => [],
showPagination: true,
defaultView: 'table',
showViewToggle: true,
showCardActions: false,
showCardSelection: false
}
)
const emit = defineEmits<{
selectionChange: [selection: any[]]
sizeChange: [size: number]
currentChange: [current: number]
viewChange: [view: string]
}>()
// 当前视图模式
const currentView = ref(props.defaultView)
// 表格引用
const tableRef = ref()
// 卡片视图选择的项目
const selectedCards = ref<any[]>([])
// 计算可见的描述字段
const visibleDescriptionsFields = computed(() => {
if (!props.fieldColumns || props.fieldColumns.length === 0) {
return props.descriptionsFields
}
// 过滤出选中的字段
const checkedColumns = props.fieldColumns.filter((col) => col.checked !== false)
return props.descriptionsFields.filter((field) =>
checkedColumns.some((col) => col.prop === field.prop)
)
})
// 监听props的defaultView变化
watch(
() => props.defaultView,
(newView) => {
currentView.value = newView
},
{ immediate: true }
)
// 监听视图切换
watch(currentView, (newView) => {
emit('viewChange', newView)
// 清空选择
selectedCards.value = []
emit('selectionChange', [])
})
// 获取项目的唯一键
const getItemKey = (item: any, index: number) => {
return item[props.rowKey] || index
}
// 获取卡片标题
const getCardTitle = (item: any) => {
return item[props.cardTitleField] || `项目 ${item[props.rowKey] || ''}`
}
// 获取字段值
const getFieldValue = (item: any, prop: string) => {
return item[prop] || '--'
}
// 处理表格选择变化
const handleSelectionChange = (selection: any[]) => {
emit('selectionChange', selection)
}
// 处理分页大小变化
const handleSizeChange = (size: number) => {
emit('sizeChange', size)
}
// 处理当前页变化
const handleCurrentChange = (current: number) => {
emit('currentChange', current)
}
// 判断卡片是否被选中
const isCardSelected = (item: any) => {
return selectedCards.value.some((selected) => selected[props.rowKey] === item[props.rowKey])
}
// 处理卡片选择
const handleCardSelect = (item: any, checked: boolean) => {
if (checked) {
if (!isCardSelected(item)) {
selectedCards.value.push(item)
}
} else {
selectedCards.value = selectedCards.value.filter(
(selected) => selected[props.rowKey] !== item[props.rowKey]
)
}
emit('selectionChange', selectedCards.value)
}
// 处理卡片点击
const handleCardClick = (item: any) => {
if (props.showCardSelection) {
const isSelected = isCardSelected(item)
handleCardSelect(item, !isSelected)
}
}
// 暴露方法
defineExpose({
tableRef,
currentView: readonly(currentView),
selectedCards: readonly(selectedCards)
})
</script>
<style lang="scss" scoped>
.art-data-viewer {
overflow: visible;
width: 100%;
.viewer-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 16px 0;
.header-left {
flex: 1;
}
.header-right {
display: flex;
align-items: center;
gap: 12px;
}
}
.table-view {
// 表格视图样式继承ArtTable
}
.descriptions-view {
overflow: visible;
.descriptions-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(480px, 1fr));
gap: 16px;
margin-bottom: 20px;
width: 100%;
min-height: 0;
.description-card {
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
&.selected {
border: 1px solid var(--el-color-primary);
}
// 字段内容样式 - 允许自然换行
.field-content {
word-wrap: break-word;
word-break: break-all;
line-height: 1.5;
}
// 针对描述列表的特殊处理
:deep(.el-descriptions) {
.el-descriptions__body {
.el-descriptions__table {
table-layout: auto;
.el-descriptions__cell {
&.is-bordered-label {
width: v-bind('props.labelWidth');
word-wrap: break-word;
vertical-align: top;
min-width: v-bind('props.labelWidth');
}
&.is-bordered-content {
vertical-align: top;
.el-descriptions__content {
word-wrap: break-word;
word-break: break-all;
line-height: 1.5;
}
}
}
}
}
}
}
}
.descriptions-pagination {
display: flex;
justify-content: center;
margin-top: 20px;
}
// 响应式调整
@media (max-width: 768px) {
.descriptions-grid {
grid-template-columns: 1fr;
gap: 12px;
}
.description-card {
:deep(.el-card__body) {
padding: 16px;
padding-top: 45px; // 移动端稍微减少顶部内边距
}
:deep(.el-descriptions) {
.el-descriptions__header {
margin-bottom: 12px;
}
}
}
}
}
// 深色模式适配
html.dark & {
.descriptions-view {
.description-card {
border-color: var(--el-border-color-dark);
}
}
}
}
</style>