初始化项目
This commit is contained in:
15
packages/hooks/package.json
Normal file
15
packages/hooks/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "@sa/hooks",
|
||||
"version": "1.0.0",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@sa/axios": "workspace:*"
|
||||
}
|
||||
}
|
||||
10
packages/hooks/src/index.ts
Normal file
10
packages/hooks/src/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import useBoolean from './use-boolean';
|
||||
import useLoading from './use-loading';
|
||||
import useCountDown from './use-count-down';
|
||||
import useContext from './use-context';
|
||||
import useSvgIconRender from './use-svg-icon-render';
|
||||
import useHookTable from './use-table';
|
||||
|
||||
export { useBoolean, useLoading, useCountDown, useContext, useSvgIconRender, useHookTable };
|
||||
|
||||
export * from './use-table';
|
||||
31
packages/hooks/src/use-boolean.ts
Normal file
31
packages/hooks/src/use-boolean.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
/**
|
||||
* Boolean
|
||||
*
|
||||
* @param initValue Init value
|
||||
*/
|
||||
export default function useBoolean(initValue = false) {
|
||||
const bool = ref(initValue);
|
||||
|
||||
function setBool(value: boolean) {
|
||||
bool.value = value;
|
||||
}
|
||||
function setTrue() {
|
||||
setBool(true);
|
||||
}
|
||||
function setFalse() {
|
||||
setBool(false);
|
||||
}
|
||||
function toggle() {
|
||||
setBool(!bool.value);
|
||||
}
|
||||
|
||||
return {
|
||||
bool,
|
||||
setBool,
|
||||
setTrue,
|
||||
setFalse,
|
||||
toggle
|
||||
};
|
||||
}
|
||||
96
packages/hooks/src/use-context.ts
Normal file
96
packages/hooks/src/use-context.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { inject, provide } from 'vue';
|
||||
import type { InjectionKey } from 'vue';
|
||||
|
||||
/**
|
||||
* Use context
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // there are three vue files: A.vue, B.vue, C.vue, and A.vue is the parent component of B.vue and C.vue
|
||||
*
|
||||
* // context.ts
|
||||
* import { ref } from 'vue';
|
||||
* import { useContext } from '@sa/hooks';
|
||||
*
|
||||
* export const { setupStore, useStore } = useContext('demo', () => {
|
||||
* const count = ref(0);
|
||||
*
|
||||
* function increment() {
|
||||
* count.value++;
|
||||
* }
|
||||
*
|
||||
* function decrement() {
|
||||
* count.value--;
|
||||
* }
|
||||
*
|
||||
* return {
|
||||
* count,
|
||||
* increment,
|
||||
* decrement
|
||||
* };
|
||||
* })
|
||||
* ``` // A.vue
|
||||
* ```vue
|
||||
* <template>
|
||||
* <div>A</div>
|
||||
* </template>
|
||||
* <script setup lang="ts">
|
||||
* import { setupStore } from './context';
|
||||
*
|
||||
* setupStore();
|
||||
* // const { increment } = setupStore(); // also can control the store in the parent component
|
||||
* </script>
|
||||
* ``` // B.vue
|
||||
* ```vue
|
||||
* <template>
|
||||
* <div>B</div>
|
||||
* </template>
|
||||
* <script setup lang="ts">
|
||||
* import { useStore } from './context';
|
||||
*
|
||||
* const { count, increment } = useStore();
|
||||
* </script>
|
||||
* ```;
|
||||
*
|
||||
* // C.vue is same as B.vue
|
||||
*
|
||||
* @param contextName Context name
|
||||
* @param fn Context function
|
||||
*/
|
||||
export default function useContext<T extends (...args: any[]) => any>(contextName: string, fn: T) {
|
||||
type Context = ReturnType<T>;
|
||||
|
||||
const { useProvide, useInject: useStore } = createContext<Context>(contextName);
|
||||
|
||||
function setupStore(...args: Parameters<T>) {
|
||||
const context: Context = fn(...args);
|
||||
return useProvide(context);
|
||||
}
|
||||
|
||||
return {
|
||||
/** Setup store in the parent component */
|
||||
setupStore,
|
||||
/** Use store in the child component */
|
||||
useStore
|
||||
};
|
||||
}
|
||||
|
||||
/** Create context */
|
||||
function createContext<T>(contextName: string) {
|
||||
const injectKey: InjectionKey<T> = Symbol(contextName);
|
||||
|
||||
function useProvide(context: T) {
|
||||
provide(injectKey, context);
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
function useInject() {
|
||||
return inject(injectKey) as T;
|
||||
}
|
||||
|
||||
return {
|
||||
useProvide,
|
||||
useInject
|
||||
};
|
||||
}
|
||||
49
packages/hooks/src/use-count-down.ts
Normal file
49
packages/hooks/src/use-count-down.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { computed, onScopeDispose, ref } from 'vue';
|
||||
import { useRafFn } from '@vueuse/core';
|
||||
|
||||
/**
|
||||
* count down
|
||||
*
|
||||
* @param seconds - count down seconds
|
||||
*/
|
||||
export default function useCountDown(seconds: number) {
|
||||
const FPS_PER_SECOND = 60;
|
||||
|
||||
const fps = ref(0);
|
||||
|
||||
const count = computed(() => Math.ceil(fps.value / FPS_PER_SECOND));
|
||||
|
||||
const isCounting = computed(() => fps.value > 0);
|
||||
|
||||
const { pause, resume } = useRafFn(
|
||||
() => {
|
||||
if (fps.value > 0) {
|
||||
fps.value -= 1;
|
||||
} else {
|
||||
pause();
|
||||
}
|
||||
},
|
||||
{ immediate: false }
|
||||
);
|
||||
|
||||
function start(updateSeconds: number = seconds) {
|
||||
fps.value = FPS_PER_SECOND * updateSeconds;
|
||||
resume();
|
||||
}
|
||||
|
||||
function stop() {
|
||||
fps.value = 0;
|
||||
pause();
|
||||
}
|
||||
|
||||
onScopeDispose(() => {
|
||||
pause();
|
||||
});
|
||||
|
||||
return {
|
||||
count,
|
||||
isCounting,
|
||||
start,
|
||||
stop
|
||||
};
|
||||
}
|
||||
16
packages/hooks/src/use-loading.ts
Normal file
16
packages/hooks/src/use-loading.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import useBoolean from './use-boolean';
|
||||
|
||||
/**
|
||||
* Loading
|
||||
*
|
||||
* @param initValue Init value
|
||||
*/
|
||||
export default function useLoading(initValue = false) {
|
||||
const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(initValue);
|
||||
|
||||
return {
|
||||
loading,
|
||||
startLoading,
|
||||
endLoading
|
||||
};
|
||||
}
|
||||
79
packages/hooks/src/use-request.ts
Normal file
79
packages/hooks/src/use-request.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { ref } from 'vue';
|
||||
import type { Ref } from 'vue';
|
||||
import { createFlatRequest } from '@sa/axios';
|
||||
import type {
|
||||
AxiosError,
|
||||
CreateAxiosDefaults,
|
||||
CustomAxiosRequestConfig,
|
||||
MappedType,
|
||||
RequestOption,
|
||||
ResponseType
|
||||
} from '@sa/axios';
|
||||
import useLoading from './use-loading';
|
||||
|
||||
export type HookRequestInstanceResponseSuccessData<T = any> = {
|
||||
data: Ref<T>;
|
||||
error: Ref<null>;
|
||||
};
|
||||
|
||||
export type HookRequestInstanceResponseFailData<ResponseData = any> = {
|
||||
data: Ref<null>;
|
||||
error: Ref<AxiosError<ResponseData>>;
|
||||
};
|
||||
|
||||
export type HookRequestInstanceResponseData<T = any, ResponseData = any> = {
|
||||
loading: Ref<boolean>;
|
||||
} & (HookRequestInstanceResponseSuccessData<T> | HookRequestInstanceResponseFailData<ResponseData>);
|
||||
|
||||
export interface HookRequestInstance<ResponseData = any> {
|
||||
<T = any, R extends ResponseType = 'json'>(
|
||||
config: CustomAxiosRequestConfig
|
||||
): HookRequestInstanceResponseData<MappedType<R, T>, ResponseData>;
|
||||
cancelRequest: (requestId: string) => void;
|
||||
cancelAllRequest: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* create a hook request instance
|
||||
*
|
||||
* @param axiosConfig
|
||||
* @param options
|
||||
*/
|
||||
export default function createHookRequest<ResponseData = any>(
|
||||
axiosConfig?: CreateAxiosDefaults,
|
||||
options?: Partial<RequestOption<ResponseData>>
|
||||
) {
|
||||
const request = createFlatRequest<ResponseData>(axiosConfig, options);
|
||||
|
||||
const hookRequest: HookRequestInstance<ResponseData> = function hookRequest<T = any, R extends ResponseType = 'json'>(
|
||||
config: CustomAxiosRequestConfig
|
||||
) {
|
||||
const { loading, startLoading, endLoading } = useLoading();
|
||||
|
||||
const data = ref<MappedType<R, T> | null>(null) as Ref<MappedType<R, T>>;
|
||||
const error = ref<AxiosError<ResponseData> | null>(null) as Ref<AxiosError<ResponseData> | null>;
|
||||
|
||||
startLoading();
|
||||
|
||||
request(config).then(res => {
|
||||
if (res.data) {
|
||||
data.value = res.data;
|
||||
} else {
|
||||
error.value = res.error;
|
||||
}
|
||||
|
||||
endLoading();
|
||||
});
|
||||
|
||||
return {
|
||||
loading,
|
||||
data,
|
||||
error
|
||||
};
|
||||
} as HookRequestInstance<ResponseData>;
|
||||
|
||||
hookRequest.cancelRequest = request.cancelRequest;
|
||||
hookRequest.cancelAllRequest = request.cancelAllRequest;
|
||||
|
||||
return hookRequest;
|
||||
}
|
||||
50
packages/hooks/src/use-svg-icon-render.ts
Normal file
50
packages/hooks/src/use-svg-icon-render.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { h } from 'vue';
|
||||
import type { Component } from 'vue';
|
||||
|
||||
/**
|
||||
* Svg icon render hook
|
||||
*
|
||||
* @param SvgIcon Svg icon component
|
||||
*/
|
||||
export default function useSvgIconRender(SvgIcon: Component) {
|
||||
interface IconConfig {
|
||||
/** Iconify icon name */
|
||||
icon?: string;
|
||||
/** Local icon name */
|
||||
localIcon?: string;
|
||||
/** Icon color */
|
||||
color?: string;
|
||||
/** Icon size */
|
||||
fontSize?: number;
|
||||
}
|
||||
|
||||
type IconStyle = Partial<Pick<CSSStyleDeclaration, 'color' | 'fontSize'>>;
|
||||
|
||||
/**
|
||||
* Svg icon VNode
|
||||
*
|
||||
* @param config
|
||||
*/
|
||||
const SvgIconVNode = (config: IconConfig) => {
|
||||
const { color, fontSize, icon, localIcon } = config;
|
||||
|
||||
const style: IconStyle = {};
|
||||
|
||||
if (color) {
|
||||
style.color = color;
|
||||
}
|
||||
if (fontSize) {
|
||||
style.fontSize = `${fontSize}px`;
|
||||
}
|
||||
|
||||
if (!icon && !localIcon) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return () => h(SvgIcon, { icon, localIcon, style });
|
||||
};
|
||||
|
||||
return {
|
||||
SvgIconVNode
|
||||
};
|
||||
}
|
||||
152
packages/hooks/src/use-table.ts
Normal file
152
packages/hooks/src/use-table.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import type { Ref } from 'vue';
|
||||
import useBoolean from './use-boolean';
|
||||
import useLoading from './use-loading';
|
||||
|
||||
export type MaybePromise<T> = T | Promise<T>;
|
||||
|
||||
export type ApiFn = (args: any) => Promise<unknown>;
|
||||
|
||||
export type TableColumnCheck = {
|
||||
key: string;
|
||||
title: string;
|
||||
checked: boolean;
|
||||
};
|
||||
|
||||
export type TableDataWithIndex<T> = T & { index: number };
|
||||
|
||||
export type TransformedData<T> = {
|
||||
rows: TableDataWithIndex<T>[];
|
||||
total: number;
|
||||
};
|
||||
|
||||
export type Transformer<T, Response> = (response: Response) => TransformedData<T>;
|
||||
|
||||
export type TableConfig<A extends ApiFn, T, C> = {
|
||||
/** api function to get table data */
|
||||
apiFn: A;
|
||||
/** row key */
|
||||
rowKey?: string;
|
||||
/** api params */
|
||||
apiParams?: Parameters<A>[0];
|
||||
/** transform api response to table data */
|
||||
transformer: Transformer<T, Awaited<ReturnType<A>>>;
|
||||
/** columns factory */
|
||||
columns: () => C[];
|
||||
/**
|
||||
* get column checks
|
||||
*
|
||||
* @param columns
|
||||
*/
|
||||
getColumnChecks: (columns: C[]) => TableColumnCheck[];
|
||||
/**
|
||||
* get columns
|
||||
*
|
||||
* @param columns
|
||||
*/
|
||||
getColumns: (columns: C[], checks: TableColumnCheck[]) => C[];
|
||||
/**
|
||||
* callback when response fetched
|
||||
*
|
||||
* @param transformed transformed data
|
||||
*/
|
||||
onFetched?: (transformed: TransformedData<T>) => MaybePromise<void>;
|
||||
/**
|
||||
* whether to get data immediately
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
immediate?: boolean;
|
||||
};
|
||||
|
||||
export default function useHookTable<A extends ApiFn, T, C>(config: TableConfig<A, T, C>) {
|
||||
const { loading, startLoading, endLoading } = useLoading();
|
||||
const { bool: empty, setBool: setEmpty } = useBoolean();
|
||||
|
||||
const { apiFn, apiParams, transformer, immediate = true, getColumnChecks, getColumns } = config;
|
||||
|
||||
const searchParams: NonNullable<Parameters<A>[0]> = reactive({ ...apiParams });
|
||||
|
||||
const allColumns = ref(config.columns()) as Ref<C[]>;
|
||||
|
||||
const data: Ref<T[]> = ref([]);
|
||||
|
||||
const columnChecks: Ref<TableColumnCheck[]> = ref(getColumnChecks(config.columns()));
|
||||
|
||||
const columns = computed(() => getColumns(allColumns.value, columnChecks.value));
|
||||
|
||||
function reloadColumns() {
|
||||
allColumns.value = config.columns();
|
||||
|
||||
const checkMap = new Map(columnChecks.value.map(col => [col.key, col.checked]));
|
||||
|
||||
const defaultChecks = getColumnChecks(allColumns.value);
|
||||
|
||||
columnChecks.value = defaultChecks.map(col => ({
|
||||
...col,
|
||||
checked: checkMap.get(col.key) ?? col.checked
|
||||
}));
|
||||
}
|
||||
|
||||
async function getData() {
|
||||
startLoading();
|
||||
|
||||
const formattedParams = formatSearchParams(searchParams);
|
||||
|
||||
const response = await apiFn(formattedParams);
|
||||
|
||||
const transformed = transformer(response as Awaited<ReturnType<A>>);
|
||||
|
||||
data.value = transformed.rows;
|
||||
|
||||
setEmpty(transformed.rows.length === 0);
|
||||
|
||||
await config.onFetched?.(transformed);
|
||||
|
||||
endLoading();
|
||||
}
|
||||
|
||||
function formatSearchParams(params: Record<string, unknown>) {
|
||||
const formattedParams: Record<string, unknown> = {};
|
||||
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value !== null && value !== undefined) {
|
||||
formattedParams[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
return formattedParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* update search params
|
||||
*
|
||||
* @param params
|
||||
*/
|
||||
function updateSearchParams(params: Partial<Parameters<A>[0]>) {
|
||||
Object.assign(searchParams, params);
|
||||
}
|
||||
|
||||
/** reset search params */
|
||||
function resetSearchParams() {
|
||||
Object.assign(searchParams, apiParams);
|
||||
getData();
|
||||
}
|
||||
|
||||
if (immediate) {
|
||||
getData();
|
||||
}
|
||||
|
||||
return {
|
||||
loading,
|
||||
empty,
|
||||
data,
|
||||
columns,
|
||||
columnChecks,
|
||||
reloadColumns,
|
||||
getData,
|
||||
searchParams,
|
||||
updateSearchParams,
|
||||
resetSearchParams
|
||||
};
|
||||
}
|
||||
20
packages/hooks/tsconfig.json
Normal file
20
packages/hooks/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"jsx": "preserve",
|
||||
"lib": ["DOM", "ESNext"],
|
||||
"baseUrl": ".",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"types": ["node"],
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"noUnusedLocals": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user