first commit

This commit is contained in:
zc
2026-02-26 17:31:18 +08:00
commit f1b87df6ca
803 changed files with 297148 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
import { ref } from 'vue'
import type { EquipmentQuery } from '@/apis/equipment/equipment'
import { getEquipmentNameList } from '@/apis/equipment/equipment'
import type { LabelValueState } from '@/types/global'
/** 设备名称列表 */
export function equipmentName(query: EquipmentQuery, options?: { onSuccess?: () => void }) {
const loading = ref(false)
const equipmentNameList = ref<LabelValueState[]>([])
const getEquipmentName = async () => {
try {
loading.value = true
const res = await getEquipmentNameList(query)
equipmentNameList.value = res.data
// eslint-disable-next-line ts/no-unused-expressions
options?.onSuccess && options.onSuccess()
} finally {
loading.value = false
}
}
return { equipmentNameList, getEquipmentName, loading }
}

9
src/hooks/app/index.ts Normal file
View File

@@ -0,0 +1,9 @@
export * from './useMenu'
export * from './useDept'
export * from './useBranch'
export * from './useRole'
export * from './useDict'
export * from './space'
export * from './point'
export * from './equipmentName'
export * from './productName'

View File

@@ -0,0 +1,22 @@
import { ref } from 'vue'
import type { LabelValueState } from '@/types/global'
import { type PeopleQuery, getPeopleNameList } from '@/apis/sysPeople/people'
/** 设备名称列表 */
export function peopleName(query: PeopleQuery, options?: { onSuccess?: () => void }) {
const loading = ref(false)
const peopleNameList = ref<LabelValueState[]>([])
const getPeopleName = async () => {
try {
loading.value = true
const res = await getPeopleNameList(query)
peopleNameList.value = res.data
// eslint-disable-next-line ts/no-unused-expressions
options?.onSuccess && options.onSuccess()
} finally {
loading.value = false
}
}
return { peopleNameList, getPeopleName, loading }
}

22
src/hooks/app/point.ts Normal file
View File

@@ -0,0 +1,22 @@
import { ref } from 'vue'
import type { TreeNodeData } from '@arco-design/web-vue'
import { listPointTree } from '@/apis'
/** 部门模块 */
export function point(options?: { onSuccess?: () => void }) {
const loading = ref(false)
const pointList = ref<TreeNodeData[]>([])
const getPointList = async (name?: string) => {
try {
loading.value = true
const res = await listPointTree({ description: name })
pointList.value = res.data
// eslint-disable-next-line ts/no-unused-expressions
options?.onSuccess && options.onSuccess()
} finally {
loading.value = false
}
}
return { pointList, getPointList, loading }
}

View File

@@ -0,0 +1,22 @@
import { ref } from 'vue'
import { getProductNameList } from '@/apis/equipment/product'
import type { LabelValueState } from '@/types/global'
/** 设备名称列表 */
export function productName(options?: { onSuccess?: () => void }) {
const loading = ref(false)
const productNameList = ref<LabelValueState[]>([])
const getProductName = async () => {
try {
loading.value = true
const res = await getProductNameList()
productNameList.value = res.data
// eslint-disable-next-line ts/no-unused-expressions
options?.onSuccess && options.onSuccess()
} finally {
loading.value = false
}
}
return { productNameList, getProductName, loading }
}

22
src/hooks/app/ruleName.ts Normal file
View File

@@ -0,0 +1,22 @@
import { ref } from 'vue'
import type { LabelValueState } from '@/types/global'
import { getRuleNameList } from '@/apis/rule/rule'
/** 设备名称列表 */
export function ruleName(options?: { onSuccess?: () => void }) {
const loading = ref(false)
const ruleNameList = ref<LabelValueState[]>([])
const getRuleName = async () => {
try {
loading.value = true
const res = await getRuleNameList()
ruleNameList.value = res.data
// eslint-disable-next-line ts/no-unused-expressions
options?.onSuccess && options.onSuccess()
} finally {
loading.value = false
}
}
return { ruleNameList, getRuleName, loading }
}

22
src/hooks/app/space.ts Normal file
View File

@@ -0,0 +1,22 @@
import { ref } from 'vue'
import type { TreeNodeData } from '@arco-design/web-vue'
import { listSpaceTree } from '@/apis'
/** 部门模块 */
export function space(options?: { onSuccess?: () => void }) {
const loading = ref(false)
const spaceList = ref<TreeNodeData[]>([])
const getSpaceList = async (name?: string) => {
try {
loading.value = true
const res = await listSpaceTree({ description: name })
spaceList.value = res.data
// eslint-disable-next-line ts/no-unused-expressions
options?.onSuccess && options.onSuccess()
} finally {
loading.value = false
}
}
return { spaceList, getSpaceList, loading }
}

View File

