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.
Files changed (30) hide show
  1. package/jnrs-vue/components.d.ts +3 -1
  2. package/jnrs-vue/package.json +3 -3
  3. package/jnrs-vue/src/App.vue +1 -1
  4. package/jnrs-vue/src/api/demos/index.ts +12 -3
  5. package/jnrs-vue/src/api/system/index.ts +3 -0
  6. package/jnrs-vue/src/assets/styles/animation.scss +15 -0
  7. package/jnrs-vue/src/components/common/CardTable.vue +1 -1
  8. package/jnrs-vue/src/components/common/DictTag.vue +8 -6
  9. package/jnrs-vue/src/components/common/ImageView.vue +1 -1
  10. package/jnrs-vue/src/components/common/PdfView.vue +1 -1
  11. package/jnrs-vue/src/components/select/SelectManager.vue +2 -2
  12. package/jnrs-vue/src/composables/useCrud.ts +131 -0
  13. package/jnrs-vue/src/layout/RouterTabs.vue +151 -3
  14. package/jnrs-vue/src/layout/SideMenu.vue +212 -139
  15. package/jnrs-vue/src/layout/TopHeader.vue +44 -22
  16. package/jnrs-vue/src/locales/en.ts +40 -1
  17. package/jnrs-vue/src/locales/index.ts +2 -2
  18. package/jnrs-vue/src/locales/zhCn.ts +40 -1
  19. package/jnrs-vue/src/main.ts +2 -2
  20. package/jnrs-vue/src/router/routes.ts +1 -1
  21. package/jnrs-vue/src/views/demos/crud/index.vue +47 -9
  22. package/jnrs-vue/src/views/demos/simpleTable/index.vue +2 -2
  23. package/jnrs-vue/src/views/home/index.vue +312 -3
  24. package/jnrs-vue/src/views/login/index.vue +2 -2
  25. package/jnrs-vue/vite.config.ts +2 -1
  26. package/jnrs-vue/viteMockServe/fail.ts +3 -3
  27. package/jnrs-vue/viteMockServe/file.ts +4 -4
  28. package/jnrs-vue/viteMockServe/success.ts +9 -1
  29. package/package.json +1 -1
  30. 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
@@ -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 {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jnrs-vue",
3
- "version": "1.2.20",
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.14",
23
- "@jnrs/vue-core": "1.2.9",
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",
@@ -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 'vue-i18n'
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
- * 表单新增(包含文件上传类需使用 formData 类型)
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 TableApi = (data?: ProjectQuery): Promise<PageTableData<ProjectItem>> => {
126
+ export const ListApi = (data?: ProjectQuery): Promise<PageTableData<ProjectItem>> => {
118
127
  return axiosRequest({
119
128
  url: '/mock/demos/table',
120
129
  method: 'get',
@@ -19,6 +19,9 @@ interface LoginParams {
19
19
  password: string
20
20
  }
21
21
 
22
+ /**
23
+ * 登录结果
24
+ */
22
25
  export interface LoginResult extends User {
23
26
  token: string
24
27
  dict: Dict
@@ -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
+ }
@@ -12,7 +12,7 @@ import { ref, onActivated } from 'vue'
12
12
  import { JnPagination, JnTable } from '@jnrs/vue-core/components'
13
13
  import { debounce } from '@jnrs/shared/lodash'
14
14
 
15
- interface Props {
15
+ export interface Props {
16
16
  /**
17
17
  * 获取数据表格 api 函数
18
18
  */
@@ -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) : 'unset'
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
- :class="{ dictTag_label_showColor: showColor }"
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
- transform: scale(0.8);
68
+ }
68
69
 
69
- .dictTag_label_showColor {
70
- font-size: 1.1em;
70
+ .dictTag_showColor {
71
+ transform: scale(0.9);
72
+ .dictLabel {
71
73
  filter: invert(0.5) brightness(0.5);
72
74
  }
73
75
  }
@@ -8,7 +8,7 @@ import { FileApi } from '@/api/common'
8
8
 
9
9
  import { JnImageView } from '@jnrs/vue-core/components'
10
10
 
11
- interface Props {
11
+ export interface Props {
12
12
  /**
13
13
  * 要加载的文件列表 | 文件名唯一标识 uniqueFileName
14
14
  */
@@ -7,7 +7,7 @@ import { FileApi } from '@/api/common'
7
7
 
8
8
  import { JnPdfView } from '@jnrs/vue-core/components'
9
9
 
10
- interface Props {
10
+ export interface Props {
11
11
  /**
12
12
  * 要加载的文件列表 | 文件名唯一标识 uniqueFileName
13
13
  */
@@ -7,7 +7,7 @@
7
7
  -->
8
8
 
9
9
  <script setup lang="ts">
10
- import { TableApi } from '@/api/demos/index'
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="TableApi"
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.name) {
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>