物业代码生成
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
import type { SortableOptions } from 'sortablejs';
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { useSortable } from '../use-sortable';
|
||||
|
||||
describe('useSortable', () => {
|
||||
beforeEach(() => {
|
||||
vi.mock('sortablejs/modular/sortable.complete.esm.js', () => ({
|
||||
default: {
|
||||
create: vi.fn(),
|
||||
},
|
||||
}));
|
||||
});
|
||||
it('should call Sortable.create with the correct options', async () => {
|
||||
// Create a mock element
|
||||
const mockElement = document.createElement('div') as HTMLDivElement;
|
||||
|
||||
// Define custom options
|
||||
const customOptions: SortableOptions = {
|
||||
group: 'test-group',
|
||||
sort: false,
|
||||
};
|
||||
|
||||
// Use the useSortable function
|
||||
const { initializeSortable } = useSortable(mockElement, customOptions);
|
||||
|
||||
// Initialize sortable
|
||||
await initializeSortable();
|
||||
|
||||
// Import sortablejs to access the mocked create function
|
||||
const Sortable = await import(
|
||||
'sortablejs/modular/sortable.complete.esm.js'
|
||||
);
|
||||
|
||||
// Verify that Sortable.create was called with the correct parameters
|
||||
expect(Sortable.default.create).toHaveBeenCalledTimes(1);
|
||||
expect(Sortable.default.create).toHaveBeenCalledWith(
|
||||
mockElement,
|
||||
expect.objectContaining({
|
||||
animation: 300,
|
||||
delay: 400,
|
||||
delayOnTouchOnly: true,
|
||||
...customOptions,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
13
packages/@core/composables/src/index.ts
Normal file
13
packages/@core/composables/src/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export * from './use-is-mobile';
|
||||
export * from './use-layout-style';
|
||||
export * from './use-namespace';
|
||||
export * from './use-priority-value';
|
||||
export * from './use-scroll-lock';
|
||||
export * from './use-simple-locale';
|
||||
export * from './use-sortable';
|
||||
export {
|
||||
useEmitAsProps,
|
||||
useForwardExpose,
|
||||
useForwardProps,
|
||||
useForwardPropsEmits,
|
||||
} from 'radix-vue';
|
7
packages/@core/composables/src/use-is-mobile.ts
Normal file
7
packages/@core/composables/src/use-is-mobile.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core';
|
||||
|
||||
export function useIsMobile() {
|
||||
const breakpoints = useBreakpoints(breakpointsTailwind);
|
||||
const isMobile = breakpoints.smaller('md');
|
||||
return { isMobile };
|
||||
}
|
84
packages/@core/composables/src/use-layout-style.ts
Normal file
84
packages/@core/composables/src/use-layout-style.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import type { VisibleDomRect } from '@vben-core/shared/utils';
|
||||
import type { CSSProperties } from 'vue';
|
||||
|
||||
import {
|
||||
CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT,
|
||||
CSS_VARIABLE_LAYOUT_CONTENT_WIDTH,
|
||||
CSS_VARIABLE_LAYOUT_FOOTER_HEIGHT,
|
||||
CSS_VARIABLE_LAYOUT_HEADER_HEIGHT,
|
||||
} from '@vben-core/shared/constants';
|
||||
import { getElementVisibleRect } from '@vben-core/shared/utils';
|
||||
import { useCssVar, useDebounceFn } from '@vueuse/core';
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
|
||||
/**
|
||||
* @zh_CN content style
|
||||
*/
|
||||
export function useLayoutContentStyle() {
|
||||
let resizeObserver: null | ResizeObserver = null;
|
||||
const contentElement = ref<HTMLDivElement | null>(null);
|
||||
const visibleDomRect = ref<null | VisibleDomRect>(null);
|
||||
const contentHeight = useCssVar(CSS_VARIABLE_LAYOUT_CONTENT_HEIGHT);
|
||||
const contentWidth = useCssVar(CSS_VARIABLE_LAYOUT_CONTENT_WIDTH);
|
||||
|
||||
const overlayStyle = computed((): CSSProperties => {
|
||||
const { height, left, top, width } = visibleDomRect.value ?? {};
|
||||
return {
|
||||
height: `${height}px`,
|
||||
left: `${left}px`,
|
||||
position: 'fixed',
|
||||
top: `${top}px`,
|
||||
width: `${width}px`,
|
||||
zIndex: 150,
|
||||
};
|
||||
});
|
||||
|
||||
const debouncedCalcHeight = useDebounceFn(
|
||||
(_entries: ResizeObserverEntry[]) => {
|
||||
visibleDomRect.value = getElementVisibleRect(contentElement.value);
|
||||
contentHeight.value = `${visibleDomRect.value.height}px`;
|
||||
contentWidth.value = `${visibleDomRect.value.width}px`;
|
||||
},
|
||||
16,
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
if (contentElement.value && !resizeObserver) {
|
||||
resizeObserver = new ResizeObserver(debouncedCalcHeight);
|
||||
resizeObserver.observe(contentElement.value);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
resizeObserver?.disconnect();
|
||||
resizeObserver = null;
|
||||
});
|
||||
|
||||
return { contentElement, overlayStyle, visibleDomRect };
|
||||
}
|
||||
|
||||
export function useLayoutHeaderStyle() {
|
||||
const headerHeight = useCssVar(CSS_VARIABLE_LAYOUT_HEADER_HEIGHT);
|
||||
|
||||
return {
|
||||
getLayoutHeaderHeight: () => {
|
||||
return Number.parseInt(`${headerHeight.value}`, 10);
|
||||
},
|
||||
setLayoutHeaderHeight: (height: number) => {
|
||||
headerHeight.value = `${height}px`;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function useLayoutFooterStyle() {
|
||||
const footerHeight = useCssVar(CSS_VARIABLE_LAYOUT_FOOTER_HEIGHT);
|
||||
|
||||
return {
|
||||
getLayoutFooterHeight: () => {
|
||||
return Number.parseInt(`${footerHeight.value}`, 10);
|
||||
},
|
||||
setLayoutFooterHeight: (height: number) => {
|
||||
footerHeight.value = `${height}px`;
|
||||
},
|
||||
};
|
||||
}
|
106
packages/@core/composables/src/use-namespace.ts
Normal file
106
packages/@core/composables/src/use-namespace.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { DEFAULT_NAMESPACE } from '@vben-core/shared/constants';
|
||||
|
||||
/**
|
||||
* @see copy https://github.com/element-plus/element-plus/blob/dev/packages/hooks/use-namespace/index.ts
|
||||
*/
|
||||
|
||||
const statePrefix = 'is-';
|
||||
|
||||
const _bem = (
|
||||
namespace: string,
|
||||
block: string,
|
||||
blockSuffix: string,
|
||||
element: string,
|
||||
modifier: string,
|
||||
) => {
|
||||
let cls = `${namespace}-${block}`;
|
||||
if (blockSuffix) {
|
||||
cls += `-${blockSuffix}`;
|
||||
}
|
||||
if (element) {
|
||||
cls += `__${element}`;
|
||||
}
|
||||
if (modifier) {
|
||||
cls += `--${modifier}`;
|
||||
}
|
||||
return cls;
|
||||
};
|
||||
|
||||
const is: {
|
||||
(name: string): string;
|
||||
// eslint-disable-next-line @typescript-eslint/unified-signatures
|
||||
(name: string, state: boolean | undefined): string;
|
||||
} = (name: string, ...args: [] | [boolean | undefined]) => {
|
||||
const state = args.length > 0 ? args[0] : true;
|
||||
return name && state ? `${statePrefix}${name}` : '';
|
||||
};
|
||||
|
||||
const useNamespace = (block: string) => {
|
||||
const namespace = DEFAULT_NAMESPACE;
|
||||
const b = (blockSuffix = '') => _bem(namespace, block, blockSuffix, '', '');
|
||||
const e = (element?: string) =>
|
||||
element ? _bem(namespace, block, '', element, '') : '';
|
||||
const m = (modifier?: string) =>
|
||||
modifier ? _bem(namespace, block, '', '', modifier) : '';
|
||||
const be = (blockSuffix?: string, element?: string) =>
|
||||
blockSuffix && element
|
||||
? _bem(namespace, block, blockSuffix, element, '')
|
||||
: '';
|
||||
const em = (element?: string, modifier?: string) =>
|
||||
element && modifier ? _bem(namespace, block, '', element, modifier) : '';
|
||||
const bm = (blockSuffix?: string, modifier?: string) =>
|
||||
blockSuffix && modifier
|
||||
? _bem(namespace, block, blockSuffix, '', modifier)
|
||||
: '';
|
||||
const bem = (blockSuffix?: string, element?: string, modifier?: string) =>
|
||||
blockSuffix && element && modifier
|
||||
? _bem(namespace, block, blockSuffix, element, modifier)
|
||||
: '';
|
||||
|
||||
// for css var
|
||||
// --el-xxx: value;
|
||||
const cssVar = (object: Record<string, string>) => {
|
||||
const styles: Record<string, string> = {};
|
||||
for (const key in object) {
|
||||
if (object[key]) {
|
||||
styles[`--${namespace}-${key}`] = object[key];
|
||||
}
|
||||
}
|
||||
return styles;
|
||||
};
|
||||
// with block
|
||||
const cssVarBlock = (object: Record<string, string>) => {
|
||||
const styles: Record<string, string> = {};
|
||||
for (const key in object) {
|
||||
if (object[key]) {
|
||||
styles[`--${namespace}-${block}-${key}`] = object[key];
|
||||
}
|
||||
}
|
||||
return styles;
|
||||
};
|
||||
|
||||
const cssVarName = (name: string) => `--${namespace}-${name}`;
|
||||
const cssVarBlockName = (name: string) => `--${namespace}-${block}-${name}`;
|
||||
|
||||
return {
|
||||
b,
|
||||
be,
|
||||
bem,
|
||||
bm,
|
||||
// css
|
||||
cssVar,
|
||||
cssVarBlock,
|
||||
cssVarBlockName,
|
||||
cssVarName,
|
||||
e,
|
||||
em,
|
||||
is,
|
||||
m,
|
||||
namespace,
|
||||
};
|
||||
};
|
||||
|
||||
type UseNamespaceReturn = ReturnType<typeof useNamespace>;
|
||||
|
||||
export type { UseNamespaceReturn };
|
||||
export { useNamespace };
|
93
packages/@core/composables/src/use-priority-value.ts
Normal file
93
packages/@core/composables/src/use-priority-value.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import type { ComputedRef, Ref } from 'vue';
|
||||
|
||||
import {
|
||||
getFirstNonNullOrUndefined,
|
||||
kebabToCamelCase,
|
||||
} from '@vben-core/shared/utils';
|
||||
import { computed, getCurrentInstance, unref, useAttrs, useSlots } from 'vue';
|
||||
|
||||
/**
|
||||
* 依次从插槽、attrs、props、state 中获取值
|
||||
* @param key
|
||||
* @param props
|
||||
* @param state
|
||||
*/
|
||||
export function usePriorityValue<
|
||||
T extends Record<string, any>,
|
||||
S extends Record<string, any>,
|
||||
K extends keyof T = keyof T,
|
||||
>(key: K, props: T, state: Readonly<Ref<NoInfer<S>>> | undefined) {
|
||||
const instance = getCurrentInstance();
|
||||
const slots = useSlots();
|
||||
const attrs = useAttrs() as T;
|
||||
|
||||
const value = computed((): T[K] => {
|
||||
// props不管有没有传,都会有默认值,会影响这里的顺序,
|
||||
// 通过判断原始props是否有值来判断是否传入
|
||||
const rawProps = (instance?.vnode?.props || {}) as T;
|
||||
|
||||
const standardRawProps = {} as T;
|
||||
|
||||
for (const [key, value] of Object.entries(rawProps)) {
|
||||
standardRawProps[kebabToCamelCase(key) as K] = value;
|
||||
}
|
||||
const propsKey =
|
||||
standardRawProps?.[key] === undefined ? undefined : props[key];
|
||||
|
||||
// slot可以关闭
|
||||
return getFirstNonNullOrUndefined(
|
||||
slots[key as string],
|
||||
attrs[key],
|
||||
propsKey,
|
||||
state?.value?.[key as keyof S],
|
||||
) as T[K];
|
||||
});
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量获取state中的值(每个值都是ref)
|
||||
* @param props
|
||||
* @param state
|
||||
*/
|
||||
export function usePriorityValues<
|
||||
T extends Record<string, any>,
|
||||
S extends Ref<Record<string, any>> = Readonly<Ref<NoInfer<T>, NoInfer<T>>>,
|
||||
>(props: T, state: S | undefined) {
|
||||
const result: { [K in keyof T]: ComputedRef<T[K]> } = {} as never;
|
||||
|
||||
(Object.keys(props) as (keyof T)[]).forEach((key) => {
|
||||
result[key] = usePriorityValue(key as keyof typeof props, props, state);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量获取state中的值(集中在一个computed,用于透传)
|
||||
* @param props
|
||||
* @param state
|
||||
*/
|
||||
export function useForwardPriorityValues<
|
||||
T extends Record<string, any>,
|
||||
S extends Ref<Record<string, any>> = Readonly<Ref<NoInfer<T>, NoInfer<T>>>,
|
||||
>(props: T, state: S | undefined) {
|
||||
const computedResult: { [K in keyof T]: ComputedRef<T[K]> } = {} as never;
|
||||
|
||||
(Object.keys(props) as (keyof T)[]).forEach((key) => {
|
||||
computedResult[key] = usePriorityValue(
|
||||
key as keyof typeof props,
|
||||
props,
|
||||
state,
|
||||
);
|
||||
});
|
||||
|
||||
return computed(() => {
|
||||
const unwrapResult: Record<string, any> = {};
|
||||
Object.keys(props).forEach((key) => {
|
||||
unwrapResult[key] = unref(computedResult[key]);
|
||||
});
|
||||
return unwrapResult as { [K in keyof T]: T[K] };
|
||||
});
|
||||
}
|
54
packages/@core/composables/src/use-scroll-lock.ts
Normal file
54
packages/@core/composables/src/use-scroll-lock.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { getScrollbarWidth, needsScrollbar } from '@vben-core/shared/utils';
|
||||
|
||||
import {
|
||||
useScrollLock as _useScrollLock,
|
||||
tryOnBeforeUnmount,
|
||||
tryOnMounted,
|
||||
} from '@vueuse/core';
|
||||
|
||||
export const SCROLL_FIXED_CLASS = `_scroll__fixed_`;
|
||||
|
||||
export function useScrollLock() {
|
||||
const isLocked = _useScrollLock(document.body);
|
||||
const scrollbarWidth = getScrollbarWidth();
|
||||
|
||||
tryOnMounted(() => {
|
||||
if (!needsScrollbar()) {
|
||||
return;
|
||||
}
|
||||
document.body.style.paddingRight = `${scrollbarWidth}px`;
|
||||
|
||||
const layoutFixedNodes = document.querySelectorAll<HTMLElement>(
|
||||
`.${SCROLL_FIXED_CLASS}`,
|
||||
);
|
||||
const nodes = [...layoutFixedNodes];
|
||||
if (nodes.length > 0) {
|
||||
nodes.forEach((node) => {
|
||||
node.dataset.transition = node.style.transition;
|
||||
node.style.transition = 'none';
|
||||
node.style.paddingRight = `${scrollbarWidth}px`;
|
||||
});
|
||||
}
|
||||
isLocked.value = true;
|
||||
});
|
||||
|
||||
tryOnBeforeUnmount(() => {
|
||||
if (!needsScrollbar()) {
|
||||
return;
|
||||
}
|
||||
isLocked.value = false;
|
||||
const layoutFixedNodes = document.querySelectorAll<HTMLElement>(
|
||||
`.${SCROLL_FIXED_CLASS}`,
|
||||
);
|
||||
const nodes = [...layoutFixedNodes];
|
||||
if (nodes.length > 0) {
|
||||
nodes.forEach((node) => {
|
||||
node.style.paddingRight = '';
|
||||
requestAnimationFrame(() => {
|
||||
node.style.transition = node.dataset.transition || '';
|
||||
});
|
||||
});
|
||||
}
|
||||
document.body.style.paddingRight = '';
|
||||
});
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
# Simple i18n
|
||||
|
||||
Simple i18 implementation
|
26
packages/@core/composables/src/use-simple-locale/index.ts
Normal file
26
packages/@core/composables/src/use-simple-locale/index.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { Locale } from './messages';
|
||||
|
||||
import { createSharedComposable } from '@vueuse/core';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { getMessages } from './messages';
|
||||
|
||||
export const useSimpleLocale = createSharedComposable(() => {
|
||||
const currentLocale = ref<Locale>('zh-CN');
|
||||
|
||||
const setSimpleLocale = (locale: Locale) => {
|
||||
currentLocale.value = locale;
|
||||
};
|
||||
|
||||
const $t = computed(() => {
|
||||
const localeMessages = getMessages(currentLocale.value);
|
||||
return (key: string) => {
|
||||
return localeMessages[key] || key;
|
||||
};
|
||||
});
|
||||
return {
|
||||
$t,
|
||||
currentLocale,
|
||||
setSimpleLocale,
|
||||
};
|
||||
});
|
24
packages/@core/composables/src/use-simple-locale/messages.ts
Normal file
24
packages/@core/composables/src/use-simple-locale/messages.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export type Locale = 'en-US' | 'zh-CN';
|
||||
|
||||
export const messages: Record<Locale, Record<string, string>> = {
|
||||
'en-US': {
|
||||
cancel: 'Cancel',
|
||||
collapse: 'Collapse',
|
||||
confirm: 'Confirm',
|
||||
expand: 'Expand',
|
||||
prompt: 'Prompt',
|
||||
reset: 'Reset',
|
||||
submit: 'Submit',
|
||||
},
|
||||
'zh-CN': {
|
||||
cancel: '取消',
|
||||
collapse: '收起',
|
||||
confirm: '确认',
|
||||
expand: '展开',
|
||||
prompt: '提示',
|
||||
reset: '重置',
|
||||
submit: '提交',
|
||||
},
|
||||
};
|
||||
|
||||
export const getMessages = (locale: Locale) => messages[locale];
|
29
packages/@core/composables/src/use-sortable.ts
Normal file
29
packages/@core/composables/src/use-sortable.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { SortableOptions } from 'sortablejs';
|
||||
import type Sortable from 'sortablejs';
|
||||
|
||||
function useSortable<T extends HTMLElement>(
|
||||
sortableContainer: T,
|
||||
options: SortableOptions = {},
|
||||
) {
|
||||
const initializeSortable = async () => {
|
||||
const Sortable = await import(
|
||||
// @ts-expect-error - This is a dynamic import
|
||||
'sortablejs/modular/sortable.complete.esm.js'
|
||||
);
|
||||
const sortable = Sortable?.default?.create?.(sortableContainer, {
|
||||
animation: 300,
|
||||
delay: 400,
|
||||
delayOnTouchOnly: true,
|
||||
...options,
|
||||
});
|
||||
return sortable as Sortable;
|
||||
};
|
||||
|
||||
return {
|
||||
initializeSortable,
|
||||
};
|
||||
}
|
||||
|
||||
export { useSortable };
|
||||
|
||||
export type { Sortable };
|
Reference in New Issue
Block a user