Initial commit: One Pipe System
完整的管理系统,包含账户管理、卡片管理、套餐管理、财务管理等功能模块。 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
410
src/components/core/views/ArtDataViewer.vue
Normal file
410
src/components/core/views/ArtDataViewer.vue
Normal file
@@ -0,0 +1,410 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user