mirror of
				https://github.com/continew-org/continew-admin-ui.git
				synced 2025-10-31 22:57:15 +08:00 
			
		
		
		
	first commit
This commit is contained in:
		
							
								
								
									
										4
									
								
								src/hooks/app/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/hooks/app/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| export * from './useDept' | ||||
| export * from './useRole' | ||||
| export * from './useDict' | ||||
| export * from './useFormCurd' | ||||
							
								
								
									
										21
									
								
								src/hooks/app/useDept.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/hooks/app/useDept.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| import { ref } from 'vue' | ||||
| import { listDeptTree } from '@/apis' | ||||
| import type { TreeNodeData } from '@arco-design/web-vue' | ||||
|  | ||||
| /** 部门模块 */ | ||||
| export function useDept(options?: { onSuccess?: () => void }) { | ||||
|   const loading = ref(false) | ||||
|   const deptList = ref<TreeNodeData[]>([]) | ||||
|  | ||||
|   const getDeptList = async (name?: string) => { | ||||
|     try { | ||||
|       loading.value = true | ||||
|       const res = await listDeptTree({ description: name }) | ||||
|       deptList.value = res.data | ||||
|       options?.onSuccess && options.onSuccess() | ||||
|     } finally { | ||||
|       loading.value = false | ||||
|     } | ||||
|   } | ||||
|   return { deptList, getDeptList, loading } | ||||
| } | ||||
							
								
								
									
										23
									
								
								src/hooks/app/useDict.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/hooks/app/useDict.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| import { ref, toRefs } from 'vue' | ||||
| import { listCommonDict } from '@/apis' | ||||
| import { useDictStore } from '@/stores' | ||||
|  | ||||
| export function useDict(...codes: Array<string>) { | ||||
|   const res = ref<any>({}) | ||||
|   return (() => { | ||||
|     const dictStore = useDictStore() | ||||
|     codes.forEach((code) => { | ||||
|       res.value[code] = [] | ||||
|       const dict = dictStore.getDict(code) | ||||
|       if (dict) { | ||||
|         res.value[code] = dict | ||||
|       } else { | ||||
|         listCommonDict(code).then((resp) => { | ||||
|           res.value[code] = resp.data | ||||
|           dictStore.setDict(code, res.value[code]) | ||||
|         }) | ||||
|       } | ||||
|     }) | ||||
|     return toRefs(res.value) | ||||
|   })() | ||||
| } | ||||
							
								
								
									
										107
									
								
								src/hooks/app/useFormCurd.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								src/hooks/app/useFormCurd.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,107 @@ | ||||
