877 lines
23 KiB
Vue
877 lines
23 KiB
Vue
<template>
|
||
<div
|
||
ref="searchRef"
|
||
style="margin-left: 4px; padding-top: 4px; margin-right: 12px"
|
||
>
|
||
<searchCriteria
|
||
@submit="onSubmit"
|
||
@reset="onReset"
|
||
@handle-change-bind-value="handleChangeBindValue"
|
||
>
|
||
<template #control>
|
||
<slot name="control"></slot>
|
||
</template>
|
||
</searchCriteria>
|
||
<hr />
|
||
</div>
|
||
<div ref="settingRef" class="setting">
|
||
<a-row :wrap="false">
|
||
<!-- 大屏用 4 列占位把操作区推到右侧;中小屏占位列隐藏,内容列占满整行再靠右 -->
|
||
<a-col :xs="0" :sm="0" :md="0" :lg="4"></a-col>
|
||
<a-col class="sc-toolbar" :xs="24" :sm="24" :md="24" :lg="20">
|
||
<a-input
|
||
flex="none"
|
||
readonly
|
||
value="视图"
|
||
class="site-input-split sc-view-label"
|
||
style="
|
||
width: 80px;
|
||
border-radius: 0;
|
||
border-top-left-radius: 5pt 5pt;
|
||
border-bottom-left-radius: 5pt 5pt;
|
||
background-color: rgb(250, 250, 250);
|
||
pointer-events: none;
|
||
"
|
||
/>
|
||
<a-select
|
||
class="sc-view-select"
|
||
v-model:value="viewOption.Value"
|
||
@select="onSelectColumn"
|
||
>
|
||
<a-select-option
|
||
v-for="o in viewOption.SelectOptions"
|
||
:key="o.DisplayName"
|
||
:value="o.BindValue"
|
||
>{{ o.DisplayName }}</a-select-option
|
||
>
|
||
</a-select>
|
||
<a-button
|
||
class="sc-toolbar-btn"
|
||
type="dashed"
|
||
:icon="h(SettingTwoTone)"
|
||
@click.prevent="onViewResult"
|
||
></a-button>
|
||
<a-button
|
||
class="sc-toolbar-btn"
|
||
type="dashed"
|
||
:icon="h(PrinterTwoTone)"
|
||
@click.prevent="onPrint"
|
||
></a-button>
|
||
</a-col>
|
||
</a-row>
|
||
<columnModal
|
||
:show-modal="showColumnModal"
|
||
@on-change-columns="onChangeColumns"
|
||
@handle-close="onHandleClose"
|
||
@on-change-view-option="onChangeViewOption"
|
||
/>
|
||
</div>
|
||
|
||
<!-- ==================== 结果区:卡片列表 ==================== -->
|
||
<div class="sc-result" style="margin-left: 2px; margin-right: 6px">
|
||
<!-- 卡片列表 -->
|
||
<div
|
||
ref="listRef"
|
||
class="sc-list"
|
||
:style="isModal ? undefined : { height: resultH || undefined }"
|
||
>
|
||
<a-spin :spinning="loading">
|
||
<div v-if="datas.length" class="sc-list-inner">
|
||
<div
|
||
v-for="(record, index) in datas"
|
||
:key="index"
|
||
class="sc-card"
|
||
:class="{
|
||
'sc-card-selected': isSelected(record),
|
||
'sc-card-collapsed': !isExpanded(index),
|
||
}"
|
||
>
|
||
<div
|
||
class="sc-card-head"
|
||
@click="onCardHeadClick($event, record, index)"
|
||
@dblclick="onCardDblclick($event, record)"
|
||
>
|
||
<span class="sc-check" @click.stop="onCardClick($event, record)">
|
||
<a-checkbox v-if="isCheckbox" :checked="isSelected(record)" />
|
||
<a-radio v-else :checked="isSelected(record)" />
|
||
</span>
|
||
<span class="sc-index">{{ getRowIndex(index) }}</span>
|
||
<span class="sc-title"><slot
|
||
name="cardTitle"
|
||
v-bind="{ record: record, index: index }"
|
||
>{{ getTitle(record) }}</slot></span>
|
||
<RightOutlined
|
||
class="sc-arrow"
|
||
:class="{ 'sc-arrow-open': isExpanded(index) }"
|
||
/>
|
||
</div>
|
||
<div v-show="isExpanded(index)" class="sc-card-body">
|
||
<div
|
||
v-for="column in allColumns"
|
||
:key="column.dataIndex"
|
||
class="sc-field"
|
||
>
|
||
<span class="sc-field-label"><slot
|
||
name="headerCell"
|
||
v-bind="{ title: column.title, column: column }"
|
||
>{{ column.title }}</slot></span>
|
||
<span class="sc-field-value"><span
|
||
class="sc-slot"
|
||
><slot
|
||
name="bodyCell"
|
||
v-bind="{
|
||
text: record[column.dataIndex],
|
||
column: column,
|
||
record: record,
|
||
index: index,
|
||
}"
|
||
></slot></span><span class="sc-fallback">{{
|
||
formatCell(record, column)
|
||
}}</span></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<a-empty v-else class="sc-empty" description="暂无数据" />
|
||
</a-spin>
|
||
</div>
|
||
|
||
<!-- 分页:固定在 footer 上方 -->
|
||
<div
|
||
ref="paginationRef"
|
||
class="sc-pagination"
|
||
:class="{ 'sc-pagination-fixed': !isModal }"
|
||
:style="!isModal ? { bottom: paginationBottom } : undefined"
|
||
>
|
||
<a-pagination
|
||
size="small"
|
||
:current="pageCurrent"
|
||
:page-size="pageSizeCurrent"
|
||
:total="paginationJ.Total"
|
||
:show-size-changer="true"
|
||
:page-size-options="['10', '20', '50', '100']"
|
||
:show-total="showTotal"
|
||
@change="onPageChange"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<slot name="footer" style="padding-left: 4px"></slot>
|
||
</template>
|
||
<script setup lang="ts">
|
||
import {
|
||
watch,
|
||
inject,
|
||
computed,
|
||
onMounted,
|
||
h,
|
||
ref,
|
||
unref,
|
||
nextTick,
|
||
onUnmounted,
|
||
} from "vue";
|
||
import { usePagination } from "vue-request";
|
||
import searchCriteria from "@/components/basic/SearchCriteria.vue";
|
||
import { SearchFilter } from "@/data/auto/searchFilter";
|
||
import { SearchCriteriaResultData } from "@/data/auto/searchCriteriaResultData";
|
||
import {
|
||
SettingTwoTone,
|
||
PrinterTwoTone,
|
||
RightOutlined,
|
||
} from "@ant-design/icons-vue";
|
||
import columnModal from "./ColumnModal.vue";
|
||
import { useWindowSize } from "@vueuse/core";
|
||
import { bsInfo } from "@/utils/browser";
|
||
import { enquiryMethodKey } from "@/injections";
|
||
|
||
//================props================
|
||
const props = defineProps({
|
||
isModal: {
|
||
type: Boolean,
|
||
default: false,
|
||
},
|
||
loading: {
|
||
type: Boolean,
|
||
default: false,
|
||
},
|
||
scrollx: {
|
||
type: Number,
|
||
default: 1024,
|
||
},
|
||
scrolly: {
|
||
type: Number,
|
||
default: 0,
|
||
},
|
||
pagination: {
|
||
type: SearchCriteriaResultData,
|
||
default: new SearchCriteriaResultData(),
|
||
},
|
||
rowSelectionType: {
|
||
type: String,
|
||
default: "radio",
|
||
},
|
||
// 卡片标题取值的列(按 dataIndex),默认取第一个字段列
|
||
cardTitleKeys: {
|
||
type: Array as () => string[],
|
||
default: () => [],
|
||
},
|
||
});
|
||
|
||
const isModal = ref(props["isModal"]);
|
||
|
||
const paginationJ = ref(props["pagination"]);
|
||
const rowSelectionType = ref(props["rowSelectionType"]);
|
||
const tableY = ref(props["scrolly"]);
|
||
|
||
const enquiry = inject(enquiryMethodKey, undefined);
|
||
|
||
const datas = enquiry?.getDatas()!;
|
||
|
||
const viewOption = ref(
|
||
enquiry
|
||
? enquiry.getViewOption()
|
||
: {
|
||
Value: "default",
|
||
SelectOptions: [{ BindValue: "default", DisplayName: "默认" }],
|
||
Columns: [{ BindValue: "default", BindColumn: [] }],
|
||
},
|
||
);
|
||
|
||
//================inject================
|
||
const isSM = ref<boolean>(false);
|
||
const { width, height } = useWindowSize();
|
||
if (bsInfo.size.sm) {
|
||
isSM.value = true;
|
||
} else {
|
||
isSM.value = false;
|
||
}
|
||
|
||
const resultH = ref("");
|
||
const showColumnModal = ref<boolean>(false);
|
||
const selected = ref<any[]>([]);
|
||
|
||
const emit = defineEmits([
|
||
"onReset",
|
||
"onSubmit",
|
||
"handleChangeBindValue",
|
||
"usePagination",
|
||
"onRowDblclick",
|
||
"onclick",
|
||
"onSelectChange",
|
||
"onViewResult",
|
||
"onPrint",
|
||
"updateColumns",
|
||
]);
|
||
|
||
const onViewResult = () => {
|
||
showColumnModal.value = true;
|
||
emit("onViewResult");
|
||
};
|
||
|
||
const onHandleClose = () => {
|
||
showColumnModal.value = false;
|
||
};
|
||
|
||
const onPrint = () => {
|
||
emit("onPrint", viewOption.value);
|
||
};
|
||
|
||
const searchRef = ref<HTMLElement | null>();
|
||
const settingRef = ref<HTMLElement | null>();
|
||
|
||
//================分页固定:始终贴在 footer 上方================
|
||
const paginationBottom = ref("64px");
|
||
const paginationRef = ref<HTMLElement | null>(null);
|
||
const listRef = ref<HTMLElement | null>(null);
|
||
const modalBodyEl = ref<HTMLElement | null>(null);
|
||
|
||
/** 获取页面级 footer(SearchCard 所在容器内的 .page-footer) */
|
||
const getPageFooter = (): HTMLElement | null => {
|
||
const container = searchRef.value?.parentElement ?? document.body;
|
||
return container.querySelector(".page-footer") as HTMLElement | null;
|
||
};
|
||
|
||
/** 普通页面:按 footer 真实高度算出分页栏的 bottom 偏移 */
|
||
const computePaginationBottom = () => {
|
||
if (isModal.value) return;
|
||
const footerEl = getPageFooter();
|
||
if (!footerEl) {
|
||
paginationBottom.value = "64px";
|
||
return;
|
||
}
|
||
// 视口底部 -> footer 顶部的距离(含 footer 高度与其 bottom 偏移) + 间距
|
||
const offset = window.innerHeight - footerEl.getBoundingClientRect().top;
|
||
paginationBottom.value = `${Math.max(Math.round(offset) + 6, 0)}px`;
|
||
};
|
||
|
||
/** 普通页面:底部为「分页栏 + 页面 footer」预留的真实高度(列表直接顶到分页栏上沿) */
|
||
const getFooterReservedHeight = (): number => {
|
||
const bottom = Number.parseInt(paginationBottom.value, 10);
|
||
const offset = Number.isFinite(bottom) ? bottom : 64;
|
||
// 分页栏自身高度(首帧未挂载时按 34 估算)
|
||
const paginationH = paginationRef.value?.offsetHeight || 34;
|
||
return offset + paginationH + 6;
|
||
};
|
||
|
||
/** 弹窗模式:给 modal 设定高度,使内容区撑满(内部 flex 生效) */
|
||
const applyModalHeight = () => {
|
||
const bodyEl = modalBodyEl.value;
|
||
if (!bodyEl) return;
|
||
const modalEl = bodyEl.closest(".ant-modal") as HTMLElement | null;
|
||
if (!modalEl) return;
|
||
const h = Math.max(window.innerHeight - 140, 240);
|
||
modalEl.style.height = `${h}px`;
|
||
};
|
||
|
||
/** 弹窗模式:让 modal body 变成纵向 flex 容器,分页贴底(modal footer 上方) */
|
||
const setupModalLayout = async () => {
|
||
if (!isModal.value) return;
|
||
await nextTick();
|
||
const bodyEl = searchRef.value?.closest(
|
||
".ant-modal-body",
|
||
) as HTMLElement | null;
|
||
if (!bodyEl) return;
|
||
modalBodyEl.value = bodyEl;
|
||
bodyEl.classList.add("sc-modal-body");
|
||
applyModalHeight();
|
||
};
|
||
|
||
/** 统一重新布局 */
|
||
const relayout = async () => {
|
||
// 先定位分页栏 / 弹窗,等 DOM 位置生效后再反推列表高度,避免用到上一帧位置
|
||
computePaginationBottom();
|
||
applyModalHeight();
|
||
await nextTick();
|
||
computResultTableHeight();
|
||
};
|
||
|
||
// 增加监听窗口宽高变化,窗口缩放自动重算
|
||
watch([width, height, isModal, tableY], () => {
|
||
void relayout();
|
||
});
|
||
|
||
const HEIGHT_OFFSETS = {
|
||
minTableHeight: 120, // 列表最小高度,防止计算结果为负
|
||
};
|
||
let resizeObserver: ResizeObserver | null = null;
|
||
|
||
/** 列表高度:优先用「列表顶部 → 分页栏顶部」的真实距离,保证滚动条紧贴页码 */
|
||
const computResultTableHeight = () => {
|
||
// 弹窗内高度由 flex 撑满,无需计算
|
||
if (isModal.value) return;
|
||
|
||
const listEl = listRef.value;
|
||
const paginationEl = paginationRef.value;
|
||
|
||
let finalY: number;
|
||
|
||
if (listEl && paginationEl) {
|
||
// 两者同为视口坐标,其差值就是真实可用高度(与页面滚动无关)
|
||
finalY =
|
||
paginationEl.getBoundingClientRect().top -
|
||
listEl.getBoundingClientRect().top -
|
||
6;
|
||
} else {
|
||
// 兜底:DOM 尚未就绪时沿用估算方式
|
||
const viewportH = window.innerHeight;
|
||
const searchHeight = searchRef.value?.offsetHeight ?? 0;
|
||
const settingHeight = settingRef.value?.offsetHeight ?? 0;
|
||
const base = tableY.value > 0 ? tableY.value : settingHeight;
|
||
finalY = viewportH - searchHeight - base - getFooterReservedHeight();
|
||
}
|
||
|
||
// 最小高度兜底,不能小于minTableHeight
|
||
finalY = Math.max(finalY, HEIGHT_OFFSETS.minTableHeight);
|
||
resultH.value = `${finalY}px`; // 直接输出px,不再calc
|
||
};
|
||
|
||
// 重新绑定observer,处理v‑if动态创建的setting DOM
|
||
const rebindObserver = () => {
|
||
if (!resizeObserver) return;
|
||
// 先全部取消
|
||
resizeObserver.disconnect();
|
||
if (searchRef.value) resizeObserver.observe(searchRef.value);
|
||
if (settingRef.value) resizeObserver.observe(settingRef.value);
|
||
if (paginationRef.value) resizeObserver.observe(paginationRef.value);
|
||
const footerEl = getPageFooter();
|
||
if (footerEl) resizeObserver.observe(footerEl);
|
||
|
||
void relayout();
|
||
};
|
||
|
||
onMounted(() => {
|
||
resizeObserver = new ResizeObserver(() => {
|
||
void relayout();
|
||
});
|
||
// 弹窗形态未显式传 is-modal 时自动识别:位于 .ant-modal-body 内即按弹窗布局
|
||
// 否则分页栏会按「页面模式」position:fixed 钉在视口底部,压住弹窗/页面 footer
|
||
if (!isModal.value && searchRef.value?.closest(".ant-modal-body")) {
|
||
isModal.value = true;
|
||
}
|
||
rebindObserver();
|
||
void setupModalLayout();
|
||
});
|
||
|
||
onUnmounted(() => {
|
||
if (resizeObserver) {
|
||
resizeObserver.disconnect();
|
||
resizeObserver = null;
|
||
}
|
||
});
|
||
|
||
// ✅监听v‑if条件变化,条件一变,重新绑定观测器
|
||
watch([isModal, isSM], () => {
|
||
rebindObserver();
|
||
void setupModalLayout();
|
||
});
|
||
|
||
watch([width, height, tableY], () => {
|
||
void relayout();
|
||
});
|
||
|
||
defineOptions({
|
||
name: "searchCard",
|
||
});
|
||
|
||
const columns = ref(
|
||
viewOption.value.Columns[0].BindColumn?.filter((x: any) => x.hidden != "Y"),
|
||
);
|
||
|
||
//接收拖动变化
|
||
const onChangeColumns = (cls: Column[]) => {
|
||
columns.value = cls?.filter((x) => x.hidden != "Y");
|
||
};
|
||
|
||
const onChangeViewOption = (vop: ViewOption) => {
|
||
viewOption.value = vop;
|
||
};
|
||
|
||
const onSelectColumn = () => {
|
||
columns.value = viewOption.value.Columns.filter(
|
||
(x: any) => x.BindValue == viewOption.value.Value,
|
||
)[0].BindColumn?.filter((x: any) => x.hidden != "Y") as Column[];
|
||
};
|
||
|
||
const onSubmit = async (filters: SearchFilter[]) => {
|
||
emit("onSubmit", filters);
|
||
};
|
||
|
||
const handleChangeBindValue = (bindValue: string, value: string) => {
|
||
emit("handleChangeBindValue", bindValue, value);
|
||
};
|
||
|
||
const onReset = async () => {
|
||
datas.value = [];
|
||
selected.value = [];
|
||
emit("onReset");
|
||
};
|
||
|
||
//================卡片列表数据================
|
||
// 字段列(排除序号列)
|
||
const fieldColumns = computed(() =>
|
||
((columns.value ?? []) as any[]).filter((c: any) => c.dataIndex !== "index"),
|
||
);
|
||
|
||
// 卡片标题使用的列
|
||
const titleColumns = computed(() => {
|
||
const keys = props.cardTitleKeys ?? [];
|
||
if (keys.length) {
|
||
return keys
|
||
.map((k) => fieldColumns.value.find((c: any) => c.dataIndex === k))
|
||
.filter(Boolean) as any[];
|
||
}
|
||
return fieldColumns.value.slice(0, 1);
|
||
});
|
||
|
||
// 卡片主体展示的列:完整展示全部字段列(含用于标题的列),保证不丢列
|
||
const allColumns = computed(() => fieldColumns.value);
|
||
|
||
// 展开状态(按列表序号记录):默认全部收起,只显示标题行
|
||
const expandedSet = ref<Record<number, boolean>>({});
|
||
|
||
const isExpanded = (index: number | string) =>
|
||
!!expandedSet.value[Number(index)];
|
||
|
||
const toggleExpand = (index: number | string) => {
|
||
const key = Number(index);
|
||
expandedSet.value = {
|
||
...expandedSet.value,
|
||
[key]: !expandedSet.value[key],
|
||
};
|
||
};
|
||
|
||
// 点击标题行:仅切换字段明细的展开/收起,不改选中状态
|
||
const onCardHeadClick = (event: any, record: any, index: number | string) => {
|
||
toggleExpand(index);
|
||
};
|
||
|
||
// 数据变化(查询/分页/排序)后重置展开状态
|
||
watch(datas, () => {
|
||
expandedSet.value = {};
|
||
});
|
||
|
||
const getTitle = (record: any) => {
|
||
const parts = titleColumns.value
|
||
.map((c: any) => record?.[c.dataIndex])
|
||
.filter((v: any) => v !== null && v !== undefined && v !== "");
|
||
return parts.length ? parts.join(" ") : "-";
|
||
};
|
||
|
||
const formatCell = (record: any, column: any) => {
|
||
const v = record?.[column.dataIndex];
|
||
return v === null || v === undefined || v === "" ? "-" : v;
|
||
};
|
||
|
||
const getRowIndex = (index: number | string) =>
|
||
(pageCurrent.value - 1) * pageSizeCurrent.value + Number(index) + 1;
|
||
|
||
//================选择================
|
||
const isCheckbox = computed(() => unref(rowSelectionType) === "checkbox");
|
||
|
||
const isSelected = (record: any) => selected.value.includes(record);
|
||
|
||
const emitSelection = () => emit("onSelectChange", selected.value);
|
||
|
||
const onCardClick = (event: any, record: any) => {
|
||
if (unref(rowSelectionType) == "radio") {
|
||
selected.value = [record];
|
||
} else {
|
||
if (selected.value.includes(record)) {
|
||
selected.value = selected.value.filter((item) => item !== record);
|
||
} else {
|
||
selected.value.push(record);
|
||
}
|
||
}
|
||
emit("onclick", event, [record]);
|
||
emitSelection();
|
||
};
|
||
|
||
const onCardDblclick = (event: any, record: any) => {
|
||
if (selected.value.includes(record)) {
|
||
selected.value = selected.value.filter((item) => item !== record);
|
||
} else {
|
||
selected.value.push(record);
|
||
}
|
||
emit("onRowDblclick", event, record);
|
||
emitSelection();
|
||
};
|
||
|
||
//================分页================
|
||
const pageCurrent = ref<number>(paginationJ.value.Page || 1);
|
||
const pageSizeCurrent = ref<number>(paginationJ.value.Limit || 20);
|
||
|
||
watch(
|
||
() => [paginationJ.value.Page, paginationJ.value.Limit],
|
||
() => {
|
||
pageCurrent.value = paginationJ.value.Page || 1;
|
||
pageSizeCurrent.value = paginationJ.value.Limit || 20;
|
||
},
|
||
);
|
||
|
||
const showTotal = (total: number) => `共 ${total} 条`;
|
||
|
||
const onPageChange = (page: number, pageSize: number) => {
|
||
pageCurrent.value = page;
|
||
pageSizeCurrent.value = pageSize;
|
||
run({ page, pageSize });
|
||
};
|
||
|
||
const queryData = async (params: PagingParams): Promise<any> => {
|
||
try {
|
||
emit("usePagination", params);
|
||
} catch (err) {}
|
||
|
||
return datas.value;
|
||
};
|
||
|
||
const { run } = usePagination(queryData, {
|
||
pagination: {
|
||
currentKey: "page",
|
||
pageSizeKey: "pageSize",
|
||
totalKey: "totalCount",
|
||
},
|
||
});
|
||
</script>
|
||
|
||
<style lang="less">
|
||
.search-criteria .ant-select-single .ant-select-selector {
|
||
border-top-left-radius: 0pt 0pt;
|
||
border-bottom-left-radius: 0pt 0pt;
|
||
border-top-right-radius: 5pt 5pt;
|
||
border-bottom-right-radius: 5pt 5pt;
|
||
border-left: 0 !important;
|
||
}
|
||
|
||
.search-criteria .ant-select-multiple .ant-select-selector {
|
||
border-top-left-radius: 0pt 0pt;
|
||
border-bottom-left-radius: 0pt 0pt;
|
||
border-top-right-radius: 5pt 5pt;
|
||
border-bottom-right-radius: 5pt 5pt;
|
||
border-left: 0 !important;
|
||
}
|
||
|
||
.search-criteria .ant-picker {
|
||
width: 100%;
|
||
}
|
||
|
||
.setting {
|
||
flex: 0 0 auto;
|
||
margin-right: 12px;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.setting .ant-select-selector {
|
||
border-radius: 0;
|
||
border-left: 0 !important;
|
||
}
|
||
|
||
.setting .ant-btn {
|
||
padding-top: 4px;
|
||
padding-bottom: 4px;
|
||
border-radius: 0;
|
||
}
|
||
|
||
/* 视图工具条:强制单行,下拉框自适应剩余宽度,按钮不被挤到下一行 */
|
||
.setting .sc-toolbar {
|
||
display: flex;
|
||
flex-wrap: nowrap;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
min-width: 0;
|
||
}
|
||
|
||
.setting .sc-toolbar .sc-view-label,
|
||
.setting .sc-toolbar .sc-view-select,
|
||
.setting .sc-toolbar .sc-toolbar-btn {
|
||
flex: 0 0 auto;
|
||
}
|
||
|
||
/* 下拉框吃掉中间的剩余宽度,窄屏也不会把按钮顶下去 */
|
||
.setting .sc-toolbar .sc-view-select {
|
||
flex: 1 1 auto;
|
||
width: auto;
|
||
min-width: 120px;
|
||
}
|
||
|
||
/* ==================== 结果区:卡片列表 ==================== */
|
||
.sc-list {
|
||
overflow-x: hidden;
|
||
overflow-y: auto;
|
||
margin-top: 4px;
|
||
padding: 2px;
|
||
-webkit-overflow-scrolling: touch;
|
||
}
|
||
|
||
.sc-list-inner {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
|
||
.sc-card {
|
||
border: 1px solid #eef0f4;
|
||
border-radius: 12px;
|
||
background: #fff;
|
||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
|
||
cursor: pointer;
|
||
user-select: none;
|
||
transition:
|
||
border-color 0.2s,
|
||
box-shadow 0.2s;
|
||
}
|
||
|
||
.sc-card-selected {
|
||
border-color: #1677ff;
|
||
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.12);
|
||
}
|
||
|
||
.sc-card-head {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 10px 12px;
|
||
border-bottom: 1px solid #eef0f4;
|
||
border-radius: 12px 12px 0 0;
|
||
background: #f7f9fc;
|
||
cursor: pointer;
|
||
-webkit-tap-highlight-color: transparent;
|
||
/* 标题再长也不外溢,多余部分由标题自身省略 */
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* 收起时:标题行独占整卡,隐藏与明细区的分割线 */
|
||
.sc-card-collapsed .sc-card-head {
|
||
border-bottom-color: transparent;
|
||
border-radius: 12px;
|
||
}
|
||
|
||
/* 右侧展开箭头:展开后顺时针旋转 90 度 */
|
||
.sc-arrow {
|
||
flex: 0 0 auto;
|
||
margin-left: 2px;
|
||
color: #98a1b0;
|
||
font-size: 12px;
|
||
transition:
|
||
transform 0.18s ease,
|
||
color 0.18s ease;
|
||
}
|
||
|
||
.sc-arrow-open {
|
||
transform: rotate(90deg);
|
||
color: #1677ff;
|
||
}
|
||
|
||
/* 勾选框单独可点:用于精确勾选/取消,不会带开合明细 */
|
||
.sc-check {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
flex: 0 0 auto;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.sc-index {
|
||
flex: 0 0 auto;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-width: 22px;
|
||
height: 22px;
|
||
padding: 0 6px;
|
||
border-radius: 11px;
|
||
background: #e8f1ff;
|
||
color: #1677ff;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.sc-title {
|
||
display: block;
|
||
flex: 1 1 auto;
|
||
min-width: 0;
|
||
max-width: 100%;
|
||
overflow: hidden;
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
color: #1f2329;
|
||
white-space: nowrap;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
/* 标题内的自定义内容(如图标 + 路径):整段按行内文本参与省略号截断 */
|
||
.sc-title .anticon {
|
||
vertical-align: -0.125em;
|
||
}
|
||
|
||
.sc-card-body {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr);
|
||
gap: 8px 14px;
|
||
padding: 10px 12px;
|
||
}
|
||
|
||
@media (min-width: 576px) {
|
||
.sc-card-body {
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
}
|
||
}
|
||
|
||
@media (min-width: 1200px) {
|
||
.sc-card-body {
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
}
|
||
}
|
||
|
||
.sc-field {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 4px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.sc-field-label {
|
||
flex: 0 0 auto;
|
||
max-width: 42%;
|
||
font-size: 12px;
|
||
color: #8a919f;
|
||
}
|
||
|
||
.sc-field-label::after {
|
||
content: ":";
|
||
}
|
||
|
||
.sc-field-value {
|
||
flex: 1 1 auto;
|
||
min-width: 0;
|
||
font-size: 14px;
|
||
color: #1f2329;
|
||
word-break: break-all;
|
||
}
|
||
|
||
/* 自定义插槽有渲染内容时,隐藏默认文本 */
|
||
.sc-slot:not(:empty) + .sc-fallback {
|
||
display: none;
|
||
}
|
||
|
||
.sc-empty {
|
||
padding: 40px 0;
|
||
}
|
||
|
||
.sc-pagination {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
padding: 6px 4px;
|
||
}
|
||
|
||
/* 普通页面:分页栏固定在页面 footer 上方(bottom 由内联 style 动态覆盖) */
|
||
.sc-pagination-fixed {
|
||
position: fixed;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 64px;
|
||
z-index: 998;
|
||
padding: 6px 20px;
|
||
background: #fff;
|
||
border-top: 1px solid #eef0f4;
|
||
}
|
||
|
||
/* 弹窗模式:内容区撑满 modal body,分页贴在 modal footer 上方 */
|
||
.sc-modal-body {
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden !important;
|
||
}
|
||
|
||
.sc-modal-body > * {
|
||
flex: 0 0 auto;
|
||
min-height: 0;
|
||
}
|
||
|
||
.sc-modal-body > .sc-result {
|
||
flex: 1 1 auto;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.sc-modal-body > .sc-result > .sc-list {
|
||
flex: 1 1 auto;
|
||
min-height: 0;
|
||
height: auto !important;
|
||
}
|
||
|
||
.sc-modal-body > .sc-result > .sc-pagination {
|
||
flex: 0 0 auto;
|
||
padding: 6px 0;
|
||
background: #fff;
|
||
border-top: 1px solid #eef0f4;
|
||
}
|
||
|
||
@media (max-width: 575px) {
|
||
.sc-pagination {
|
||
justify-content: center;
|
||
}
|
||
|
||
.sc-pagination-fixed {
|
||
padding: 6px 8px;
|
||
}
|
||
}
|
||
</style>
|