Merge pull request 'add' (#13) from develop into main

main
p4zgomatu 3 months ago
commit c206fd4bf3

@ -1,7 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="4bee2ad7-6bf7-4a48-9f02-04204a99b147" name="Changes" comment="" />
<list default="true" id="4bee2ad7-6bf7-4a48-9f02-04204a99b147" name="Changes" comment="">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
@ -15,10 +20,10 @@
</option>
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="ProjectColorInfo"><![CDATA[{
"customColor": "",
"associatedIndex": 1
}]]></component>
<component name="ProjectColorInfo">{
&quot;customColor&quot;: &quot;&quot;,
&quot;associatedIndex&quot;: 1
}</component>
<component name="ProjectId" id="2wPHHFebSwYCHvl09mTI9f7jYjH" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
@ -28,7 +33,7 @@
"keyToString": {
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.git.unshallow": "true",
"git-widget-placeholder": "feature/lxf",
"git-widget-placeholder": "develop",
"kotlin-language-version-configured": "true",
"last_opened_file_path": "D:/SC/Nursing-home-management-system",
"node.js.detected.package.eslint": "true",
@ -37,6 +42,7 @@
"node.js.selected.package.tslint": "(autodetect)",
"nodejs_package_manager_path": "npm",
"settings.editor.selected.configurable": "MavenSettings",
"ts.external.directory.path": "D:\\xx\\Nursing-home-management-system\\client\\node_modules\\typescript\\lib",
"vue.rearranger.settings.migration": "true"
}
}]]></component>
@ -57,6 +63,7 @@
<option name="presentableId" value="Default" />
<updated>1742217574434</updated>
<workItem from="1745936199577" duration="445000" />
<workItem from="1745995470593" duration="507000" />
</task>
<servers />
</component>

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

@ -0,0 +1,109 @@
<template>
<!-- 根元素 div通过 style 绑定动态样式v-show 根据 isShow 的值控制元素的显示与隐藏 -->
<div :style="style" v-show="isShow">
<!-- 插槽用于在父组件中插入内容 -->
<slot></slot>
</div>
</template>
<script setup lang="ts" name="GridItem">
// vue
import { computed, inject, Ref, ref, useAttrs, watch } from 'vue'
// '../interface/index' BreakPoint Responsive
import { BreakPoint, Responsive } from '../interface/index'
// Props
type Props = {
// 0
offset?: number
// 1
span?: number
// false
suffix?: boolean
// xs undefined
xs?: Responsive
// sm undefined
sm?: Responsive
// md undefined
md?: Responsive
// lg undefined
lg?: Responsive
// xl undefined
xl?: Responsive
}
// 使 withDefaults defineProps
const props = withDefaults(defineProps<Props>(), {
offset: 0,
span: 1,
suffix: false,
xs: undefined,
sm: undefined,
md: undefined,
lg: undefined,
xl: undefined
})
// props
const attrs = useAttrs() as any
// isShow true
const isShow = ref(true)
// breakPoint Ref<BreakPoint> ref('xl')
const breakPoint = inject<Ref<BreakPoint>>('breakPoint', ref('xl'))
// shouldHiddenIndex Ref<number> ref(-1)
const shouldHiddenIndex = inject<Ref<number>>('shouldHiddenIndex', ref(-1))
// shouldHiddenIndex breakPoint
watch(
// shouldHiddenIndex.value breakPoint.value
() => [shouldHiddenIndex.value, breakPoint.value],
// n shouldHiddenIndex.value breakPoint.value
n => {
// attrs index
if (attrs.index) {
//
isShow.value =!(n[0]!== -1 && parseInt(attrs.index) >= n[0])
}
},
// immediate: true
{ immediate: true }
)
// gap 0
const gap = inject('gap', 0)
// cols Ref<number> ref(4)
const cols = inject<Ref<number>>('cols', ref(4))
// style props
const style = computed(() => {
// span 使 props.span
let span = props[breakPoint.value]?.span?? props.span
// offset 使 props.offset
let offset = props[breakPoint.value]?.offset?? props.offset
//
if (props.suffix) {
return {
// gridColumnStart
gridColumnStart: cols.value - span - offset + 1,
// gridColumnEnd
gridColumnEnd: `span ${span + offset}`,
// marginLeft
marginLeft:
offset!== 0
? `calc(((100% + ${gap}px) / ${span + offset}) * ${offset})`
: 'unset'
}
} else {
return {
// gridColumn
gridColumn: `span ${
span + offset > cols.value? cols.value : span + offset
}/span ${span + offset > cols.value? cols.value : span + offset}`,
// marginLeft
marginLeft:
offset!== 0
? `calc(((100% + ${gap}px) / ${span + offset}) * ${offset})`
: 'unset'
}
}
})
</script>

@ -0,0 +1,207 @@
<template>
<!-- 根元素 div通过 style 绑定动态样式用于包裹插槽内容 -->
<div :style="style">
<!-- 插槽用于父组件插入内容 -->
<slot></slot>
</div>
</template>
<script setup lang="ts" name="Grid">
// 'vue' Vue
import {
ref,
watch,
useSlots,
computed,
provide,
onBeforeMount,
onMounted,
onUnmounted,
onDeactivated,
onActivated,
VNodeArrayChildren,
VNode
} from 'vue'
// BreakPoint
import type { BreakPoint } from './interface/index'
// Props
type Props = {
//
cols?: number | Record<BreakPoint, number>
// false
collapsed?: boolean
// 1
collapsedRows?: number
// [, ]
gap?: [number, number] | number
}
// 使 withDefaults
const props = withDefaults(defineProps<Props>(), {
//
cols: () => ({ xs: 1, sm: 2, md: 2, lg: 3, xl: 4 }),
//
collapsed: false,
// 1
collapsedRows: 1,
// 0
gap: 0
})
// collapsed true findIndex
onBeforeMount(() => props.collapsed && findIndex())
// resize window resize
onMounted(() => {
resize({ target: { innerWidth: window.innerWidth } } as any)
window.addEventListener('resize', resize)
})
// resize window resize
onActivated(() => {
resize({ target: { innerWidth: window.innerWidth } } as any)
window.addEventListener('resize', resize)
})
// window resize
onUnmounted(() => {
window.removeEventListener('resize', resize)
})
// window resize
onDeactivated(() => {
window.removeEventListener('resize', resize)
})
// resize breakPoint
const resize = (e: UIEvent) => {
//
let width = (e.target as Window).innerWidth
// breakPoint
switch (!!width) {
case width < 768:
breakPoint.value = 'xs'
break
case width >= 768 && width < 992:
breakPoint.value ='sm'
break
case width >= 992 && width < 1200:
breakPoint.value ='md'
break
case width >= 1200 && width < 1920:
breakPoint.value = 'lg'
break
case width >= 1920:
breakPoint.value = 'xl'
break
}
}
// gap gap 使 gap
provide('gap', Array.isArray(props.gap)? props.gap[0] : props.gap)
// breakPoint
let breakPoint = ref<BreakPoint>('xl')
provide('breakPoint', breakPoint)
// index
const hiddenIndex = ref(-1)
provide('shouldHiddenIndex', hiddenIndex)
// cols cols breakPoint 使 cols
const cols = computed(() => {
if (typeof props.cols === 'object')
return props.cols[breakPoint.value]?? props.cols
return props.cols
})
provide('cols', cols)
//
const slots = useSlots().default!()
// findIndex index
const findIndex = () => {
//
let fields: VNodeArrayChildren = []
//
let suffix: any = null
//
slots.forEach((slot: any) => {
// 'GridItem' suffix
if (
typeof slot.type === 'object' &&
slot.type.name === 'GridItem' &&
slot.props?.suffix!== undefined
)
suffix = slot
// symbol
if (typeof slot.type ==='symbol' && Array.isArray(slot.children))
slot.children.forEach((child: any) => fields.push(child))
})
// suffix
let suffixCols = 0
if (suffix) {
suffixCols =
(suffix.props![breakPoint.value]?.span?? suffix.props?.span?? 1) +
(suffix.props![breakPoint.value]?.offset?? suffix.props?.offset?? 0)
}
try {
//
let find = false
//
fields.reduce((prev = 0, current, index) => {
prev +=
((current as VNode)!.props![breakPoint.value]?.span??
(current as VNode)!.props?.span??
1) +
((current as VNode)!.props![breakPoint.value]?.offset??
(current as VNode)!.props?.offset??
0)
if ((prev as number) > props.collapsedRows * cols.value - suffixCols) {
hiddenIndex.value = index
find = true
throw 'find it'
}
return prev
}, 0)
// hiddenIndex -1
if (!find) hiddenIndex.value = -1
} catch (e) {
// console.warn(e);
}
}
// breakPoint collapsed true findIndex
watch(
() => breakPoint.value,
() => {
if (props.collapsed) findIndex()
}
)
// collapsed true findIndex hiddenIndex -1
watch(
() => props.collapsed,
value => {
if (value) return findIndex()
hiddenIndex.value = -1
}
)
//
const gap = computed(() => {
if (typeof props.gap === 'number') return `${props.gap}px`
if (Array.isArray(props.gap)) return `${props.gap[1]}px ${props.gap[0]}px`
return 'unset'
})
//
const style = computed(() => {
return {
display: 'grid',
gridGap: gap.value,
gridTemplateColumns: `repeat(${cols.value}, minmax(0, 1fr))`
}
})
// breakPoint便访
defineExpose({ breakPoint })
</script>

@ -0,0 +1,12 @@
// 定义一个类型别名 BreakPoint它的值只能是 'xs'、'sm'、'md'、'lg' 或 'xl' 中的一个,
// 通常用于表示响应式布局中的不同屏幕断点
export type BreakPoint = 'xs' |'sm' |'md' | 'lg' | 'xl';
// 定义一个类型别名 Responsive它是一个对象类型包含两个可选属性 span 和 offset。
// span 用于表示在响应式布局中元素所占的列数,类型为 number 类型的可选值。
// offset 用于表示在响应式布局中元素的偏移量,类型为 number 类型的可选值。
// 这个类型通常用于描述在不同屏幕断点下元素的布局属性
export type Responsive = {
span?: number;
offset?: number;
};

