579 lines
20 KiB
Vue
579 lines
20 KiB
Vue
<script setup lang="ts">
|
||
/**
|
||
* 族谱图(原生递归树容器):全平台不依赖 @antv/g6,用 scroll-view + 递归 family-tree-node 绘制。
|
||
*
|
||
* 一个组件承载两种数据源(对齐旧 WebVue 的两张 G6 图):
|
||
* - mode="sub"(谱系图):根 = 当前族员(props.root.CurrentPerson),子树按需懒加载——
|
||
* 点未加载的子女 emit("select-child"),页面拉完写进 props.loadedNodes 再回来展开;
|
||
* - mode="full"(祖谱系图):组件内部 FindAllByTreeId 拉全族三张表(人员/夫妻/亲子)拍平索引,
|
||
* 从始祖向下渲染;同一孩子挂多个父时只在「承嗣/主承担」父下展开,其余父仅显示引用卡(对齐旧图)。
|
||
*
|
||
* 展开态、性别/关系文案、头像查表都集中在这里,node 只经 inject 的 api 取用(见 types.ts)。
|
||
*/
|
||
import type { LoadedTreeNode } from "./types";
|
||
import type { FamilyTreeChildrenMapViewEntity } from "@/data/auto/familyTreeChildrenMapViewEntity";
|
||
import type { FamilyTreeData } from "@/data/auto/familyTreeData";
|
||
import type { FamilyTreeSpouseMapViewEntity } from "@/data/auto/familyTreeSpouseMapViewEntity";
|
||
import { useLocale } from "uview-pro";
|
||
import { computed, provide, ref, watch } from "vue";
|
||
import { usePolicyOptions } from "@/composables/usePolicyOptions";
|
||
import familyTreeService from "@/service/master/familyTreeService";
|
||
import { FAMILY_TREE_CHART_API } from "./types";
|
||
|
||
const props = defineProps({
|
||
visible: { type: Boolean, default: false },
|
||
/** sub=当前族员子树(懒加载);full=祖谱全树 */
|
||
mode: { type: String as () => "sub" | "full", default: "sub" },
|
||
treeId: { type: String, default: "" },
|
||
/** sub 模式数据源:当前族员所在族谱 */
|
||
root: { type: Object as () => FamilyTreeData | null, default: null },
|
||
/** sub 模式已展开加载的族员(personId -> 子树) */
|
||
loadedNodes: { type: Object as () => Record<string, LoadedTreeNode>, default: () => ({}) },
|
||
title: { type: String, default: "" },
|
||
});
|
||
|
||
const emit = defineEmits<{
|
||
(e: "update:visible", value: boolean): void;
|
||
(e: "select-child", personId: string): void;
|
||
(e: "pick-person", personId: string): void;
|
||
}>();
|
||
|
||
const { t } = useLocale();
|
||
const { getPolicyText } = usePolicyOptions();
|
||
|
||
/** 展开状态:personId -> 是否展开(子树/全树共用) */
|
||
const expanded = ref<Record<string, boolean>>({});
|
||
const loading = ref(false);
|
||
|
||
// ===== 全树模式:FindAllByTreeId 拍平出的索引 =====
|
||
interface FullIndex {
|
||
rootId: string;
|
||
personMap: Map<string, any>;
|
||
spousesOf: Map<string, any[]>;
|
||
childrenOf: Map<string, any[]>;
|
||
parentsOf: Map<string, { parentId: string; relation?: string }[]>;
|
||
/** 当前族人 PersonId(FindAllByTreeId 的 CurrentPerson) */
|
||
currentId: string;
|
||
/** 主系链:父 PersonId -> 该父子女中位于「祖→当前族人」链上的那一个,排序时置顶(旧图主系居首同款) */
|
||
lineageChildOf: Map<string, string>;
|
||
}
|
||
const fullIndex = ref<FullIndex | null>(null);
|
||
|
||
/** 承祧 / 入嗣 / 收养类关系:孩子过继到该父名下,由其承担后代世系(对齐旧图) */
|
||
const ADOPTIVE_RELATIONS = ["C", "E", "F", "G", "H", "I", "J", "K", "L"];
|
||
|
||
function isMale(gender?: string): boolean {
|
||
if (!gender) {
|
||
return true;
|
||
}
|
||
const label = getPolicyText("GenderPolicy", gender).toLowerCase();
|
||
return label.includes("男") || label.includes("male") || gender === "M" || gender === "1";
|
||
}
|
||
|
||
function childTag(relationType?: string): string {
|
||
return relationType ? getPolicyText("RelationTypePolicy", relationType) || "子女" : "子女";
|
||
}
|
||
|
||
function byOrder(a: { Orderby?: number }, b: { Orderby?: number }): number {
|
||
return (a.Orderby ?? 0) - (b.Orderby ?? 0);
|
||
}
|
||
|
||
/** 全族头像查表:sub 用 root.CurrentPerson.FamilyTreePersons + loadedNodes;full 用 personMap */
|
||
function getAvatar(personId: string): string {
|
||
const subPersons = (props.root?.CurrentPerson?.FamilyTreePersons ?? []) as any[];
|
||
const hit = subPersons.find(p => p?.PersonId === personId);
|
||
if (hit?.AvatarUrl) {
|
||
return hit.AvatarUrl;
|
||
}
|
||
const loaded = props.loadedNodes[personId];
|
||
if (loaded?.person?.AvatarUrl) {
|
||
return loaded.person.AvatarUrl;
|
||
}
|
||
const full = fullIndex.value?.personMap.get(personId);
|
||
return full?.AvatarUrl ?? "";
|
||
}
|
||
|
||
/** 性别查表:关系视图里的 Gender 是拥有者(丈夫/父亲)本人的字段,配偶/子女必须回查族人档案拿真实性别 */
|
||
function getGender(personId: string): string {
|
||
const subPersons = (props.root?.CurrentPerson?.FamilyTreePersons ?? []) as any[];
|
||
const hit = subPersons.find(p => p?.PersonId === personId);
|
||
if (hit?.Gender) {
|
||
return hit.Gender;
|
||
}
|
||
const loaded = props.loadedNodes[personId];
|
||
if (loaded?.person?.Gender) {
|
||
return loaded.person.Gender;
|
||
}
|
||
return fullIndex.value?.personMap.get(personId)?.Gender ?? "";
|
||
}
|
||
|
||
const rootPersonId = computed(() => {
|
||
if (props.mode === "full") {
|
||
return fullIndex.value?.rootId ?? "";
|
||
}
|
||
return props.root?.CurrentPerson?.FamilyPerson?.PersonId ?? "";
|
||
});
|
||
|
||
const hasRoot = computed(() => Boolean(rootPersonId.value) && describe(rootPersonId.value) !== null);
|
||
|
||
/** 把一份「人 + 配偶 + 子女」原始记录归一化成 ResolvedNode(sub:来自 CurrentPerson/loadedNodes) */
|
||
function resolveSubNode(personId: string, record: { person: any; spouses: FamilyTreeSpouseMapViewEntity[]; children: FamilyTreeChildrenMapViewEntity[] }, isRoot: boolean) {
|
||
const p = record.person;
|
||
const self = {
|
||
personId,
|
||
name: `${p.Surname ?? ""}${p.GenealogyName ?? ""}`,
|
||
commonName: p.CommonName ?? "",
|
||
gender: p.Gender ?? "",
|
||
avatarUrl: p.AvatarUrl ?? getAvatar(personId),
|
||
tag: isRoot ? "本人" : "族员",
|
||
};
|
||
const spouses = [...record.spouses].sort(byOrder).map((s: any) => ({
|
||
personId: s.SpouseId,
|
||
name: `${s.SpouseSurname ?? ""}${s.SpouseGenealogyName ?? ""}`,
|
||
commonName: s.SpouseCommonName ?? "",
|
||
gender: getGender(s.SpouseId),
|
||
avatarUrl: getAvatar(s.SpouseId),
|
||
tag: "配偶",
|
||
}));
|
||
const children = [...record.children].sort(byOrder).map((c: any) => {
|
||
const childId = c.ChildrenId;
|
||
const loaded = childId === rootPersonId.value || Boolean(props.loadedNodes[childId]);
|
||
return {
|
||
personId: childId,
|
||
card: {
|
||
personId: childId,
|
||
name: `${c.ChildrenSurname ?? ""}${c.ChildrenGenealogyName ?? ""}`,
|
||
commonName: c.ChildrenCommonName ?? "",
|
||
gender: getGender(childId),
|
||
avatarUrl: getAvatar(childId),
|
||
tag: childTag(c.RelationType),
|
||
},
|
||
loaded,
|
||
expandable: loaded,
|
||
};
|
||
});
|
||
return { self, spouses, children, loaded: true };
|
||
}
|
||
|
||
/** 全树节点归一化:索引数据恒已加载,expandable 由「主承担父」判定 */
|
||
function resolveFullNode(personId: string): any | null {
|
||
const index = fullIndex.value;
|
||
if (!index) {
|
||
return null;
|
||
}
|
||
const info = index.personMap.get(personId);
|
||
if (!info) {
|
||
return null;
|
||
}
|
||
const isRoot = personId === index.rootId;
|
||
const self = {
|
||
personId,
|
||
name: `${info.Surname ?? ""}${info.GenealogyName ?? ""}`,
|
||
commonName: info.CommonName ?? "",
|
||
gender: info.Gender ?? "",
|
||
avatarUrl: getAvatar(personId),
|
||
tag: isRoot ? "始祖" : "族员",
|
||
};
|
||
const spouses = [...(index.spousesOf.get(personId) ?? [])].sort(byOrder).map((s: any) => ({
|
||
personId: s.id,
|
||
name: s.name,
|
||
commonName: s.commonName ?? "",
|
||
gender: s.gender || index.personMap.get(s.id)?.Gender || "",
|
||
avatarUrl: getAvatar(s.id),
|
||
tag: "配偶",
|
||
}));
|
||
// 主系置顶:链向当前族人的那个子女排第 1,其余兄弟依原始顺序跟在后
|
||
const spineId = index.lineageChildOf.get(personId);
|
||
const children = [...(index.childrenOf.get(personId) ?? [])].sort((a: any, b: any) => {
|
||
if (spineId && a.id === spineId && b.id !== spineId) {
|
||
return -1;
|
||
}
|
||
if (spineId && b.id === spineId && a.id !== spineId) {
|
||
return 1;
|
||
}
|
||
return byOrder(a, b);
|
||
}).map((c: any) => {
|
||
const primary = primaryParentOf(c.id);
|
||
return {
|
||
personId: c.id,
|
||
card: {
|
||
personId: c.id,
|
||
name: c.name,
|
||
commonName: c.commonName ?? "",
|
||
gender: c.gender || index.personMap.get(c.id)?.Gender || "",
|
||
avatarUrl: getAvatar(c.id),
|
||
tag: childTag(c.relation),
|
||
},
|
||
loaded: true,
|
||
expandable: primary === personId,
|
||
};
|
||
});
|
||
return { self, spouses, children, loaded: true };
|
||
}
|
||
|
||
function primaryParentOf(personId: string): string {
|
||
const parents = fullIndex.value?.parentsOf.get(personId) ?? [];
|
||
if (parents.length === 0) {
|
||
return "";
|
||
}
|
||
const adopted = parents.find(p => ADOPTIVE_RELATIONS.includes(p.relation ?? ""));
|
||
return (adopted ?? parents[0]).parentId;
|
||
}
|
||
|
||
/** 供 node 调用:按模式解析某人;无法解析返回 null */
|
||
function describe(personId: string) {
|
||
if (!personId) {
|
||
return null;
|
||
}
|
||
if (props.mode === "full") {
|
||
return resolveFullNode(personId);
|
||
}
|
||
if (personId === rootPersonId.value) {
|
||
const cp = props.root?.CurrentPerson;
|
||
if (!cp?.FamilyPerson) {
|
||
return null;
|
||
}
|
||
return resolveSubNode(personId, {
|
||
person: cp.FamilyPerson,
|
||
spouses: cp.FamilyTreeSpouseMaps ?? [],
|
||
children: cp.FamilyTreeChildrenMaps ?? [],
|
||
}, true);
|
||
}
|
||
const loaded = props.loadedNodes[personId];
|
||
if (loaded) {
|
||
return resolveSubNode(personId, loaded, false);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function toggle(personId: string): void {
|
||
expanded.value = { ...expanded.value, [personId]: !expanded.value[personId] };
|
||
}
|
||
|
||
// ===== 缩放与布局方向(弹窗右上控制条)=====
|
||
const ZOOM_MIN = 0.5;
|
||
const ZOOM_MAX = 2;
|
||
const ZOOM_STEP = 0.1;
|
||
const zoom = ref(1);
|
||
// 默认纵向:世代向下推进、主系居首列(⇅ 可切回横向阶梯)
|
||
const vertical = ref(true);
|
||
const zoomLabel = computed(() => `${Math.round(zoom.value * 100)}%`);
|
||
|
||
function clampZoom(value: number): number {
|
||
return Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, Math.round(value * 10) / 10));
|
||
}
|
||
|
||
function zoomIn(): void {
|
||
zoom.value = clampZoom(zoom.value + ZOOM_STEP);
|
||
}
|
||
|
||
function zoomOut(): void {
|
||
zoom.value = clampZoom(zoom.value - ZOOM_STEP);
|
||
}
|
||
|
||
function toggleDirection(): void {
|
||
vertical.value = !vertical.value;
|
||
}
|
||
|
||
function requestLoad(personId: string): void {
|
||
emit("select-child", personId);
|
||
}
|
||
|
||
function requestPick(personId: string): void {
|
||
emit("pick-person", personId);
|
||
}
|
||
|
||
provide(FAMILY_TREE_CHART_API, {
|
||
describe,
|
||
expanded,
|
||
toggle,
|
||
requestLoad,
|
||
requestPick,
|
||
isMale,
|
||
vertical,
|
||
});
|
||
|
||
/** 全树:从 FamilyTreeData 拍平三张索引表(照抄旧 buildIndex 的字段映射) */
|
||
function buildFullIndex(resp: FamilyTreeData): FullIndex {
|
||
const personMap = new Map<string, any>();
|
||
const spousesOf = new Map<string, any[]>();
|
||
const childrenOf = new Map<string, any[]>();
|
||
const parentsOf = new Map<string, { parentId: string; relation?: string }[]>();
|
||
const cp = resp?.CurrentPerson;
|
||
|
||
(cp?.FamilyTreePersons ?? []).forEach((p: any) => {
|
||
if (p?.PersonId) {
|
||
personMap.set(p.PersonId, { ...p });
|
||
}
|
||
});
|
||
const ancestor = cp?.FamilyPerson;
|
||
if (ancestor?.PersonId && !personMap.has(ancestor.PersonId)) {
|
||
personMap.set(ancestor.PersonId, { ...ancestor });
|
||
}
|
||
|
||
const pushSpouse = (ownerId: string, entry: any) => {
|
||
if (!ownerId) {
|
||
return;
|
||
}
|
||
const list = spousesOf.get(ownerId) ?? [];
|
||
if (!list.some(x => x.id === entry.id)) {
|
||
list.push(entry);
|
||
spousesOf.set(ownerId, list);
|
||
}
|
||
};
|
||
(cp?.FamilyTreeSpouseMaps ?? []).forEach((s: any) => {
|
||
pushSpouse(s.PersonId, {
|
||
id: s.SpouseId,
|
||
name: `${s.SpouseSurname ?? ""}${s.SpouseGenealogyName ?? ""}`,
|
||
commonName: s.SpouseCommonName,
|
||
// 不取 s.Gender:那是拥有者本人的性别,配偶真实性别由 personMap 回查(见 resolveFullNode)
|
||
gender: undefined,
|
||
orderby: s.Orderby,
|
||
});
|
||
pushSpouse(s.SpouseId, {
|
||
id: s.PersonId,
|
||
name: `${s.Surname ?? ""}${s.GenealogyName ?? ""}`,
|
||
commonName: s.CommonName,
|
||
gender: undefined,
|
||
orderby: s.Orderby,
|
||
});
|
||
});
|
||
|
||
(cp?.FamilyTreeChildrenMaps ?? []).forEach((c: any) => {
|
||
const list = childrenOf.get(c.PersonId) ?? [];
|
||
if (!list.some(x => x.id === c.ChildrenId)) {
|
||
list.push({
|
||
id: c.ChildrenId,
|
||
name: `${c.ChildrenSurname ?? ""}${c.ChildrenGenealogyName ?? ""}`,
|
||
commonName: c.ChildrenCommonName,
|
||
// 同上:c.Gender 是父亲的性别,子女真实性别由 personMap 回查
|
||
gender: undefined,
|
||
relation: c.RelationType,
|
||
orderby: c.Orderby,
|
||
});
|
||
childrenOf.set(c.PersonId, list);
|
||
const parentList = parentsOf.get(c.ChildrenId) ?? [];
|
||
parentList.push({ parentId: c.PersonId, relation: c.RelationType });
|
||
parentsOf.set(c.ChildrenId, parentList);
|
||
}
|
||
});
|
||
|
||
const rootId = ancestor?.PersonId || resp?.FamilyTree?.AncestorPersonId || "";
|
||
|
||
// 主系链:从当前族人逐级向上到始祖,记下每一环「父 -> 链上子」,渲染时把该子排到子女首位。
|
||
// 锚点优先用页面当前选中族人(props.root,如切到表妹);FindAllByTreeId 的 CurrentPerson 是登录者本人,仅作兜底
|
||
const currentId = props.root?.CurrentPerson?.FamilyPerson?.PersonId || cp?.FamilyPerson?.PersonId || "";
|
||
const lineageChildOf = new Map<string, string>();
|
||
const visited = new Set<string>();
|
||
let cursor = currentId;
|
||
while (cursor && !visited.has(cursor)) {
|
||
visited.add(cursor);
|
||
const parentId = (parentsOf.get(cursor) ?? [])[0]?.parentId ?? "";
|
||
if (!parentId) {
|
||
break;
|
||
}
|
||
lineageChildOf.set(parentId, cursor);
|
||
cursor = parentId;
|
||
}
|
||
return { rootId, personMap, spousesOf, childrenOf, parentsOf, currentId, lineageChildOf };
|
||
}
|
||
|
||
async function loadFull(): Promise<void> {
|
||
const treeId = props.treeId.trim();
|
||
if (!treeId) {
|
||
fullIndex.value = null;
|
||
return;
|
||
}
|
||
try {
|
||
loading.value = true;
|
||
const resp = await familyTreeService.FindAllByTreeId(treeId);
|
||
if (resp?.CurrentPerson) {
|
||
fullIndex.value = buildFullIndex(resp);
|
||
}
|
||
else {
|
||
fullIndex.value = null;
|
||
}
|
||
}
|
||
catch (error) {
|
||
console.error(error);
|
||
fullIndex.value = null;
|
||
}
|
||
finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
/** 弹窗打开:重置展开态到根节点;full 默认全树展开(旧祖谱系图同款),sub 仅展开根待懒加载 */
|
||
watch(
|
||
() => props.visible,
|
||
(on) => {
|
||
if (!on) {
|
||
return;
|
||
}
|
||
expanded.value = {};
|
||
if (props.mode === "full") {
|
||
loadFull().then(() => {
|
||
const next: Record<string, boolean> = {};
|
||
if (rootPersonId.value) {
|
||
next[rootPersonId.value] = true;
|
||
}
|
||
// 所有有子女的族员一律展开;非主承担父由 node 的 expandable 门控,不会重复渲染子树
|
||
fullIndex.value?.childrenOf.forEach((_list: any[], parentId: string) => {
|
||
next[parentId] = true;
|
||
});
|
||
expanded.value = next;
|
||
});
|
||
}
|
||
else if (rootPersonId.value) {
|
||
expanded.value = { [rootPersonId.value]: true };
|
||
}
|
||
},
|
||
);
|
||
|
||
/** sub 模式:新加载的族员自动展开,方便直接看其子树(对齐旧图 watch loadedNodes) */
|
||
watch(
|
||
() => Object.keys(props.loadedNodes),
|
||
(ids) => {
|
||
if (!props.visible || props.mode !== "sub") {
|
||
return;
|
||
}
|
||
const next = { ...expanded.value };
|
||
for (const id of ids) {
|
||
next[id] = true;
|
||
}
|
||
expanded.value = next;
|
||
},
|
||
);
|
||
|
||
function close(): void {
|
||
emit("update:visible", false);
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<u-popup :model-value="props.visible" mode="bottom" round="0" :safe-area-inset-bottom="true" @close="close">
|
||
<view class="ftc">
|
||
<view class="ftc__bar">
|
||
<text class="ftc__title">
|
||
{{ props.title || t('FamilyTreePage.pedigreeChart') }}
|
||
</text>
|
||
<u-icon name="close" size="40" color="#8f959e" @click="close" />
|
||
</view>
|
||
<!-- 悬浮控制条:放大/缩小/百分比复位/横纵布局切换(对齐旧图 G6 工具钮) -->
|
||
<view class="ftc__ctrl">
|
||
<view class="ftc__ctrl-btn" @tap="zoomIn">
|
||
<text class="ftc__ctrl-glyph">+</text>
|
||
</view>
|
||
<view class="ftc__ctrl-btn ftc__ctrl-btn--label" @tap="zoom = 1">
|
||
<text class="ftc__ctrl-label">{{ zoomLabel }}</text>
|
||
</view>
|
||
<view class="ftc__ctrl-btn" @tap="zoomOut">
|
||
<text class="ftc__ctrl-glyph">-</text>
|
||
</view>
|
||
<view class="ftc__ctrl-btn" @tap="toggleDirection">
|
||
<text class="ftc__ctrl-glyph">⇅</text>
|
||
</view>
|
||
</view>
|
||
<scroll-view class="ftc__scroll" scroll-x scroll-y :show-scrollbar="true">
|
||
<view v-if="hasRoot" class="ftc__tree" :style="{ zoom }">
|
||
<family-tree-node :person-id="rootPersonId" :preview="null" :expandable="true" :depth="0" />
|
||
</view>
|
||
<view v-else class="ftc__empty">
|
||
<text>{{ loading ? t('AdminPage.loadingTip') : t('FamilyTreePage.chartEmpty') }}</text>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
</u-popup>
|
||
</template>
|
||
|
||
<style lang="scss" scoped>
|
||
.ftc {
|
||
position: relative;
|
||
display: flex;
|
||
flex-direction: column;
|
||
|
||
/* 默认满屏:bottom 弹窗内容撞满视口,顶部预留安全区 */
|
||
height: 100vh;
|
||
background-color: #f5f6f8;
|
||
}
|
||
|
||
.ftc__bar {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: calc(28rpx + env(safe-area-inset-top)) 32rpx 28rpx;
|
||
background-color: #ffffff;
|
||
border-bottom: 2rpx solid #f2f3f5;
|
||
}
|
||
|
||
.ftc__title {
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
color: #1f2329;
|
||
}
|
||
|
||
/* 悬浮控制条:右侧竖排,白底圆角轻阴影 */
|
||
.ftc__ctrl {
|
||
position: absolute;
|
||
top: calc(120rpx + env(safe-area-inset-top));
|
||
right: 24rpx;
|
||
z-index: 10;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
border-radius: 16rpx;
|
||
background: #ffffff;
|
||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.12);
|
||
}
|
||
|
||
.ftc__ctrl-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 88rpx;
|
||
height: 76rpx;
|
||
border-bottom: 2rpx solid #f2f3f5;
|
||
}
|
||
|
||
.ftc__ctrl-btn--label {
|
||
border-bottom-color: #f2f3f5;
|
||
}
|
||
|
||
.ftc__ctrl-glyph {
|
||
font-size: 34rpx;
|
||
color: #1f2329;
|
||
}
|
||
|
||
.ftc__ctrl-label {
|
||
font-size: 24rpx;
|
||
color: #646a73;
|
||
}
|
||
|
||
.ftc__scroll {
|
||
flex: 1;
|
||
min-height: 0;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.ftc__tree {
|
||
display: inline-block;
|
||
box-sizing: border-box;
|
||
|
||
/* 按内容实际宽度撑开:inline-block 默认被可用宽度封顶,超出部分只溢出不扩滚动区,
|
||
scroll-x 必须靠 max-content 拿到真实宽度才能横向滚起来;窄于容器时由 min-width 撑满 */
|
||
width: max-content;
|
||
min-width: 100%;
|
||
padding: 24rpx;
|
||
}
|
||
|
||
.ftc__empty {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 320rpx;
|
||
font-size: 26rpx;
|
||
color: #8f959e;
|
||
}
|
||
</style>
|