mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-21 04:46:26 +00:00
1. 将原others目录下的通用组件按功能迁移到新目录: - 导航类组件移至navigation - 展示类组件移至display - 操作类组件移至actions - 表格类组件移至tables - 表单类组件保留或归类 2. 同步更新所有引用这些组件的文件路径 3. 新增多个通用业务组件,补充组件库覆盖范围 4. 优化部分页面交互体验,添加Tooltip增强信息展示 5. 修复部分滚动容器样式问题,使用ElScrollbar统一处理滚动
225 lines
6.0 KiB
Vue
225 lines
6.0 KiB
Vue
<template>
|
|
<ElScrollbar v-if="scrollbar" :max-height="maxHeight" :view-style="{ overflowX: 'hidden' }">
|
|
<FaDescriptionsContent v-bind="contentProps">
|
|
<template v-for="(_, name) in $slots" :key="name" #[name]="slotProps">
|
|
<slot :name="name" v-bind="slotProps" />
|
|
</template>
|
|
</FaDescriptionsContent>
|
|
</ElScrollbar>
|
|
<FaDescriptionsContent v-else v-bind="contentProps">
|
|
<template v-for="(_, name) in $slots" :key="name" #[name]="slotProps">
|
|
<slot :name="name" v-bind="slotProps" />
|
|
</template>
|
|
</FaDescriptionsContent>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import { h, defineComponent, type PropType, type VNode } from "vue";
|
|
import { ElDescriptions, ElDescriptionsItem, ElTag } from "element-plus";
|
|
|
|
export type TagType = "primary" | "success" | "warning" | "danger" | "info";
|
|
|
|
export interface TagConfig {
|
|
map?: Record<string, { type?: TagType; text?: string }>;
|
|
type?: TagType;
|
|
}
|
|
|
|
export interface DescriptionsItem {
|
|
label: string;
|
|
prop: string;
|
|
span?: number;
|
|
tag?: boolean | TagConfig;
|
|
slot?: string;
|
|
labelClassName?: string;
|
|
className?: string;
|
|
}
|
|
|
|
const TAG_TYPES: Set<string> = new Set(["primary", "success", "warning", "danger", "info"]);
|
|
|
|
function getNestedValue(obj: Record<string, unknown> | null, path: string): unknown {
|
|
if (!obj) return undefined;
|
|
return path.split(".").reduce((acc, key) => {
|
|
if (acc && typeof acc === "object" && key in acc) {
|
|
return (acc as Record<string, unknown>)[key];
|
|
}
|
|
return undefined;
|
|
}, obj as unknown);
|
|
}
|
|
|
|
function resolveTagType(value: unknown, tag: boolean | TagConfig): TagType {
|
|
if (typeof tag === "boolean") {
|
|
return tag ? "success" : "danger";
|
|
}
|
|
if (tag.map) {
|
|
const raw = value == null ? "" : String(value);
|
|
if (raw in tag.map) {
|
|
const t = tag.map[raw]!.type;
|
|
if (t && TAG_TYPES.has(t)) return t;
|
|
}
|
|
}
|
|
if (tag.type && TAG_TYPES.has(tag.type)) return tag.type;
|
|
return "info";
|
|
}
|
|
|
|
function resolveTagText(value: unknown, tag: boolean | TagConfig): string {
|
|
const raw = value == null ? "" : String(value);
|
|
if (typeof tag === "boolean") {
|
|
return raw;
|
|
}
|
|
if (tag.map && raw in tag.map) {
|
|
return tag.map[raw]!.text ?? raw;
|
|
}
|
|
return raw;
|
|
}
|
|
|
|
const FaDescriptionsContent = defineComponent({
|
|
name: "FaDescriptionsContent",
|
|
props: {
|
|
bindings: {
|
|
type: Object as PropType<Record<string, unknown>>,
|
|
required: true,
|
|
},
|
|
nsClass: { type: String, default: "" },
|
|
items: {
|
|
type: Array as PropType<DescriptionsItem[]>,
|
|
default: () => [],
|
|
},
|
|
data: {
|
|
type: Object as PropType<Record<string, unknown> | null>,
|
|
default: null,
|
|
},
|
|
span: { type: Number, default: 2 },
|
|
},
|
|
setup(props, { slots }) {
|
|
return () => {
|
|
const slotObj: Record<string, () => VNode[]> = {};
|
|
|
|
// Title slot
|
|
const titleSlot = slots.title;
|
|
if (titleSlot) {
|
|
slotObj.title = () => titleSlot();
|
|
}
|
|
|
|
// default slot 存在时具名 slot 不会生效(因为父组件完全接管了渲染内容)
|
|
const defaultSlot = slots.default;
|
|
if (defaultSlot) {
|
|
slotObj.default = () => defaultSlot();
|
|
} else {
|
|
const itemNodes: VNode[] = (props.items || []).map((item) => {
|
|
const value = props.data ? getNestedValue(props.data, item.prop) : undefined;
|
|
|
|
const itemChildren: (VNode | string)[] = [];
|
|
|
|
if (item.slot) {
|
|
const dynamicSlot = slots[item.slot];
|
|
if (dynamicSlot) {
|
|
itemChildren.push(...dynamicSlot({ item, value, row: props.data }));
|
|
}
|
|
} else if (item.tag != null) {
|
|
const tag: boolean | TagConfig = item.tag;
|
|
itemChildren.push(
|
|
h(
|
|
ElTag,
|
|
{ type: resolveTagType(value, tag) },
|
|
{ default: () => resolveTagText(value, tag) }
|
|
)
|
|
);
|
|
} else {
|
|
const propSlot = slots[item.prop];
|
|
if (propSlot) {
|
|
itemChildren.push(...propSlot({ item, value, row: props.data }));
|
|
} else {
|
|
itemChildren.push(String(value ?? ""));
|
|
}
|
|
}
|
|
|
|
return h(
|
|
ElDescriptionsItem,
|
|
{
|
|
label: item.label,
|
|
span: item.span || props.span,
|
|
"label-class-name": item.labelClassName,
|
|
class: item.className,
|
|
},
|
|
{ default: () => itemChildren }
|
|
);
|
|
});
|
|
|
|
slotObj.default = () => itemNodes;
|
|
}
|
|
|
|
return h(ElDescriptions, { ...props.bindings, class: props.nsClass }, slotObj);
|
|
};
|
|
},
|
|
});
|
|
</script>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, useAttrs } from "vue";
|
|
import { useNamespace } from "element-plus/es/hooks/use-namespace/index";
|
|
|
|
defineOptions({ name: "FaDescriptions" });
|
|
|
|
defineSlots<{
|
|
default(props: object): any;
|
|
title(props: object): any;
|
|
[slotName: string]: (props: {
|
|
item: DescriptionsItem;
|
|
value: unknown;
|
|
row: Record<string, unknown> | null;
|
|
}) => any;
|
|
}>();
|
|
|
|
const attrs = useAttrs();
|
|
const ns = useNamespace("descriptions");
|
|
|
|
interface Props {
|
|
column?: number;
|
|
border?: boolean;
|
|
size?: "default" | "small";
|
|
labelWidth?: string;
|
|
items?: DescriptionsItem[];
|
|
data?: Record<string, unknown> | null;
|
|
span?: number;
|
|
scrollbar?: boolean;
|
|
maxHeight?: string;
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
column: 4,
|
|
border: true,
|
|
size: undefined,
|
|
labelWidth: undefined,
|
|
items: () => [],
|
|
data: null,
|
|
span: 2,
|
|
scrollbar: true,
|
|
maxHeight: "70vh",
|
|
});
|
|
|
|
const bindings = computed(() => {
|
|
const bind: Record<string, unknown> = {
|
|
column: props.column,
|
|
border: props.border,
|
|
...attrs,
|
|
};
|
|
if (props.size !== undefined) bind.size = props.size;
|
|
if (props.labelWidth !== undefined) bind.labelWidth = props.labelWidth;
|
|
return bind;
|
|
});
|
|
|
|
const contentProps = computed(() => ({
|
|
bindings: bindings.value,
|
|
nsClass: ns.b(),
|
|
items: props.items,
|
|
data: props.data,
|
|
span: props.span,
|
|
}));
|
|
</script>
|
|
|
|
<style scoped>
|
|
:deep(.fa-descriptions) {
|
|
width: 100%;
|
|
}
|
|
</style>
|