@@ -0,0 +1,22 @@
import { ref } from 'vue'
import type { TreeNodeData } from '@arco-design/web-vue'
import { listBranchTree } from '@/apis'
/** 部门模块 */
export function useBranch(options?: { onSuccess?: () => void }) {
const loading = ref(false)
const branchList = ref<TreeNodeData[]>([])
const getBranchList = async (name?: string) => {
try {
loading.value = true
const res = await listBranchTree({ description: name })
branchList.value = res.data
// eslint-disable-next-line ts/no-unused-expressions
options?.onSuccess && options.onSuccess()
} finally {
loading.value = false
}
}
return { branchList, getBranchList, loading }
}

22
src/hooks/app/useDept.ts Normal file
View File

@@ -0,0 +1,22 @@
import { ref } from 'vue'
import type { TreeNodeData } from '@arco-design/web-vue'
import { listDeptTree } from '@/apis'
/** 部门模块 */
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
// eslint-disable-next-line ts/no-unused-expressions
options?.onSuccess && options.onSuccess()
} finally {
loading.value = false
}
}
return { deptList, getDeptList, loading }
}

43
src/hooks/app/useDict.ts Normal file
View File

@@ -0,0 +1,43 @@
import { ref, toRefs } from 'vue'
import { listCommonDict } from '@/apis'
import { useDictStore } from '@/stores'
const pendingRequests = new Map<string, Promise<any>>()
export function useDict(...codes: string[]) {
const dictStore = useDictStore()
const dictData = ref<Record<string, App.DictItem[]>>({})
codes.forEach(async (code) => {
dictData.value[code] = []
const cached = dictStore.getDict(code)
if (cached) {
dictData.value[code] = cached
return
}
if (!pendingRequests.has(code)) {
const request = listCommonDict(code)
.then(({ data }) => {
dictStore.setDict(code, data)
return data
})
.catch((error) => {
console.error(`Failed to load dict: ${code}`, error)
return []
})
.finally(() => {
pendingRequests.delete(code)
})
pendingRequests.set(code, request)
}
pendingRequests.get(code)!.then((data) => {
dictData.value[code] = data
})
})
return toRefs(dictData.value)
}

21
src/hooks/app/useMenu.ts Normal file
View File

@@ -0,0 +1,21 @@
import { ref } from 'vue'
import type { TreeNodeData } from '@arco-design/web-vue'
import { listMenuTree } from '@/apis'
/** 菜单模块 */
export function useMenu(options?: { onSuccess?: () => void }) {
const loading = ref(false)
const menuList = ref<TreeNodeData[]>([])
const getMenuList = async (name?: string) => {
try {
loading.value = true
const res = await listMenuTree({ description: name })
menuList.value = res.data
options?.onSuccess && options.onSuccess()
} finally {
loading.value = false
}
}
return { menuList, getMenuList, loading }
}

22
src/hooks/app/useRole.ts Normal file
View File

@@ -0,0 +1,22 @@
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
// eslint-disable-next-line ts/no-unused-expressions
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/useDevice'
export * from './modules/useBreakpoint'
export * from './modules/useDownload'
export * from './modules/useResetReactive'

View File

@@ -0,0 +1,24 @@
import { type ComputedRef, computed } 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.length ? arr.value[arr.value.length - 1] : 'xs'
})
return { breakpoint }
}

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 chartOption = computed<EChartsOption>(() => {
return sourceOption(isDark.value)
})
return { chartOption }
}

View File

@@ -0,0 +1,36 @@
import { onMounted, ref } from 'vue'
import { Message } from '@arco-design/web-vue'
interface ComponentOption {
label: string
value: string
}
export const useComponentPaths = () => {
const componentOptions = ref<ComponentOption[]>([])
const loadComponentPaths = async () => {
try {
const modules = import.meta.glob('@/views/**/index.vue')
const paths = Object.keys(modules)
componentOptions.value = paths.map((path) => {
// 格式转化
path = path.replace('/src/views/', '')
const label = `@view/${path}`
const value = path.split('.vue')[0]
return { label, value }
})
} catch (error) {
Message.error('加载组件路径失败')
console.error('加载组件路径失败:', error)
}
}
onMounted(async () => {
await loadComponentPaths()
})
return {
componentOptions,
}
}

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 > 571)
const isMobile = computed(() => !isDesktop.value)
return { isMobile, isDesktop }
}

View File