@ -0,0 +1,45 @@
<template>
<!-- Component Vue 中的动态组件:is="icon" 表示根据 icon 的值来动态渲染对应的图标组件 -->
<!-- :theme="theme" 绑定图标主题如轮廓outline填充filled -->
<!-- :size="size" 绑定图标大小可以是数字或字符串类型 -->
<!-- :spin="spin" 绑定图标是否旋转的状态布尔类型 -->
<!-- :fill="fill" 绑定图标的填充颜色可以是字符串或字符串数组 -->
<!-- :strokeLinecap="strokeLinecap" 绑定图标描边端点的样式 -->
<!-- :strokeLinejoin="strokeLinejoin" 绑定图标描边连接处的样式 -->
<!-- :strokeWidth="strokeWidth" 绑定图标描边的宽度 -->
<Component
:is="icon"
:theme="theme"
:size="size"
:spin="spin"
:fill="fill"
:strokeLinecap="strokeLinecap"
:strokeLinejoin="strokeLinejoin"
:strokeWidth="strokeWidth"
/>
</template>
<script setup lang="ts">
// '@icon-park/vue-next/lib/runtime' Icon
import type { Icon } from '@icon-park/vue-next/lib/runtime'
// 使 defineProps
defineProps<{
// icon Icon
icon: Icon
// theme 'outline' | 'filled' | 'two-tone' |'multi-color'
theme?: 'outline' | 'filled' | 'two-tone' |'multi-color'
// size number | string
size?: number | string
// spin boolean
spin?: boolean
// fill string | string[]
fill?: string | string[]
// strokeLinecap 'butt' | 'round' |'square'
strokeLinecap?: 'butt' | 'round' |'square'
// strokeLinejoin 'miter' | 'round' | 'bevel'
strokeLinejoin?: 'miter' | 'round' | 'bevel'
// strokeWidth number
strokeWidth?: number
}>()
</script>

@ -0,0 +1,88 @@
<template>
<!-- 注释说明该部分是列设置 -->
<!-- el-drawer Element Plus 中的抽屉组件用于展示列设置的内容 -->
<!-- title="列设置" 设置抽屉的标题 -->
<!-- v-model="drawerVisible" 双向绑定抽屉的显示隐藏状态drawerVisible 是一个响应式变量 -->
<!-- size="450px" 设置抽屉的宽度为 450px -->
<el-drawer title="列设置" v-model="drawerVisible" size="450px">
<!-- 定义一个包含表格的 div 容器 -->
<div class="table-main">
<!-- el-table Element Plus 中的表格组件用于展示列设置的数据 -->
<!-- :data="colSetting" 绑定表格的数据colSetting 是一个数组包含列的设置信息 -->
<!-- :border="true" 设置表格有边框 -->
<!-- row-key="prop" 为表格的每一行设置唯一的标识这里使用 prop 作为标识 -->
<!-- default-expand-all 表示默认展开所有行对于树形表格 -->
<!-- :tree-props="{ children: '_children' }" 配置表格为树形表格指定子节点的字段为 _children -->
<el-table
:data="colSetting"
:border="true"
row-key="prop"
default-expand-all
:tree-props="{ children: '_children' }"
>
<!-- el-table-column 是表格列组件用于定义表格的列 -->
<!-- prop="label" 绑定列的数据字段为 label -->
<!-- align="center" 设置列内容居中对齐 -->
<!-- label="列名" 设置列的标题为列名 -->
<el-table-column prop="label" align="center" label="列名" />
<!-- 定义显示列prop="isShow" 绑定数据字段为 isShow -->
<!-- v-slot="scope" 定义作用域插槽scope 包含当前行的数据 -->
<el-table-column
prop="isShow"
align="center"
label="显示"
v-slot="scope"
>
<!-- el-switch Element Plus 中的开关组件v-model="scope.row.isShow" 双向绑定当前行的 isShow 字段 -->
<el-switch v-model="scope.row.isShow"></el-switch>
</el-table-column>
<!-- 定义排序列prop="sortable" 绑定数据字段为 sortable -->
<!-- v-slot="scope" 定义作用域插槽scope 包含当前行的数据 -->
<el-table-column
prop="sortable"
align="center"
label="排序"
v-slot="scope"
>
<!-- el-switch Element Plus 中的开关组件v-model="scope.row.sortable" 双向绑定当前行的 sortable 字段 -->
<el-switch v-model="scope.row.sortable"></el-switch>
</el-table-column>
<!-- 定义表格为空时显示的内容#empty 是表格的 empty 插槽 -->
<template #empty>
<div class="table-empty">
<div>暂无可配置列</div>
</div>
</template>
</el-table>
</div>
</el-drawer>
</template>
<script setup lang="ts" name="ColSetting">
// Vue ref
import { ref } from 'vue'
// ProTable ColumnProps
import { ColumnProps } from '@/components/ProTable/interface'
// 使 defineProps colSetting ColumnProps
defineProps<{ colSetting: ColumnProps[] }>()
// drawerVisible false
const drawerVisible = ref<boolean>(false)
// openColSetting drawerVisible true
const openColSetting = () => {
drawerVisible.value = true
}
// openColSetting 便
defineExpose({
openColSetting
})
</script>
<style scoped lang="scss">
// cursor-move move
.cursor-move {
cursor: move;
}
</style>

@ -0,0 +1,47 @@
<template>
<!-- 注释说明该部分是分页组件 -->
<!-- el-pagination Element Plus 中的分页组件 -->
<!-- :current-page="pageable.pageNum" 双向绑定当前页码pageable.pageNum 是一个响应式变量表示当前页码 -->
<!-- :page-size="pageable.pageSize" 双向绑定每页显示的数量pageable.pageSize 是一个响应式变量表示每页显示的数量 -->
<!-- :page-sizes="[10, 25, 50, 100]" 设置每页显示数量的可选值 -->
<!-- :background="true" 设置分页组件的背景颜色 -->
<!-- layout="total, sizes, prev, pager, next, jumper" 设置分页组件的布局包括总数量每页显示数量选择器上一页页码下一页跳转到指定页 -->
<!-- :total="pageable.total" 绑定总数量pageable.total 是一个响应式变量表示数据的总数量 -->
<!-- @size-change="handleSizeChange" 监听每页显示数量改变的事件当用户选择不同的每页显示数量时会调用 handleSizeChange 函数 -->
<!-- @current-change="handleCurrentChange" 监听当前页码改变的事件当用户点击上一页下一页或跳转到指定页时会调用 handleCurrentChange 函数 -->
<el-pagination
:current-page="pageable.pageNum"
:page-size="pageable.pageSize"
:page-sizes="[10, 25, 50, 100]"
:background="true"
layout="total, sizes, prev, pager, next, jumper"
:total="pageable.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
></el-pagination>
</template>
<script setup lang="ts" name="Pagination">
// Pageable
// pageNum number
// pageSize number
// total number
interface Pageable {
pageNum: number
pageSize: number
total: number
}
// PaginationProps
// pageable Pageable
// handleSizeChange number size
// handleCurrentChange number currentPage
interface PaginationProps {
pageable: Pageable
handleSizeChange: (size: number) => void
handleCurrentChange: (currentPage: number) => void
}
// 使 defineProps PaginationProps
defineProps<PaginationProps>()
</script>

@ -0,0 +1,103 @@
<template>
<!-- 使用动态组件根据 renderLoop 函数的返回值来渲染不同的表格列 -->
<component :is="renderLoop(column)"></component>
</template>
<script lang="tsx" setup name="TableColumn">
// vue
import { inject, ref, useSlots } from 'vue'
// ProTable ColumnProps
import { ColumnProps } from '@/components/ProTable/interface'
//
import { filterEnum, formatValue, handleRowAccordingToProp } from '@/utils/util'
// 使 defineProps column ColumnProps
defineProps<{ column: ColumnProps }>()
//
const slots = useSlots()
// enumMap Ref<Map<any, any>> Map
const enumMap = inject('enumMap', ref(new Map()))
// renderCellData
const renderCellData = (item: ColumnProps, scope: { [key: string]: any }) => {
// prop enumMap
return enumMap.value.get(item.prop) && item.isFilterEnum
// filterEnum
? filterEnum(
handleRowAccordingToProp(scope.row, item.prop!),
enumMap.value.get(item.prop)!,
item.fieldNames
)
//
: formatValue(handleRowAccordingToProp(scope.row, item.prop!))
}
// getTagType
const getTagType = (item: ColumnProps, scope: { [key: string]: any }) => {
// filterEnum
return filterEnum(
handleRowAccordingToProp(scope.row, item.prop!),
enumMap.value.get(item.prop),
item.fieldNames,
'tag'
) as any
}
// renderLoop
const renderLoop = (item: ColumnProps) => {
return (
<>
//
{item.isShow && (
// el-table-column
<el-table-column
//
{...item}
//
align={item.align?? 'center'}
// prop 'operation'
showOverflowTooltip={
item.showOverflowTooltip?? item.prop!== 'operation'
}
>
{{
//
default: (scope: any) => {
//
if (item._children)
//
return item._children.map(child => renderLoop(child))
//
if (item.render) return item.render(scope)
// prop
if (slots[item.prop!]) return slots[item.prop!]!(scope)
//
if (item.tag)
return (
// el-tag
<el-tag type={getTagType(item, scope)}>
{renderCellData(item, scope)}
</el-tag>
)
//
return renderCellData(item, scope)
},
//
header: () => {
//
if (item.headerRender) return item.headerRender(item)
// propHeader
if (slots[`${item.prop}Header`])
return slots[`${item.prop}Header`]!({ row: item })
// label
return item.label
}
}}
</el-table-column>
)}
</>
)
}
</script>