| import { reactive, computed, ref, type Ref } from 'vue' | ||||
| import { useRoute, useRouter } from 'vue-router' | ||||
| import { Modal, Message, type FormInstance } from '@arco-design/web-vue' | ||||
| import { isEqual } from 'lodash' | ||||
|  | ||||
| type Option<T> = { | ||||
|   key?: string | ||||
|   formRef?: Ref<FormInstance> | ||||
|   initApi: () => Promise<ApiRes<T>> | ||||
|   detailApi: (form: T) => Promise<ApiRes<T>> | ||||
|   addApi: (form: T) => Promise<ApiRes<T>> | ||||
|   editApi: (form: T) => Promise<ApiRes<T>> | ||||
|   onError?: (error: any) => void | ||||
|   onSuccess?: (result: T) => void | ||||
|   addToEdit?: boolean // 新增成功调到编辑 | ||||
| } | ||||
|  | ||||
| export function useFormCurd<T = any>(option: Option<T>) { | ||||
|   const route = useRoute() | ||||
|   const router = useRouter() | ||||
|  | ||||
|   const form = reactive({}) | ||||
|   const originForm = reactive({}) // 原始表单数据 | ||||
|   const isEdit = computed(() => !!route.query[option?.key || 'id']) | ||||
|   const isChanged = ref(false) // 表单的数据是否改变过 | ||||
|   const loading = ref(false) | ||||
|   const saveLoading = ref(false) // 保存按钮的加载状态 | ||||
|   const title = computed(() => (isEdit.value ? '编辑' : '新增')) | ||||
|  | ||||
|   const initForm = async () => { | ||||
|     try { | ||||
|       loading.value = true | ||||
|       const res = isEdit.value ? await option.detailApi(form as T) : await option.initApi() | ||||
|       if (res.success) { | ||||
|         Object.assign(form, res.data) | ||||
|         Object.assign(originForm, res.data) | ||||
|         isChanged.value = false | ||||
|       } | ||||
|     } catch (error) { | ||||
|       option.onError && option.onError(error) | ||||
|     } finally { | ||||
|       loading.value = false | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   initForm() | ||||
|  | ||||
|   watch( | ||||
|     () => route.query, | ||||
|     () => { | ||||
|       initForm() | ||||
|     } | ||||
|   ) | ||||
|  | ||||
|   watch( | ||||
|     () => form, | ||||
|     (newVal) => { | ||||
|       // console.log('newVal', toRaw(newVal)) | ||||
|       // console.log('originForm', toRaw(originForm)) | ||||
|       if (!isEqual(newVal, originForm)) { | ||||
|         isChanged.value = true | ||||
|       } | ||||
|     }, | ||||
|     { immediate: true, deep: true } | ||||
|   ) | ||||
|  | ||||
|   const save = async () => { | ||||
|     try { | ||||
|       const valid = await option?.formRef?.value?.validate() | ||||
|       if (valid) return | ||||
|       saveLoading.value = true | ||||
|       const res = isEdit.value ? await option.editApi(form as T) : await option.addApi(form as T) | ||||
|       if (res.success) { | ||||
|         Message.success(isEdit.value ? '修改成功' : '新增成功') | ||||
|         if (!isEdit.value && option.addToEdit === true) { | ||||
|           router.replace({ path: route.fullPath, query: { [option.key as string]: res.data[option.key as string] } }) | ||||
|         } | ||||
|         option.onSuccess && option.onSuccess(res.data) | ||||
|       } | ||||
|     } catch (error) { | ||||
|       option.onError && option.onError(error) | ||||
|     } finally { | ||||
|       saveLoading.value = false | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   const back = () => { | ||||
|     if (isChanged.value) { | ||||
|       Modal.warning({ | ||||
|         title: '提示', | ||||
|         content: '您确定丢弃更改的内容吗?', | ||||
|         hideCancel: false, | ||||
|         onOk: () => { | ||||
|           router.back() | ||||
|         } | ||||
|       }) | ||||
|     } else { | ||||
|       router.back() | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   const reset = () => { | ||||
|     option?.formRef?.value?.resetFields() | ||||
|   } | ||||
|  | ||||
|   return { form: form as T, title, loading, isEdit, back, save, saveLoading, reset } | ||||
| } | ||||
							
								
								
									
										21
									
								
								src/hooks/app/useRole.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/hooks/app/useRole.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| import { ref } from 'vue' | ||||
| import { listRoleDict } from '@/apis' | ||||
| import type { LabelValueState } from '@/types/global' | ||||
|  | ||||
| /** 角色模块 */ | ||||
| export function useRole(options?: { onSuccess?: () => void }) { | ||||
|   const loading = ref(false) | ||||
|   const roleList = ref<LabelValueState[]>([]) | ||||
|  | ||||
|   const getRoleList = async () => { | ||||
|     try { | ||||
|       loading.value = true | ||||
|       const res = await listRoleDict() | ||||
|       roleList.value = res.data | ||||
|       options?.onSuccess && options.onSuccess() | ||||
|     } finally { | ||||
|       loading.value = false | ||||
|     } | ||||
|   } | ||||
|   return { roleList, getRoleList, loading } | ||||
| } | ||||
							
								
								
									
										9
									
								
								src/hooks/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/hooks/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| export * from './modules/useLoading' | ||||
| export * from './modules/usePagination' | ||||
| export * from './modules/useRequest' | ||||
| export * from './modules/useChart' | ||||
| export * from './modules/useTable' | ||||
| export * from './modules/useForm' | ||||
| export * from './modules/useDevice' | ||||
| export * from './modules/useBreakpoint' | ||||
| export * from './modules/useBreakpointIndex' | ||||
							
								
								
									
										24
									
								
								src/hooks/modules/useBreakpoint.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/hooks/modules/useBreakpoint.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| import { computed, type ComputedRef } from 'vue' | ||||
| import { useBreakpoints } from '@vueuse/core' | ||||
| import type { ColProps } from '@arco-design/web-vue' | ||||
|  | ||||
| type ColBreakpoint = Pick<ColProps, 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl'> | ||||
| type Breakpoint = keyof ColBreakpoint | ||||
|  | ||||
| export function useBreakpoint() { | ||||
|   const breakpoints = useBreakpoints({ | ||||
|     xs: 576, // <576 | ||||
|     sm: 576, // >= 576 | ||||
|     md: 768, // >=768 | ||||
|     lg: 992, // >=992 | ||||
|     xl: 1200, // >=1200 | ||||
|     xxl: 1600 // >=1600 | ||||
|   }) | ||||
|  | ||||
|   const arr = breakpoints.current() as ComputedRef<Breakpoint[]> | ||||
|   const breakpoint = computed(() => { | ||||
|     return arr.value[arr.value.length - 1] || 'xs' | ||||
|   }) | ||||
|  | ||||
|   return { breakpoint } | ||||
| } | ||||
							
								
								
									
										24
									
								
								src/hooks/modules/useBreakpointIndex.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/hooks/modules/useBreakpointIndex.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| import { useBreakpoint } from '@/hooks' | ||||
|  | ||||
| type BreakpointMap = { | ||||
|   xs: number | ||||
|   sm: number | ||||
|   md: number | ||||
|   lg: number | ||||
|   xl: number | ||||
|   xxl: number | ||||
| } | ||||
|  | ||||
| export function useBreakpointIndex(callback: (v: number) => void, breakpointObj?: BreakpointMap) { | ||||
|   const { breakpoint } = useBreakpoint() | ||||
|  | ||||
|   watch( | ||||
|     () => breakpoint.value, | ||||
|     (v) => { | ||||
|       const def = { xs: 0, sm: 0, md: 0, lg: 1, xl: 1, xxl: 2 } | ||||
|       const obj = breakpointObj ? breakpointObj : def | ||||
|       callback(obj[v as keyof typeof obj]) | ||||
|     }, | ||||
|     { immediate: true } | ||||
|   ) | ||||
| } | ||||
							
								
								
									
										26
									
								
								src/hooks/modules/useChart.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/hooks/modules/useChart.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| import { computed } from 'vue' | ||||
| import type { EChartsOption } from 'echarts' | ||||
| import { useAppStore } from '@/stores' | ||||
|  | ||||
| // 获取代码提示 | ||||
| // 从'echarts'中导入{ SeriesOption }; | ||||
| // 因为配置项太多,这提供了一个相对方便的代码提示。 | ||||
| // 当使用vue时,注意反应性问题。需要保证对应的函数可以被触发,TypeScript不会报错,代码编写方便。 | ||||
|  | ||||
| interface optionsFn { | ||||
|   (isDark: boolean): EChartsOption | ||||
| } | ||||
|  | ||||
| export function useChart(sourceOption: optionsFn) { | ||||
|   const appStore = useAppStore() | ||||
|   const isDark = computed(() => appStore.theme === 'dark') | ||||
|  | ||||
|   // echarts support https://echarts.apache.org/zh/theme-builder.html | ||||
|   // 这里不使用 | ||||
|   // TODO 图表主题 | ||||
|   const option = computed<EChartsOption>(() => { | ||||
|     return sourceOption(isDark.value) | ||||
|   }) | ||||
|  | ||||
|   return { option } | ||||
| } | ||||
							
								
								
									
										17
									
								
								src/hooks/modules/useDevice.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/hooks/modules/useDevice.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| import { computed } from 'vue' | ||||
| import { useWindowSize } from '@vueuse/core' | ||||
|  | ||||
| /** | ||||
|  * 响应式布局容器固定宽度 | ||||
|  * | ||||
|  * 大屏(>=1200px) | ||||
|  * 中屏(>=992px) | ||||
|  * 小屏(>=768px) | ||||
|  */ | ||||
| export function useDevice() { | ||||
|   const { width } = useWindowSize() | ||||
|   const isDesktop = computed(() => width.value > 768) | ||||
|   const isMobile = computed(() => !isDesktop.value) | ||||
|  | ||||
|   return { isMobile, isDesktop } | ||||
| } | ||||
							
								
								
									
										17
									
								
								src/hooks/modules/useForm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/hooks/modules/useForm.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| import { reactive } from 'vue' | ||||
| import _ from 'lodash' | ||||
|  | ||||
| export function useForm<F extends object>(initValue: F) { | ||||
|   const getInitValue = () => _.cloneDeep(initValue) | ||||
|  | ||||
|   const form = reactive(getInitValue()) | ||||
|  | ||||
|   const resetForm = () => { | ||||
|     for (const key in form) { | ||||
|       delete form[key] | ||||
|     } | ||||
|     Object.assign(form, getInitValue()) | ||||
|   } | ||||
|  | ||||
|   return { form, resetForm } | ||||
| } | ||||
							
								
								
									
										19
									
								
								src/hooks/modules/useLoading.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/hooks/modules/useLoading.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| import { ref } from 'vue' | ||||
|  | ||||
| export function useLoading(initValue = false) { | ||||
|   const loading = ref(initValue) | ||||
|  | ||||
|   const setLoading = (value: boolean) => { | ||||
|     loading.value = value | ||||
|   } | ||||
|  | ||||
|   const toggle = () => { | ||||
|     loading.value = !loading.value | ||||
|   } | ||||
|  | ||||
|   return { | ||||
|     loading, | ||||
|     setLoading, | ||||
|     toggle | ||||
|   } | ||||
| } | ||||
							
								
								
									
										57
									
								
								src/hooks/modules/usePagination.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								src/hooks/modules/usePagination.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| import { reactive, toRefs, watch } from 'vue' | ||||
| import { useBreakpoint } from '@/hooks' | ||||
|  | ||||
| type Callback = () => void | ||||
|  | ||||
| type Options = { | ||||
|   defaultPageSize: number | ||||
| } | ||||
|  | ||||
| export function usePagination(callback: Callback, options: Options = { defaultPageSize: 10 }) { | ||||
|   const { breakpoint } = useBreakpoint() | ||||
|  | ||||
|   const pagination = reactive({ | ||||
|     showPageSize: true, | ||||
|     showTotal: true, | ||||
|     current: 1, | ||||
|     pageSize: options.defaultPageSize, | ||||
|     total: 0, | ||||
|     simple: false, | ||||
|     onChange: (size: number) => { | ||||
|       pagination.current = size | ||||
|       callback && callback() | ||||
|     }, | ||||
|     onPageSizeChange: (size: number) => { | ||||
|       pagination.current = 1 | ||||
|       pagination.pageSize = size | ||||
|       callback && callback() | ||||
|     } | ||||
|   }) | ||||
|  | ||||
|   watch( | ||||
|     () => breakpoint.value, | ||||
|     () => { | ||||
|       pagination.simple = ['xs'].includes(breakpoint.value) | ||||
|       pagination.showTotal = !['xs'].includes(breakpoint.value) | ||||
|     }, | ||||
|     { immediate: true } | ||||
|   ) | ||||
|  | ||||
|   const changeCurrent = pagination.onChange | ||||
|   const changePageSize = pagination.onPageSizeChange | ||||
|   function setTotal(value: number) { | ||||
|     pagination.total = value | ||||
|   } | ||||
|  | ||||
|   const { current, pageSize, total } = toRefs(pagination) | ||||
|  | ||||
|   return { | ||||
|     current, | ||||
|     pageSize, | ||||
|     total, | ||||
|     pagination, | ||||
|     changeCurrent, | ||||
|     changePageSize, | ||||
|     setTotal | ||||
|   } | ||||
| } | ||||
							
								
								
									
										20
									
								
								src/hooks/modules/useRequest.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/hooks/modules/useRequest.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| import { ref, type UnwrapRef } from 'vue' | ||||
| import type { AxiosResponse } from 'axios' | ||||
| import { useLoading } from '@/hooks' | ||||
|  | ||||
| export function useRequest<T>( | ||||
|   api: () => Promise<AxiosResponse<ApiRes<T>>>, | ||||
|   defaultValue = [] as unknown as T, | ||||
|   isLoading = true | ||||
| ) { | ||||
|   const { loading, setLoading } = useLoading(isLoading) | ||||
|   const response = ref<T>(defaultValue) | ||||
|   api() | ||||
|     .then((res) => { | ||||
|       response.value = res.data as unknown as UnwrapRef<T> | ||||
|     }) | ||||
|     .finally(() => { | ||||
|       setLoading(false) | ||||
|     }) | ||||
|   return { loading, response } | ||||
| } | ||||
							
								
								
									
										88
									
								
								src/hooks/modules/useTable.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								src/hooks/modules/useTable.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,88 @@ | ||||
| import type { TableInstance, TableData } from '@arco-design/web-vue' | ||||
| import { Modal, Message } from '@arco-design/web-vue' | ||||
| import { usePagination } from '@/hooks' | ||||
|  | ||||
| interface Options<T> { | ||||
|   formatResult?: (data: T[]) => any | ||||
|   onSuccess?: () => void | ||||
|   immediate?: boolean | ||||
|   rowKey?: keyof T | ||||
| } | ||||
|  | ||||
| type PaginationParams = { page: number; size: number } | ||||
| type Api<T> = (params: PaginationParams) => Promise<ApiRes<PageRes<T[]>>> | ||||
|  | ||||
| export function useTable<T>(api: Api<T>, options?: Options<T>) { | ||||
|   const { formatResult, onSuccess, immediate, rowKey } = options || {} | ||||
|   const { pagination, setTotal } = usePagination(() => getTableData()) | ||||
|   const loading = ref(false) | ||||
|   const tableData = ref<T[]>([]) | ||||
|  | ||||
|   const getTableData = async () => { | ||||
|     try { | ||||
|       loading.value = true | ||||
|       const res = await api({ page: pagination.current, size: pagination.pageSize }) | ||||
|       tableData.value = formatResult ? formatResult(res.data.list) : res.data.list | ||||
|       setTotal(res.data.total) | ||||
|       onSuccess && onSuccess() | ||||
|     } finally { | ||||
|       loading.value = false | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // 是否立即出发 | ||||
|   const isImmediate = immediate ?? true | ||||
|   isImmediate && getTableData() | ||||
|  | ||||
|   // 查询 | ||||
|   const search = () => { | ||||
|     selectedKeys.value = [] | ||||
|     pagination.onChange(1) | ||||
|   } | ||||
|  | ||||
|   // 多选 | ||||
|   const selectedKeys = ref<(string | number)[]>([]) | ||||
|   const select: TableInstance['onSelect'] = (rowKeys) => { | ||||
|     selectedKeys.value = rowKeys | ||||
|   } | ||||
|  | ||||
|   // 全选 | ||||
|   const selectAll: TableInstance['onSelectAll'] = (checked) => { | ||||
|     const key = rowKey ?? 'id' | ||||
|     const arr = (tableData.value as TableData[]).filter((i) => !(i?.disabled ?? false)) | ||||
|     selectedKeys.value = checked ? arr.map((i) => i[key as string]) : [] | ||||
|   } | ||||
|  | ||||
|   // 删除 | ||||
|   const handleDelete = async <T>( | ||||
|     deleteApi: () => Promise<ApiRes<T>>, | ||||
|     options?: { title?: string; content?: string; successTip?: string; showModal?: boolean } | ||||
|   ): Promise<boolean | undefined> => { | ||||
|     const onDelete = async () => { | ||||
|       try { | ||||
|         const res = await deleteApi() | ||||
|         if (res.success) { | ||||
|           Message.success(options?.successTip || '删除成功') | ||||
|           selectedKeys.value = [] | ||||
|           getTableData() | ||||
|         } | ||||
|         return true | ||||
|       } catch (error) { | ||||
|         return true | ||||
|       } | ||||
|     } | ||||
|     const flag = options?.showModal ?? true // 是否显示对话框 | ||||
|     if (!flag) { | ||||
|       return onDelete() | ||||
|     } | ||||
|     Modal.warning({ | ||||
|       title: options?.title || '提示', | ||||
|       content: options?.content || '是否确定删除该条数据?', | ||||
|       hideCancel: false, | ||||
|       maskClosable: false, | ||||
|       onBeforeOk: onDelete | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   return { loading, tableData, getTableData, search, pagination, selectedKeys, select, selectAll, handleDelete } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user