@@ -0,0 +1,49 @@
import { Message, Notification } from '@arco-design/web-vue'
/**
* @description 接收数据流生成 blob创建链接下载文件
* @param {Function} api 导出表格的api方法 (必传)
* @param {string} tempName 导出的文件名 (必传)
* @param {object} params 导出的参数 (默认{})
* @param {boolean} isNotify 是否有导出消息提示 (默认为 true)
* @param {string} fileType 导出的文件格式 (默认为.xlsx)
*/
interface NavigatorWithMsSaveOrOpenBlob extends Navigator {
msSaveOrOpenBlob: (blob: Blob, fileName: string) => void
}
export const useDownload = async (api: () => Promise<any>, isNotify = false, tempName = '', fileType = '.xlsx') => {
try {
const res = await api()
if (res.headers['content-disposition']) {
tempName = decodeURI(res.headers['content-disposition'].split(';')[1].split('=')[1])
} else {
tempName = tempName || new Date().getTime() + fileType
}
if (isNotify && !res?.code) {
Notification.warning({
title: '温馨提示',
content: '如果数据庞大会导致下载缓慢哦,请您耐心等待!',
})
}
if (res.status !== 200 || res.data == null || !(res.data instanceof Blob)) {
Message.error('导出失败,请稍后再试!')
return
}
const blob = new Blob([res.data])
// 兼容 edge 不支持 createObjectURL 方法
if ('msSaveOrOpenBlob' in (navigator as unknown as NavigatorWithMsSaveOrOpenBlob)) {
;(window.navigator as unknown as NavigatorWithMsSaveOrOpenBlob).msSaveOrOpenBlob(blob, tempName + fileType)
}
const blobUrl = window.URL.createObjectURL(blob)
const exportFile = document.createElement('a')
exportFile.style.display = 'none'
exportFile.download = tempName
exportFile.href = blobUrl
document.body.appendChild(exportFile)
exportFile.click()
// 去除下载对 url 的影响
document.body.removeChild(exportFile)
window.URL.revokeObjectURL(blobUrl)
} catch (error) {
// console.log(error)
}
}

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,59 @@
import { reactive, toRefs, watch } from 'vue'
import { useBreakpoint } from '@/hooks'
type Callback = () => void
export interface Options {
defaultPageSize: number
defaultSizeOptions: number[]
}
export function usePagination(callback: Callback, options: Options = { defaultPageSize: 10, defaultSizeOptions: [10, 20, 30, 40, 50] }) {
const { breakpoint } = useBreakpoint()
const pagination = reactive({
showPageSize: true,
showTotal: true,
current: 1,
pageSize: options.defaultPageSize,
pageSizeOptions: options.defaultSizeOptions,
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 { type UnwrapRef, ref } 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,15 @@
import { reactive } from 'vue'
import { cloneDeep } from 'lodash-es'
export function useResetReactive<T extends object>(value: T) {
const getInitValue = () => cloneDeep(value)
const state = reactive(getInitValue())
const reset = () => {
Object.keys(state).forEach((key) => delete state[key])
Object.assign(state, getInitValue())
}
return [state, reset] as const
}

View File

@@ -0,0 +1,127 @@
import type { TableData, TableInstance } from '@arco-design/web-vue'
import { Message, Modal } from '@arco-design/web-vue'
import type { Options as paginationOptions } from './usePagination'
import { useBreakpoint, usePagination } from '@/hooks'
interface Options<T, U> {
formatResult?: (data: T[]) => U[]
onSuccess?: () => void
immediate?: boolean
rowKey?: keyof T
paginationOption?: paginationOptions
}
interface PaginationParams { page: number, size: number }
type Api<T> = (params: PaginationParams) => Promise<ApiRes<PageRes<T[]>>> | Promise<ApiRes<T[]>>
export function useTable<T extends U, U = T>(api: Api<T>, options?: Options<T, U>) {
const { formatResult, onSuccess, immediate, rowKey } = options || {}
const { pagination, setTotal } = usePagination(() => getTableData(), options?.paginationOption)
const loading = ref(false)
const tableData: Ref<U[]> = ref([])
async function getTableData() {
try {
loading.value = true
const res = await api({ page: pagination.current, size: pagination.pageSize })
const data = !Array.isArray(res.data) ? res.data.list : res.data
tableData.value = formatResult ? formatResult(data) : data
const total = !Array.isArray(res.data) ? res.data.total : data.length
setTotal(total)
// eslint-disable-next-line ts/no-unused-expressions
onSuccess && onSuccess()
} finally {
loading.value = false
}
}
// 是否立即触发
const isImmediate = immediate ?? true
// eslint-disable-next-line ts/no-unused-expressions
isImmediate && getTableData()
// 多选
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 search = () => {
selectedKeys.value = []
pagination.onChange(1)
}
// 刷新
const refresh = () => {
getTableData()
}
// 删除
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 = []
await getTableData()
}
return res.success
} catch (error) {
return false
}
}
const flag = options?.showModal ?? true // 是否显示对话框
if (!flag) {
return onDelete()
}
Modal.warning({
title: options?.title || '提示',
content: options?.content || '是否确定删除该条数据?',
okButtonProps: { status: 'danger' },
hideCancel: false,
maskClosable: false,
onBeforeOk: onDelete,
})
}
const { breakpoint } = useBreakpoint()
// 表格操作列在小屏下不固定在右侧
const fixed = computed(() => !['xs', 'sm'].includes(breakpoint.value) ? 'right' : undefined)
return {
/** 表格加载状态 */
loading,
/** 表格数据 */
tableData,
/** 获取表格数据 */
getTableData,
/** 搜索页码会重置为1 */
search,
/** 分页的传参 */
pagination,
/** 选择的行keys */
selectedKeys,
/** 选择行 */
select,
/** 全选行 */
selectAll,
/** 处理删除、批量删除 */
handleDelete,
/** 刷新表格数据,页码会缓存 */
refresh,
/** 操作列在小屏场景下不固定在右侧 */
fixed,
}
}