@ -0,0 +1,87 @@
## ProTable 文档 📚
### 1、ProTable 属性ProTableProps
> 使用 `v-bind="$attrs"` 通过属性透传将 **ProTable** 组件属性全部透传到 **el-table** 上,所以我们支持 **el-table** 的所有 **Props** 属性。在此基础上,还扩展了以下 **Props**
| 属性名 | 类型 | 是否必传 | 默认值 | 属性描述 |
| :----------: | :---------: | :------: | :-----------------------------------: | :--------------------------------------------------------------------------------------------------: |
| columns | ColumnProps | ✅ | — | ProTable 组件会根据此字段渲染搜索表单与表格列,详情见 ColumnProps |
| requestApi | Function | ✅ | — | 获取表格数据的请求 API |
| requestAuto | Boolean | ❌ | true | 表格初始化是否自动执行请求 API |
| dataCallback | Function | ❌ | — | 后台返回数据的回调函数,可对后台返回数据进行处理 |
| title | String | ❌ | — | 表格标题,目前只在打印的时候用到 |
| pagination | Boolean | ❌ | true | 是否显示分页组件pagination 为 false 后台返回数据应该没有分页信息 和 list 字段data 就是 list 数据 |
| initParam | Object | ❌ | {} | 表格请求的初始化参数,该值变化会自动请求表格数据 |
| toolButton | Boolean | ❌ | true | 是否显示表格功能按钮 |
| rowKey | String | ❌ | 'id' | 当表格数据多选时,所指定的 id |
| searchCol | Object | ❌ | { xs: 1, sm: 2, md: 2, lg: 3, xl: 4 } | 表格搜索项每列占比配置 |
### 2、Column 配置ColumnProps
> 使用 `v-bind="column"` 通过属性透传将每一项 **column** 属性全部透传到 **el-table-column** 上,所以我们支持 **el-table-column** 的所有 **Props** 属性。在此基础上,还扩展了以下 **Props**
| 属性名 | 类型 | 是否必传 | 默认值 | 属性描述 |
| :----------: | :----------------: | :------: | :----: | :---------------------------------------------------------------------------------------------: |
| tag | Boolean | ❌ | false | 当前单元格值是否为标签展示,可通过 enum 数据中 tagType 字段指定 tag 类型 |
| isShow | Boolean | ❌ | true | 当前列是否显示在表格内(只对 prop 列生效) |
| search | SearchProps | ❌ | — | 搜索项配置,详情见 SearchProps |
| enum | Object \| Function | ❌ | — | 字典,可格式化单元格内容,还可以作为搜索框的下拉选项(字典可以为 API 请求函数,内部会自动执行) |
| isFilterEnum | Boolean | ❌ | true | 当前单元格值是否根据 enum 格式化(例如 enum 只作为搜索项数据,不参与内容格式化) |
| fieldNames | Object | ❌ | — | 指定字典 label && value 的 key 值 |
| headerRender | Function | ❌ | — | 自定义表头内容渲染tsx 语法、h 语法) |
| render | Function | ❌ | — | 自定义单元格内容渲染tsx 语法、h 语法) |
| \_children | ColumnProps | ❌ | — | 多级表头 |
### 3、搜索项 配置SearchProps
> 使用 `v-bind="column.search.props“` 通过属性透传将 **search.props** 属性全部透传到每一项搜索组件上,所以我们支持 **input、select、tree-select、date-packer、time-picker、time-select、switch** 大部分属性,并在其基础上还扩展了以下 **Props**
| 属性名 | 类型 | 是否必传 | 默认值 | 属性描述 |
| :----------: | :----: | :------: | :----: | :--------------------------------------------------------------------------------------------------------------------------------------------: |
| el | String | ✅ | — | 当前项搜索框的类型支持input、input-number、select、select-v2、tree-select、cascader、date-packer、time-picker、time-select、switch、slider |
| props | Object | ❌ | — | 根据 element plus 官方文档来传递,该属性所有值会透传到组件 |
| defaultValue | Any | ❌ | — | 搜索项默认值 |
| key | String | ❌ | — | 当搜索项 key 不为 prop 属性时,可通过 key 指定 |
| order | Number | ❌ | — | 搜索项排序(从小到大) |
| span | Number | ❌ | 1 | 搜索项所占用的列数,默认为 1 列 |
| offset | Number | ❌ | — | 搜索字段左侧偏移列数 |
### 4、ProTable 事件:
> 根据 **ElementPlus Table** 文档在 **ProTable** 组件上绑定事件即可,组件会通过 **$attrs** 透传给 **el-table**
>
> [el-table 事件文档链接](https://element-plus.org/zh-CN/component/table.html#table-%E4%BA%8B%E4%BB%B6)
### 5、ProTable 方法:
> **ProTable** 组件暴露了 **el-table** 实例和一些组件内部的参数和方法:
>
> [el-table 方法文档链接](https://element-plus.org/zh-CN/component/table.html#table-%E6%96%B9%E6%B3%95)
| 方法名 | 描述 |
| :-------------: | :-------------------------------------------------------------------: |
| element | `el-table` 实例,可以通过`element.方法名`来调用 `el-table` 的所有方法 |
| tableData | 当前页面所展示的数据 |
| searchParam | 所有的搜索参数,不包含分页 |
| pageable | 当前表格的分页数据 |
| getTableList | 获取、刷新表格数据的方法(携带所有参数) |
| reset | 重置表格查询参数,相当于点击重置搜索按钮 |
| clearSelection | 清空表格所选择的数据,除此方法之外还可使用 `element.clearSelection()` |
| enumMap | 当前表格使用的所有字典数据Map 数据结构) |
| isSelected | 表格是否选中数据 |
| selectedList | 表格选中的数据列表 |
| selectedListIds | 表格选中的数据列表的 id |
### 6、ProTable 插槽:
| 插槽名 | 描述 |
| :----------------------: | :-------------------------------------------------------------------------------------------------------------------------------------: |
| — | 默认插槽,支持直接在 ProTable 中写 el-table-column 标签 |
| tableHeader | 自定义表格头部左侧区域的插槽,一般情况该区域放操作按钮 |
| toolButton | 自定义表格头部左右侧侧功能区域的插槽 |
| append | 插入至表格最后一行之后的内容, 如果需要对表格的内容进行无限滚动操作,可能需要用到这个 slot。 若表格有合计行,该 slot 会位于合计行之上。 |
| empty | 当表格数据为空时自定义的内容 |
| pagination | 分页组件插槽 |
| `column.prop` | 单元格的作用域插槽 |
| `column.prop` + "Header" | 表头的作用域插槽 |

@ -0,0 +1,397 @@
<!-- 📚📚📚 Pro-Table 文档: https://juejin.cn/post/7166068828202336263 -->
<template>
<!-- 查询表单 card -->
<!-- SearchForm 是自定义的搜索表单组件 -->
<!-- :search="search" 绑定搜索方法 -->
<!-- :reset="reset" 绑定重置方法 -->
<!-- :searchParam="searchParam" 绑定搜索参数 -->
<!-- :columns="searchColumns" 绑定搜索列配置 -->
<!-- :searchCol="searchCol" 绑定搜索项每列占比配置 -->
<!-- v-show="isShowSearch" 根据 isShowSearch 的值显示或隐藏搜索表单 -->
<!-- ref="searchForm" 为搜索表单组件添加引用 -->
<SearchForm
:search="search"
:reset="reset"
:searchParam="searchParam"
:columns="searchColumns"
:searchCol="searchCol"
v-show="isShowSearch"
ref="searchForm"
/>
<!-- 表格内容 card -->
<MyCard class="mt-2">
<div class="card table-main">
<div class="table-header">
<div class="flex justify-between header-button-lf mb-2">
<div>
<!-- 具名插槽 tableHeader传递了 selectedListIdsselectedList isSelected 数据 -->
<slot
name="tableHeader"
:selectedListIds="selectedListIds"
:selectedList="selectedList"
:isSelected="isSelected"
/>
</div>
<div class="header-button-ri">
<!-- 具名插槽 toolButton包含了一些功能按钮 -->
<slot name="toolButton">
<!-- 刷新按钮点击时调用 getTableList 方法获取表格数据 -->
<el-button :icon="Refresh" circle @click="getTableList" />
<!-- <el-button :icon="Printer" circle v-if="columns.length" @click="handlePrint" /> -->
<!-- 列设置按钮 columns 存在时显示点击时调用 openColSetting 方法打开列设置 -->
<el-button
:icon="Operation"
circle
v-if="columns.length"
@click="openColSetting"
/>
<!-- 搜索按钮 searchColumns 存在时显示点击时切换 isShowSearch 的值显示或隐藏搜索表单 -->
<el-button
:icon="Search"
circle
v-if="searchColumns.length"
@click="isShowSearch = !isShowSearch"
/>
</slot>
</div>
</div>
</div>
<!-- 表格主体 -->
<!-- el-table Element Plus 的表格组件 -->
<!-- ref="tableRef" 为表格组件添加引用 -->
<!-- v-bind="$attrs" 绑定父组件传递的所有属性 -->
<!-- :data="tableData" 绑定表格数据 -->
<!-- :border="border" 绑定表格是否有纵向边框 -->
<!-- :row-key="rowKey" 绑定行数据的唯一标识 -->
<!-- @selection-change="selectionChange" 监听表格行选择变化事件 -->
<el-table
ref="tableRef"
v-bind="$attrs"
:data="tableData"
:border="border"
:row-key="rowKey"
@selection-change="selectionChange"
>
<!-- 默认插槽 -->
<slot></slot>
<!-- 循环渲染表格列 -->
<template v-for="item in tableColumns" :key="item">
<!-- selection || index 类型的列 -->
<el-table-column
v-bind="item"
:align="item.align?? 'center'"
:reserve-selection="item.type =='selection'"
v-if="item.type =='selection' || item.type == 'index'"
>
</el-table-column>
<!-- expand 类型的列支持 tsx 语法和作用域插槽 -->
<el-table-column
v-bind="item"
:align="item.align?? 'center'"
v-if="item.type == 'expand'"
v-slot="scope"
>
<!-- 如果有自定义渲染函数则使用该函数渲染 -->
<component :is="item.render" :row="scope.row" v-if="item.render">
</component>
<!-- 否则使用插槽渲染 -->
<slot :name="item.type" :row="scope.row" v-else></slot>
</el-table-column>
<!-- 其他类型的列通过 TableColumn 组件递归渲染 -->
<TableColumn
v-if="!item.type && item.prop && item.isShow"
:column="item"
>
<!-- 循环渲染插槽内容 -->
<template v-for="slot in Object.keys($slots)" #[slot]="scope">
<slot :name="slot" :row="scope.row"></slot>
</template>
</TableColumn>
</template>
<!-- 插入表格最后一行之后的插槽 -->
<template #append>
<slot name="append"> </slot>
</template>
<!-- 表格无数据情况 -->
<template #empty>
<div class="table-empty">
<slot name="empty">
<div>暂无数据</div>
</slot>
</div>
</template>
</el-table>
<!-- 分页组件插槽 -->
<slot name="pagination">
<div class="mt-2 flex flex-row-reverse">
<!-- Pagination 是自定义的分页组件 pagination true 时显示 -->
<Pagination
v-if="pagination"
:pageable="pageable"
:handleSizeChange="handleSizeChange"
:handleCurrentChange="handleCurrentChange"
/>
</div>
</slot>
</div>
</MyCard>
<!-- 列设置 -->
<!-- ColSetting 是自定义的列设置组件 toolButton true 时显示 -->
<!-- v-model:colSetting="colSetting" 双向绑定列设置数据 -->
<ColSetting v-if="toolButton" ref="colRef" v-model:colSetting="colSetting" />
</template>
<script setup lang="ts" name="ProTable">
// Vue
import { ref, watch, computed, provide, onMounted } from 'vue'
// Hooks
import { useTable } from '@/hooks/useTable'
//
import { BreakPoint } from '@/components/Grid/interface'
//
import { ColumnProps } from '@/components/ProTable/interface'
// Element Plus
import { ElTable, TableProps } from 'element-plus'
// Element Plus
import { Refresh, Printer, Operation, Search } from '@element-plus/icons-vue'
//
import {
filterEnum,
formatValue,
handleProp,
handleRowAccordingToProp
} from '@/utils/util'
//
import SearchForm from '@/components/SearchForm/index.vue'
//
import Pagination from './components/Pagination.vue'
//
import ColSetting from './components/ColSetting.vue'
//
import TableColumn from './components/TableColumn.vue'
// Hooks
import { useSelection } from '@/hooks/useSelection'
// import printJS from "print-js";
//
const searchForm = ref()
// ProTable TableProps data
interface ProTableProps extends Partial<Omit<TableProps<any>, 'data'>> {
//
columns: ColumnProps[]
// API
requestApi: (params: any) => Promise<any>
//
requestAuto?: boolean
//
dataCallback?: (data: any) => any
//
title?: string
// true
pagination?: boolean
// {}
initParam?: any
// true
border?: boolean
// true
toolButton?: boolean
// Key Table id id
rowKey?: string
// { xs: 1, sm: 2, md: 2, lg: 3, xl: 4 }
searchCol?: number | Record<BreakPoint, number>
}
//
const props = withDefaults(defineProps<ProTableProps>(), {
requestAuto: true,
columns: () => [],
pagination: true,
initParam: {},
border: true,
toolButton: true,
rowKey: 'id',
searchCol: () => ({ xs: 1, sm: 2, md: 2, lg: 3, xl: 4 })
})
//
const isShowSearch = ref(true)
// DOM
const tableRef = ref<InstanceType<typeof ElTable>>()
// 使 Hooks
const { selectionChange, selectedList, selectedListIds, isSelected } =
useSelection(props.rowKey)
// 使 Hooks
const {
tableData,
pageable,
searchParam,
searchInitParam,
getTableList,
search,
reset,
handleSizeChange,
handleCurrentChange
} = useTable(
props.requestApi,
props.initParam,
props.pagination,
props.dataCallback
)
//
const clearSelection = () => tableRef.value!.clearSelection()
// requestAuto true getTableList
onMounted(() => props.requestAuto && getTableList())
// initParam
watch(() => props.initParam, getTableList, { deep: true })
//
const tableColumns = ref<ColumnProps[]>(props.columns)
// enumMap provide
const enumMap = ref(new Map<string, { [key: string]: any }[]>())
provide('enumMap', enumMap)
// enumMap
const setEnumMap = async (col: ColumnProps) => {
if (!col.enum) return
// enum enumMap
if (typeof col.enum!== 'function')
return enumMap.value.set(col.prop!, col.enum!)
// enum enumMap
const { data } = await col.enum()
enumMap.value.set(col.prop!, data)
}
// columns
const flatColumnsFunc = (
columns: ColumnProps[],
flatArr: ColumnProps[] = []
) => {
columns.forEach(async col => {
//
if (col._children?.length) flatArr.push(...flatColumnsFunc(col._children))
flatArr.push(col)
// column isShow isFilterEnum
col.isShow = col.isShow?? true
col.isFilterEnum = col.isFilterEnum?? true
// enumMap
setEnumMap(col)
})
//
return flatArr.filter(item =>!item._children?.length)
}
// columns
const flatColumns = ref<ColumnProps[]>()
flatColumns.value = flatColumnsFunc(tableColumns.value)
//
const searchColumns = flatColumns.value.filter(item => item.search?.el)
//
searchColumns.forEach((column, index) => {
column.search!.order = column.search!.order?? index + 2
if (
column.search?.defaultValue!== undefined &&
column.search?.defaultValue!== null
) {
searchInitParam.value[column.search.key?? handleProp(column.prop!)] =
column.search?.defaultValue
searchParam.value[column.search.key?? handleProp(column.prop!)] =
column.search?.defaultValue
}
})
//
searchColumns.sort((a, b) => a.search!.order! - b.search!.order!)
//
const colRef = ref()
//
const colSetting = tableColumns.value!.filter(
item =>
![
'selection',
'index',
'expand'
].includes(item.type!) && item.prop!== 'operation'
)
//
const openColSetting = () => colRef.value.openColSetting()
// 201-238
// enum
const printData = computed(() => {
const printDataList = JSON.parse(
JSON.stringify(
selectedList.value.length? selectedList.value : tableData.value
)
)
//
const needTransformCol = flatColumns.value!.filter(
item =>
(item.enum || (item.prop && item.prop.split('.').length > 1)) &&
item.isFilterEnum
)
needTransformCol.forEach(colItem => {
printDataList.forEach((tableItem: { [key: string]: any }) => {
tableItem[handleProp(colItem.prop!)] =
colItem.prop!.split('.').length > 1 &&!colItem.enum
? formatValue(handleRowAccordingToProp(tableItem, colItem.prop!))
: filterEnum(
handleRowAccordingToProp(tableItem, colItem.prop!),
enumMap.value.get(colItem.prop!),
colItem.fieldNames
)
for (const key in tableItem) {
if (tableItem[key] === null)
tableItem[key] = formatValue(tableItem[key])
}
})
})
return printDataList
})
// 💥 printJs
// const handlePrint = () => {
// const header = `<div style="text-align: center"><h2>${props.title}</h2></div>`;
// const gridHeaderStyle = "border: 1px solid #ebeef5;height: 45px;color: #232425;text-align: center;background-color: #fafafa;";
// const gridStyle = "border: 1px solid #ebeef5;height: 40px;color: #494b4e;text-align: center";
// printJS({
// printable: printData.value,
// header: props.title && header,
// properties: flatColumns
// .value!.filter(item => !["selection", "index", "expand"].includes(item.type!) && item.isShow && item.prop !== "operation")
// .map((item: ColumnProps) => ({ field: handleProp(item.prop!), displayName: item.label })),
// type: "json",
// gridHeaderStyle,
// gridStyle
// });
// };
//
defineExpose({
element: tableRef,
tableData,
searchParam,
pageable,
getTableList,
reset,
clearSelection,
enumMap,
isSelected,
selectedList,
selectedListIds
})
</script>

@ -0,0 +1,84 @@
// 从 "element-plus/es/components/table/src/table-column/defaults" 导入 TableColumnCtx 类型
// TableColumnCtx 可能是 Element Plus 中表格列的上下文类型,包含了表格列的一些默认属性和方法
import { TableColumnCtx } from "element-plus/es/components/table/src/table-column/defaults";
// 从 "@/components/Grid/interface" 导入 BreakPoint 和 Responsive 类型
// BreakPoint 可能表示响应式布局中的不同断点(如 xs, sm, md, lg, xl 等)
// Responsive 可能用于描述在不同断点下的响应式配置
import { BreakPoint, Responsive } from "@/components/Grid/interface";
// 定义一个接口 EnumProps用于描述枚举属性
export interface EnumProps {
// 选项框显示的文字,类型为字符串
label: string;
// 选项框值,可以是任意类型
value: any;
// 是否禁用此选项,可选,默认为 undefined类型为布尔值
disabled?: boolean;
// 当 tag 为 true 时,此选择会指定 tag 显示类型,可选,默认为 undefined类型为字符串
tagType?: string;
// 为树形选择时,可以通过 children 属性指定子选项,可选,默认为 undefined类型为 EnumProps 数组
children?: EnumProps[];
// 允许添加任意其他属性,属性名和值的类型都为任意类型
[key: string]: any;
}
// 定义一个类型别名 TypeProp其值只能是 "index"、"selection" 或 "expand" 中的一个
export type TypeProp = "index" | "selection" | "expand";
// 定义一个类型别名 SearchType用于表示搜索框的类型
// 可以是 "input"、"input-number" 等多种 Element Plus 组件类型
export type SearchType =
| "input"
| "input-number"
| "select"
| "select-v2"
| "tree-select"
| "cascader"
| "date-picker"
| "time-picker"
| "time-select"
| "switch"
| "slider";
// 定义一个接口 SearchProps用于描述搜索项的属性
export type SearchProps = {
// 当前项搜索框的类型,类型为 SearchType
el: SearchType;
// 搜索项参数,根据 element plus 官方文档来传递,该属性所有值会透传到组件,可选,默认为 undefined
props?: any;
// 当搜索项 key 不为 prop 属性时,可通过 key 指定,可选,默认为 undefined类型为字符串
key?: string;
// 搜索项排序(从大到小),可选,默认为 undefined类型为数字
order?: number;
// 搜索项所占用的列数,默认为 1 列,可选,默认为 undefined类型为数字
span?: number;
// 搜索字段左侧偏移列数,可选,默认为 undefined类型为数字
offset?: number;
// 搜索项默认值,可以是字符串、数字、布尔值或任意类型的数组,可选,默认为 undefined
defaultValue?: string | number | boolean | any[];
// 扩展为包含 BreakPoint 类型的键和 Responsive 类型的值的部分记录,用于实现响应式配置
} & Partial<Record<BreakPoint, Responsive>>;
// 定义一个接口 ColumnProps用于描述表格列的属性
// 扩展自 Partial<Omit<TableColumnCtx<T>, "children" | "renderHeader" | "renderCell">>,表示部分继承 TableColumnCtx 类型并去除 "children"、"renderHeader" 和 "renderCell" 属性
export interface ColumnProps<T = any>
extends Partial<Omit<TableColumnCtx<T>, "children" | "renderHeader" | "renderCell">> {
// 是否是标签展示,可选,默认为 undefined类型为布尔值
tag?: boolean;
// 是否显示在表格当中,可选,默认为 undefined类型为布尔值
isShow?: boolean;
// 搜索项配置,可选,默认为 undefined类型为 SearchProps 或 undefined
search?: SearchProps | undefined;
// 枚举类型(渲染值的字典),可以是 EnumProps 数组或一个返回 Promise<any> 的函数,可选,默认为 undefined
enum?: EnumProps[] | ((params?: any) => Promise<any>);
// 当前单元格值是否根据 enum 格式化示例enum 只作为搜索项数据),可选,默认为 undefined类型为布尔值
isFilterEnum?: boolean;
// 指定 label && value 的 key 值,可选,默认为 undefined类型为包含 label 和 value 键的对象
fieldNames?: { label: string; value: string };
// 自定义表头内容渲染tsx语法可选默认为 undefined类型为一个接受 ColumnProps 类型参数并返回任意类型的函数
headerRender?: (row: ColumnProps) => any;
// 自定义单元格内容渲染tsx语法可选默认为 undefined类型为一个接受包含 row 属性且类型为 T 的对象并返回任意类型的函数
render?: (scope: { row: T }) => any;
// 多级表头,可选,默认为 undefined类型为 ColumnProps<T> 数组
_children?: ColumnProps<T>[];
}

@ -0,0 +1,12 @@
// 从当前目录下的 src 文件夹中的 index.vue 文件导入组件
// 这里将导入的组件命名为 reImageVerify它应该是一个 Vue 组件
import reImageVerify from './src/index.vue'
/** 图形验证码组件 */
// 使用具名导出的方式,将导入的组件以 ReImageVerify 这个名称导出
// 具名导出允许在导入时指定具体要导入的内容,方便在其他文件中使用
export const ReImageVerify = reImageVerify
// 使用默认导出的方式,将导入的组件作为默认导出
// 默认导出允许在导入时可以使用任意名称来接收该组件,通常一个文件只能有一个默认导出
export default ReImageVerify

@ -0,0 +1,133 @@
import { ref, onMounted } from 'vue'
/**
*
* @param width -
* @param height -
*/
// 定义一个名为 useImageVerify 的函数,用于生成和管理图形验证码,接受图形宽度和高度作为参数,有默认值
export const useImageVerify = (width = 120, height = 40) => {
// 创建一个响应式引用 domRef用于存储 <canvas> 元素的引用
const domRef = ref<HTMLCanvasElement>()
// 创建一个响应式变量 imgCode用于存储生成的图形验证码字符串
const imgCode = ref('')
// 定义一个函数 setImgCode用于设置 imgCode 的值
function setImgCode(code: string) {
imgCode.value = code
}
// 定义一个函数 getImgCode用于在 <canvas> 上绘制验证码并获取生成的验证码字符串
function getImgCode() {
// 如果 domRef 没有值(即 <canvas> 元素未挂载),则直接返回
if (!domRef.value) return
// 调用 draw 函数绘制验证码,并将返回的验证码字符串赋值给 imgCode
imgCode.value = draw(domRef.value, width, height)
}
// 在组件挂载后执行的钩子函数,调用 getImgCode 函数生成验证码
onMounted(() => {
getImgCode()
})
// 返回一个包含 domRef、imgCode、setImgCode 和 getImgCode 的对象,以便在其他地方使用这些变量和函数
return {
domRef,
imgCode,
setImgCode,
getImgCode
}
}
// 定义一个函数 randomNum用于生成指定范围内的随机整数
function randomNum(min: number, max: number) {
// 使用 Math.random() 生成一个 0 到 1 之间的随机小数,然后通过计算得到指定范围内的整数
const num = Math.floor(Math.random() * (max - min) + min)
return num
}
// 定义一个函数 randomColor用于生成指定范围内的随机颜色RGB 格式)
function randomColor(min: number, max: number) {
// 分别生成随机的红r、绿g、蓝b分量值
const r = randomNum(min, max)
const g = randomNum(min, max)
const b = randomNum(min, max)
// 返回 RGB 格式的颜色字符串
return `rgb(${r},${g},${b})`
}
// 定义一个函数 draw用于在 <canvas> 元素上绘制图形验证码
function draw(dom: HTMLCanvasElement, width: number, height: number) {
// 初始化一个空字符串 imgCode用于存储生成的验证码
let imgCode = ''
// 定义一个字符串 NUMBER_STRING包含数字 0 到 9用于生成验证码字符
const NUMBER_STRING = '0123456789'
// 获取 <canvas> 元素的 2D 绘图上下文 ctx
const ctx = dom.getContext('2d')
// 如果获取上下文失败ctx 为 null则直接返回空的验证码字符串
if (!ctx) return imgCode
// 设置绘图上下文的填充样式为随机颜色(背景色)
ctx.fillStyle = randomColor(180, 230)
// 使用填充样式绘制一个矩形,覆盖整个 <canvas> 区域,作为验证码的背景
ctx.fillRect(0, 0, width, height)
// 循环 4 次,生成 4 个验证码字符
for (let i = 0; i < 4; i += 1) {
// 从 NUMBER_STRING 中随机选取一个字符作为验证码字符
const text = NUMBER_STRING[randomNum(0, NUMBER_STRING.length)]
// 将选取的字符添加到 imgCode 字符串中
imgCode += text
// 随机生成字体大小
const fontSize = randomNum(18, 41)
// 随机生成字符的旋转角度
const deg = randomNum(-30, 30)
// 设置绘图上下文的字体样式
ctx.font = `${fontSize}px Simhei`
// 设置文本基线为顶部
ctx.textBaseline = 'top'
// 设置绘图上下文的填充样式为随机颜色(字符颜色)
ctx.fillStyle = randomColor(80, 150)
// 保存当前绘图上下文的状态
ctx.save()
// 平移绘图上下文的原点到指定位置(每个字符的起始位置)
ctx.translate(30 * i + 15, 15)
// 旋转绘图上下文指定的角度
ctx.rotate((deg * Math.PI) / 180)
// 在指定位置绘制字符
ctx.fillText(text, -15 + 5, -15)
// 恢复之前保存的绘图上下文状态
ctx.restore()
}
// 循环 5 次,绘制 5 条随机直线
for (let i = 0; i < 5; i += 1) {
// 开始绘制路径
ctx.beginPath()
// 设置路径的起始点为随机位置
ctx.moveTo(randomNum(0, width), randomNum(0, height))
// 设置路径的终点为随机位置
ctx.lineTo(randomNum(0, width), randomNum(0, height))
// 设置绘图上下文的描边样式为随机颜色
ctx.strokeStyle = randomColor(180, 230)
// 关闭路径
ctx.closePath()
// 绘制路径(直线)
ctx.stroke()
}
// 循环 41 次,绘制 41 个随机圆形
for (let i = 0; i < 41; i += 1) {
// 开始绘制路径
ctx.beginPath()
// 绘制一个圆形,圆心为随机位置,半径为 1
ctx.arc(randomNum(0, width), randomNum(0, height), 1, 0, 2 * Math.PI)
// 关闭路径
ctx.closePath()
// 设置绘图上下文的填充样式为随机颜色
ctx.fillStyle = randomColor(150, 200)
// 填充圆形
ctx.fill()
}
// 返回生成的验证码字符串
return imgCode
}

@ -0,0 +1,55 @@
<script setup lang="ts">
// Vue watch
import { watch } from 'vue'
// useImageVerify
import { useImageVerify } from './hooks'
// Props
interface Props {
code?: string // code
}
// Emits
interface Emits {
(e: 'update:code', code: string): void // 'update:code' code
}
// 使 withDefaults props
const props = withDefaults(defineProps<Props>(), {
code: '' // code
})
// emit
const emit = defineEmits<Emits>()
// useImageVerify domRefimgCodesetImgCode getImgCode
const { domRef, imgCode, setImgCode, getImgCode } = useImageVerify()
// props.code props.code setImgCode
watch(
() => props.code,
newValue => {
setImgCode(newValue)
}
)
// imgCode imgCode 'update:code'
watch(imgCode, newValue => {
emit('update:code', newValue)
})
// getImgCode 便
defineExpose({ getImgCode })
</script>
<template>
<!-- 定义一个 canvas 元素设置其宽度为 120高度为 40添加类名 cursor-pointer 使其具有指针样式
绑定 ref domRef以便在脚本中访问该元素绑定 click 事件为 getImgCode 函数 -->
<canvas
ref="domRef"
width="120"
height="40"
class="cursor-pointer"
@click="getImgCode"
/>
</template>

@ -0,0 +1,117 @@
<template>
<!-- 根据条件渲染不同的组件 -->
<component
v-if="column.search?.el" <!-- 如果 column.search.el 存在则渲染该组件 -->
:is="`el-${column.search.el}`" <!-- 根据 column.search.el 的值动态渲染对应的 Element Plus 组件例如 'el-input''el-select' -->
v-bind="handleSearchProps" <!-- 绑定处理后的搜索属性 -->
v-model.trim="searchParam[column.search.key?? handleProp(column.prop!)]" <!-- 双向绑定搜索参数使用 trim 修饰符去除首尾空格 -->
:data="column.search?.el === 'tree-select'? columnEnum : []" <!-- 如果是 tree-select 组件绑定 columnEnum 数据否则绑定空数组 -->
:options="
['cascader','select-v2'].includes(column.search?.el)? columnEnum : []
" <!-- 如果是 cascader select-v2 组件绑定 columnEnum 数据作为选项否则绑定空数组 -->
:placeholder="placeholder" <!-- 绑定占位符文本 -->
:clearable="clearable" <!-- 绑定是否可清除的属性 -->
range-separator="至" <!-- 日期范围选择器的分隔符 -->
start-placeholder="开始时间" <!-- 日期范围选择器开始时间的占位符 -->
end-placeholder="结束时间" <!-- 日期范围选择器结束时间的占位符 -->
>
<!-- column.search.el 'cascader' 渲染默认插槽内容 -->
<template #default="{ data }" v-if="column.search.el === 'cascader'">
<span>{{ data[fieldNames.label] }}</span> <!-- 显示数据的 label 字段 -->
</template>
<!-- column.search.el 'select' 渲染子选项 -->
<template v-if="column.search.el ==='select'">
<component
:is="`el-option`" <!-- 渲染 Element Plus el-option 组件 -->
v-for="(col, index) in columnEnum" <!-- 遍历 columnEnum 数据 -->
:key="index" <!-- 设置唯一的 key -->
:label="col[fieldNames.label]" <!-- 绑定选项的 label -->
:value="col[fieldNames.value]" <!-- 绑定选项的值 -->
></component>
</template>
<!-- 如果以上条件都不满足渲染插槽内容 -->
<slot v-else></slot>
</component>
</template>
<script setup lang="ts" name="SearchFormItem">
// Vue
import { computed, inject, onMounted, ref } from 'vue'
// handleProp
import { handleProp } from '@/utils/util'
//
import { ColumnProps } from '@/components/ProTable/interface'
// SearchFormItem
interface SearchFormItem {
column: ColumnProps //
searchParam: { [key: string]: any } //
}
// props
const props = defineProps<SearchFormItem>()
// fieldNames label && value key
const fieldNames = computed(() => {
return {
label: props.column.fieldNames?.label?? 'label', // column.fieldNames.label 使使 'label'
value: props.column.fieldNames?.value?? 'value' // column.fieldNames.value 使使 'value'
}
})
// enumMap
const enumMap = inject('enumMap', ref(new Map()))
// columnEnum props.column.prop enumMap
const columnEnum = computed(() => {
let enumData = enumMap.value.get(props.column.prop)
if (!enumData) return [] //
if (props.column.search?.el ==='select-v2' && props.column.fieldNames) {
enumData = enumData.map((item: { [key: string]: any }) => {
return {
...item,
label: item[fieldNames.value.label], // label
value: item[fieldNames.value.value] // value
}
})
}
return enumData
})
// searchProps(el tree-selectcascader label value)
const handleSearchProps = computed(() => {
const label = fieldNames.value.label
const value = fieldNames.value.value
const searchEl = props.column.search?.el
const searchProps = props.column.search?.props?? {}
let handleProps = searchProps
if (searchEl === 'tree-select')
handleProps = {
...searchProps,
props: { label,...searchProps.props }, // tree-select props
nodeKey: value
}
if (searchEl === 'cascader')
handleProps = {
...searchProps,
props: { label, value,...searchProps.props } // cascader props
}
return handleProps
})
// placeholder
const placeholder = computed(() => {
const search = props.column.search
return (
search?.props?.placeholder?? (search?.el === 'input'? '请输入' : '请选择') //
)
})
// ()
const clearable = computed(() => {
const search = props.column.search
return (
search?.props?.clearable??
(search?.defaultValue == null || search?.defaultValue == undefined) // props
)
})
</script>

@ -0,0 +1,142 @@
<template>
<!-- MyCard 组件可能是一个自定义的卡片组件 -->
<MyCard>
<!-- columns 数组长度大于 0 时显示搜索表单 -->
<div class="card table-search" v-if="columns.length">
<!-- el-form 组件用于创建表单 -->
<el-form ref="formRef" :model="searchParam">
<!-- Grid 组件可能是一个用于布局的网格组件 -->
<Grid
ref="gridRef"
:collapsed="collapsed"
:gap="[20, 0]"
:cols="searchCol"
>
<!-- 循环渲染 GridItem 组件每个 GridItem 对应一个搜索表单字段 -->
<GridItem
v-for="(item, index) in columns"
:key="item.prop"
v-bind="getResponsive(item)"
:index="index"
>
<!-- el-form-item 组件用于创建表单字段 -->
<el-form-item :label="`${item.label} :`">
<!-- SearchFormItem 组件用于渲染具体的搜索表单字段 -->
<SearchFormItem :column="item" :searchParam="searchParam" />
</el-form-item>
</GridItem>
<!-- 带有 suffix 标志的 GridItem 组件用于放置操作按钮 -->
<GridItem suffix>
<div class="operation">
<!-- 搜索按钮点击时调用 search 方法 -->
<el-button
class="bg-blue clickSearchBtn"
type="primary"
:icon="Search"
@click="search"
>
搜索
</el-button>
<!-- 重置按钮点击时调用 reset 方法 -->
<el-button :icon="Delete" @click="reset"></el-button>
<!-- 展开/合并按钮根据 collapsed 的值显示不同的文本和图标 -->
<el-button
v-if="showCollapse"
link
class="search-isOpen"
@click="collapsed =!collapsed"
>
{{ collapsed? '展开' : '合并' }}
<el-icon class="el-icon--right">
<component :is="collapsed? ArrowDown : ArrowUp"></component>
</el-icon>
</el-button>
</div>
</GridItem>
</Grid>
</el-form>
</div>
</MyCard>
</template>
<script setup lang="ts" name="SearchForm">
// Vue
import { computed, onMounted, ref } from 'vue'
// ProTable
import { ColumnProps } from '@/components/ProTable/interface'
// Grid
import { BreakPoint } from '@/components/Grid/interface'
// Element Plus
import { Delete, Search, ArrowDown, ArrowUp } from '@element-plus/icons-vue'
// SearchFormItem
import SearchFormItem from './components/SearchFormItem.vue'
// Grid
import Grid from '@/components/Grid/index.vue'
// GridItem
import GridItem from '@/components/Grid/components/GridItem.vue'
// MyCard
import MyCard from '../my-card/my-card.vue'
// ProTableProps
interface ProTableProps {
//
columns?: ColumnProps[]
//
searchParam?: { [key: string]: any }
//
searchCol: number | Record<BreakPoint, number>
//
search: (params: any) => void
//
reset: (params: any) => void
}
// 使 withDefaults
const props = withDefaults(defineProps<ProTableProps>(), {
columns: () => [],
searchParam: () => ({})
})
//
const getResponsive = (item: ColumnProps) => {
return {
// item.search.span
span: item.search?.span,
// item.search.offset 0
offset: item.search?.offset?? 0,
// item.search
xs: item.search?.xs,
sm: item.search?.sm,
md: item.search?.md,
lg: item.search?.lg,
xl: item.search?.xl
}
}
//
const collapsed = ref(true)
// ref Grid
const gridRef = ref()
//
const breakPoint = computed<BreakPoint>(() => gridRef.value?.breakPoint)
// /
const showCollapse = computed(() => {
let show = false
// columns
props.columns.reduce((prev, current) => {
prev +=
(current.search![breakPoint.value]?.span?? current.search?.span?? 1) +
(current.search![breakPoint.value]?.offset?? current.search?.offset?? 0)
// searchCol /
if (typeof props.searchCol!== 'number') {
if (prev >= props.searchCol[breakPoint.value]) show = true
} else {
if (prev > props.searchCol) show = true
}
return prev
}, 0)
return show
})
</script>

@ -0,0 +1,49 @@
<template>
<!-- SVG 图标容器添加了 svg-icon 类用于样式设置area-hidden="true" 用于隐藏无障碍区域 -->
<svg class="svg-icon" area-hidden="true">
<!-- use 元素用于引用外部 SVG 符号:xlink:href 绑定了动态的图标名称 -->
<use :xlink:href="iconName"></use>
</svg>
</template>
<script setup lang="ts">
// Vue defineProps computed
import { defineProps, computed } from 'vue'
//
const props = defineProps({
// icon
icon: {
type: String,
required: true
},
// size 16
size: {
type: [Number, String],
default: 16
}
})
// icon #icon-
const iconName = computed(() => {
return `#icon-${props.icon}`
})
// size
const iconSize = computed(() => {
return props.size + 'px'
})
</script>
<style lang="scss" scoped>
// .svg-icon SVG
.svg-icon {
// 1em
width: 1em;
// 1em
height: 1em;
//
fill: currentColor;
// iconSize
font-size: v-bind(iconSize);
}
</style>

@ -0,0 +1,118 @@
<template>
<!-- el-dialog Element Plus 中的对话框组件用于展示选择老人的界面 -->
<!-- style="width: 70%" 设置对话框的宽度为父容器的 70% -->
<!-- v-model="dialogVisible" 双向绑定对话框的显示隐藏状态dialogVisible 是一个响应式变量 -->
<!-- title="选择老人" 设置对话框的标题 -->
<!-- destroy-on-close 表示在对话框关闭时销毁其内容 -->
<el-dialog
style="width: 70%"
v-model="dialogVisible"
title="选择老人"
destroy-on-close
>
<!-- 定义一个包含表格的 div 容器 -->
<div class="table-box">
<!-- ProTable 是自定义的表格组件用于展示用户列表 -->
<!-- ref="proTable" 为表格组件添加引用方便在脚本中访问 -->
<!-- title="用户列表" 设置表格的标题 -->
<!-- :columns="columns" 绑定表格的列配置项columns 是一个数组包含每列的信息 -->
<!-- :requestApi="getTableList" 绑定数据请求方法getTableList 用于获取表格数据 -->
<ProTable
ref="proTable"
title="用户列表"
:columns="columns"
:requestApi="getTableList"
>
<!-- 定义表格操作列的插槽内容 -->
<template #operation="scope">
<!-- el-popconfirm Element Plus 中的弹出确认框组件 -->
<!-- title="Are you sure to choose this?" 设置确认框的提示信息 -->
<!-- @confirm="checkElder(scope.row)" 当用户确认时调用 checkElder 方法并传递当前行的数据 -->
<!-- confirm-button-type="warning" 设置确认按钮的类型为警告样式 -->
<el-popconfirm
title="Are you sure to choose this?"
@confirm="checkElder(scope.row)"
confirm-button-type="warning"
>
<!-- 定义确认框的触发元素 -->
<template #reference>
<!-- el-button Element Plus 中的按钮组件 -->
<!-- size="small" 设置按钮大小为小 -->
<!-- link 设置按钮为链接样式 -->
<!-- :icon="View" 绑定图标View 是从 @element-plus/icons-vue 导入的图标组件 -->
<!-- 选择 是按钮的文本内容 -->
<el-button size="small" link :icon="View">选择</el-button>
</template>
</el-popconfirm>
</template>
</ProTable>
</div>
</el-dialog>
</template>
<script setup lang="ts" name="useProTable">
// Vue ref
import { ref } from "vue";
// ProTable ColumnProps
import { ColumnProps } from "@/components/ProTable/interface";
// ProTable
import ProTable from "@/components/ProTable/index.vue";
// Element Plus View
import { View } from "@element-plus/icons-vue";
// dialogVisible
const dialogVisible = ref(false);
// proTable ProTable
const proTable = ref();
// drawerProps DialogProps
const drawerProps = ref<DialogProps>();
// DialogProps elderApi
interface DialogProps {
elderApi: (params: any) => Promise<any>;
}
// getTableList params
//
// elderApi
let getTableList = async (params: any) => {
let newParams = JSON.parse(JSON.stringify(params));
return drawerProps.value?.elderApi(newParams);
};
// columns ColumnProps
const columns: ColumnProps<any>[] = [
{ prop: "rank", label: "序号", width: 55 },
{ prop: "name", label: "姓名", search: { el: "input" } },
{ prop: "idNum", label: "身份证号" },
{ prop: "sex", label: "性别" },
{ prop: "age", label: "年龄" },
{ prop: "phone", label: "电话", search: { el: "input" } },
{ prop: "address", label: "地址" },
{ prop: "operation", label: "操作", width: 70 }
];
// elderAcceptParams DialogProps params
// drawerProps
const elderAcceptParams = (params: DialogProps) => {
drawerProps.value = params;
dialogVisible.value = true;
};
// elderAcceptParams 便
defineExpose({
elderAcceptParams
});
// emit
const emit = defineEmits<{
(event: "getCheckElderInfo", val: any): void
}>();
// checkElder row
// "getCheckElderInfo"
const checkElder = (row: any) => {
emit("getCheckElderInfo", row);
dialogVisible.value = false;
};
</script>
<style lang="scss" scoped></style>

@ -0,0 +1,30 @@
<template>
<!-- 根元素 div应用了多个类名用于设置样式和布局 -->
<!-- class="flex flex-col rounded card-wrap p-3" 表示使用 flex 布局方向为列边框圆角应用 card-wrap 类的样式内边距为 3 -->
<div class="flex flex-col rounded card-wrap p-3">
<!-- 条件渲染的 div如果 title 存在即不是 undefined 或空字符串等假值则显示该 div -->
<!-- class="pb-2" 表示该元素底部外边距为 2 -->
<!-- v-if="title" Vue 的条件渲染指令 -->
<div class="pb-2" v-if="title">{{ title }}</div>
<!-- 插槽用于父组件插入内容会被父组件的内容替换 -->
<slot></slot>
</div>
</template>
<script lang="ts" setup>
// 使 defineProps
// title ?
defineProps<{
title?: string
}>()
</script>
<style lang="scss" scoped>
// .card-wrap
// 1px 线 #eee 18px
.card-wrap {
background-color: #ffffff;
border: 1px solid #eee;
font-size: 18px;
}
</style>

@ -0,0 +1,90 @@
<template>
<!-- Element Plus 的对话框组件 el-dialog -->
<!-- style="width: 70%" 设置对话框的宽度为父容器的 70% -->
<!-- v-model="dialogVisible" 双向绑定对话框的显示隐藏状态dialogVisible 为一个响应式变量 -->
<!-- title="选择床位" 设置对话框的标题为选择床位 -->
<!-- destroy-on-close 表示在对话框关闭时销毁其中的内容 -->
<el-dialog
style="width: 70%"
v-model="dialogVisible"
title="选择床位"
destroy-on-close
>
<!-- Element Plus 的树状组件 el-tree -->
<!-- :data="data" 绑定树状结构的数据data 是一个响应式变量 -->
<!-- :props="defaultProps" 配置树状结构的属性如节点的标识显示文本子节点的字段等 -->
<!-- accordion 使树状结构以手风琴模式显示即同一时间只有一个节点可以展开 -->
<!-- @node-click="checkBed" 监听节点点击事件当点击节点时调用 checkBed 函数 -->
<el-tree
:data="data"
:props="defaultProps"
accordion
@node-click="checkBed"
/>
</el-dialog>
</template>
<script setup lang="ts" name="useProTable">
// Vue ref
import { ref } from "vue";
// API getBuildTree
import { getBuildTree } from "@/apis/bookManage";
// data undefined
const data: any = ref();
// dialogVisible false
const dialogVisible = ref(false);
// drawerProps DialogProps undefined
const drawerProps = ref<DialogProps>();
// DialogProps
// treeApi PromisePromise
interface DialogProps {
treeApi: (params: any) => Promise<any>;
}
// defaultProps el-tree
// id: "id" "id"
// label: "name" "name"
// children: "childrenList" "childrenList"
const defaultProps = {
id: "id",
label: "name",
children: "childrenList"
};
// treeAcceptParams
const treeAcceptParams = async (params: DialogProps) => {
// drawerProps
drawerProps.value = params;
//
dialogVisible.value = true;
// getBuildTree
const res: any = await getBuildTree();
// data
data.value = res.data;
};
// treeAcceptParams 便
defineExpose({
treeAcceptParams
});
// "getCheckBedInfo"
//
const emit = defineEmits<{
(event: "getCheckBedInfo", val: any): void
}>();
// checkBed
const checkBed = (bed: any) => {
// level 4
if (bed.level === 4) {
// "getCheckBedInfo"
emit("getCheckBedInfo", bed);
//
dialogVisible.value = false;
}
};
</script>
<style lang="scss" scoped></style>

@ -0,0 +1,189 @@
<template>
<!-- Element Plus 的上传组件 el-upload -->
<!-- v-model:file-list="imageList" 双向绑定上传文件列表imageList 是一个响应式变量 -->
<!-- :action="requestUrl" 设置上传文件的接口地址requestUrl 是一个变量 -->
<!-- :headers="{ token: token }" 设置上传请求的头部信息包含 token -->
<!-- list-type="picture-card" 设置上传文件列表的展示类型为图片卡片形式 -->
<!-- :before-upload="uploadBefore" 在文件上传前调用 uploadBefore 函数进行处理 -->
<!-- :on-error="handleError" 当文件上传失败时调用 handleError 函数 -->
<!-- :on-success="handleSuccess" 当文件上传成功时调用 handleSuccess 函数 -->
<!-- :on-preview="handlePreview" 当点击预览图片时调用 handlePreview 函数 -->
<!-- :on-remove="handleRemove" 当移除上传文件时调用 handleRemove 函数 -->
<el-upload
v-model:file-list="imageList"
:action="requestUrl"
:headers="{ token: token }"
list-type="picture-card"
:before-upload="uploadBefore"
:on-error="handleError"
:on-success="handleSuccess"
:on-preview="handlePreview"
:on-remove="handleRemove"
>
<!-- Element Plus 的图标组件 el-icon展示一个加号图标 -->
<el-icon>
<Plus />
</el-icon>
</el-upload>
<!-- Element Plus 的对话框组件 el-dialog用于展示预览图片 -->
<!-- v-model="dialogVisible" 双向绑定对话框的显示隐藏状态dialogVisible 是一个响应式变量 -->
<el-dialog v-model="dialogVisible">
<!-- img 标签用于显示预览图片w-full 可能是一个自定义的类名表示宽度占满:src="dialogImageUrl" 绑定图片的源地址dialogImageUrl 是一个响应式变量 -->
<img w-full :src="dialogImageUrl" alt="Preview Image" />
</el-dialog>
</template>
<!--
TODO 上传图片 / 使用方法
// 1.
import elderListDialog from "@/components/elderListDialog/index.vue";
// 2.使
<uploadImage :uploadParams="uploadParams" @setImageData='setImageData' />
// 3. single / multiple
ref({
uploadType: "single",
imageList: ref<UploadUserFile[]>([])
});
const setImageData = (url: string) => {
formData.value.picture = url;
};
// 4.()
//
uploadParams.value.imageList = []
//
uploadParams.value.imageList.push({
uid: 0,
name: "0.jpeg",
url: formData.value.image
});
-->
<script lang="ts" setup>
// Vuex store
import store from "@/store";
// URL
import { baseUrl } from "@/utils/http";
// Element Plus ElMessage
import { ElMessage } from "element-plus";
// Element Plus Plus
import { Plus } from "@element-plus/icons-vue";
// Vue
import { defineEmits, onMounted, ref, watch } from "vue";
// Element Plus
import type { UploadUserFile, ElUpload } from "element-plus";
// dialogImageUrl URL
const dialogImageUrl = ref("");
// dialogVisible false
const dialogVisible = ref(false);
// imageList
const imageList = ref<UploadUserFile[]>([]);
// props uploadParams
const props = defineProps({
uploadParams: Object
});
//
onMounted(() => {
// uploadParams imageList imageList
props.uploadParams?.["imageList"].forEach((image: any) => {
imageList.value.push(image);
});
});
// URL
const requestUrl = baseUrl + "file/uploadImg";
// Vuex store token
const token = store.state.app.token;
// imageUrlList URL
const imageUrlList = ref<any[]>([]);
// "setImageData"
const emits = defineEmits(["setImageData"]);
// updateData
const updateData = () => {
// imageUrlList
imageUrlList.value = [];
// imageList URL imageUrlList
imageList.value.forEach(image => imageUrlList.value.push(image.url));
// data
const data = ref<any>();
//
if (props.uploadParams?.["uploadType"] === "single") {
// URL data
data.value = imageUrlList.value[0];
} else {
// imageUrlList data
data.value = imageUrlList;
}
// "setImageData"
emits("setImageData", data);
};
// imageList
watch(imageList, (value, oldValue, onCleanup) => {
// imageList 1
if (props.uploadParams?.["uploadType"] === "single" && imageList.value.length > 1) {
//
imageList.value.splice(0, 1);
//
updateData();
}
});
// uploadBefore
const uploadBefore = (file: any) => {
// jpeg png
const isJPG = file.type === "image/jpeg" || file.type === "image/png";
// 2MB
const isLt2M = file.size / 1024 / 1024 < 2;
// jpeg png
if (!isJPG) {
ElMessage.error("只能上传jpg/png文件");
}
// 2MB
if (!isLt2M) {
ElMessage.error("文件大小不能超过2MB");
}
//
return isJPG && isLt2M;
};
// handleError
const handleError = (response: any) => {
//
ElMessage.error(response.data.msg);
};
// handleSuccess
const handleSuccess = (response: any, uploadFile: any) => {
// URL url
uploadFile.url = response.data.url;
//
updateData();
};
// handleRemove
const handleRemove = async (uploadFile: any) => {
// imageList
imageList.value.filter((image) => image.uid!== uploadFile.uid);
//
updateData();
// TODO imageId : uploadFile.uid
};
// handlePreview
const handlePreview = (uploadFile: any) => {
// URL URL
dialogImageUrl.value = uploadFile.url!;
//
dialogVisible.value = true;
};
</script>

@ -0,0 +1,47 @@
<template>
<div>
<!-- 定义一个 Element Plus 的按钮点击该按钮会触发 addNew 方法 -->
<el-button @click="addNew"></el-button>
<!-- 使用 v-for 指令遍历 componentList 数组将数组中的每个元素渲染为一个组件 -->
<div v-for="(item, index) in componentList" :key="index">
<!-- 使用 el-component 动态添加组件 -->
<!-- :is="item.type" 指定要渲染的组件类型该类型从 componentList 数组元素的 type 属性获取 -->
<!-- :props="item.props" 传递组件所需的 props 数据这些数据从 componentList 数组元素的 props 属性获取 -->
<el-component :is="item.type" :props="item.props"></el-component>
</div>
</div>
</template>
<script>
// HelloWorld
import HelloWorld from './components/HelloWorld.vue'
export default {
// data
data() {
return {
// componentList
componentList: []
}
},
// methods
methods: {
// addNew componentList
addNew() {
// componentList
// type HelloWorld
// props HelloWorld msg 'Hello World!'
this.componentList.push({
type: HelloWorld,
props: {
msg: 'Hello World!'
}
})
}
},
// components HelloWorld
components: {
HelloWorld
}
}
</script>

@ -0,0 +1,42 @@
// 定义一个名为 Table 的命名空间
export namespace Table {
// 在 Table 命名空间内,定义一个接口 Pageable用于描述分页相关的属性
export interface Pageable {
// 当前页码,类型为数字
pageNum: number
// 每页显示的数量,类型为数字
pageSize: number
// 数据的总数量,类型为数字
total: number
}
// 在 Table 命名空间内,定义一个接口 TableStateProps用于描述表格相关的状态属性
export interface TableStateProps {
// 表格数据,是一个任意类型的数组
tableData: any[]
// 分页相关的属性,类型为 Pageable 接口
pageable: Pageable
// 搜索参数,是一个键值对对象,键为字符串,值为任意类型
searchParam: {
[key: string]: any
}
// 初始化的搜索参数,是一个键值对对象,键为字符串,值为任意类型
searchInitParam: {
[key: string]: any
}
// 用于获取总数的参数,是一个键值对对象,键为字符串,值为任意类型
totalParam: {
[key: string]: any
}
// 可选的图标相关属性,是一个键值对对象,键为字符串,值为任意类型,默认为 undefined
icon?: {
[key: string]: any
}
}
}
// 定义一个名为 HandleData 的命名空间
export namespace HandleData {
// 在 HandleData 命名空间内,定义一个类型别名 MessageType
// MessageType 的值可以是空字符串,或者是'success'、'warning'、'info'、'error' 中的一个
export type MessageType = '' |'success' | 'warning' | 'info' | 'error'
}

@ -0,0 +1,52 @@
// 从 element-plus 库中导入 ElMessageBox 和 ElMessage 组件,分别用于显示确认框和提示消息
import { ElMessageBox, ElMessage } from 'element-plus'
// 从 './interface' 文件中导入 HandleData 命名空间,用于获取其中定义的类型
import { HandleData } from './interface'
/**
* @description ()
* @param {Function} api api()
* @param {Object} params {id,params}()
* @param {String} message ()
* @param {String} confirmType icon(, warning)
* @return Promise
*/
// 定义一个泛型函数 useHandleDataP 表示 api 函数参数的类型R 表示 api 函数返回值的类型
export const useHandleData = <P = any, R = any>(
// api 是一个函数,接收类型为 P 的参数并返回一个 PromisePromise 的解析值类型为 R
api: (params: P) => Promise<R>,
// params 是 api 函数的参数,类型与 api 函数的参数类型一致
params: Parameters<typeof api>[0],
// message 是一个字符串,用于显示确认框中的提示信息
message: string,
// confirmType 是一个字符串,类型为 HandleData.MessageType默认值为 'warning'
confirmType: HandleData.MessageType = 'warning'
) => {
// 返回一个 Promise
return new Promise((resolve, reject) => {
// 显示一个确认框
ElMessageBox.confirm(`是否${message}?`, '温馨提示', {
// 确认按钮的文本
confirmButtonText: '确定',
// 取消按钮的文本
cancelButtonText: '取消',
// 确认框的图标类型
type: confirmType,
// 是否可拖动
draggable: true
})
.then(async () => {
// 调用 api 函数并等待其返回结果
const res = await api(params)
// 如果结果为 falsy 值,则 reject 该 Promise
if (!res) return reject(false)
// 显示一个成功提示消息
ElMessage({
type:'success',
message: `${message}成功!`
})
// resolve 该 Promise表示操作成功
resolve(true)
})
})
}

@ -0,0 +1,25 @@
// 从 '@/components/SvgIcon/index.vue' 导入 SvgIcon 组件
import SvgIcon from '@/components/SvgIcon/index.vue'
// 从 'vue' 导入 h 函数和 defineComponent 函数
import { h, defineComponent } from 'vue'
// 定义一个名为 useRenderIcon 的函数它接受两个参数iconName字符串类型和 attrs可选的任意类型
export function useRenderIcon(iconName: string, attrs?: any) {
// 使用 defineComponent 函数定义一个新的组件
return defineComponent({
// 新组件的名称为 'SvgIcon'
name: 'SvgIcon',
// 定义组件的渲染函数
render() {
// 使用 h 函数创建一个 SvgIcon 组件的虚拟节点
// 第一个参数是要创建的组件(这里是 SvgIcon
// 第二个参数是一个对象,包含要传递给 SvgIcon 组件的属性
// 'icon' 属性的值是传入的 iconName
// '...attrs' 表示将 attrs 对象中的所有属性也传递给 SvgIcon 组件
return h(SvgIcon, {
icon: iconName,
...attrs
})
}
})
}

@ -0,0 +1,51 @@
import { ref, computed } from 'vue'
/**
* @description
* @param {String} rowKey id
* */
// 定义一个名为 useSelection 的函数,用于处理表格多选数据的操作
// 接受一个可选参数 rowKey默认值为 'id',表示行数据中用于唯一标识的键名
export const useSelection = (rowKey = 'id') => {
// 创建一个响应式变量 isSelected用于表示是否选中了数据
// 初始值为 false类型为 boolean
const isSelected = ref<boolean>(false)
// 创建一个响应式变量 selectedList用于存储选中的数据列表
// 初始值为空数组,类型为 any[]
const selectedList = ref([])
// 创建一个计算属性 selectedListIds用于获取当前选中的所有数据的 id 数组
// 计算属性会根据依赖项(这里是 selectedList的变化而自动更新
const selectedListIds = computed((): string[] => {
// 初始化一个空数组 ids 用于存储 id
const ids: string[] = []
// 遍历 selectedList 中的每一项数据
selectedList.value.forEach(item => ids.push(item[rowKey]))
// 返回包含所有 id 的数组
return ids
})
/**
* @description
* @param {Array} rowArr
* @return void
*/
// 定义一个函数 selectionChange用于处理表格多选数据的变化事件
// 接受一个参数 rowArr类型为 any[],表示当前选中的所有数据
const selectionChange = (rowArr: any) => {
// 如果选中的数据列表为空,则将 isSelected 设置为 false
// 否则将 isSelected 设置为 true
rowArr.length === 0? (isSelected.value = false) : (isSelected.value = true)
// 更新 selectedList 为当前选中的数据列表
selectedList.value = rowArr
}
// 返回一个包含 isSelected、selectedList、selectedListIds 和 selectionChange 的对象
// 以便在其他地方使用这些变量和函数来处理表格多选数据
return {
isSelected,
selectedList,
selectedListIds,
selectionChange
}
}

@ -0,0 +1,98 @@
// import { ref } from 'vue'
// import { useRoute, onBeforeRouteUpdate } from 'vue-router'
// import { useCookies } from '@vueuse/integrations/useCookies'
// import { router } from '~/router'
// export function useTabList() {
// const route = useRoute()
// const cookie = useCookies()
// const activeTab = ref(route.path)
// const tabList = ref([
// {
// title: '后台首页',
// path: '/'
// }
// ])
// // 添加标签导航
// function addTab(tab) {
// let noTab = tabList.value.findIndex((t) => t.path == tab.path) == -1
// if (noTab) {
// tabList.value.push(tab)
// }
// cookie.set('tabList', tabList.value)
// }
// // 初始化标签导航列表
// function initTabList() {
// let tbs = cookie.get('tabList')
// if (tbs) {
// tabList.value = tbs
// }
// }
// initTabList()
// onBeforeRouteUpdate((to, from) => {
// activeTab.value = to.path
// addTab({
// title: to.meta.title,
// path: to.path
// })
// })
// const changeTab = (t) => {
// activeTab.value = t
// router.push(t)
// }
// const removeTab = (t) => {
// let tabs = tabList.value
// let a = activeTab.value
// if (a == t) {
// tabs.forEach((tab, index) => {
// if (tab.path == t) {
// const nextTab = tabs[index + 1] || tabs[index - 1]
// if (nextTab) {
// a = nextTab.path
// }
// }
// })
// }
// activeTab.value = a
// tabList.value = tabList.value.filter((tab) => tab.path != t)
// cookie.set('tabList', tabList.value)
// }
// const handleClose = (c) => {
// if (c == 'clearAll') {
// // 切换回首页
// activeTab.value = '/'
// // 过滤只剩下首页
// tabList.value = [
// {
// title: '后台首页',
// path: '/'
// }
// ]
// } else if (c == 'clearOther') {
// // 过滤只剩下首页和当前激活
// tabList.value = tabList.value.filter(
// (tab) => tab.path == '/' || tab.path == activeTab.value
// )
// }
// cookie.set('tabList', tabList.value)
// }
// return {
// activeTab,
// tabList,
// changeTab,
// removeTab,
// handleClose
// }
// }

@ -0,0 +1,210 @@
import { Table } from './interface'
import { reactive, computed, toRefs } from 'vue'
/**
* @description table
* @param {Function} api api ()
* @param {Object} initParam ({})
* @param {Boolean} isPageable (true)
* @param {Function} dataCallBack ()
* */
// 定义一个名为 useTable 的函数,用于封装表格页面的操作方法
// api 是获取表格数据的 API 方法,是必传参数
// initParam 是获取数据的初始化参数,非必传,默认值为空对象
// isPageable 表示是否有分页功能,非必传,默认值为 true
// dataCallBack 是对后台返回的数据进行处理的方法,非必传
export const useTable = (
api: (params: any) => Promise<any>,
initParam: object = {},
isPageable = true,
dataCallBack?: (data: any) => any
) => {
// 使用 reactive 创建一个响应式的 state 对象,包含表格数据、分页数据、查询参数等
const state = reactive<Table.TableStateProps>({
// 表格数据,初始值为空数组
tableData: [],
// 分页数据,包含当前页数、每页显示条数和总条数
pageable: {
// 当前页数,初始值为 1
pageNum: 1,
// 每页显示条数,初始值为 10
pageSize: 10,
// 总条数,初始值为 0
total: 0
},
// 查询参数,只包括查询条件,初始值为空对象
searchParam: {},
// 初始化默认的查询参数,初始值为空对象
searchInitParam: {},
// 总参数,包含分页和查询参数,初始值为空对象
totalParam: {}
})
/**
* @description (,)
* */
// 定义一个计算属性 pageParam用于获取和设置分页查询参数
const pageParam = computed({
// 获取分页查询参数
get: () => {
return {
pageNum: state.pageable.pageNum,
pageSize: state.pageable.pageSize
}
},
// 设置分页查询参数,这里暂时为空,可根据需要实现
set: (newVal: any) => { // 我是分页更新之后的值
}
})
/**
* @description
* @return void
* */
// 定义一个异步函数 getTableList用于获取表格数据
const getTableList = async () => {
try {
// 先把初始化参数和分页参数放到总参数里面
Object.assign(
state.totalParam,
initParam,
isPageable? pageParam.value : {}
)
// 请求前格式化数据,如果总参数中有 consultDate 字段,则进行处理
if (state.totalParam.consultDate) {
state.totalParam.startTime = state.totalParam.consultDate[0]
state.totalParam.endTime = state.totalParam.consultDate[1]
delete state.totalParam.consultDate
}
// 调用 API 方法获取数据
let { data } = await api({
...state.searchInitParam,
...state.totalParam
})
// 如果有数据处理回调函数,则对数据进行处理
dataCallBack && (data = dataCallBack(data))
// 获取当前表格数据,如果有分页,则取 data.list否则取 data
state.tableData = isPageable? data.list : data
// 从返回的数据中获取分页信息
const { pageNum, pageSize, total } = data
// 如果有分页,则更新分页信息
isPageable && updatePageable({ pageNum, pageSize, total })
} catch (error) {
// 如果发生错误,打印错误信息
console.log(error)
}
}
/**
* @description
* @return void
* */
// 定义一个函数 updatedTotalParam用于更新总参数
const updatedTotalParam = () => {
state.totalParam = {}
// 处理查询参数,可以给查询参数加自定义前缀操作
const nowSearchParam: { [key: string]: any } = {}
// 防止手动清空输入框携带参数(这里可以自定义查询参数前缀)
for (const key in state.searchParam) {
// * 某些情况下参数为 false/0 也应该携带参数
if (
state.searchParam[key] ||
state.searchParam[key] === false ||
state.searchParam[key] === 0
) {
nowSearchParam[key] = state.searchParam[key]
}
}
// 将处理后的查询参数和分页参数合并到总参数中
Object.assign(
state.totalParam,
nowSearchParam,
isPageable? pageParam.value : {}
)
}
/**
* @description
* @param {Object} resPageable
* @return void
* */
// 定义一个函数 updatePageable用于更新分页信息
const updatePageable = (resPageable: Table.Pageable) => {
// 使用 Object.assign 方法将后台返回的分页数据合并到 state.pageable 中
Object.assign(state.pageable, resPageable)
}
/**
* @description
* @return void
* */
// 定义一个函数 search用于进行表格数据查询
const search = () => {
// 将当前页数设置为 1
state.pageable.pageNum = 1
// 更新总参数
updatedTotalParam()
// 获取表格数据
getTableList()
}
/**
* @description
* @return void
* */
// 定义一个函数 reset用于重置表格数据
const reset = () => {
// 将当前页数设置为 1
state.pageable.pageNum = 1
// 清空搜索参数
state.searchParam = {}
// 重置搜索表单时,如果有默认搜索参数,则重置为默认值
Object.keys(state.searchInitParam).forEach(key => {
state.searchParam[key] = state.searchInitParam[key]
})
// 更新总参数
updatedTotalParam()
// 获取表格数据
getTableList()
}
/**
* @description
* @param {Number} val
* @return void
* */
// 定义一个函数 handleSizeChange用于处理每页条数改变的事件
const handleSizeChange = (val: number) => {
// 将当前页数设置为 1
state.pageable.pageNum = 1
// 更新每页显示条数
state.pageable.pageSize = val
// 获取表格数据
getTableList()
}
/**
* @description
* @param {Number} val
* @return void
* */
// 定义一个函数 handleCurrentChange用于处理当前页改变的事件
const handleCurrentChange = (val: number) => {
// 更新当前页数
state.pageable.pageNum = val
// 获取表格数据
getTableList()
}
// 返回一个包含 state 的响应式引用、获取表格数据的函数、查询函数、重置函数、处理每页条数改变和当前页改变的函数以及更新总参数的函数
return {
...toRefs(state),
getTableList,
search,
reset,
handleSizeChange,
handleCurrentChange,
updatedTotalParam
}
}
Loading…
Cancel
Save