create-jnrs-template-vue 1.1.12 → 1.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/jnrs-template-vue/README.md +5 -1
- package/jnrs-template-vue/components.d.ts +3 -4
- package/jnrs-template-vue/package.json +5 -4
- package/jnrs-template-vue/src/App.vue +3 -1
- package/jnrs-template-vue/src/api/common/index.ts +20 -5
- package/jnrs-template-vue/src/api/demos/index.ts +86 -0
- package/jnrs-template-vue/src/api/system/index.ts +1 -1
- package/jnrs-template-vue/src/api/user/index.ts +1 -1
- package/jnrs-template-vue/src/components/base/ImageView.vue +58 -0
- package/jnrs-template-vue/src/components/common/JnPagination.vue +83 -0
- package/jnrs-template-vue/src/components/common/JnTable.vue +133 -0
- package/jnrs-template-vue/src/composables/{common → base}/useAvatar.ts +1 -1
- package/jnrs-template-vue/src/composables/{useForm.ts → common/useForm.ts} +1 -1
- package/jnrs-template-vue/src/composables/{useModal.ts → common/useModal.ts} +1 -1
- package/jnrs-template-vue/src/composables/{useTable.ts → common/usePagination.ts} +1 -1
- package/jnrs-template-vue/src/composables/common/useTable.ts +35 -0
- package/jnrs-template-vue/src/composables/{useUser.ts → common/useUser.ts} +1 -1
- package/jnrs-template-vue/src/composables/tools/useReactivityTableHeight.ts +63 -0
- package/jnrs-template-vue/src/layout/SideMenu.vue +1 -1
- package/jnrs-template-vue/src/layout/SideMenuItem.vue +1 -1
- package/jnrs-template-vue/src/layout/TopHeader.vue +3 -3
- package/jnrs-template-vue/src/layout/index.vue +1 -1
- package/jnrs-template-vue/src/router/index.ts +1 -1
- package/jnrs-template-vue/src/utils/file.ts +15 -1
- package/jnrs-template-vue/src/utils/{common.ts → packages.ts} +33 -1
- package/jnrs-template-vue/src/utils/permissions.ts +1 -1
- package/jnrs-template-vue/src/views/demos/crud/index.vue +104 -2
- package/jnrs-template-vue/src/views/demos/unitTest/RequestPage.vue +141 -0
- package/jnrs-template-vue/src/views/demos/unitTest/index.vue +5 -99
- package/jnrs-template-vue/src/views/login/index.vue +12 -12
- package/jnrs-template-vue/src/views/system/menu/index.vue +1 -1
- package/jnrs-template-vue/src/views/system/mine/baseInfo.vue +3 -3
- package/jnrs-template-vue/src/views/system/mine/securitySettings.vue +1 -1
- package/jnrs-template-vue/viteMockServe/index.ts +9 -0
- package/jnrs-template-vue/viteMockServe/tableRes.json +275 -0
- package/package.json +1 -1
- package/jnrs-template-vue/src/api/mock/index.ts +0 -34
- package/jnrs-template-vue/src/api/request.ts +0 -40
- package/jnrs-template-vue/src/composables/index.ts +0 -11
- package/jnrs-template-vue/src/stores/index.ts +0 -2
- package/jnrs-template-vue/src/stores/mock.ts +0 -21
|
@@ -20,6 +20,7 @@ jnrs-template-vue/
|
|
|
20
20
|
├── node_modules/ # 项目依赖
|
|
21
21
|
├── viteMockServe/ # Mock 服务配置(用于开发环境模拟 API)
|
|
22
22
|
├── public/ # 静态资源
|
|
23
|
+
├── layout/ # 布局
|
|
23
24
|
├── src/ # 源码目录
|
|
24
25
|
│ ├── components/ # Vue 组件
|
|
25
26
|
│ ├── composables/ # 组合式函数
|
|
@@ -41,4 +42,7 @@ jnrs-template-vue/
|
|
|
41
42
|
├── .env.production # 生产环境变量(全局)
|
|
42
43
|
├── .gitignore # Git 忽略规则
|
|
43
44
|
├── .prettierrc.json # Prettier 格式化配置
|
|
44
|
-
```
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## ⚠️ 注意事项
|
|
48
|
+
- layout 中使用了 KeepAlive 内置组件进行组件实例缓存,使用生命周期进行如“副作用清理“时注意用 onActivated / onDeactivated 替代 onMounted / onUnmounted
|
|
@@ -12,12 +12,12 @@ export {}
|
|
|
12
12
|
declare module 'vue' {
|
|
13
13
|
export interface GlobalComponents {
|
|
14
14
|
ElAside: typeof import('element-plus/es')['ElAside']
|
|
15
|
+
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
|
15
16
|
ElBadge: typeof import('element-plus/es')['ElBadge']
|
|
16
17
|
ElButton: typeof import('element-plus/es')['ElButton']
|
|
17
18
|
ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
|
|
18
19
|
ElCard: typeof import('element-plus/es')['ElCard']
|
|
19
20
|
ElCascader: typeof import('element-plus/es')['ElCascader']
|
|
20
|
-
ElCol: typeof import('element-plus/es')['ElCol']
|
|
21
21
|
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
|
|
22
22
|
ElContainer: typeof import('element-plus/es')['ElContainer']
|
|
23
23
|
ElDatePickerPanel: typeof import('element-plus/es')['ElDatePickerPanel']
|
|
@@ -32,10 +32,9 @@ declare module 'vue' {
|
|
|
32
32
|
ElMenu: typeof import('element-plus/es')['ElMenu']
|
|
33
33
|
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
|
34
34
|
ElOption: typeof import('element-plus/es')['ElOption']
|
|
35
|
+
ElPagination: typeof import('element-plus/es')['ElPagination']
|
|
35
36
|
ElPopover: typeof import('element-plus/es')['ElPopover']
|
|
36
|
-
|
|
37
|
-
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
|
38
|
-
ElRow: typeof import('element-plus/es')['ElRow']
|
|
37
|
+
ElProgress: typeof import('element-plus/es')['ElProgress']
|
|
39
38
|
ElSelect: typeof import('element-plus/es')['ElSelect']
|
|
40
39
|
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
|
41
40
|
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jnrs-template-vue",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.14",
|
|
4
4
|
"description": "JNRS 信息化管理系统模板",
|
|
5
5
|
"author": "Talia-Tan",
|
|
6
6
|
"private": true,
|
|
@@ -19,9 +19,10 @@
|
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"@element-plus/icons-vue": "^2.3.2",
|
|
22
|
-
"@jnrs/core": "1.1.
|
|
23
|
-
"@jnrs/shared": "1.1.
|
|
24
|
-
"@jnrs/vue-core": "1.1.
|
|
22
|
+
"@jnrs/core": "1.1.6",
|
|
23
|
+
"@jnrs/shared": "1.1.6",
|
|
24
|
+
"@jnrs/vue-core": "1.1.6",
|
|
25
|
+
"@vueuse/core": "^14.1.0",
|
|
25
26
|
"element-plus": "^2.11.9",
|
|
26
27
|
"pinia": "^3.0.4",
|
|
27
28
|
"pinia-plugin-persistedstate": "^4.7.1",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { onMounted, watch } from 'vue'
|
|
3
|
-
import { useSystemStore } from '
|
|
3
|
+
import { useSystemStore } from '@jnrs/vue-core/pinia'
|
|
4
4
|
import { ElConfigProvider } from 'element-plus'
|
|
5
5
|
import zhCn from 'element-plus/es/locale/lang/zh-CN'
|
|
6
6
|
import en from 'element-plus/es/locale/lang/en'
|
|
@@ -9,6 +9,7 @@ import { changeLocales as changeLocalesForShared } from '@jnrs/shared/locales'
|
|
|
9
9
|
import { changeLocales as changeLocalesForCore } from '@jnrs/core/locales'
|
|
10
10
|
|
|
11
11
|
const { locale } = useI18n()
|
|
12
|
+
|
|
12
13
|
const { theme } = useSystemStore()
|
|
13
14
|
watch(
|
|
14
15
|
() => theme.locale,
|
|
@@ -21,6 +22,7 @@ watch(
|
|
|
21
22
|
immediate: true
|
|
22
23
|
}
|
|
23
24
|
)
|
|
25
|
+
|
|
24
26
|
const localeMap = {
|
|
25
27
|
zhCn: zhCn,
|
|
26
28
|
en: en
|
|
@@ -1,9 +1,24 @@
|
|
|
1
|
-
import { request } from '
|
|
1
|
+
import { request } from '@jnrs/vue-core'
|
|
2
|
+
// import { useFetch } from '@vueuse/core'
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* 文件下载
|
|
6
|
+
* @param uniqueFileName 文件唯一名称(列表中一般是在 item.attachmentDocument/imageDocument.attachments[].uniqueFileName)
|
|
7
|
+
* @returns Blob
|
|
8
|
+
*/
|
|
9
|
+
export const FileApi = (uniqueFileName: string): Promise<Blob> => {
|
|
5
10
|
return request({
|
|
6
|
-
url: '/api/files/' +
|
|
7
|
-
method: 'get'
|
|
11
|
+
url: '/api/files/' + uniqueFileName,
|
|
12
|
+
method: 'get',
|
|
13
|
+
responseType: 'blob'
|
|
8
14
|
})
|
|
9
15
|
}
|
|
16
|
+
|
|
17
|
+
// export const FileApi = (uniqueFileName: string) => {
|
|
18
|
+
// return useFetch(`/api/files/${uniqueFileName}`, {
|
|
19
|
+
// method: 'get',
|
|
20
|
+
// headers: {
|
|
21
|
+
// responseType: 'blob'
|
|
22
|
+
// }
|
|
23
|
+
// }).blob()
|
|
24
|
+
// }
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { request } from '@jnrs/vue-core'
|
|
2
|
+
import type { ApiResponse } from '@/types'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 404 错误
|
|
6
|
+
*/
|
|
7
|
+
export const NotFoundApi = () => {
|
|
8
|
+
return request({
|
|
9
|
+
url: '/notFound',
|
|
10
|
+
method: 'get'
|
|
11
|
+
})
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 无权限
|
|
16
|
+
* @param showErrorMsg 是否显示错误信息
|
|
17
|
+
*/
|
|
18
|
+
export const NoAuth = (showErrorMsg: boolean) => {
|
|
19
|
+
return request({
|
|
20
|
+
url: '/auth/no',
|
|
21
|
+
method: 'post',
|
|
22
|
+
showErrorMsg
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 数据详情
|
|
28
|
+
*/
|
|
29
|
+
export const DetailsApi = (): Promise<ApiResponse> => {
|
|
30
|
+
return request({
|
|
31
|
+
url: '/details',
|
|
32
|
+
method: 'get'
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 附件(文件)信息
|
|
38
|
+
*/
|
|
39
|
+
export interface Attachment {
|
|
40
|
+
id: number
|
|
41
|
+
documentId: number
|
|
42
|
+
fileName: string
|
|
43
|
+
uniqueFileName: string
|
|
44
|
+
fileType: string // MIME 类型
|
|
45
|
+
fileSize: number // 单位:字节(Bytes)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 文档容器(用于图片或附件)
|
|
50
|
+
*/
|
|
51
|
+
export interface Document {
|
|
52
|
+
id: number
|
|
53
|
+
description: string | null
|
|
54
|
+
attachments: Attachment[]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 项目项
|
|
59
|
+
*/
|
|
60
|
+
export interface ProjectItem {
|
|
61
|
+
id: string
|
|
62
|
+
programCode: string
|
|
63
|
+
program: string
|
|
64
|
+
code: string
|
|
65
|
+
name: string
|
|
66
|
+
description: string
|
|
67
|
+
type: '敏捷型' | '瀑布型' | '混合型'
|
|
68
|
+
manager: string
|
|
69
|
+
budget: string
|
|
70
|
+
plannedStartDate: string
|
|
71
|
+
plannedFinishDate: string
|
|
72
|
+
progress: string
|
|
73
|
+
status: '进行中' | '已完成' | '未开始' | '已暂停'
|
|
74
|
+
imageDocument?: Document // 图片类附件
|
|
75
|
+
attachmentDocument?: Document // 普通文件类附件
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 数据详情
|
|
80
|
+
*/
|
|
81
|
+
export const DemosTableApi = (): Promise<ProjectItem[]> => {
|
|
82
|
+
return request({
|
|
83
|
+
url: '/mock/demos/table',
|
|
84
|
+
method: 'get'
|
|
85
|
+
})
|
|
86
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, onBeforeUnmount } from 'vue'
|
|
3
|
+
import { FileApi } from '@/api/common'
|
|
4
|
+
|
|
5
|
+
// 响应式数据
|
|
6
|
+
const squareUrl = ref<string>('')
|
|
7
|
+
|
|
8
|
+
let currentObjectUrl: string | null = null
|
|
9
|
+
|
|
10
|
+
// 加载图片
|
|
11
|
+
async function loadImage() {
|
|
12
|
+
try {
|
|
13
|
+
const blob = await FileApi('20251216134412632-1376c8a7-1f31-451c-9eb4-944aaa03497b.png')
|
|
14
|
+
|
|
15
|
+
// 创建 URL 并保存引用
|
|
16
|
+
currentObjectUrl = URL.createObjectURL(blob)
|
|
17
|
+
squareUrl.value = currentObjectUrl
|
|
18
|
+
} catch (error) {
|
|
19
|
+
console.error('加载头像失败:', error)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// 图片加载成功 → 立即释放内存(浏览器已缓存图像)
|
|
24
|
+
function handleImageLoad() {
|
|
25
|
+
if (currentObjectUrl) {
|
|
26
|
+
URL.revokeObjectURL(currentObjectUrl)
|
|
27
|
+
currentObjectUrl = null
|
|
28
|
+
console.log('✅ 头像加载完成,已释放 Blob URL')
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 图片加载失败 → 也释放内存
|
|
33
|
+
function handleImageError() {
|
|
34
|
+
if (currentObjectUrl) {
|
|
35
|
+
URL.revokeObjectURL(currentObjectUrl)
|
|
36
|
+
currentObjectUrl = null
|
|
37
|
+
console.error('❌ 头像加载失败')
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 组件卸载前 → 确保清理(防止用户快速跳转导致未加载完)
|
|
42
|
+
onBeforeUnmount(() => {
|
|
43
|
+
if (currentObjectUrl) {
|
|
44
|
+
URL.revokeObjectURL(currentObjectUrl)
|
|
45
|
+
currentObjectUrl = null
|
|
46
|
+
console.log('🧹 组件卸载,强制释放 Blob URL')
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// 启动加载
|
|
51
|
+
loadImage()
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
<template>
|
|
55
|
+
<el-avatar v-if="squareUrl" shape="square" :src="squareUrl" @load="handleImageLoad" @error="handleImageError" />
|
|
56
|
+
<!-- 可选:加载中状态 -->
|
|
57
|
+
<div v-else>加载中...</div>
|
|
58
|
+
</template>
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@Author : TanRui
|
|
3
|
+
@WeChat : Tan578853789
|
|
4
|
+
@File : JnPagination.vue
|
|
5
|
+
@Date : 2025/12/18
|
|
6
|
+
@Desc. : 分页组件
|
|
7
|
+
-->
|
|
8
|
+
|
|
9
|
+
<script setup lang="ts">
|
|
10
|
+
import { ref, watch } from 'vue'
|
|
11
|
+
import type { ComponentSize } from 'element-plus'
|
|
12
|
+
|
|
13
|
+
type PaginationModelValue = {
|
|
14
|
+
currentPage: number
|
|
15
|
+
pageSize: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface PaginationProps {
|
|
19
|
+
total: number
|
|
20
|
+
modelValue?: PaginationModelValue
|
|
21
|
+
size?: ComponentSize
|
|
22
|
+
background?: boolean
|
|
23
|
+
marginTop?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const props = withDefaults(defineProps<PaginationProps>(), {
|
|
27
|
+
modelValue: () => ({ currentPage: 1, pageSize: 10 }),
|
|
28
|
+
size: 'small',
|
|
29
|
+
background: true,
|
|
30
|
+
marginTop: '10px'
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const emit = defineEmits<{
|
|
34
|
+
(e: 'update:modelValue', value: { currentPage: number; pageSize: number }): void
|
|
35
|
+
(e: 'change'): void
|
|
36
|
+
}>()
|
|
37
|
+
|
|
38
|
+
const localPage = ref(props.modelValue.currentPage)
|
|
39
|
+
const localSize = ref(props.modelValue.pageSize)
|
|
40
|
+
|
|
41
|
+
watch(
|
|
42
|
+
() => props.modelValue,
|
|
43
|
+
(newVal) => {
|
|
44
|
+
localPage.value = newVal.currentPage
|
|
45
|
+
localSize.value = newVal.pageSize
|
|
46
|
+
},
|
|
47
|
+
{ deep: true }
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
// 页码变化
|
|
51
|
+
const handleCurrentChange = (page: number) => {
|
|
52
|
+
localPage.value = page
|
|
53
|
+
const newValue = { currentPage: page, pageSize: localSize.value }
|
|
54
|
+
emit('update:modelValue', newValue)
|
|
55
|
+
emit('change')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 每页条数变化时重置到第一页
|
|
59
|
+
const handleSizeChange = (size: number) => {
|
|
60
|
+
localSize.value = size
|
|
61
|
+
localPage.value = 1
|
|
62
|
+
const newValue = { currentPage: 1, pageSize: size }
|
|
63
|
+
emit('update:modelValue', newValue)
|
|
64
|
+
emit('change')
|
|
65
|
+
}
|
|
66
|
+
</script>
|
|
67
|
+
|
|
68
|
+
<template>
|
|
69
|
+
<el-pagination
|
|
70
|
+
v-model:current-page="localPage"
|
|
71
|
+
v-model:page-size="localSize"
|
|
72
|
+
:total="total"
|
|
73
|
+
:page-sizes="[10, 20, 50, 100]"
|
|
74
|
+
:size="size"
|
|
75
|
+
:background="background"
|
|
76
|
+
layout="->, total, sizes, prev, pager, next, jumper"
|
|
77
|
+
:style="{
|
|
78
|
+
marginTop: marginTop
|
|
79
|
+
}"
|
|
80
|
+
@current-change="handleCurrentChange"
|
|
81
|
+
@size-change="handleSizeChange"
|
|
82
|
+
/>
|
|
83
|
+
</template>
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@Author : TanRui
|
|
3
|
+
@WeChat : Tan578853789
|
|
4
|
+
@File : JnTable.vue
|
|
5
|
+
@Date : 2025/12/18
|
|
6
|
+
@Desc. : 表格组件
|
|
7
|
+
-->
|
|
8
|
+
|
|
9
|
+
<script setup lang="ts" generic="T extends Record<string, any>">
|
|
10
|
+
import { computed } from 'vue'
|
|
11
|
+
import type { UseReactivityTableHeightOptions } from '@/composables/tools/useReactivityTableHeight'
|
|
12
|
+
import { useReactivityTableHeight } from '@/composables/tools/useReactivityTableHeight'
|
|
13
|
+
|
|
14
|
+
type PaginationModelValue = {
|
|
15
|
+
currentPage: number
|
|
16
|
+
pageSize: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface Props {
|
|
20
|
+
/**
|
|
21
|
+
* 表数据
|
|
22
|
+
*/
|
|
23
|
+
data?: T[]
|
|
24
|
+
/**
|
|
25
|
+
* 行数据的 Key,在使用 reserve-selection 功能与显示树形数据时,该属性是必填的。
|
|
26
|
+
* 支持多层访问:user.info.id
|
|
27
|
+
*/
|
|
28
|
+
rowKey?: string
|
|
29
|
+
/**
|
|
30
|
+
* 总是显示横向滚动条
|
|
31
|
+
*/
|
|
32
|
+
scrollbar?: boolean
|
|
33
|
+
/**
|
|
34
|
+
* 表格高度,默认为 100%
|
|
35
|
+
* @default 100%
|
|
36
|
+
*/
|
|
37
|
+
height?: string | number
|
|
38
|
+
/**
|
|
39
|
+
* 表格高度是否自动计算,默认为 false
|
|
40
|
+
* @default false
|
|
41
|
+
*/
|
|
42
|
+
autoHeight?: boolean
|
|
43
|
+
/**
|
|
44
|
+
* 表格高度自动计算配置项
|
|
45
|
+
*/
|
|
46
|
+
autoHeightOptions?: UseReactivityTableHeightOptions
|
|
47
|
+
/**
|
|
48
|
+
* 是否显示序号列
|
|
49
|
+
*/
|
|
50
|
+
showIndexColumn?: boolean
|
|
51
|
+
/**
|
|
52
|
+
* 是否显示选择列
|
|
53
|
+
*/
|
|
54
|
+
showSelectionColumn?: boolean
|
|
55
|
+
/**
|
|
56
|
+
* 当前页码和每页大小(用于序号计算)
|
|
57
|
+
*/
|
|
58
|
+
pagination?: PaginationModelValue
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const {
|
|
62
|
+
rowKey = 'id',
|
|
63
|
+
scrollbar = true,
|
|
64
|
+
height = null,
|
|
65
|
+
autoHeight = true,
|
|
66
|
+
autoHeightOptions = {},
|
|
67
|
+
showIndexColumn = true,
|
|
68
|
+
showSelectionColumn = true,
|
|
69
|
+
pagination = { currentPage: 0, pageSize: 0 }
|
|
70
|
+
} = defineProps<Props>()
|
|
71
|
+
|
|
72
|
+
const { maxHeight: reactiveMaxHeight } = useReactivityTableHeight(autoHeightOptions ?? {})
|
|
73
|
+
|
|
74
|
+
// 表格最终高度
|
|
75
|
+
const finalHeight = computed(() => {
|
|
76
|
+
if (height != null) {
|
|
77
|
+
return height
|
|
78
|
+
}
|
|
79
|
+
if (autoHeight) {
|
|
80
|
+
return reactiveMaxHeight.value
|
|
81
|
+
}
|
|
82
|
+
return '100%'
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
// 索引列计算
|
|
86
|
+
const getIndex = (index: number) => {
|
|
87
|
+
return index + (pagination.currentPage - 1) * pagination.pageSize + 1
|
|
88
|
+
}
|
|
89
|
+
</script>
|
|
90
|
+
|
|
91
|
+
<template>
|
|
92
|
+
<el-table
|
|
93
|
+
class="jn_table"
|
|
94
|
+
header-cell-class-name="jn_table_header"
|
|
95
|
+
ref="jnTableRef"
|
|
96
|
+
:row-key="rowKey"
|
|
97
|
+
:data="data"
|
|
98
|
+
border
|
|
99
|
+
:scrollbar-always-on="scrollbar"
|
|
100
|
+
:height="finalHeight"
|
|
101
|
+
>
|
|
102
|
+
<el-table-column v-if="showIndexColumn" label="序号" type="index" align="center" width="60">
|
|
103
|
+
<template #default="{ $index }">
|
|
104
|
+
<span>{{ getIndex($index) }}</span>
|
|
105
|
+
</template>
|
|
106
|
+
</el-table-column>
|
|
107
|
+
<slot></slot>
|
|
108
|
+
<el-table-column
|
|
109
|
+
v-if="showSelectionColumn"
|
|
110
|
+
type="selection"
|
|
111
|
+
width="60"
|
|
112
|
+
align="center"
|
|
113
|
+
fixed="right"
|
|
114
|
+
:reserve-selection="true"
|
|
115
|
+
/>
|
|
116
|
+
</el-table>
|
|
117
|
+
</template>
|
|
118
|
+
|
|
119
|
+
<style lang="scss" scoped>
|
|
120
|
+
.jn_table {
|
|
121
|
+
width: 100%;
|
|
122
|
+
|
|
123
|
+
:deep(.jn_table_header) {
|
|
124
|
+
color: var(--jnrs-font-primary) !important;
|
|
125
|
+
background: var(--jnrs-background-head) !important;
|
|
126
|
+
|
|
127
|
+
.cell {
|
|
128
|
+
padding: 0 8px;
|
|
129
|
+
line-height: 1.2em;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
</style>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ref, toRefs, watch } from 'vue'
|
|
2
2
|
// import { getImgSrc } from '@/utils/common'
|
|
3
|
-
// import { useCommonStore } from '
|
|
3
|
+
// import { useCommonStore } from '@jnrs/vue-core/pinia'
|
|
4
4
|
// import imgFailed from '@/assets/img/common/404.png'
|
|
5
5
|
import defaultAvatar from '@/assets/images/common/avatar.png'
|
|
6
6
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @Author : TanRui
|
|
3
|
+
* @WeChat : Tan578853789
|
|
4
|
+
* @File : useTable.ts
|
|
5
|
+
* @Date : 2025/11/30
|
|
6
|
+
* @Desc. : 表格
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { ref, computed } from 'vue'
|
|
10
|
+
|
|
11
|
+
export function useTable<T>(initialData: T[] = []) {
|
|
12
|
+
const data = ref<T[]>(initialData)
|
|
13
|
+
const pageSize = ref(10)
|
|
14
|
+
const currentPage = ref(1)
|
|
15
|
+
|
|
16
|
+
// 分页数据
|
|
17
|
+
const paginatedData = computed(() => {
|
|
18
|
+
const start = (currentPage.value - 1) * pageSize.value
|
|
19
|
+
return data.value?.slice(start, start + pageSize.value)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
// 设置新的数据
|
|
23
|
+
const setNewData = (newData: T[]) => {
|
|
24
|
+
data.value = newData
|
|
25
|
+
currentPage.value = 1
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
data,
|
|
30
|
+
pageSize,
|
|
31
|
+
currentPage,
|
|
32
|
+
paginatedData,
|
|
33
|
+
setNewData
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @Author : TanRui
|
|
3
|
+
* @WeChat : Tan578853789
|
|
4
|
+
* @File : useReactivityTableHeight.ts
|
|
5
|
+
* @Date : 2025/12/10
|
|
6
|
+
* @Desc. : 响应式根据视口动态计算表格高度
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { ref, onActivated, onDeactivated } from 'vue'
|
|
10
|
+
import { debounce } from '@jnrs/shared/lodash'
|
|
11
|
+
|
|
12
|
+
// 总底部高度
|
|
13
|
+
const PADDING_HEIGHT = 40
|
|
14
|
+
|
|
15
|
+
// 总边距
|
|
16
|
+
const FOOT_HEIGHT = 40
|
|
17
|
+
|
|
18
|
+
export interface UseReactivityTableHeightOptions {
|
|
19
|
+
// 表格类名,默认值 el-table
|
|
20
|
+
className?: string
|
|
21
|
+
// 默认高度,默认值 300
|
|
22
|
+
bottomGap?: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param className 表格类名,默认值 el-table,如果页面存在多个表格,则必须分别传入自定义类名
|
|
27
|
+
* @param bottomGap 底部间隔,默认值 77
|
|
28
|
+
* @return maxHeight 表格高度
|
|
29
|
+
*/
|
|
30
|
+
export function useReactivityTableHeight({
|
|
31
|
+
className = 'el-table',
|
|
32
|
+
bottomGap = PADDING_HEIGHT + FOOT_HEIGHT
|
|
33
|
+
}: UseReactivityTableHeightOptions = {}) {
|
|
34
|
+
const maxHeight = ref(500)
|
|
35
|
+
|
|
36
|
+
const calculateTableHeight = debounce(
|
|
37
|
+
() => {
|
|
38
|
+
const element = document.querySelector(`.${className}`)
|
|
39
|
+
if (element) {
|
|
40
|
+
const rect = element.getBoundingClientRect()
|
|
41
|
+
const innerHeight = window.innerHeight
|
|
42
|
+
const gap = rect.top + bottomGap
|
|
43
|
+
const mh = innerHeight - gap
|
|
44
|
+
maxHeight.value = mh > 100 ? mh : 100
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
300,
|
|
48
|
+
{ leading: false }
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
// 初始化高度计算
|
|
52
|
+
onActivated(() => {
|
|
53
|
+
calculateTableHeight()
|
|
54
|
+
window.addEventListener('resize', calculateTableHeight)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
// 清理事件监听器
|
|
58
|
+
onDeactivated(() => {
|
|
59
|
+
window.removeEventListener('resize', calculateTableHeight)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
return { maxHeight }
|
|
63
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import SideMenuItem from './SideMenuItem.vue'
|
|
3
3
|
import { storeToRefs } from 'pinia'
|
|
4
4
|
import { useRoute } from '@jnrs/vue-core/router'
|
|
5
|
-
import { useSystemStore, useMenuStore } from '
|
|
5
|
+
import { useSystemStore, useMenuStore } from '@jnrs/vue-core/pinia'
|
|
6
6
|
|
|
7
7
|
const systemStore = useSystemStore()
|
|
8
8
|
const { menuCollapse } = storeToRefs(systemStore)
|
|
@@ -4,10 +4,10 @@ import { ref, computed } from 'vue'
|
|
|
4
4
|
import { storeToRefs } from 'pinia'
|
|
5
5
|
import { ElMessageBox } from 'element-plus'
|
|
6
6
|
import { handleRouter } from '@jnrs/vue-core/router'
|
|
7
|
-
import { useSystemStore, useAuthStore, useMenuStore } from '
|
|
8
|
-
import { useAvatar } from '@/composables/
|
|
7
|
+
import { useSystemStore, useAuthStore, useMenuStore } from '@jnrs/vue-core/pinia'
|
|
8
|
+
import { useAvatar } from '@/composables/base/useAvatar'
|
|
9
9
|
import { LogoutApi } from '@/api/system'
|
|
10
|
-
import { getDictLabel, getDictColor } from '@/utils/
|
|
10
|
+
import { getDictLabel, getDictColor } from '@/utils/packages'
|
|
11
11
|
|
|
12
12
|
const { avatar } = useAvatar()
|
|
13
13
|
const { userInfo, clearAuth } = useAuthStore()
|