- 新增 fieldColumns 配置项控制卡片内字段排列列数 - 支持通过 colSpan 设置字段跨列显示 - 优化卡片内容布局样式和字段显示效果 - 调整对账模块相关页面布局
427 lines
11 KiB
Vue
427 lines
11 KiB
Vue
<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>
|