add space component

This commit is contained in:
Timi
2026-08-29 23:22:10 +08:00
parent 33f976b1ea
commit 9b61376830
4 changed files with 275 additions and 2 deletions
+14 -2
View File
@@ -9,6 +9,7 @@ import ArticleView from "./article-view";
import NotifyEditor from "./notify-editor"; import NotifyEditor from "./notify-editor";
import NotifyView from "./notify-view"; import NotifyView from "./notify-view";
import BottomPopup from "./bottom-popup"; import BottomPopup from "./bottom-popup";
import Space from "./space";
export default [ export default [
RootLayout, RootLayout,
@@ -20,7 +21,8 @@ export default [
ArticleView, ArticleView,
NotifyEditor, NotifyEditor,
NotifyView, NotifyView,
BottomPopup BottomPopup,
Space
]; ];
export { export {
@@ -33,9 +35,19 @@ export {
ArticleView, ArticleView,
NotifyEditor, NotifyEditor,
NotifyView, NotifyView,
BottomPopup BottomPopup,
Space
}; };
export type {
SpaceAlign,
SpaceDirection,
SpaceProps,
SpaceSeparator,
SpaceSize,
SpaceSlots
} from "./space";
export type { export type {
CodeEditorExpose CodeEditorExpose
} from "./code-editor"; } from "./code-editor";
+14
View File
@@ -0,0 +1,14 @@
import view from "./index.vue";
import { WebToolkit } from "timi-web";
export type {
SpaceAlign,
SpaceDirection,
SpaceProps,
SpaceSeparator,
SpaceSize,
SpaceSlots
} from "./type";
export const Space = WebToolkit.withInstall(view);
export default Space;
+214
View File
@@ -0,0 +1,214 @@
<template>
<div
class="t-space"
:class="spaceClass"
:style="spaceStyle"
>
<space-item
v-for="item in children"
:key="getChildKey(item)"
:child="item.node"
:separator="separatorContent"
:show-separator="hasSeparator && item.index < children.length - 1"
/>
</div>
</template>
<script lang="ts" setup>
import { Comment, Fragment, Teleport, defineComponent, h, isVNode, onBeforeUpdate, useSlots } from "vue";
import type { CSSProperties, PropType, VNode, VNodeChild } from "vue";
import type { SpaceProps, SpaceSlots, SpaceSize } from "./type";
defineOptions({
name: "TSpace"
});
const SIZE_MAP: Record<string, string> = {
small: "8px",
medium: "16px",
large: "24px"
};
interface FlatChild {
node: VNodeChild;
index: number;
}
const SpaceItem = defineComponent({
name: "SpaceItem",
props: {
child: {
type: null as unknown as PropType<VNodeChild>,
required: true
},
separator: {
type: null as unknown as PropType<VNodeChild>
},
showSeparator: Boolean
},
setup(itemProps) {
return () => [
h("div", { class: "t-space-item" }, toRenderableChildren(itemProps.child)),
itemProps.showSeparator
? h("div", { class: "t-space-item-separator" }, toRenderableChildren(itemProps.separator))
: null
];
}
});
const props = defineProps({
align: {
type: String as PropType<SpaceProps["align"]>
},
breakLine: Boolean,
direction: {
type: String as PropType<SpaceProps["direction"]>,
default: "horizontal"
},
separator: {
type: null as unknown as PropType<SpaceProps["separator"]>
},
size: {
type: [String, Number, Array] as PropType<SpaceProps["size"]>,
default: "medium"
}
});
const slots = useSlots() as SpaceSlots;
let children = flattenChildren(slots.default?.() ?? []);
let separatorContent = resolveSeparatorContent();
let hasSeparator = hasSeparatorContent(separatorContent);
onBeforeUpdate(() => {
children = flattenChildren(slots.default?.() ?? []);
separatorContent = resolveSeparatorContent();
hasSeparator = hasSeparatorContent(separatorContent);
});
function resolveSeparatorContent(): VNodeChild | undefined {
if (props.separator !== undefined) {
return typeof props.separator === "function" ? props.separator() : props.separator;
}
return slots.separator?.();
}
function hasSeparatorContent(separator: VNodeChild | undefined): boolean {
return Array.isArray(separator) ? 0 < separator.length : Boolean(separator);
}
const spaceClass = computed(() => ({
"t-space-align-start": props.align === "start",
"t-space-align-end": props.align === "end",
"t-space-align-baseline": props.align === "baseline",
"t-space-align-center": props.align === "center",
"t-space-vertical": props.direction === "vertical",
"t-space-horizontal": props.direction === "horizontal",
"t-space--break-line": props.breakLine
}));
const spaceStyle = computed<CSSProperties>(() => ({
gap: normalizeGap(props.size)
}));
function normalizeGap(size: SpaceSize | SpaceSize[] | undefined): string {
if (Array.isArray(size)) {
return size.map(normalizeSize).join(" ");
}
return normalizeSize(size ?? "medium");
}
function normalizeSize(size: SpaceSize): string {
if (typeof size === "number") {
return `${size}px`;
}
return SIZE_MAP[size] ?? size;
}
type RenderableChild = string | number | VNode;
function toRenderableChildren(content: VNodeChild | undefined): RenderableChild[] {
if (content === null || content === undefined || typeof content === "boolean") {
return [];
}
if (Array.isArray(content)) {
return content.flatMap(item => toRenderableChildren(item as VNodeChild));
}
return [content as RenderableChild];
}
function flattenChildren(nodes: VNodeChild[]): FlatChild[] {
const result: FlatChild[] = [];
const position = { value: 0 };
function append(items: VNodeChild[]): void {
for (const node of items) {
if (isVNode(node) && node.type === Fragment && Array.isArray(node.children)) {
append(node.children as VNodeChild[]);
continue;
}
const index = position.value;
position.value += 1;
const skipped = isVNode(node)
&& (node.type === Comment
|| node.type === Teleport
|| (typeof node.type === "symbol" && !node.children));
if (skipped) {
continue;
}
result.push({ node, index });
}
}
append(nodes);
return result;
}
function getChildKey(item: FlatChild): PropertyKey {
if (isVNode(item.node) && item.node.key !== null && item.node.key !== undefined) {
return item.node.key;
}
return item.index;
}
defineSlots<SpaceSlots>();
</script>
<style lang="less" scoped>
.t-space {
display: inline-flex;
:deep(.t-space-item) {
width: inherit;
}
&-align-start {
align-items: flex-start;
}
&-align-end {
align-items: flex-end;
}
&-align-baseline {
align-items: baseline;
}
&-align-center {
align-items: center;
}
&-vertical {
flex-direction: column;
> :deep(.t-space-item-separator) {
width: 100%;
}
}
&--break-line {
flex-wrap: wrap;
}
}
</style>
+33
View File
@@ -0,0 +1,33 @@
import type { VNodeChild } from "vue";
/** Space 对齐方式。 */
export type SpaceAlign = "start" | "end" | "center" | "baseline";
/** Space 排列方向。 */
export type SpaceDirection = "vertical" | "horizontal";
/** Space 预设或自定义间距。 */
export type SpaceSize = number | string;
/** Space 分隔符内容。 */
export type SpaceSeparator = string | VNodeChild | (() => VNodeChild);
/** Space 组件属性。 */
export interface SpaceProps {
/** 对齐方式。 */
align?: SpaceAlign;
/** 是否自动换行,仅在 horizontal 时有效。 */
breakLine?: boolean;
/** 间距方向。 */
direction?: SpaceDirection;
/** 分隔符,也可以使用 separator 插槽。 */
separator?: SpaceSeparator;
/** 间距大小,数组依次表示横向和纵向间距。 */
size?: SpaceSize | SpaceSize[];
}
/** Space 插槽。 */
export interface SpaceSlots {
default?: () => VNodeChild[];
separator?: () => VNodeChild[];
}