完整的管理系统,包含账户管理、卡片管理、套餐管理、财务管理等功能模块。 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
411 lines
10 KiB
Vue
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>
|