create-jnrs-vue 1.2.20 → 1.2.21
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-vue/components.d.ts +3 -1
- package/jnrs-vue/package.json +3 -3
- package/jnrs-vue/src/App.vue +1 -1
- package/jnrs-vue/src/api/demos/index.ts +12 -3
- package/jnrs-vue/src/api/system/index.ts +3 -0
- package/jnrs-vue/src/assets/styles/animation.scss +15 -0
- package/jnrs-vue/src/components/common/CardTable.vue +1 -1
- package/jnrs-vue/src/components/common/DictTag.vue +8 -6
- package/jnrs-vue/src/components/common/ImageView.vue +1 -1
- package/jnrs-vue/src/components/common/PdfView.vue +1 -1
- package/jnrs-vue/src/components/select/SelectManager.vue +2 -2
- package/jnrs-vue/src/composables/useCrud.ts +131 -0
- package/jnrs-vue/src/layout/RouterTabs.vue +151 -3
- package/jnrs-vue/src/layout/SideMenu.vue +212 -139
- package/jnrs-vue/src/layout/TopHeader.vue +44 -22
- package/jnrs-vue/src/locales/en.ts +40 -1
- package/jnrs-vue/src/locales/index.ts +2 -2
- package/jnrs-vue/src/locales/zhCn.ts +40 -1
- package/jnrs-vue/src/main.ts +2 -2
- package/jnrs-vue/src/router/routes.ts +1 -1
- package/jnrs-vue/src/views/demos/crud/index.vue +47 -9
- package/jnrs-vue/src/views/demos/simpleTable/index.vue +2 -2
- package/jnrs-vue/src/views/home/index.vue +312 -3
- package/jnrs-vue/src/views/login/index.vue +2 -2
- package/jnrs-vue/vite.config.ts +2 -1
- package/jnrs-vue/viteMockServe/fail.ts +3 -3
- package/jnrs-vue/viteMockServe/file.ts +4 -4
- package/jnrs-vue/viteMockServe/success.ts +9 -1
- package/package.json +1 -1
- package/jnrs-vue/src/layout/RouterTabs /344/277/256/345/244/215/350/267/257/347/224/261/350/267/263/350/275/254/346/220/272/345/270/246/345/217/202/346/225/260/351/227/256/351/242/230.vue" +0 -150
package/jnrs-vue/components.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ declare module 'vue' {
|
|
|
18
18
|
ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
|
|
19
19
|
ElCard: typeof import('element-plus/es')['ElCard']
|
|
20
20
|
ElCascader: typeof import('element-plus/es')['ElCascader']
|
|
21
|
+
ElCol: typeof import('element-plus/es')['ElCol']
|
|
21
22
|
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
|
|
22
23
|
ElContainer: typeof import('element-plus/es')['ElContainer']
|
|
23
24
|
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
|
@@ -34,6 +35,8 @@ declare module 'vue' {
|
|
|
34
35
|
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
|
35
36
|
ElOption: typeof import('element-plus/es')['ElOption']
|
|
36
37
|
ElPopover: typeof import('element-plus/es')['ElPopover']
|
|
38
|
+
ElRow: typeof import('element-plus/es')['ElRow']
|
|
39
|
+
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
|
37
40
|
ElSelect: typeof import('element-plus/es')['ElSelect']
|
|
38
41
|
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
|
39
42
|
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
|
@@ -42,7 +45,6 @@ declare module 'vue' {
|
|
|
42
45
|
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
|
43
46
|
ElTabs: typeof import('element-plus/es')['ElTabs']
|
|
44
47
|
ElTag: typeof import('element-plus/es')['ElTag']
|
|
45
|
-
ElUpload: typeof import('element-plus/es')['ElUpload']
|
|
46
48
|
ElWatermark: typeof import('element-plus/es')['ElWatermark']
|
|
47
49
|
}
|
|
48
50
|
export interface GlobalDirectives {
|
package/jnrs-vue/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jnrs-vue",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.21",
|
|
4
4
|
"description": "JNRS 信息化管理系统",
|
|
5
5
|
"author": "talia_tan",
|
|
6
6
|
"private": true,
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"@element-plus/icons-vue": "^2.3.2",
|
|
22
|
-
"@jnrs/shared": "1.1.
|
|
23
|
-
"@jnrs/vue-core": "1.2.
|
|
22
|
+
"@jnrs/shared": "1.1.15",
|
|
23
|
+
"@jnrs/vue-core": "1.2.10",
|
|
24
24
|
"@jnrs/lingshu-smart": "2.2.4",
|
|
25
25
|
"@vueuse/core": "^14.1.0",
|
|
26
26
|
"element-plus": "^2.13.3",
|
package/jnrs-vue/src/App.vue
CHANGED
|
@@ -4,7 +4,7 @@ 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'
|
|
7
|
-
import { useI18n } from '
|
|
7
|
+
import { useI18n } from '@/locales'
|
|
8
8
|
import { changeLocales as changeLocalesForShared } from '@jnrs/shared/locales'
|
|
9
9
|
|
|
10
10
|
const { locale } = useI18n()
|
|
@@ -97,7 +97,7 @@ export const DetailsApi = (): Promise<FileContainer> => {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
/**
|
|
100
|
-
*
|
|
100
|
+
* 表单编辑(包含文件上传类需使用 formData 类型)
|
|
101
101
|
*/
|
|
102
102
|
export const EditApi = (data: AddProjectItem) => {
|
|
103
103
|
const formData = objectToFormData({
|
|
@@ -112,9 +112,18 @@ export const EditApi = (data: AddProjectItem) => {
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
/**
|
|
115
|
-
*
|
|
115
|
+
* 删除数据
|
|
116
|
+
*/
|
|
117
|
+
export const DelApi = (id: string) => {
|
|
118
|
+
return axiosRequest({
|
|
119
|
+
url: `/mock/demos/delete/${id}`,
|
|
120
|
+
method: 'delete'
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* 列表数据
|
|
116
125
|
*/
|
|
117
|
-
export const
|
|
126
|
+
export const ListApi = (data?: ProjectQuery): Promise<PageTableData<ProjectItem>> => {
|
|
118
127
|
return axiosRequest({
|
|
119
128
|
url: '/mock/demos/table',
|
|
120
129
|
method: 'get',
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/* fade-transform transition */
|
|
2
|
+
.fade-transform-leave-active,
|
|
3
|
+
.fade-transform-enter-active {
|
|
4
|
+
transition: all 0.3s cubic-bezier(0.55, 0, 0.1, 1);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.fade-transform-enter-from {
|
|
8
|
+
opacity: 0;
|
|
9
|
+
transform: translateX(-20px);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.fade-transform-leave-to {
|
|
13
|
+
opacity: 0;
|
|
14
|
+
transform: translateX(20px);
|
|
15
|
+
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { computed } from 'vue'
|
|
11
11
|
import { getDictLabel, getDictColor } from '@/utils/packages'
|
|
12
12
|
|
|
13
|
-
interface Props {
|
|
13
|
+
export interface Props {
|
|
14
14
|
dictName: string
|
|
15
15
|
value: string | number
|
|
16
16
|
/**
|
|
@@ -30,7 +30,7 @@ interface Props {
|
|
|
30
30
|
const { dictName = '', value = '', showColor = true, popover = '' } = defineProps<Props>()
|
|
31
31
|
|
|
32
32
|
const computedColor = computed(() => {
|
|
33
|
-
return showColor ? getDictColor(dictName, value) : '
|
|
33
|
+
return showColor ? getDictColor(dictName, value) : 'inherit'
|
|
34
34
|
})
|
|
35
35
|
</script>
|
|
36
36
|
|
|
@@ -39,12 +39,13 @@ const computedColor = computed(() => {
|
|
|
39
39
|
<template #reference>
|
|
40
40
|
<span
|
|
41
41
|
class="dictTag"
|
|
42
|
+
:class="{ dictTag_showColor: showColor }"
|
|
42
43
|
:style="{
|
|
43
44
|
backgroundColor: computedColor
|
|
44
45
|
}"
|
|
45
46
|
>
|
|
46
47
|
<span
|
|
47
|
-
|
|
48
|
+
class="dictLabel"
|
|
48
49
|
:style="{
|
|
49
50
|
color: computedColor
|
|
50
51
|
}"
|
|
@@ -64,10 +65,11 @@ const computedColor = computed(() => {
|
|
|
64
65
|
padding: 1px 8px;
|
|
65
66
|
border-radius: 4px;
|
|
66
67
|
white-space: nowrap;
|
|
67
|
-
|
|
68
|
+
}
|
|
68
69
|
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
.dictTag_showColor {
|
|
71
|
+
transform: scale(0.9);
|
|
72
|
+
.dictLabel {
|
|
71
73
|
filter: invert(0.5) brightness(0.5);
|
|
72
74
|
}
|
|
73
75
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
-->
|
|
8
8
|
|
|
9
9
|
<script setup lang="ts">
|
|
10
|
-
import {
|
|
10
|
+
import { ListApi } from '@/api/demos/index'
|
|
11
11
|
import { JnSelectTemplate } from '@jnrs/vue-core/components'
|
|
12
12
|
</script>
|
|
13
13
|
|
|
@@ -16,7 +16,7 @@ import { JnSelectTemplate } from '@jnrs/vue-core/components'
|
|
|
16
16
|
tableName="项目经理"
|
|
17
17
|
:keyValue="{ name: 'manager', id: 'managerId', code: 'code' }"
|
|
18
18
|
optionSecondaryField="code"
|
|
19
|
-
:listApi="
|
|
19
|
+
:listApi="ListApi"
|
|
20
20
|
>
|
|
21
21
|
<template #table>
|
|
22
22
|
<el-table-column prop="manager" label="项目经理" align="center" sortable />
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { ref, type Ref } from 'vue'
|
|
2
|
+
import type { FormInstance } from 'element-plus'
|
|
3
|
+
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
4
|
+
import { debounce } from '@jnrs/shared/lodash'
|
|
5
|
+
import { objectMatchAssign } from '@jnrs/shared'
|
|
6
|
+
import type { Pagination, PageTableData } from '@/types'
|
|
7
|
+
|
|
8
|
+
interface CrudOptions<TItem, TForm, TQuery> {
|
|
9
|
+
// 初始值
|
|
10
|
+
defaultForm: () => TForm
|
|
11
|
+
defaultQuery?: () => TQuery
|
|
12
|
+
// API
|
|
13
|
+
listApi: (params: Partial<Pagination> & TQuery) => Promise<PageTableData<TItem> | TItem[]>
|
|
14
|
+
saveApi: (data: TForm) => Promise<unknown>
|
|
15
|
+
deleteApi?: (id: number) => Promise<unknown>
|
|
16
|
+
// 可选:是否启用分页,默认 true
|
|
17
|
+
pagination?: boolean
|
|
18
|
+
// 可选:数据转换
|
|
19
|
+
transformItem?: (item: TItem) => TItem
|
|
20
|
+
beforeEdit?: (row: TItem, form: TForm) => TForm
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function useCrud<TItem, TForm, TQuery = object>(options: CrudOptions<TItem, TForm, TQuery>) {
|
|
24
|
+
const { defaultForm, defaultQuery, listApi, saveApi, deleteApi, pagination: enablePagination = true, transformItem, beforeEdit } = options
|
|
25
|
+
|
|
26
|
+
// 列表状态
|
|
27
|
+
const loading = ref(false)
|
|
28
|
+
const tableData = ref<TItem[]>([]) as Ref<TItem[]>
|
|
29
|
+
const total = ref(0)
|
|
30
|
+
const pagination = ref<Pagination>({ pageNo: 1, pageSize: 20 })
|
|
31
|
+
const queryForm = ref(defaultQuery?.() ?? {}) as Ref<TQuery>
|
|
32
|
+
|
|
33
|
+
// 表单状态
|
|
34
|
+
const dialogRef = ref()
|
|
35
|
+
const formRef = ref<FormInstance>()
|
|
36
|
+
const form = ref(defaultForm()) as Ref<TForm>
|
|
37
|
+
|
|
38
|
+
// 获取列表
|
|
39
|
+
const getList = debounce(async () => {
|
|
40
|
+
loading.value = true
|
|
41
|
+
try {
|
|
42
|
+
const params = {
|
|
43
|
+
...(enablePagination ? pagination.value : {}),
|
|
44
|
+
...queryForm.value
|
|
45
|
+
} as Partial<Pagination> & TQuery
|
|
46
|
+
const res = await listApi(params)
|
|
47
|
+
// 支持返回数组或 { list, count } 格式
|
|
48
|
+
if (Array.isArray(res)) {
|
|
49
|
+
tableData.value = transformItem ? res.map(transformItem) : res
|
|
50
|
+
total.value = res.length
|
|
51
|
+
} else {
|
|
52
|
+
tableData.value = transformItem ? res.list.map(transformItem) : res.list
|
|
53
|
+
total.value = res.count ?? res.list.length
|
|
54
|
+
}
|
|
55
|
+
} catch (e) {
|
|
56
|
+
console.error(e)
|
|
57
|
+
} finally {
|
|
58
|
+
loading.value = false
|
|
59
|
+
}
|
|
60
|
+
}, 300)
|
|
61
|
+
|
|
62
|
+
// 打开新增
|
|
63
|
+
const openCreate = () => {
|
|
64
|
+
dialogRef.value?.open()
|
|
65
|
+
queueMicrotask(() => {
|
|
66
|
+
formRef.value?.resetFields()
|
|
67
|
+
form.value = defaultForm() as TForm
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 打开编辑
|
|
72
|
+
const openEdit = (row: TItem) => {
|
|
73
|
+
dialogRef.value?.open()
|
|
74
|
+
queueMicrotask(() => {
|
|
75
|
+
formRef.value?.resetFields()
|
|
76
|
+
const matched = objectMatchAssign(
|
|
77
|
+
defaultForm() as Record<string, unknown>,
|
|
78
|
+
row as Record<string, unknown>
|
|
79
|
+
) as TForm
|
|
80
|
+
form.value = (beforeEdit ? beforeEdit(row, matched) : matched) as TForm
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 提交表单
|
|
85
|
+
const submitForm = async () => {
|
|
86
|
+
const valid = await formRef.value?.validate().catch(() => false)
|
|
87
|
+
if (!valid) return
|
|
88
|
+
|
|
89
|
+
loading.value = true
|
|
90
|
+
try {
|
|
91
|
+
await saveApi(form.value)
|
|
92
|
+
ElMessage.success('保存成功')
|
|
93
|
+
dialogRef.value?.close()
|
|
94
|
+
getList()
|
|
95
|
+
} catch (e) {
|
|
96
|
+
console.error(e)
|
|
97
|
+
} finally {
|
|
98
|
+
loading.value = false
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 删除
|
|
103
|
+
const handleDelete = async (id: number) => {
|
|
104
|
+
if (!deleteApi) return
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
await ElMessageBox.confirm('确定要删除吗?', '操作确认', {
|
|
108
|
+
confirmButtonText: '删除',
|
|
109
|
+
cancelButtonText: '取消',
|
|
110
|
+
type: 'warning'
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
loading.value = true
|
|
114
|
+
await deleteApi(id)
|
|
115
|
+
ElMessage.success('删除成功')
|
|
116
|
+
getList()
|
|
117
|
+
} catch (e) {
|
|
118
|
+
if (e !== 'cancel') {
|
|
119
|
+
console.error(e)
|
|
120
|
+
}
|
|
121
|
+
} finally {
|
|
122
|
+
loading.value = false
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
loading, tableData, total, pagination, queryForm,
|
|
128
|
+
dialogRef, formRef, form,
|
|
129
|
+
getList, openCreate, openEdit, submitForm, handleDelete
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -18,6 +18,12 @@ const tabLabel = computed(() => {
|
|
|
18
18
|
})
|
|
19
19
|
const route = useRoute()
|
|
20
20
|
|
|
21
|
+
// 右键菜单相关
|
|
22
|
+
const contextMenuVisible = ref(false)
|
|
23
|
+
const contextMenuLeft = ref(0)
|
|
24
|
+
const contextMenuTop = ref(0)
|
|
25
|
+
const selectedTab = ref<MenuItem | null>(null)
|
|
26
|
+
|
|
21
27
|
// 监听路由变化,更新标签页
|
|
22
28
|
watch(
|
|
23
29
|
() => route.name,
|
|
@@ -50,7 +56,7 @@ onMounted(() => {
|
|
|
50
56
|
|
|
51
57
|
// 首页判断
|
|
52
58
|
const isHome = (d: MenuItem) => {
|
|
53
|
-
return d.path === '/' || d.path === ''
|
|
59
|
+
return d.name === 'Home' || d.path === '/' || d.path === ''
|
|
54
60
|
}
|
|
55
61
|
|
|
56
62
|
// 添加标签页
|
|
@@ -78,7 +84,7 @@ const removeTab = (routerName: string) => {
|
|
|
78
84
|
// 如果删除的是当前激活的标签页,就跳转到前一个标签页
|
|
79
85
|
if (activeRouterName.value === routerName) {
|
|
80
86
|
const nextTab = menuTabs.value[currentIndex] || menuTabs.value[currentIndex - 1]
|
|
81
|
-
if (nextTab
|
|
87
|
+
if (nextTab?.name) {
|
|
82
88
|
activeRouterName.value = nextTab.name
|
|
83
89
|
handleRouter({
|
|
84
90
|
name: nextTab.name
|
|
@@ -96,6 +102,80 @@ const handleTabClick = (tab: TabsPaneContext) => {
|
|
|
96
102
|
name: tab.props.name
|
|
97
103
|
})
|
|
98
104
|
}
|
|
105
|
+
|
|
106
|
+
// 打开右键菜单
|
|
107
|
+
const openContextMenu = (e: MouseEvent, item: MenuItem) => {
|
|
108
|
+
e.preventDefault()
|
|
109
|
+
selectedTab.value = item
|
|
110
|
+
contextMenuLeft.value = e.clientX
|
|
111
|
+
contextMenuTop.value = e.clientY
|
|
112
|
+
contextMenuVisible.value = true
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 关闭右键菜单
|
|
116
|
+
const closeContextMenu = () => {
|
|
117
|
+
contextMenuVisible.value = false
|
|
118
|
+
selectedTab.value = null
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 关闭其他标签页
|
|
122
|
+
const closeOtherTabs = () => {
|
|
123
|
+
if (!selectedTab.value?.name) return
|
|
124
|
+
const tabName = selectedTab.value.name
|
|
125
|
+
menuTabs.value = menuTabs.value.filter((tab) => isHome(tab) || tab.name === tabName)
|
|
126
|
+
// 如果当前激活的不是选中的标签页,跳转到选中的标签页
|
|
127
|
+
if (activeRouterName.value !== tabName) {
|
|
128
|
+
activeRouterName.value = tabName
|
|
129
|
+
handleRouter({ name: tabName })
|
|
130
|
+
}
|
|
131
|
+
closeContextMenu()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 关闭所有标签页
|
|
135
|
+
const closeAllTabs = () => {
|
|
136
|
+
// 保留首页
|
|
137
|
+
const homeTab = menuTabs.value.find((tab) => isHome(tab))
|
|
138
|
+
menuTabs.value = homeTab ? [homeTab] : []
|
|
139
|
+
// 跳转到首页
|
|
140
|
+
if (homeTab?.name) {
|
|
141
|
+
activeRouterName.value = homeTab.name
|
|
142
|
+
handleRouter({ name: homeTab.name })
|
|
143
|
+
}
|
|
144
|
+
closeContextMenu()
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 关闭右侧标签页
|
|
148
|
+
const closeRightTabs = () => {
|
|
149
|
+
if (!selectedTab.value?.name) return
|
|
150
|
+
const tabName = selectedTab.value.name
|
|
151
|
+
const currentIndex = menuTabs.value.findIndex((tab) => tab.name === tabName)
|
|
152
|
+
if (currentIndex === -1) return
|
|
153
|
+
menuTabs.value = menuTabs.value.filter((tab, index) => isHome(tab) || index <= currentIndex)
|
|
154
|
+
// 如果当前激活的标签页被关闭了,跳转到选中的标签页
|
|
155
|
+
if (!menuTabs.value.some((tab) => tab.name === activeRouterName.value)) {
|
|
156
|
+
activeRouterName.value = tabName
|
|
157
|
+
handleRouter({ name: tabName })
|
|
158
|
+
}
|
|
159
|
+
closeContextMenu()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 点击其他地方关闭右键菜单
|
|
163
|
+
const handleClickOutside = () => {
|
|
164
|
+
if (contextMenuVisible.value) {
|
|
165
|
+
closeContextMenu()
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 监听点击事件
|
|
170
|
+
onMounted(() => {
|
|
171
|
+
document.addEventListener('click', handleClickOutside)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
// 组件卸载时移除事件监听
|
|
175
|
+
import { onUnmounted } from 'vue'
|
|
176
|
+
onUnmounted(() => {
|
|
177
|
+
document.removeEventListener('click', handleClickOutside)
|
|
178
|
+
})
|
|
99
179
|
</script>
|
|
100
180
|
|
|
101
181
|
<template>
|
|
@@ -103,10 +183,34 @@ const handleTabClick = (tab: TabsPaneContext) => {
|
|
|
103
183
|
<el-tabs v-model="activeRouterName" type="card" @tab-remove="removeTab" @tab-click="handleTabClick">
|
|
104
184
|
<el-tab-pane v-for="item in menuTabs" :key="item.name" :name="item.name" :closable="!isHome(item)">
|
|
105
185
|
<template #label>
|
|
106
|
-
<span>{{ tabLabel(item) }}</span>
|
|
186
|
+
<span @contextmenu="(e: MouseEvent) => openContextMenu(e, item)">{{ tabLabel(item) }}</span>
|
|
107
187
|
</template>
|
|
108
188
|
</el-tab-pane>
|
|
109
189
|
</el-tabs>
|
|
190
|
+
|
|
191
|
+
<!-- 右键菜单 -->
|
|
192
|
+
<Teleport to="body">
|
|
193
|
+
<Transition name="fade">
|
|
194
|
+
<div
|
|
195
|
+
v-show="contextMenuVisible"
|
|
196
|
+
class="context-menu"
|
|
197
|
+
:style="{ left: contextMenuLeft + 'px', top: contextMenuTop + 'px' }"
|
|
198
|
+
>
|
|
199
|
+
<div class="context-menu-item" @click="closeOtherTabs">
|
|
200
|
+
<el-icon><Close /></el-icon>
|
|
201
|
+
<span>关闭其他</span>
|
|
202
|
+
</div>
|
|
203
|
+
<div class="context-menu-item" @click="closeRightTabs">
|
|
204
|
+
<el-icon><ArrowRight /></el-icon>
|
|
205
|
+
<span>关闭右侧</span>
|
|
206
|
+
</div>
|
|
207
|
+
<div class="context-menu-item" @click="closeAllTabs">
|
|
208
|
+
<el-icon><CircleClose /></el-icon>
|
|
209
|
+
<span>关闭所有</span>
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
</Transition>
|
|
213
|
+
</Teleport>
|
|
110
214
|
</div>
|
|
111
215
|
</template>
|
|
112
216
|
|
|
@@ -122,10 +226,13 @@ const handleTabClick = (tab: TabsPaneContext) => {
|
|
|
122
226
|
color: var(--jnrs-font-primary-06);
|
|
123
227
|
font-size: 12px;
|
|
124
228
|
height: var(--jnrs-routerTabs-height);
|
|
229
|
+
transition: all 0.3s ease;
|
|
125
230
|
}
|
|
126
231
|
:deep(.el-tabs__item.is-active) {
|
|
127
232
|
color: var(--jnrs-color-primary);
|
|
128
233
|
border-bottom-color: var(--jnrs-color-primary);
|
|
234
|
+
background: var(--jnrs-color-primary-005);
|
|
235
|
+
font-weight: bold;
|
|
129
236
|
}
|
|
130
237
|
:deep(.el-tabs__nav) {
|
|
131
238
|
border-radius: 0;
|
|
@@ -139,4 +246,45 @@ const handleTabClick = (tab: TabsPaneContext) => {
|
|
|
139
246
|
color: var(--jnrs-color-primary);
|
|
140
247
|
}
|
|
141
248
|
}
|
|
249
|
+
|
|
250
|
+
.context-menu {
|
|
251
|
+
position: fixed;
|
|
252
|
+
z-index: 9999;
|
|
253
|
+
background: var(--jnrs-card-primary);
|
|
254
|
+
border-radius: 8px;
|
|
255
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
|
256
|
+
border: 1px solid var(--jnrs-background-primary);
|
|
257
|
+
padding: 4px 0;
|
|
258
|
+
min-width: 120px;
|
|
259
|
+
|
|
260
|
+
.context-menu-item {
|
|
261
|
+
display: flex;
|
|
262
|
+
align-items: center;
|
|
263
|
+
gap: 8px;
|
|
264
|
+
padding: 8px 16px;
|
|
265
|
+
cursor: pointer;
|
|
266
|
+
font-size: 13px;
|
|
267
|
+
color: var(--jnrs-font-primary-08);
|
|
268
|
+
transition: all 0.2s;
|
|
269
|
+
|
|
270
|
+
&:hover {
|
|
271
|
+
background: var(--jnrs-color-primary-005);
|
|
272
|
+
color: var(--jnrs-color-primary);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.el-icon {
|
|
276
|
+
font-size: 14px;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.fade-enter-active,
|
|
282
|
+
.fade-leave-active {
|
|
283
|
+
transition: opacity 0.15s ease;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.fade-enter-from,
|
|
287
|
+
.fade-leave-to {
|
|
288
|
+
opacity: 0;
|
|
289
|
+
}
|
|
142
290
|
</style>
|