first commit

This commit is contained in:
2024-04-08 21:34:02 +08:00
commit a41a7f32ab
223 changed files with 44629 additions and 0 deletions

4
src/hooks/app/index.ts Normal file
View 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
View 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
View 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)
})()
}

View 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
View 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
View 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'

View 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 }
}

View 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 }
)
}

View 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 }
}

View 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 }
}

View 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 }
}

View 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
}
}

View 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
}
}

View 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 }
}

View 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 }
}