Files
finance-master/apps/finance/src/components/card-list/card-list.vue
chenghuan ccfa4b5f15
Some checks failed
Lock Threads / action (push) Has been cancelled
Issue Close Require / close-issues (push) Has been cancelled
Close stale issues / stale (push) Has been cancelled
feat(card-list): 添加字段列数配置支持多列布局
- 新增 fieldColumns 配置项控制卡片内字段排列列数
- 支持通过 colSpan 设置字段跨列显示
- 优化卡片内容布局样式和字段显示效果
- 调整对账模块相关页面布局
2026-01-07 14:41:52 +08:00

427 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts" setup>
import type { CardListProps, ExtendedCardListApi } from './types';
import { computed, h, nextTick, onMounted, onUnmounted, ref } from 'vue';
import { usePriorityValues } from '@vben/hooks';
import { cn } from '@vben/utils';
import { VbenLoading } from '@vben-core/shadcn-ui';
import { Card, DatePicker, Empty, Input, Select } from 'ant-design-vue';
import SelectDropdownRender from '#/components/SelectDropdownRender/index.vue';
const props = withDefaults(defineProps<Props>(), {});
// 创建一个全局缓存对象
const selectOptionsCache: Record<string, any[]> = {};
interface Props extends CardListProps {
api: ExtendedCardListApi;
}
const listData = computed(() => {
return gridOptions.value?.data || [];
});
const state = props.api?.useStore?.();
const { class: className, gridClass, gridOptions, gridEvents } = usePriorityValues(props, state);
// 计算标题字段
const titleFieldName = computed(() => {
const titleField = gridOptions.value?.titleField;
if (titleField) return titleField;
const columns = gridOptions.value?.columns || [];
if (columns.length > 0) return columns[0]?.field || '';
return '';
});
// 卡片网格样式
const gridStyle = computed(() => {
const gridHeight = gridOptions.value?.gridHeight || 'auto';
const gridColumns = gridOptions.value?.gridColumns || 3;
const cardGap = gridOptions.value?.cardGap || '10px';
return {
display: 'grid',
gridTemplateColumns: `repeat(${gridColumns}, 1fr)`,
gap: cardGap,
height: gridHeight,
overflow: 'auto',
padding: '5px',
};
});
// 卡片样式
const cardStyle = (item: any) => {
const cardHeight = gridOptions.value?.cardHeight;
const cardWidth = gridOptions.value?.cardWidth || '100%';
const style: any = {
width: cardWidth,
minWidth: 0, // 添加最小宽度为0防止内容撑开
overflow: 'hidden', // 添加溢出隐藏
borderColor: item.error ? 'red' : 'inherit',
};
if (cardHeight && cardHeight !== 'auto') {
style.height = cardHeight;
}
return style;
};
// 卡片内容样式
const cardContentStyle = computed(() => {
const fieldColumns = gridOptions.value?.fieldColumns || 1;
if (fieldColumns > 1) {
return {
display: 'grid',
gridTemplateColumns: `repeat(${fieldColumns}, 1fr)`,
columnGap: '10px', // 控制列间距
rowGap: '8px', // 控制行间距,设置更小的值
};
}
return {};
});
// 卡片字段样式
const cardFieldStyle = (column: any) => {
const fieldColumns = gridOptions.value?.fieldColumns || 1;
const colSpan = column?.colSpan || 1;
if (fieldColumns > 1) {
return {
marginBottom: '0px',
paddingBottom: '0px',
height: 'auto',
lineHeight: 'normal',
gridColumn: `span ${colSpan}`,
};
}
return {};
};
// const errorCardStyle = (item: any) => {
// const errorData = gridOptions.value?.errorData || [];
// return errorData.some((errorItem) => errorItem.id === item.id)
// ? {
// ...cardStyle.value,
// borderColor: 'red',
// }
// : false;
// };
// 格式化字段值
const formatFieldValue = (column: any, row: any) => {
if (!column?.field) return '';
const value = row[column.field];
if (column?.formatter) {
return column.formatter(value, row);
}
return value;
};
// 渲染编辑组件
const renderEditComponent = (column: any, row: any) => {
const fieldName = column.field;
const value = row[fieldName];
const editRender = column.editRender;
const editProps = editRender?.props || {};
// 创建更新函数,避免直接修改只读对象
const updateValue = (val: any) => {
// 获取当前数据数组
const currentData = gridOptions.value?.data || [];
// 找到要修改的项的索引
const index = currentData.findIndex((item: any) => item.id === row.id);
if (index !== -1) {
// 创建新的数据数组,避免直接修改原数组
const newData = [...currentData];
// 创建新的对象,避免直接修改原对象
newData[index] = { ...newData[index], [fieldName]: val };
// 更新数据
props.api.setData(newData);
}
};
switch (editRender.name) {
case 'date-picker':
case 'DatePicker': {
return h(DatePicker, {
value,
valueFormat: 'YYYY-MM-DD',
...editProps,
'onUpdate:value': updateValue,
});
}
case 'Input':
case 'input': {
return h(Input, {
value,
...editProps,
'onUpdate:value': updateValue,
});
}
case 'input-number':
case 'InputNumber': {
return h(Input, {
value,
type: 'number',
...editProps,
'onUpdate:value': updateValue,
});
}
case 'Select':
case 'select': {
return h(Select, {
value,
...editProps,
'onUpdate:value': updateValue,
});
}
case 'SelectDropdownRender':
case 'selectDropdownRender': {
// 创建一个响应式的选项数组
const options = ref<any[]>([]);
// 创建一个唯一的缓存键,基于 row 的关键属性
const cacheKey = `${row.paymentId}_${row.come}_${row.orderType}_${row.courseId}`;
// 检查缓存中是否已有数据
if (selectOptionsCache[cacheKey]) {
options.value = selectOptionsCache[cacheKey];
} else if (editProps?.fetchOptions) {
// 如果缓存中没有,则获取数据并存储到缓存
editProps.fetchOptions(row).then((res: any) => {
options.value = res;
// 存储到缓存
selectOptionsCache[cacheKey] = res;
});
}
return h(SelectDropdownRender, {
value,
...editProps,
options,
'onUpdate:value': updateValue,
});
}
default: {
return formatFieldValue(column, row);
}
}
};
// 处理卡片点击
const handleCardClick = (row: any) => {
gridEvents.value?.cardClick?.(row);
};
// 初始化
async function init() {
await nextTick();
if (props.api?.mount) {
props.api.mount();
}
}
onMounted(() => {
init();
});
onUnmounted(() => {
if (props.api?.unmount) {
props.api.unmount();
}
});
</script>
<template>
<div :class="cn('h-full overflow-auto rounded-md', className)">
<div :class="cn('rounded-md bg-card', gridClass)">
<!-- 加载状态 -->
<VbenLoading v-if="gridOptions?.loading" :spinning="true" />
<!-- 卡片列表 -->
<div v-else-if="listData.length > 0" :style="gridStyle">
<Card
v-for="(item, index) in listData"
:key="item[titleFieldName]"
:class="gridOptions?.cardClass"
:style="cardStyle(item)"
@click="handleCardClick(item)"
>
<!-- 自定义标题 -->
<template #title v-if="gridOptions?.showTitle">
<div class="custom-card-title" :title="item[titleFieldName]">
{{ item[titleFieldName] }}
</div>
</template>
<!-- 卡片右上角额外内容 -->
<template #extra v-if="$slots['card-extra']">
<slot name="card-extra" :row="item" :index="index"></slot>
</template>
<div class="card-content" :style="cardContentStyle">
<div
v-for="column in gridOptions?.columns"
:key="column?.field || ''"
class="card-field"
:style="cardFieldStyle(column)"
v-show="
!column?.show ||
(typeof column.show === 'function' ? column.show(item) : column.show)
"
>
<!-- 自定义插槽 -->
<div v-if="column?.slots?.default" class="field-item">
<span class="field-title">{{ column?.title }}:</span>
<span class="field-value field-edit">
<slot
:name="column.slots.default"
:row="item"
:field="column?.field || ''"
:value="item[column?.field || '']"
:index="index"
></slot>
</span>
</div>
<!-- 编辑渲染组件 -->
<div v-else-if="column?.editRender" class="field-item">
<span class="field-title">{{ column?.title }}:</span>
<span class="field-value field-edit">
<component :is="renderEditComponent(column, item)" />
</span>
</div>
<!-- 默认显示 -->
<div v-else class="field-item">
<span class="field-title">{{ column?.title }}:</span>
<span class="field-value">{{ formatFieldValue(column, item) }}</span>
</div>
</div>
</div>
<!-- 卡片底部操作区域 -->
<template #actions v-if="$slots['card-actions']">
<slot name="card-actions" :row="item"></slot>
</template>
</Card>
</div>
<!-- 空状态 -->
<div v-else class="flex flex-col items-center justify-center py-10">
<Empty :description="gridOptions?.emptyText || '暂无数据'" />
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.card-content {
.card-field {
.field-item {
display: flex;
align-items: center;
flex-wrap: nowrap;
.field-title {
flex-shrink: 0;
margin-right: 8px;
font-weight: 500;
color: rgba(0, 0, 0, 0.85);
white-space: nowrap;
}
.field-value {
flex: 1;
min-width: 0;
word-break: break-all;
color: rgba(0, 0, 0, 0.65);
overflow: hidden;
text-overflow: ellipsis;
&.field-edit {
display: flex;
align-items: center;
:deep(.ant-input),
:deep(.ant-picker),
:deep(.ant-select) {
width: 100%;
}
}
}
}
}
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
:deep(.ant-card) {
display: flex;
flex-direction: column;
width: 100%; // 确保卡片不超出容器宽度
min-width: 0; // 防止内容撑开
.ant-card-body {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
padding: 12px !important;
min-width: 0; // 防止内容撑开
}
.card-content {
overflow-y: auto;
min-width: 0; // 防止内容撑开
}
.ant-card-head {
min-height: auto;
padding: 0 12px;
border-bottom: 1px solid #f0f0f0;
min-width: 0; // 防止内容撑开
height: auto; // 允许头部高度自适应
display: flex; // 使用flex布局
.ant-card-head-wrapper {
align-items: flex-start; // 顶部对齐
width: 100%;
}
.ant-card-head-title {
padding: 8px 0;
width: 100%;
height: auto; // 允许标题高度自适应
white-space: normal; // 允许换行
word-wrap: break-word;
word-break: break-all;
}
.ant-card-extra {
padding: 8px 0; // 给extra区域添加与标题相同的内边距
}
}
}
.custom-card-title {
word-wrap: break-word;
word-break: break-all;
width: 100%;
height: auto;
white-space: normal; // 允许换行
line-height: 1.5;
}
</style>