mirror of
https://github.com/certd/certd.git
synced 2026-07-08 21:57:36 +08:00
🔱: [client] sync upgrade with 7 commits [trident-sync]
chore: Merge branch 'vben' # Conflicts: # package.json perf: antdv示例改成使用vben框架 chore: vben chore: vben chore: vben
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,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
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";
|
||||
@@ -0,0 +1,7 @@
|
||||
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core";
|
||||
|
||||
export function useIsMobile() {
|
||||
const breakpoints = useBreakpoints(breakpointsTailwind);
|
||||
const isMobile = breakpoints.smaller("md");
|
||||
return { isMobile };
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import type { CSSProperties } from "vue";
|
||||
|
||||
import type { VisibleDomRect } from "/@/vben/shared/utils";
|
||||
|
||||
import { computed, onMounted, onUnmounted, ref } 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/shared/constants";
|
||||
import { getElementVisibleRect } from "/@/vben/shared/utils";
|
||||
|
||||
import { useCssVar, useDebounceFn } from "@vueuse/core";
|
||||
|
||||
/**
|
||||
* @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`;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { DEFAULT_NAMESPACE } from "../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 };
|
||||
@@ -0,0 +1,71 @@
|
||||
import type { ComputedRef, Ref } from "vue";
|
||||
|
||||
import { computed, getCurrentInstance, unref, useAttrs, useSlots } from "vue";
|
||||
|
||||
import { getFirstNonNullOrUndefined, kebabToCamelCase } from "../shared/utils";
|
||||
|
||||
/**
|
||||
* 依次从插槽、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] };
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { getScrollbarWidth, needsScrollbar } from "../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
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { Locale } from "./messages";
|
||||
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
import { createSharedComposable } from "@vueuse/core";
|
||||
|
||||
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
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
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",
|
||||
reset: "Reset",
|
||||
submit: "Submit"
|
||||
},
|
||||
"zh-CN": {
|
||||
cancel: "取消",
|
||||
collapse: "收起",
|
||||
confirm: "确认",
|
||||
expand: "展开",
|
||||
reset: "重置",
|
||||
submit: "提交"
|
||||
}
|
||||
};
|
||||
|
||||
export const getMessages = (locale: Locale) => messages[locale];
|
||||
@@ -0,0 +1,26 @@
|
||||
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