diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 5982179..952675a 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -25,6 +25,8 @@ module.exports = { rules: { 'vue/multi-word-component-names': 'off', // 禁用vue文件强制多个单词命名 '@typescript-eslint/no-explicit-any': 'off', //允许使用any + 'no-unused-vars': 'off', // 禁用未使用变量的检查 + '@typescript-eslint/no-unused-vars': 'off', // 禁用 TypeScript 中未使用变量的检查 '@typescript-eslint/no-this-alias': [ 'error', { diff --git a/package.json b/package.json index ae2acc9..6796b91 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@vueuse/core": "^12.7.0", "axios": "^1.8.1", "element-plus": "^2.9.5", + "lodash": "^4.17.21", "lodash-es": "^4.17.21", "mockjs": "^1.1.0", "moment": "^2.30.1", @@ -48,6 +49,7 @@ "devDependencies": { "@commitlint/cli": "19.2.0", "@commitlint/config-conventional": "19.1.0", + "@types/lodash-es": "^4.17.12", "@types/mockjs": "^1.0.10", "@types/node": "^22.13.8", "@typescript-eslint/eslint-plugin": "^6.19.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6fa8bf2..76f2def 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: element-plus: specifier: ^2.9.5 version: 2.9.5(vue@3.5.13(typescript@5.7.3)) + lodash: + specifier: ^4.17.21 + version: 4.17.21 lodash-es: specifier: ^4.17.21 version: 4.17.21 @@ -57,6 +60,9 @@ importers: '@commitlint/config-conventional': specifier: 19.1.0 version: 19.1.0 + '@types/lodash-es': + specifier: ^4.17.12 + version: 4.17.12 '@types/mockjs': specifier: ^1.0.10 version: 1.0.10 diff --git a/src/api/index.ts b/src/api/index.ts index 9ced41d..5c6475d 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,5 +1,5 @@ const api = {} -const apiFiles = import.meta.glob('./modules/*.js', { eager: true }) +const apiFiles = import.meta.glob('./modules/*.ts', { eager: true }) Object.entries(apiFiles).forEach(([path, module]) => { const fileName = (path.match(/\/(\w+)\./) as RegExpMatchArray)[1] diff --git a/src/components/Column/index.vue b/src/components/Column/index.vue index fdea395..0f5787f 100644 --- a/src/components/Column/index.vue +++ b/src/components/Column/index.vue @@ -37,7 +37,7 @@ withDefaults( scrollbar: boolean }>(), { - scrollbar: false, + scrollbar: true, }, ) diff --git a/src/components/List/index.vue b/src/components/List/index.vue new file mode 100644 index 0000000..24552d7 --- /dev/null +++ b/src/components/List/index.vue @@ -0,0 +1,133 @@ + + + + + diff --git a/src/hooks/index.ts b/src/hooks/index.ts index e69de29..8c3a9d6 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -0,0 +1,3 @@ +export * from './modules/useCurrentInstance.ts' +export * from './modules/useLoadMore.ts' +export * from './modules/usePageList.ts' diff --git a/src/hooks/modules/useCurrentInstance.ts b/src/hooks/modules/useCurrentInstance.ts new file mode 100644 index 0000000..2db28b5 --- /dev/null +++ b/src/hooks/modules/useCurrentInstance.ts @@ -0,0 +1,47 @@ +/* + * @description: 当前组件实例hooks,用以提供常用工具等 + */ + +import type { ComponentInternalInstance, ComponentPublicInstance } from 'vue' +import { getCurrentInstance } from 'vue' +import { useRoute, useRouter } from 'vue-router' + +// 定义 proxy 上的自定义属性类型 +interface CustomProperties { + $api: any + $HTTP: any + $HTTP_CODE: any + $dict: any + $is: any + $date: any +} + +export const useCurrentInstance = () => { + const router = useRouter() + const route = useRoute() + + const currentInstance = getCurrentInstance() + + if (!currentInstance) { + throw new Error('useCurrentInstance must be called within a setup function') + } + + const { proxy } = currentInstance as ComponentInternalInstance & { proxy: ComponentPublicInstance & CustomProperties } + + const { $api, $HTTP, $HTTP_CODE, $dict, $is, $date } = proxy + + // console.log('useCurrentInstance', currentInstance, proxy, $api, $HTTP, $HTTP_CODE, $dict, $is, $date) + + return { + router, + route, + currentInstance, + proxy, + $api, + $HTTP, + $HTTP_CODE, + $dict, + $is, + $date, + } +} diff --git a/src/hooks/modules/useLoadMore.ts b/src/hooks/modules/useLoadMore.ts new file mode 100644 index 0000000..25645a8 --- /dev/null +++ b/src/hooks/modules/useLoadMore.ts @@ -0,0 +1,67 @@ +/** + * @description: 加载更多 + */ +import { debounce } from 'lodash-es' +import { onActivated, onBeforeUnmount, onDeactivated } from 'vue' + +interface UseLoadMoreOptions { + type?: 'top' | 'bottom' | 'both' + scrollTopCallback?: () => void + scrollBottomCallback?: () => void + container?: HTMLElement + distance?: number +} + +export const useLoadMore = (options: UseLoadMoreOptions = {}) => { + const { + type = 'both', + scrollTopCallback, + scrollBottomCallback, + container = document.documentElement, + distance = 0, + } = options + + // 判断滚动条是否滚动到顶部 + const isScrollTop = (distance: number = 0) => { + const scrollTop = container.scrollTop + return scrollTop - distance <= 0 + } + + // 判断滚动条是否滚动到底部 + const isScrollBottom = (distance: number = 0) => { + const scrollHeight = container.scrollHeight + const scrollTop = container.scrollTop + const clientHeight = container.clientHeight + return scrollHeight - distance <= scrollTop + clientHeight + } + + // 滚动监听回调 + const handleScroll = debounce(() => { + if (['top', 'both'].includes(type) && isScrollTop(distance)) { + scrollTopCallback?.() + } + + if (['bottom', 'both'].includes(type) && isScrollBottom(distance)) { + scrollBottomCallback?.() + } + }, 300) + + container.addEventListener('scroll', handleScroll) + + onBeforeUnmount(() => { + container.removeEventListener('scroll', handleScroll) + }) + + onActivated(() => { + container.addEventListener('scroll', handleScroll) + }) + + onDeactivated(() => { + container.removeEventListener('scroll', handleScroll) + }) + + return { + isScrollTop, + isScrollBottom, + } +} diff --git a/src/hooks/modules/usePageList.ts b/src/hooks/modules/usePageList.ts new file mode 100644 index 0000000..a17df3b --- /dev/null +++ b/src/hooks/modules/usePageList.ts @@ -0,0 +1,151 @@ +/** + * @description: 页面列表 + * */ + +import { HTTP_CODE } from '@/settings/config/http' +import { isFunction } from '@/utils/helper/is' +import { ElMessage } from 'element-plus' +import type { Ref } from 'vue' +import { computed, ref } from 'vue' + +interface PageListOptions { + getPageListApi: (params: any) => Promise + searchParams: Ref<{ [key: string]: any }> + dataAppend?: 'start' | 'end' +} + +interface PageListResult { + list: Ref + page: Ref + limit: Ref + count: Ref + loadding: Ref + getPageList: () => Promise + initData: () => void + refreshing: Ref + refreshPageList: () => Promise + handleRefresh: () => Promise + loadMore: () => Promise + isNoMore: Ref + isEmpty: Ref +} + +export const usePageList = (options: PageListOptions): PageListResult => { + const { getPageListApi, searchParams, dataAppend = 'end' } = options + + const appendData = (rows: T[]) => { + if (dataAppend === 'start') { + list.value = [...rows, ...list.value] + } else if (dataAppend === 'end') { + list.value = [...list.value, ...rows] + } + } + + if (!isFunction(getPageListApi)) { + throw new Error(`getPageListApi: ${getPageListApi},需要为函数`) + } + + const originData = { + list: [] as T[], + page: 1, + limit: 10, + count: 0, + } + + const initData = () => { + list.value = [...originData.list] + page.value = originData.page + limit.value = originData.limit + count.value = originData.count + } + + const list = ref([...originData.list]) as Ref + const page = ref(originData.page) as Ref + const limit = ref(originData.limit) as Ref + const count = ref(originData.count) as Ref + const loadding = ref(false) as Ref + + const getPageList = async () => { + loadding.value = true + const params = { + ...searchParams.value, + page: page.value, + limit: limit.value, + } + + const res = await getPageListApi({ params }) + .catch(() => { + ElMessage({ + type: 'error', + message: '获取待办列表失败', + duration: 3000, + }) + }) + .finally(() => { + loadding.value = false + }) + + if (res && res.data) { + const { code, data, message } = res.data + const { count: total, rows } = data + + if (code === HTTP_CODE.HTTP_SUCCESS_CODE) { + if (rows.length) { + appendData(rows) + page.value = page.value + 1 + count.value = total + } + } else { + ElMessage({ + type: 'error', + message, + duration: 3000, + }) + } + } + } + + const refreshing = ref(false) as Ref + const refreshPageList = async () => { + initData() + await getPageList() + } + + const handleRefresh = async () => { + refreshing.value = true + await refreshPageList().finally(() => { + refreshing.value = false + }) + } + + const loadMore = async () => { + if (isNoMore.value) { + return + } + await getPageList() + } + + const isNoMore = computed(() => { + return list.value.length >= count.value && page.value > 1 + }) + + const isEmpty = computed(() => { + return list.value.length === 0 && page.value > 1 + }) + + return { + list, + page, + limit, + count, + loadding, + getPageList, + initData, + refreshing, + refreshPageList, + handleRefresh, + loadMore, + isNoMore, + isEmpty, + } +} diff --git a/src/layout/themes/default/components/aside/index.vue b/src/layout/themes/default/components/aside/index.vue index 0d4d766..6173b6c 100644 --- a/src/layout/themes/default/components/aside/index.vue +++ b/src/layout/themes/default/components/aside/index.vue @@ -7,7 +7,7 @@ import GcUser from '../user/index.vue'