fix:完善字典管理功能优化全局组件

This commit is contained in:
liangdong
2026-01-05 17:14:09 +08:00
parent bae034d6eb
commit 98c941e60c
26 changed files with 1225 additions and 563 deletions

View File

@@ -40,7 +40,6 @@
[x] i18n注入 语言全局化实现 [x] i18n注入 语言全局化实现
[x] 状态管理工具Pinia实现 [x] 状态管理工具Pinia实现
[x] ElementPlus 组件引入 并且实现i18n [x] ElementPlus 组件引入 并且实现i18n
[] 引入unocss 样式 [x] 路由由后端控制实现动态路由
[] 路由由后端控制实现动态路由

6
components.d.ts vendored
View File

@@ -27,6 +27,7 @@ declare module 'vue' {
ElDescriptions: typeof import('element-plus/es')['ElDescriptions'] ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem'] ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
ElDialog: typeof import('element-plus/es')['ElDialog'] ElDialog: typeof import('element-plus/es')['ElDialog']
ElDivider: typeof import('element-plus/es')['ElDivider']
ElDrawer: typeof import('element-plus/es')['ElDrawer'] ElDrawer: typeof import('element-plus/es')['ElDrawer']
ElDropdown: typeof import('element-plus/es')['ElDropdown'] ElDropdown: typeof import('element-plus/es')['ElDropdown']
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
@@ -49,6 +50,8 @@ declare module 'vue' {
ElRow: typeof import('element-plus/es')['ElRow'] ElRow: typeof import('element-plus/es')['ElRow']
ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
ElSelect: typeof import('element-plus/es')['ElSelect'] ElSelect: typeof import('element-plus/es')['ElSelect']
ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
ElSkeletonItem: typeof import('element-plus/es')['ElSkeletonItem']
ElSubMenu: typeof import('element-plus/es')['ElSubMenu'] ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
ElTable: typeof import('element-plus/es')['ElTable'] ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
@@ -59,8 +62,10 @@ declare module 'vue' {
ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem'] ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
ElTooltip: typeof import('element-plus/es')['ElTooltip'] ElTooltip: typeof import('element-plus/es')['ElTooltip']
ElTree: typeof import('element-plus/es')['ElTree'] ElTree: typeof import('element-plus/es')['ElTree']
EmojiPicker: typeof import('./src/components/comment/emojiPicker.vue')['default']
GlobaIcon: typeof import('./src/components/globaIcon/index.vue')['default'] GlobaIcon: typeof import('./src/components/globaIcon/index.vue')['default']
GlobalIcon: typeof import('./src/components/GlobalIcon/index.vue')['default'] GlobalIcon: typeof import('./src/components/GlobalIcon/index.vue')['default']
NameAvatar: typeof import('./src/components/nameAvatar/index.vue')['default']
OverflowTabs: typeof import('./src/components/overflowTabs/index.vue')['default'] OverflowTabs: typeof import('./src/components/overflowTabs/index.vue')['default']
PageForm: typeof import('./src/components/pageForm/index.vue')['default'] PageForm: typeof import('./src/components/pageForm/index.vue')['default']
ProTable: typeof import('./src/components/proTable/index.vue')['default'] ProTable: typeof import('./src/components/proTable/index.vue')['default']
@@ -69,6 +74,7 @@ declare module 'vue' {
StageBreadcrumbs: typeof import('./src/components/stageBreadcrumbs/index.vue')['default'] StageBreadcrumbs: typeof import('./src/components/stageBreadcrumbs/index.vue')['default']
StandardMenu: typeof import('./src/components/standardMenu/index.vue')['default'] StandardMenu: typeof import('./src/components/standardMenu/index.vue')['default']
StandMenu: typeof import('./src/components/standMenu/index.vue')['default'] StandMenu: typeof import('./src/components/standMenu/index.vue')['default']
Xxx: typeof import('./src/components/comment/xxx.vue')['default']
} }
export interface GlobalDirectives { export interface GlobalDirectives {
vLoading: typeof import('element-plus/es')['ElLoadingDirective'] vLoading: typeof import('element-plus/es')['ElLoadingDirective']

View File

@@ -25,6 +25,7 @@
"dayjs": "^1.11.19", "dayjs": "^1.11.19",
"sass": "^1.97.1", "sass": "^1.97.1",
"typescript": "~5.9.3", "typescript": "~5.9.3",
"unicode-emoji-json": "^0.8.0",
"unplugin-auto-import": "^20.3.0", "unplugin-auto-import": "^20.3.0",
"unplugin-vue-components": "^30.0.0", "unplugin-vue-components": "^30.0.0",
"vite": "^7.2.4", "vite": "^7.2.4",

25
pnpm-lock.yaml generated
View File

@@ -51,6 +51,9 @@ importers:
typescript: typescript:
specifier: ~5.9.3 specifier: ~5.9.3
version: 5.9.3 version: 5.9.3
unicode-emoji-json:
specifier: ^0.8.0
version: 0.8.0
unplugin-auto-import: unplugin-auto-import:
specifier: ^20.3.0 specifier: ^20.3.0
version: 20.3.0(@vueuse/core@10.11.1(vue@3.5.26(typescript@5.9.3))) version: 20.3.0(@vueuse/core@10.11.1(vue@3.5.26(typescript@5.9.3)))
@@ -317,42 +320,36 @@ packages:
engines: {node: '>= 10.0.0'} engines: {node: '>= 10.0.0'}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [glibc]
'@parcel/watcher-linux-arm-musl@2.5.1': '@parcel/watcher-linux-arm-musl@2.5.1':
resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==}
engines: {node: '>= 10.0.0'} engines: {node: '>= 10.0.0'}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [musl]
'@parcel/watcher-linux-arm64-glibc@2.5.1': '@parcel/watcher-linux-arm64-glibc@2.5.1':
resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==}
engines: {node: '>= 10.0.0'} engines: {node: '>= 10.0.0'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@parcel/watcher-linux-arm64-musl@2.5.1': '@parcel/watcher-linux-arm64-musl@2.5.1':
resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==}
engines: {node: '>= 10.0.0'} engines: {node: '>= 10.0.0'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@parcel/watcher-linux-x64-glibc@2.5.1': '@parcel/watcher-linux-x64-glibc@2.5.1':
resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==}
engines: {node: '>= 10.0.0'} engines: {node: '>= 10.0.0'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@parcel/watcher-linux-x64-musl@2.5.1': '@parcel/watcher-linux-x64-musl@2.5.1':
resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==}
engines: {node: '>= 10.0.0'} engines: {node: '>= 10.0.0'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@parcel/watcher-win32-arm64@2.5.1': '@parcel/watcher-win32-arm64@2.5.1':
resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==}
@@ -413,67 +410,56 @@ packages:
resolution: {integrity: sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==} resolution: {integrity: sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.54.0': '@rollup/rollup-linux-arm-musleabihf@4.54.0':
resolution: {integrity: sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==} resolution: {integrity: sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.54.0': '@rollup/rollup-linux-arm64-gnu@4.54.0':
resolution: {integrity: sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==} resolution: {integrity: sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.54.0': '@rollup/rollup-linux-arm64-musl@4.54.0':
resolution: {integrity: sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==} resolution: {integrity: sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.54.0': '@rollup/rollup-linux-loong64-gnu@4.54.0':
resolution: {integrity: sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==} resolution: {integrity: sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==}
cpu: [loong64] cpu: [loong64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-ppc64-gnu@4.54.0': '@rollup/rollup-linux-ppc64-gnu@4.54.0':
resolution: {integrity: sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==} resolution: {integrity: sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-gnu@4.54.0': '@rollup/rollup-linux-riscv64-gnu@4.54.0':
resolution: {integrity: sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==} resolution: {integrity: sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.54.0': '@rollup/rollup-linux-riscv64-musl@4.54.0':
resolution: {integrity: sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==} resolution: {integrity: sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.54.0': '@rollup/rollup-linux-s390x-gnu@4.54.0':
resolution: {integrity: sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==} resolution: {integrity: sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.54.0': '@rollup/rollup-linux-x64-gnu@4.54.0':
resolution: {integrity: sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==} resolution: {integrity: sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.54.0': '@rollup/rollup-linux-x64-musl@4.54.0':
resolution: {integrity: sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==} resolution: {integrity: sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-openharmony-arm64@4.54.0': '@rollup/rollup-openharmony-arm64@4.54.0':
resolution: {integrity: sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==} resolution: {integrity: sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==}
@@ -959,6 +945,9 @@ packages:
undici-types@7.16.0: undici-types@7.16.0:
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
unicode-emoji-json@0.8.0:
resolution: {integrity: sha512-3wDXXvp6YGoKGhS2O2H7+V+bYduOBydN1lnI0uVfr1cIdY02uFFiEH1i3kE5CCE4l6UqbLKVmEFW9USxTAMD1g==}
unimport@5.6.0: unimport@5.6.0:
resolution: {integrity: sha512-8rqAmtJV8o60x46kBAJKtHpJDJWkA2xcBqWKPI14MgUb05o1pnpnCnXSxedUXyeq7p8fR5g3pTo2BaswZ9lD9A==} resolution: {integrity: sha512-8rqAmtJV8o60x46kBAJKtHpJDJWkA2xcBqWKPI14MgUb05o1pnpnCnXSxedUXyeq7p8fR5g3pTo2BaswZ9lD9A==}
engines: {node: '>=18.12.0'} engines: {node: '>=18.12.0'}
@@ -1872,6 +1861,8 @@ snapshots:
undici-types@7.16.0: {} undici-types@7.16.0: {}
unicode-emoji-json@0.8.0: {}
unimport@5.6.0: unimport@5.6.0:
dependencies: dependencies:
acorn: 8.15.0 acorn: 8.15.0

View File

@@ -66,26 +66,26 @@ export const saveDictTypeValue = (id:string,data: addDataProps) => {
// 删除字典类型值 // 删除字典类型值
export const deleteDictTypeValue = (typeId:string,id: string) => { export const deleteDictTypeValue = (typeId:string,id: string) => {
return request.delete(`/auth/v1/backend/dict/${typeId}/${id}`); return request.delete(`/auth/v1/backend/dict/type/${typeId}/${id}`);
}; };
// 更新字典类型值 // 更新字典类型值
export const updateDictTypeValue = (typeId:string,id: string,data: addDataProps) => { export const updateDictTypeValue = (typeId:string,id: string,data: addDataProps) => {
return request.put(`/auth/v1/backend/dict/${typeId}/${id}`, data); return request.put(`/auth/v1/backend/dict/type/${typeId}/${id}`, data);
}; };
// 获取下级菜单数据 // 获取下级菜单数据
export const getNextDictMenu = (id:string,parentId:string) => { export const getNextDictMenu = (id:string,parentId:string,params: paramsProps) => {
return request.get(`/auth/v1/backend/dict/type/${id}/data/${parentId}`); return request.get(`/auth/v1/backend/dict/type/${id}/data/${parentId}`,params);
}; };
// 启用接口 // 启用接口
export const enableTypeDict = (id:string)=>{ export const enableTypeDict = (typeId:string,id:string)=>{
return request.post(`/auth/v1/backend/dict/type/${typeId}/${id}/enable`); return request.post(`/auth/v1/backend/dict/type/${typeId}/${id}/enable`);
} }
// 禁用接口 // 禁用接口
export const disableTypeDict = (id:string)=>{ export const disableTypeDict = (typeId:string,id:string)=>{
return request.post(`/auth/v1/backend/dict/type/${typeId}/${id}/disable`) return request.post(`/auth/v1/backend/dict/type/${typeId}/${id}/disable`)
} }

View File

@@ -1,329 +0,0 @@
<template>
<div class="comment-app">
<section class="main-publisher">
<el-input
v-model="mainInput"
type="textarea"
:rows="3"
placeholder="发一条友善的评论吧..."
resize="none"
/>
<div class="pub-footer">
<div class="tools">
<el-button link class="tool-item">
<el-icon><Picture /></el-icon> 插入图片 (预留)
</el-button>
<el-button link class="tool-item">
<span class="emoji-icon">😀</span> 表情 (预留)
</el-button>
</div>
<el-button type="primary" round @click="submitMainComment"
>发布评论</el-button
>
</div>
</section>
<div class="comment-list">
<div v-for="item in commentData" :key="item.id" class="comment-group">
<div class="parent-node">
<el-avatar :size="42" :src="item.avatar" />
<div class="node-main">
<div class="user-info">
<span class="nickname">{{ item.nickname }}</span>
<el-tag v-if="item.isOwner" size="small">作者</el-tag>
</div>
<div class="content">{{ item.content }}</div>
<div class="node-footer">
<span class="time">{{ item.time }}</span>
<div class="actions">
<el-button link @click="openReply(item, item)">回复</el-button>
<el-button link>点赞</el-button>
</div>
</div>
<div v-if="item.children?.length" class="sub-container">
<div
v-for="reply in item.children"
:key="reply.id"
class="sub-node"
>
<el-avatar :size="24" :src="reply.avatar" />
<div class="sub-main">
<div class="sub-text">
<span class="sub-nickname">{{ reply.nickname }}</span>
<template v-if="reply.replyTo !== item.nickname">
<span class="reply-label">回复</span>
<span class="target-name">@{{ reply.replyTo }}</span>
</template>
<span class="sep"></span>
<span class="content-body">{{ reply.content }}</span>
</div>
<div class="node-footer">
<span class="time">{{ reply.time }}</span>
<el-button link @click="openReply(reply, item)"
>回复</el-button
>
</div>
</div>
</div>
</div>
<div
v-if="activeReply.groupId === item.id"
class="inline-publisher"
>
<el-input
v-model="replyInput"
:placeholder="`回复 @${activeReply.targetName}...`"
type="textarea"
autosize
/>
<div class="pub-footer">
<div class="tools">
<el-button link size="small">😀 表情</el-button>
</div>
<div class="btns">
<el-button size="small" @click="cancelReply">取消</el-button>
<el-button
size="small"
type="primary"
round
@click="submitReply"
>发布</el-button
>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive } from "vue";
import { ElMessage } from "element-plus";
import { Picture } from "@element-plus/icons-vue";
// 1. 模拟当前登录用户
const currentUser = {
nickname: "前端练习生",
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=Lucky",
};
// 2. 响应式数据
const mainInput = ref("");
const replyInput = ref("");
const commentData = ref([
{
id: 1,
nickname: "张三",
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=1",
content: "扁平化递归不仅性能好,在手机端显示也非常整齐!",
time: "2小时前",
isOwner: true,
children: [
{
id: 101,
nickname: "李四",
replyTo: "张三",
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=2",
content: "确实,无限缩进到后面根本没法看。",
time: "1小时前",
},
],
},
]);
// 记录当前正在回复的状态
const activeReply = reactive({
groupId: null, // 所属的一级评论ID
targetName: "", // 正在回复的具体人名
targetId: null, // 正在回复的评论ID
});
// 3. 逻辑方法
const submitMainComment = () => {
if (!mainInput.value.trim()) return ElMessage.warning("内容不能为空");
const newComment = {
id: Date.now(),
...currentUser,
content: mainInput.value,
time: "刚刚",
children: [],
};
commentData.value.unshift(newComment);
mainInput.value = "";
ElMessage.success("发表成功");
};
const openReply = (target, group) => {
activeReply.groupId = group.id;
activeReply.targetName = target.nickname;
activeReply.targetId = target.id;
replyInput.value = "";
};
const cancelReply = () => {
activeReply.groupId = null;
};
const submitReply = () => {
if (!replyInput.value.trim()) return ElMessage.warning("回复内容不能为空");
const targetGroup = commentData.value.find(
(item) => item.id === activeReply.groupId
);
if (targetGroup) {
const newReply = {
id: Date.now(),
...currentUser,
replyTo: activeReply.targetName,
content: replyInput.value,
time: "刚刚",
};
targetGroup.children.push(newReply);
ElMessage.success("回复成功");
cancelReply();
}
};
</script>
<style scoped lang="scss">
$color-primary: #409eff;
$color-bg-sub: #f5f7fa;
$color-text-main: #303133;
$color-text-sub: #909399;
$border-color: #ebeef5;
.comment-app {
max-width: 760px;
margin: 40px auto;
padding: 0 20px;
// 发布区域公共样式
.main-publisher,
.inline-publisher {
border: 1px solid $border-color;
border-radius: 8px;
padding: 12px;
background: #fff;
.pub-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10px;
.tool-item {
color: $color-text-sub;
font-size: 14px;
.emoji-icon {
font-size: 16px;
margin-right: 4px;
}
}
}
}
.inline-publisher {
margin-top: 16px;
background-color: #fff;
border: 1px solid #dcdfe6;
}
.comment-list {
margin-top: 32px;
.comment-group {
margin-bottom: 28px;
.parent-node {
display: flex;
gap: 14px;
.node-main {
flex: 1;
.user-info {
display: flex;
align-items: center;
gap: 8px;
.nickname {
font-weight: 600;
font-size: 14px;
color: $color-text-main;
}
}
.content {
margin: 8px 0;
font-size: 15px;
line-height: 1.6;
color: $color-text-main;
}
}
}
}
}
// 底部信息和动作
.node-footer {
display: flex;
align-items: center;
gap: 15px;
font-size: 13px;
color: $color-text-sub;
.actions {
display: flex;
gap: 10px;
.el-button {
font-size: 13px;
color: $color-text-sub;
&:hover {
color: $color-primary;
}
}
}
}
// 扁平化子评论容器
.sub-container {
background-color: $color-bg-sub;
border-radius: 6px;
padding: 4px 12px 12px;
margin-top: 12px;
.sub-node {
display: flex;
gap: 10px;
margin-top: 12px;
.sub-main {
flex: 1;
.sub-text {
font-size: 14px;
line-height: 1.5;
.sub-nickname {
font-weight: 600;
color: #555;
}
.reply-label {
margin: 0 4px;
color: $color-text-sub;
font-size: 13px;
}
.target-name {
color: $color-primary;
margin-right: 2px;
}
.content-body {
color: $color-text-main;
}
}
}
}
}
}
</style>

View File

@@ -0,0 +1,57 @@
<template>
<el-avatar
:size="size"
:src="src"
:style="{ backgroundColor: !src ? bgColor : '' }"
class="mj-name-avatar"
>
<span v-if="!src" class="avatar-text">{{ displayText }}</span>
</el-avatar>
</template>
<script setup>
import { computed } from 'vue';
defineOptions({name: 'NameAvatar'})
const props = defineProps({
name: { type: String, default: '' },
src: { type: String, default: '' },
size: { type: Number, default: 40 }
});
const displayText = computed(() => {
return props.name ? props.name.charAt(0) : '';
});
const bgColor = computed(() => {
if (!props.name) return '#409EFF';
let hash = 0;
for (let i = 0; i < props.name.length; i++) {
hash = props.name.charCodeAt(i) + ((hash << 5) - hash);
}
const colors = [
'#337ecc', '#409eff', '#53a8ff', '#79bbff', '#95d475',
'#eebe77', '#f89898', '#b37feb', '#ff85c0',
'#52c41a', '#faad14', '#f5222d', '#722ed1', '#13c2c2',
'#eb2f96', '#a0d911', '#fa8c16', '#e74c3c', '#9b59b6',
'#1abc9c', '#34495e', '#f39c12', '#e67e22', '#3498db',
'#9b59b6', '#2ecc71', '#f1c40f', '#d35400', '#7f8c8d'
];
return colors[Math.abs(hash) % colors.length];
});
</script>
<style scoped lang="scss">
.mj-name-avatar {
--el-avatar-bg-color:transparent;
user-select: none;
font-weight: 500;
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
.avatar-text {
color: var(--el-avatar-text-color);
font-size: 16px;
letter-spacing: -0.5px;
line-height: 1;
}
}
</style>

View File

@@ -1,12 +1,20 @@
<template> <template>
<div class="pro-table-container"> <div class="pro-table-container">
<el-table :data="data" v-bind="$attrs" v-loading="innerLoading" class="hover-action-table" header-row-class-name="header-row-name"> <el-table
<template v-for="(col,index) in columns" :key="col.prop"> :data="data"
v-bind="$attrs"
v-loading="innerLoading"
class="hover-action-table"
header-row-class-name="header-row-name"
>
<template v-for="(col, index) in columns" :key="col.prop">
<el-table-column <el-table-column
v-if="!col.slot && col.prop !== 'actions'" v-if="!col.slot && col.prop !== 'actions'"
v-bind="col" v-bind="col"
> >
<template #default="scope" v-if="!col.formatter">{{ scope.row[col.prop] || "-" }}</template> <template #default="scope" v-if="!col.formatter">{{
scope.row[col.prop] || "-"
}}</template>
</el-table-column> </el-table-column>
<el-table-column v-else-if="col.slot" v-bind="col"> <el-table-column v-else-if="col.slot" v-bind="col">
@@ -22,7 +30,78 @@
<el-table-column v-else-if="col.prop === 'actions'" v-bind="col"> <el-table-column v-else-if="col.prop === 'actions'" v-bind="col">
<template #default="scope"> <template #default="scope">
<div class="action-group"> <div class="action-group">
<slot name="actions" :row="scope.row"></slot> <slot name="actions" :row="scope.row">
<template v-if="col.actions && col.actions.length > 0">
<!-- 显示前maxButtons个按钮 -->
<template
v-for="(btn, idx) in col.actions.slice(
0,
getVisibleButtonCount(col)
)"
:key="idx"
>
<span
v-if="!shouldHideButton(btn, scope.row)"
v-permission="btn.permission"
>
<el-button
v-bind="getButtonProps(btn)"
@click="handleButtonClick(btn, scope.row)"
>
{{
typeof btn.label === "function"
? btn.label(scope.row)
: btn.label
}}
</el-button>
</span>
</template>
<!-- 如果按钮超过maxButtons显示下拉菜单 -->
<el-dropdown
v-if="
col.actions.length >
(col.maxButtons || MAX_BUTTON_LENGTH) &&
hasDropdownPermission(
col.actions.slice(col.maxButtons || MAX_BUTTON_LENGTH)
)
"
class="dropdown-menu-table"
trigger="hover"
>
<el-button link type="primary">
{{ col.dropdownText || "更多" }}
<el-icon><arrow-down /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<template
v-for="(btn, idx) in col.actions.slice(getVisibleButtonCount(col), col.actions.length)"
:key="idx"
>
<span
v-permission="btn.permission"
v-if="!shouldHideButton(btn, scope.row)"
>
<el-dropdown-item>
<el-button
v-bind="getButtonProps(btn)"
@click="handleButtonClick(btn, scope.row)"
>
{{
typeof btn.label === "function"
? btn.label(scope.row)
: btn.label
}}
</el-button>
</el-dropdown-item>
</span>
</template>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</slot>
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
@@ -54,7 +133,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from "vue"; import { ref, computed } from "vue";
import { ArrowDown } from "@element-plus/icons-vue";
const props = defineProps({ const props = defineProps({
columns: { type: Array, required: true }, columns: { type: Array, required: true },
data: { type: Array, required: true }, data: { type: Array, required: true },
@@ -64,40 +143,50 @@ const props = defineProps({
// 是否立即请求数据 // 是否立即请求数据
immediate: { type: Boolean, default: true }, immediate: { type: Boolean, default: true },
// 是否在激活时刷新数据 // 是否在激活时刷新数据
refreshOnActivated: { type: Boolean, default: true } refreshOnActivated: { type: Boolean, default: true },
}); });
const emit = defineEmits(["current-change", "size-change", "update:data", "update:total"]); const emit = defineEmits([
"current-change",
"size-change",
"update:data",
"update:total",
]);
// 内部控制 loading // 内部控制 loading
const innerLoading = ref(false); const innerLoading = ref(false);
// 默认按钮长度
const MAX_BUTTON_LENGTH = 3;
// 标记是否是首次挂载 // 标记是否是首次挂载
let isFirstMount = true; let isFirstMount = true;
// 参数传递逻辑 // 参数传递逻辑
const params = computed(()=>{ const params = computed(() => {
if(!props.pagination){ if (!props.pagination) {
return {} return {};
}else if(typeof props.pagination === 'object' && props.pagination !== null){ } else if (
typeof props.pagination === "object" &&
props.pagination !== null
) {
return { return {
pageNo:props.pagination.currentPage, pageNo: props.pagination.currentPage,
pageSize:props.pagination.pageSize pageSize: props.pagination.pageSize,
} };
}else{ } else {
return { return {
pageNo:1, pageNo: 1,
pageSize:20 pageSize: 20,
} };
} }
}) });
// 请求方法 // 请求方法
const refresh = async () => { const refresh = async () => {
if (!props.requestApi) return; if (!props.requestApi) return;
innerLoading.value = true; innerLoading.value = true;
try { try {
const res = await props.requestApi(params.value); const res = await props.requestApi(params.value);
emit("update:data", res?.records || []); emit("update:data", res?.records || []);
emit("update:total", res?.total || 0); emit("update:total", res?.total || 0);
@@ -123,7 +212,9 @@ onMounted(() => {
if (props.immediate) { if (props.immediate) {
refresh(); refresh();
} }
setTimeout(() => { isFirstMount = false; }, 0); setTimeout(() => {
isFirstMount = false;
}, 0);
}); });
// 兼容设置了keep-alive // 兼容设置了keep-alive
@@ -142,6 +233,35 @@ const paginationConfig = computed(() => ({
background: false, background: false,
...props.pagination, ...props.pagination,
})); }));
// action按钮组的数据
const handleButtonClick = (button, row) => {
if (button.onClick) {
button.onClick(row);
}
};
const shouldHideButton = (button, row) => {
if (typeof button.show === "function") {
return !button.show(row);
}
return button.show === false;
};
const getVisibleButtonCount = (col) => {
const { actions, maxButtons } = col;
const totalButtons = maxButtons || MAX_BUTTON_LENGTH;
return totalButtons === actions.length ? totalButtons : Math.min(totalButtons, actions.length)-1;
};
const hasDropdownPermission = (dropdownActions) => {
return dropdownActions.some(
(btn) => btn.permission && !shouldHideButton(btn, null)
);
};
const getButtonProps = (button) => {
const { label, onClick, show, permission, ...buttonProps } = button;
return buttonProps;
};
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@@ -152,9 +272,9 @@ const paginationConfig = computed(() => ({
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05); box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
border: 1px solid #f0f0f0; border: 1px solid #f0f0f0;
:deep(.header-row-name){ :deep(.header-row-name) {
th.el-table__cell{ th.el-table__cell {
background-color: #FBFCFD; background-color: #fbfcfd;
} }
} }
@@ -202,5 +322,8 @@ const paginationConfig = computed(() => ({
color: #409eff; color: #409eff;
font-weight: bold; font-weight: bold;
} }
.dropdown-menu-table{
vertical-align: bottom;
}
} }
</style> </style>

View File

@@ -10,7 +10,7 @@ const statusDict = {
// 字典状态颜色 // 字典状态颜色
const statusDictColor = { const statusDictColor = {
1:'#66E5BE', 1:'#66E5BE',
0:'#ff0000' 0:'#90A1B9'
} }
// 设置字典转换为目标格式 // 设置字典转换为目标格式

View File

@@ -21,13 +21,16 @@ const app = createApp(App);
// 导入全局的i18n文件 // 导入全局的i18n文件
const loadLocalMessages = async () => { const loadLocalMessages = async () => {
const messages: Record<string, any> = {}; const messages: Record<string, any> = {};
const locales = import.meta.glob("./locales/*.ts", { eager: true }); const locales = import.meta.glob(["./locales/*.ts","./modules/**/locales/*.ts"], { eager: true });
// 遍历所有匹配的文件 // 遍历所有匹配的文件
Object.keys(locales).forEach((path) => { Object.keys(locales).forEach((path) => {
const lang = path.match(/\.\/locales\/(.+)\.ts$/)?.[1]; const lang = path.match(/.*\/locales\/(.+)\.ts$/)?.[1];
if (lang && locales[path]) { if (lang && locales[path]) {
messages[lang] = locales[path].default; if (!messages[lang]) {
messages[lang] = {};
}
messages[lang] = { ...messages[lang], ...locales[path].default };
} }
}); });

View File

@@ -0,0 +1,84 @@
<template>
<el-popover
:placement="placement"
:width="width"
trigger="click"
popper-class="emoji-popover"
ref="emojiPopoverRef"
>
<template #reference>
<slot>
<el-button link title="emoji" class="default-emoji-trigger">😊</el-button>
</slot>
</template>
<el-scrollbar height="240px">
<div class="emoji-container">
<span
v-for="emoji in emotionList"
:key="emoji"
class="emoji-item"
@click="selectEmoji(emoji)"
>
{{ emoji }}
</span>
</div>
</el-scrollbar>
</el-popover>
</template>
<script setup lang="ts">
import { useEmoji } from "./useEmoji";
const { emotionList } = useEmoji();
const props = defineProps({
placement: { type: String, default: "bottom-start" },
width: { type: [Number, String], default: "auto" },
});
defineOptions({ name: "EmojiPicker" });
const emojiPopoverRef = ref(null);
const emit = defineEmits(["select"]);
// 筛选表情后的事件
const selectEmoji = (emoji) => {
emit("select", emoji);
nextTick(() => {
if (emojiPopoverRef.value) {
emojiPopoverRef.value.hide();
}
});
};
</script>
<style scoped lang="scss">
.emoji-container {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 4px;
padding: 5px;
.emoji-item {
cursor: pointer;
font-size: 20px;
padding: 6px;
border-radius: 4px;
text-align: center;
transition: background-color 0.2s ease;
user-select: none;
&:hover {
background-color: #f0f2f5;
transform: scale(1.1);
}
}
:deep(.el-scrollbar__bar.is-horizontal) {
display: none;
}
}
.default-emoji-trigger {
font-size: 20px;
padding: 4px;
}
</style>

View File

@@ -0,0 +1,539 @@
<template>
<div class="comment-app">
<section class="main-publisher">
<div class="input-wrapper">
<el-input
v-model="mainInput"
type="textarea"
:rows="4"
:placeholder="t('comment.placeholder')"
resize="none"
/>
<div class="input-tools">
<div class="left-icons">
<!-- 提到-后续迭代 -->
<!-- <el-button link title="提到">@</el-button> -->
<!-- 图片功能-后续迭代 -->
<!-- <el-button link title="图片"><el-icon><Picture /></el-icon></el-button> -->
<!-- 附件功能-后续迭代 -->
<!-- <el-button link title="附件"><el-icon><Paperclip /></el-icon></el-button> -->
<!-- 表情功能 -->
<emoji-picker @select="(e) => onSelectEmoji(e, 'main')" />
</div>
<el-divider direction="vertical" />
<el-button
class="send-btn"
type="primary"
link
@click="submitMainComment"
>
<el-icon :size="20" :title="t('comment.send')"><Promotion /></el-icon>
</el-button>
</div>
</div>
</section>
<div class="comment-list">
<!-- 骨架屏 -->
<div v-if="loading">
<div v-for="n in 2" :key="'skeleton-' + n" class="comment-group">
<el-skeleton :rows="1" :animated="true">
<template #template>
<div class="skeleton-comment-parent-node">
<el-skeleton-item variant="circle" class="skeleton-avatar" />
<div class="skeleton-node-main">
<div class="skeleton-user-info">
<el-skeleton-item
variant="p"
style="width: 80px; height: 18px; margin-right: 10px"
/>
<el-skeleton-item
variant="p"
style="width: 60px; height: 14px"
/>
</div>
<el-skeleton-item
variant="p"
style="width: 100%; height: 16px; margin: 8px 0"
/>
<el-skeleton-item
variant="p"
style="width: 70%; height: 16px; margin-bottom: 12px"
/>
<div class="skeleton-actions">
<el-skeleton-item
variant="button"
style="width: 40px; height: 24px; margin-right: 8px"
/>
<el-skeleton-item
variant="button"
style="width: 40px; height: 24px"
/>
</div>
</div>
</div>
</template>
</el-skeleton>
</div>
</div>
<!-- 详细的内容展示 -->
<template v-else>
<div v-for="item in commentData" :key="item.id" class="comment-group">
<div class="parent-node">
<name-avatar :name="item.nickname" :src="item.avatar" :size="36" />
<div class="node-main">
<!-- 当前用户信息展示 -->
<div class="user-info">
<span class="nickname">{{ item.nickname }}</span>
<span class="time">{{ item.time }}</span>
</div>
<!-- 回复内容模块 -->
<div class="content" v-html="parseMention(item.content)"></div>
<!-- 回复内容-子集内容操作模块 -->
<div class="actions">
<el-button link @click="openReply(item, item)">
<el-icon><ChatDotSquare /></el-icon>&nbsp;回复
</el-button>
<el-button
link
class="delete-btn"
@click="deleteMainComment(item)"
v-if="item.canDelete"
>
<el-icon><Delete /></el-icon>&nbsp;删除
</el-button>
</div>
<!-- 回复内容展示二级-子集评论内容 -->
<div v-if="item.children?.length" class="sub-container">
<div
v-for="reply in item.children"
:key="reply.id"
class="sub-node"
>
<name-avatar
:name="reply.nickname"
:src="reply.avatar"
:size="36"
/>
<div class="sub-main">
<div class="sub-header">
<div class="sub-user-info">
<span class="nickname">{{ reply.nickname }}</span>
<span class="reply-text">回复</span>
<span class="target-name">@{{ reply.replyTo }}</span>
</div>
<span class="time">{{ reply.time }}</span>
</div>
<div class="content-body">{{ reply.content }}</div>
<!-- 回复 删除功能 -->
<div class="actions">
<el-button link @click="openReply(item, item)">
回复
</el-button>
<!-- 删除功能-只有自己评论的可以删除 -->
<el-button
link
class="delete-btn"
v-if="reply.canDelete"
@click="deleteReply(reply, item)"
>
删除
</el-button>
</div>
</div>
</div>
</div>
<!-- 统一回复输入框 -->
<div
v-if="activeReply.groupId === item.id"
class="inline-publisher"
>
<div class="input-wrapper">
<el-input
v-model="replyInput"
:placeholder="`${t('comment.reply')} @${activeReply.targetName}...`"
type="textarea"
:autosize="{ minRows: 2, maxRows: 4 }"
resize="none"
/>
<div class="input-tools">
<div class="left-icons">
<!-- 表情功能 -->
<emoji-picker
@select="(e) => onSelectEmoji(e, 'reply')"
/>
</div>
<el-divider direction="vertical" />
<el-button
class="send-btn"
type="primary"
link
@click="submitReply"
>
<el-icon :size="20" :title="t('comment.send')"><Promotion /></el-icon>
</el-button>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
</div>
</div>
</template>
<script setup>
import { ref, reactive } from "vue";
import { ElMessage } from "element-plus";
import EmojiPicker from "./EmojiPicker.vue";
import {
Picture,
Paperclip,
Promotion,
ChatDotSquare,
Delete,
} from "@element-plus/icons-vue";
import NameAvatar from "@/components/nameAvatar/index.vue";
import { useI18n } from "vue-i18n";
import { useUserStore } from "@/store";
const userStore = useUserStore();
const { t } = useI18n();
// 当前用户信息
const currentUser = computed(() => {
return {
nickname: userStore.userInfo.nickname,
avatar: userStore.userInfo.avatar,
};
});
// 评论业务逻辑
const activeReply = reactive({ parentId: null, targetName: "" });
const mainInput = ref("");
const replyInput = ref("");
const loading = ref(true); //当前骨架屏显示
const commentData = ref([
{
id: 1,
nickname: "李星倩",
avatar: "",
content: "已完成ROI测算请审核。",
time: "10分钟前",
canDelete: true,
children: [
{
id: 101,
nickname: "冯娜",
replyTo: "李星倩",
avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=2",
content: "收到,数据已入库,我马上看下。",
time: "刚刚",
},
],
},
]);
// 回复
const openReply = (target, group) => {
activeReply.groupId = group.id;
activeReply.targetName = target.nickname;
replyInput.value = "";
};
// 删除回复-删除评论
const deleteReply = (target, group) => {
const index = group.children.findIndex((item) => item.id === target.id);
group.children.splice(index, 1);
};
// 删除主评论-以及所有的子评论
const deleteMainComment = (target) => {
const index = commentData.value.findIndex((item) => item.id === target.id);
if (index !== -1) {
commentData.value.splice(index, 1);
if (activeReply.groupId === target.id) {
cancelReply();
}
}
};
// emoji输入框选择
const onSelectEmoji = (emoji, type) => {
console.log("emoji", emoji, type);
if (type === "main") {
mainInput.value += emoji;
} else {
replyInput.value += emoji;
}
};
const cancelReply = () => {
activeReply.groupId = null;
};
// @ 识别解析函数
const parseMention = (text) => {
if (!text) return "";
// 基础转义
let safeText = text.replace(/</g, "&lt;").replace(/>/g, "&gt;");
// 正则匹配 @用户,包裹为 span
return safeText.replace(
/@([\u4e00-\u9fa5\w-]+)/g,
'<span class="mention-link">@$1</span>'
);
};
// @提及 圈人操作
const handleMentionAction = (name) => {
const mentionStr = typeof name === "string" ? `@${name} ` : "@";
mainInput.value += mentionStr;
};
const submitMainComment = () => {
if (!mainInput.value.trim()) return ElMessage.warning("内容不能为空");
commentData.value.unshift({
id: Date.now(),
...currentUser.value,
content: mainInput.value,
time: "刚刚",
canDelete: true,
children: [],
});
mainInput.value = "";
};
const submitReply = () => {
const targetGroup = commentData.value.find(
(i) => i.id === activeReply.groupId
);
if (targetGroup) {
targetGroup.children.push({
id: Date.now(),
...currentUser.value,
replyTo: activeReply.targetName,
content: replyInput.value,
time: "刚刚",
});
cancelReply();
}
};
// 初始化
onMounted(() => {
setTimeout(() => {
loading.value = false;
}, 500);
});
</script>
<style scoped lang="scss">
$color-blue: #409eff;
$color-blue-bg: #f5f8ff;
$color-text-main: #303133;
$color-text-sub: #99a2aa;
$color-border: #e4e7ed;
$color-white: #fff;
.comment-app {
max-width: 800px;
margin: 20px auto;
font-family: -apple-system, sans-serif;
// 1. 输入框样式
.input-wrapper {
border: 1px solid $color-border;
border-radius: 12px;
padding: 12px;
position: relative;
transition: border-color 0.2s;
background-color: $color-white;
&:focus-within {
border-color: $color-blue;
}
:deep(.el-textarea__inner) {
border: none;
padding: 0;
box-shadow: none;
font-size: 15px;
}
.input-tools {
display: flex;
align-items: center;
justify-content: flex-end;
margin-top: 8px;
.left-icons {
display: flex;
gap: 5px;
.el-button {
color: $color-text-sub;
font-size: 18px;
padding: 4px;
}
.el-button + .el-button {
margin-left: 0;
}
}
.send-btn {
margin-left: 8px;
color: $color-blue;
}
}
}
// 2. 评论列表样式
.comment-list {
margin-top: 30px;
.comment-group {
margin-bottom: 30px;
.parent-node {
display: flex;
gap: 12px;
.user-avatar {
background-color: $color-blue;
color: #fff;
font-weight: bold;
}
.node-main {
flex: 1;
.user-info {
display: flex;
align-items: center;
gap: 10px;
.nickname {
font-weight: bold;
color: $color-text-main;
}
.time {
font-size: 13px;
color: $color-text-sub;
}
}
.content {
margin: 8px 0;
font-size: 15px;
color: $color-text-main;
}
.actions {
display: flex;
margin-bottom: 12px;
.el-button {
font-size: 13px;
color: $color-text-sub;
display: flex;
align-items: center;
gap: 4px;
&:hover {
color: $color-blue;
}
}
.delete-btn:hover {
color: #f56c6c;
}
}
}
}
}
}
// 3. 子评论卡片样式
.sub-container {
background-color: $color-blue-bg;
border-radius: 8px;
border-left: 3px solid #d9e5ff;
padding: 16px;
margin-top: 10px;
.sub-node {
display: flex;
gap: 10px;
margin-bottom: 15px;
&:last-child {
margin-bottom: 0;
}
.sub-main {
flex: 1;
.sub-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
.sub-user-info {
font-size: 14px;
.nickname {
font-weight: bold;
}
.reply-text {
margin: 0 6px;
color: $color-text-sub;
}
.target-name {
color: $color-blue;
font-weight: 500;
}
}
.time {
font-size: 12px;
color: $color-text-sub;
}
}
.content-body {
font-size: 14px;
color: $color-text-main;
margin-bottom: 3px;
}
}
}
}
.inline-publisher {
margin-top: 15px;
.inline-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 8px;
}
}
}
// 评论组件骨架屏
.skeleton-comment-parent-node {
display: flex;
gap: 12px;
width: 100%;
.skeleton-avatar {
width: 36px;
height: 36px;
flex-shrink: 0;
}
.skeleton-node-main {
flex: 1;
.skeleton-user-info {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.skeleton-actions {
display: flex;
margin-bottom: 12px;
}
}
}
</style>

View File

@@ -0,0 +1,7 @@
export default {
comment: {
placeholder: 'Enter your comment',
send:'Send',
reply:'Reply'
},
}

View File

@@ -0,0 +1,7 @@
export default {
comment: {
placeholder: '输入评论',
send:'发送',
reply:'回复'
},
}

View File

@@ -0,0 +1,18 @@
// useEmoji.ts
import emojiData from 'unicode-emoji-json'
export function useEmoji() {
const getEmotionList = (): string[] => {
return Object.keys(emojiData).filter(key => {
const item = emojiData[key];
return emojiData[key].group === 'Smileys & Emotion' && parseFloat(item.emoji_version) <= 12.0
})
}
// 预先生成好数据,避免组件每次渲染都执行过滤逻辑
const emotionList = getEmotionList()
return {
emotionList
}
}

View File

@@ -19,7 +19,7 @@
<span class="userinfo-username">{{ userInfo.username }}</span> <span class="userinfo-username">{{ userInfo.username }}</span>
<span class="userinfo-role">SUPER ADMIN</span> <span class="userinfo-role">SUPER ADMIN</span>
</div> </div>
<el-avatar :size="30" :src="userInfo.avatar" /> <name-avatar :name="userInfo.nickname" :src="userInfo.avatar" :size="30" />
</div> </div>
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
@@ -50,6 +50,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { Monitor, Bell } from "@element-plus/icons-vue"; import { Monitor, Bell } from "@element-plus/icons-vue";
import TokenManager from "@/utils/storage"; import TokenManager from "@/utils/storage";
import NameAvatar from "@/components/nameAvatar/index.vue";
import { useUserStore } from "@/store"; import { useUserStore } from "@/store";
defineOptions({ name: "RightMenuGroup" }); defineOptions({ name: "RightMenuGroup" });
const userStore = useUserStore(); const userStore = useUserStore();

View File

@@ -0,0 +1,99 @@
.mj-drawer-content {
.pro-table-container {
border-radius: 2px;
:deep(.pro-table-footer) {
padding-top: 12px;
padding-bottom: 12px;
}
}
.mj-drawer-top-container {
.top-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
background: #fff;
padding: 16px 20px;
}
.left-actions {
display: flex;
gap: 12px;
}
/* 搜索框自定义 */
.custom-search-input {
--el-input-height: 30px;
&:deep(.el-input__wrapper) {
background-color: #f5f7fa;
border-radius: 10px;
box-shadow: none;
border: 1px solid #e2e8f0;
}
}
/* 新增字段按钮样式 */
.add-field-btn {
background: linear-gradient(to right, #2b65f6, #1e4edb);
border: none;
border-radius: 10px;
padding: 10px 20px;
font-weight: bold;
box-shadow: 0 4px 12px rgba(43, 101, 246, 0.3);
}
/* 筛选卡片内部样式 */
.filter-card {
padding: 10px 5px;
}
.filter-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 18px;
}
.filter-header .title {
font-weight: 600;
font-size: 15px;
color: #333;
}
.reset-link {
font-size: 13px;
color: #2b65f6;
}
.filter-group {
margin-bottom: 20px;
}
.filter-group label {
display: block;
font-size: 13px;
color: #94a3b8;
margin-bottom: 8px;
font-weight: 500;
}
/* 统一输入框底色 */
.full-width-select :deep(.el-input__wrapper),
.full-width-date {
width: 100% !important;
background-color: #f5f7fa !important;
box-shadow: none !important;
border: 1px solid #e4e7ed !important;
height: 40px;
}
/* 应用筛选按钮 */
.apply-btn {
width: 100%;
height: 42px;
background-color: #2b65f6;
border-radius: 6px;
font-weight: bold;
margin-top: 10px;
}
}
}

View File

@@ -2,7 +2,7 @@
<el-drawer <el-drawer
v-model="visible" v-model="visible"
title="字段配置" title="字段配置"
size="70%" :size="size || '70%'"
:close-on-click-modal="false" :close-on-click-modal="false"
:close-on-press-escape="false" :close-on-press-escape="false"
destroy-on-close destroy-on-close
@@ -71,7 +71,7 @@
</CommonFilter> </CommonFilter>
</div> </div>
<div class="right-actions"> <div class="right-actions" v-if="!hasChild">
<el-button <el-button
type="primary" type="primary"
class="add-field-btn" class="add-field-btn"
@@ -95,7 +95,11 @@
> >
<!-- 名称点击 --> <!-- 名称点击 -->
<template #labelName="{ row }"> <template #labelName="{ row }">
<el-button link type="primary" @click="onLevelNext">{{ row.label }}</el-button> <el-button link type="primary" @click="onLevelNext(row)" v-if="!hasChild">{{
row.label
}}</el-button>
<span v-else>{{ row.label }}</span>
</template> </template>
<!-- 状态插槽 --> <!-- 状态插槽 -->
<template #status="{ row }"> <template #status="{ row }">
@@ -109,17 +113,16 @@
{{ DictManage.statusDict[row.status] }} {{ DictManage.statusDict[row.status] }}
</div> </div>
</template> </template>
<template #actions="{ row }"> <!-- <template #actions="{ row }">
<el-button link type="primary" @click="handleAddNext(row)" <el-button link type="primary" v-if="!hasChild" @click="handleAddNext(row)"
>添加二级字段</el-button >添加二级字段</el-button>
>
<el-button link type="primary" @click="handleEdit(row)" <el-button link type="primary" @click="handleEdit(row)"
>编辑</el-button >编辑</el-button
> >
<el-button link type="danger" @click="handleDelete(row)" <el-button link type="danger" @click="handleDelete(row)"
>删除</el-button >删除</el-button
> >
</template> </template> -->
</CommonTable> </CommonTable>
<!-- 新增字段 --> <!-- 新增字段 -->
<dictFieldLevelManage <dictFieldLevelManage
@@ -129,6 +132,13 @@
:parentId="parentId" :parentId="parentId"
@confirm-success="onConfirmSuccess" @confirm-success="onConfirmSuccess"
/> />
<!-- 当前弹层的组件-支持嵌套展开 -->
<dict-field-config
v-for="child in childModals"
:key="child.key"
:ref="(el) => setChildModalRef(el, child.key)"
/>
</div> </div>
</el-drawer> </el-drawer>
</template> </template>
@@ -146,14 +156,6 @@ import {
disableTypeDict, disableTypeDict,
} from "@/api/stage/dict"; } from "@/api/stage/dict";
defineOptions({ name: "DictFieldConfig" }); defineOptions({ name: "DictFieldConfig" });
interface User {
id: number;
date: string;
name: string;
address: string;
hasChildren?: boolean;
children?: User[];
}
const dictTitle = ref(""); const dictTitle = ref("");
const addVisible = ref(false); const addVisible = ref(false);
@@ -162,14 +164,18 @@ const searchQuery = ref("");
const filterForm = reactive({ const filterForm = reactive({
status: "", status: "",
}); });
const size = ref<string>(""); //抽屉大小
const tableRef = ref(null); const tableRef = ref(null);
const visible = ref<boolean>(false); const visible = ref<boolean>(false);
const parentId = ref<string>(""); const parentId = ref<string>("");
const total = ref(0); const total = ref(0);
const list = ref([]); const list = ref([]);
const hasChild = ref<boolean>(false); //是否是子级弹窗
const columns = [ const childId = ref<string|number>(''); // 子集的id
const childModals = ref([]); //子弹窗的列表
const childModalRefs = ref({}); // 子弹窗的引用
const onCloseCallback = ref<Function | null>(null); // 关闭回调函数
const columns = computed(()=>[
{ {
prop: "id", prop: "id",
label: "字典编码", label: "字典编码",
@@ -201,28 +207,95 @@ const columns = [
label: "更新时间", label: "更新时间",
align: "center", align: "center",
showOverflowTooltip: true, showOverflowTooltip: true,
width:200,
formatter: (val) => { formatter: (val) => {
return val.createTime return val.updateTime
? dayjs(val.createTime).format("YYYY-MM-DD HH:mm") ? dayjs(val.updateTime).format("YYYY-MM-DD HH:mm")
: "-"; : "-";
}, },
}, },
{ prop: "actions", label: "操作", align: "right", width: "300" }, {
]; prop: "actions",
label: "操作",
align: "right",
width: "300",
actions:[
{
label: "添加二级字段",
type: "primary",
link:true,
permission: ["edit"],
show:()=>{
return !hasChild.value
},
onClick: (row) => handleAddNext(row),
},
{
label: "编辑",
type: "primary",
link:true,
permission: ["edit"],
onClick: (row) => handleEdit(row),
},
{
label: "删除",
type: "danger",
link:true,
permission: ["delete"],
onClick: (row) => handleDelete(row),
},
]
},
])
// 设置子弹窗引用
const setChildModalRef = (el, key) => {
if (el) {
childModalRefs.value[key] = el;
}
};
// 点击获取二级菜单数据 // 点击获取二级菜单数据
const onLevelNext = () =>{ const onLevelNext = (row) => {
console.log('next') const childKey = `child-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
} childModals.value.push({
key: childKey,
data: row,
hasChild: true,
});
nextTick(() => {
const childRef = childModalRefs.value[childKey];
if (childRef) {
childRef.open({
...row,
parentId:parentId.value,
hasChild: true,
onClose: () => removeChildModal(childKey)
});
}
});
};
// 移除当前组件
const removeChildModal = (key: string) => {
const index = childModals.value.findIndex(child => child.key === key);
if (index !== -1) {
childModals.value.splice(index, 1);
}
// 同时清理引用
delete childModalRefs.value[key];
};
// 请求数据信息 // 请求数据信息
const fetchData = async (params) => { const fetchData = async (params) => {
try { try {
const response = await getDictTypeValue(parentId.value, { const queryParams = {
...params, ...params,
keyword: searchQuery.value, keyword: searchQuery.value,
...filterForm, ...filterForm,
}); }
const response = hasChild.value ? await getNextDictMenu(parentId.value,childId.value,queryParams) : await getDictTypeValue(parentId.value, queryParams);
return response; return response;
} catch (error) { } catch (error) {
console.log("getTableData Error", error); console.log("getTableData Error", error);
@@ -242,25 +315,19 @@ const handleDictStatus = async (row) => {
} }
}; };
// FIXME:tree懒加载数据
const load = async (
row: User,
treeNode: unknown,
resolve: (data: User[]) => void
) => {
try {
const resp = await getNextDictMenu(parentId.value, row.id);
console.log("获取当前返回的二级数据信息==>:", resp);
resolve([]);
} catch (error) {
resolve([]);
console.log("fetch tree error", error);
}
};
// 新增二级菜单数据 // 新增二级菜单数据
const addFields = () => { const addFields = () => {
dictTitle.value = "新增字段"; dictTitle.value = "新增字段";
addVisible.value = true; addVisible.value = true;
Object.assign(selectItem,{
id:null,
parentId:null,
label: "",
value: "",
sort: 0,
status:1,
remark:''
})
}; };
// 确定刷新数据 // 确定刷新数据
@@ -283,6 +350,7 @@ const onReset = () => {
const handleAddNext = async (item) => { const handleAddNext = async (item) => {
addVisible.value = true; addVisible.value = true;
dictTitle.value = "添加二级字段"; dictTitle.value = "添加二级字段";
Object.assign(selectItem,{},{parentId:item.id});
}; };
// 编辑当前字段 // 编辑当前字段
const handleEdit = (item) => { const handleEdit = (item) => {
@@ -309,8 +377,17 @@ const handleDelete = async (item) => {
defineExpose({ defineExpose({
open: async (item) => { open: async (item) => {
parentId.value = item.id; parentId.value = item.parentId;
visible.value = true; visible.value = true;
hasChild.value = item.hasChild ?? false;
// 处理子集的弹窗
if (hasChild.value) {
size.value = "60%";
childId.value = item.id;
if (item.onClose) {
onCloseCallback.value = item.onClose;
}
}
await nextTick(); await nextTick();
if (tableRef.value) { if (tableRef.value) {
await tableRef.value.refresh(); await tableRef.value.refresh();
@@ -318,107 +395,13 @@ defineExpose({
}, },
close() { close() {
visible.value = false; visible.value = false;
if (onCloseCallback.value) {
onCloseCallback.value();
onCloseCallback.value = null;
}
}, },
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.mj-drawer-content { @use "./dictField.scss" as *;
.pro-table-container {
border-radius: 2px;
:deep(.pro-table-footer) {
padding-top: 12px;
padding-bottom: 12px;
}
}
.mj-drawer-top-container {
.top-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
background: #fff;
padding: 16px 20px;
}
.left-actions {
display: flex;
gap: 12px;
}
/* 搜索框自定义 */
.custom-search-input {
--el-input-height: 30px;
&:deep(.el-input__wrapper) {
background-color: #f5f7fa;
border-radius: 10px;
box-shadow: none;
border: 1px solid #e2e8f0;
}
}
/* 新增字段按钮样式 */
.add-field-btn {
background: linear-gradient(to right, #2b65f6, #1e4edb);
border: none;
border-radius: 10px;
padding: 10px 20px;
font-weight: bold;
box-shadow: 0 4px 12px rgba(43, 101, 246, 0.3);
}
/* 筛选卡片内部样式 */
.filter-card {
padding: 10px 5px;
}
.filter-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 18px;
}
.filter-header .title {
font-weight: 600;
font-size: 15px;
color: #333;
}
.reset-link {
font-size: 13px;
color: #2b65f6;
}
.filter-group {
margin-bottom: 20px;
}
.filter-group label {
display: block;
font-size: 13px;
color: #94a3b8;
margin-bottom: 8px;
font-weight: 500;
}
/* 统一输入框底色 */
.full-width-select :deep(.el-input__wrapper),
.full-width-date {
width: 100% !important;
background-color: #f5f7fa !important;
box-shadow: none !important;
border: 1px solid #e4e7ed !important;
height: 40px;
}
/* 应用筛选按钮 */
.apply-btn {
width: 100%;
height: 42px;
background-color: #2b65f6;
border-radius: 6px;
font-weight: bold;
margin-top: 10px;
}
}
}
</style> </style>

View File

@@ -41,7 +41,7 @@
<el-radio :value="0">停用</el-radio> <el-radio :value="0">停用</el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="备注:"> <el-form-item label="备注:" prop="remark">
<el-input <el-input
type="textarea" type="textarea"
:autosize="{ minRows: 4, maxRows: 5 }" :autosize="{ minRows: 4, maxRows: 5 }"
@@ -105,6 +105,7 @@ const rules = reactive({
label: [{ required: true, message: "请输入字段名称", trigger: "blur" }], label: [{ required: true, message: "请输入字段名称", trigger: "blur" }],
value: [{ required: true, message: "请输入字典值", trigger: "blur" }], value: [{ required: true, message: "请输入字典值", trigger: "blur" }],
sort: [{ required: true, message: "请输入排序", trigger: "blur" }], sort: [{ required: true, message: "请输入排序", trigger: "blur" }],
remark:[{ required: false, message: "请输入备注", trigger: "blur" }]
}); });
// 确定 // 确定
@@ -113,6 +114,8 @@ const onConfirm = async (formEl: FormInstance | undefined) => {
await formEl.validate(async (valid, fields) => { await formEl.validate(async (valid, fields) => {
if (valid) { if (valid) {
loading.value = true; loading.value = true;
console.log("获取外部的数据信息:",form,parentId)
// 如果是一级新增字段就是parentId为0 如果是添加二级字段就是parentId为父级的id
try { try {
const response = row.id ? await updateDictTypeValue(parentId,row.id,form) : await saveDictTypeValue(parentId,form); const response = row.id ? await updateDictTypeValue(parentId,row.id,form) : await saveDictTypeValue(parentId,form);
ElMessage.success(row.id ? '修改成功' : '新增成功'); ElMessage.success(row.id ? '修改成功' : '新增成功');

View File

@@ -29,7 +29,7 @@
<span></span> <span></span>
</div> </div>
</template> </template>
<el-input placeholder="请输入字典类型" v-model="form.key" :disabled="form.id"></el-input> <el-input placeholder="请输入字典类型" v-model="form.key" :disabled="form.id ? true : false"></el-input>
</el-form-item> </el-form-item>
<el-form-item label="状态:" prop="status"> <el-form-item label="状态:" prop="status">
<el-radio-group v-model="form.status"> <el-radio-group v-model="form.status">

View File

@@ -12,7 +12,11 @@
<div class="mj-filter-content"> <div class="mj-filter-content">
<div class="filter-header"> <div class="filter-header">
<span class="title">条件筛选</span> <span class="title">条件筛选</span>
<el-link type="primary" underline="never" class="reset-btn" @click="onReset" <el-link
type="primary"
underline="never"
class="reset-btn"
@click="onReset"
>重置</el-link >重置</el-link
> >
</div> </div>
@@ -35,7 +39,12 @@
</el-select> </el-select>
</div> </div>
</div> </div>
<el-button type="primary" class="apply-btn" @click="fetchTableData">应用筛选</el-button> <el-button
type="primary"
class="apply-btn"
@click="fetchTableData"
>应用筛选</el-button
>
</div> </div>
</CommonFilter> </CommonFilter>
<div class="search-dict-input"> <div class="search-dict-input">
@@ -84,7 +93,7 @@
</div> </div>
</template> </template>
<template #actions="{ row }"> <!-- <template #actions="{ row }">
<el-button link type="primary" @click="handleEdit(row)">编辑</el-button> <el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
<el-button link type="primary" @click="handlefieldsConfig(row)" <el-button link type="primary" @click="handlefieldsConfig(row)"
>字段配置</el-button >字段配置</el-button
@@ -92,7 +101,7 @@
<el-button link type="danger" @click="handleDelete(row)" <el-button link type="danger" @click="handleDelete(row)"
>删除</el-button >删除</el-button
> >
</template> </template> -->
</CommonTable> </CommonTable>
<!-- 新增-编辑字典弹窗 --> <!-- 新增-编辑字典弹窗 -->
@@ -112,7 +121,12 @@ import CommonTable from "@/components/proTable/index.vue";
import dictFieldConfig from "./dictFieldConfig.vue"; import dictFieldConfig from "./dictFieldConfig.vue";
import dictManage from "./dictManage.vue"; import dictManage from "./dictManage.vue";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { getDictValues, deleteDictValue,disableDict,enableDict } from "@/api/stage/dict"; import {
getDictValues,
deleteDictValue,
disableDict,
enableDict,
} from "@/api/stage/dict";
import { DictManage } from "@/dict"; import { DictManage } from "@/dict";
import { formatIndex } from "@/utils/utils"; import { formatIndex } from "@/utils/utils";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
@@ -164,33 +178,60 @@ const columns = [
}, },
}, },
{ {
prop:'updateByName', prop: "updateByName",
label:'最后修改人', label: "最后修改人",
align:'center' align: "center",
},
{
prop: "actions",
label: "操作",
align: "right",
width: "300",
actions: [
{
label: "编辑",
type: "primary",
link:true,
permission: ["edit"],
onClick: (row) => handleEdit(row),
},
{
label: "字段配置",
type: "primary",
link:true,
permission: ["config"],
onClick: (row) => handlefieldsConfig(row),
},
{
label: "删除",
type: "danger",
link:true,
permission: ["delete"],
onClick: (row) => handleDelete(row),
}
],
}, },
{ prop: "actions", label: "操作", align: "right", width: "200" },
]; ];
// 返回的data数据信息 // 返回的data数据信息
const dataValue = ref([]); const dataValue = ref([]);
// popover关闭事件 // popover关闭事件
const onPopoverHide = () =>{ const onPopoverHide = () => {
filterForm.status = ''; filterForm.status = "";
} };
// 筛选重置 // 筛选重置
const onReset = () =>{ const onReset = () => {
filterForm.status = ''; filterForm.status = "";
fetchTableData(); fetchTableData();
} };
// 获取当前的table数据信息 // 获取当前的table数据信息
const getTableData = async (params) => { const getTableData = async (params) => {
try { try {
const response = await getDictValues({ const response = await getDictValues({
...params, ...params,
keyword: searchVal.value, keyword: searchVal.value,
...filterForm ...filterForm,
}); });
return response; return response;
} catch (error) { } catch (error) {
@@ -201,26 +242,30 @@ const getTableData = async (params) => {
const fetchTableData = () => { const fetchTableData = () => {
dictTableRef.value && dictTableRef.value.refresh(); dictTableRef.value && dictTableRef.value.refresh();
}; };
const clearSelectItem = () => {
Object.assign(selectItem,{id:null,name:'',key:'',status:1,remark:''})
};
// 新增字典信息 // 新增字典信息
const addDict = () => { const addDict = () => {
dictVisible.value = true; dictVisible.value = true;
clearSelectItem();
}; };
// 编辑字典信息 // 编辑字典信息
const handleEdit = (item) => { const handleEdit = (item) => {
addDict(); dictVisible.value = true;
Object.assign(selectItem, item); Object.assign(selectItem, item);
}; };
// 启用-禁用事件 // 启用-禁用事件
const handleDictStatus = async (row)=>{ const handleDictStatus = async (row) => {
try { try {
row.status === 1 ? await disableDict(row.id) : await enableDict(row.id); row.status === 1 ? await disableDict(row.id) : await enableDict(row.id);
ElMessage.success('操作成功'); ElMessage.success("操作成功");
onConfirmSuccess(); onConfirmSuccess();
} catch (error) { } catch (error) {
console.log('error',error); console.log("error", error);
} }
} };
// 刷新Table数据信息 // 刷新Table数据信息
const onConfirmSuccess = () => { const onConfirmSuccess = () => {
@@ -228,7 +273,7 @@ const onConfirmSuccess = () => {
}; };
// TODO:字段配置 // TODO:字段配置
const handlefieldsConfig = (ite) => { const handlefieldsConfig = (ite) => {
fieldsConfigRef.value.open(ite); fieldsConfigRef.value.open({ ...ite, parentId: ite.id });
}; };
// 删除字典类型数据 // 删除字典类型数据

View File

@@ -4,7 +4,7 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import Comment from "@/components/comment/index.vue"; import Comment from "@/modules/Comment/index.vue";
import { reactive, ref, onMounted } from "vue"; import { reactive, ref, onMounted } from "vue";
defineOptions({ name: "Personnel" }); defineOptions({ name: "Personnel" });

View File

@@ -33,6 +33,7 @@ const useUserStore = defineStore("user", {
routes: [] as RouteMenu[], routes: [] as RouteMenu[],
isRoutesLoaded: false, // 标记路由是否已加载 isRoutesLoaded: false, // 标记路由是否已加载
isBackendUser:true, //标记是否是后台用户 isBackendUser:true, //标记是否是后台用户
role:['edit','delete','config','add'] //当前的权限列表
}; };
}, },
getters: { getters: {

View File

@@ -73,7 +73,7 @@ body {
// 筛选框全局样式内容 // 筛选框全局样式内容
.mj-filter-content { .mj-filter-content {
width: 380px; min-width: 380px;
background: #fff; background: #fff;
border-radius: 8px; border-radius: 8px;
padding: 20px; padding: 20px;
@@ -111,14 +111,11 @@ body {
} }
.custom-select { .custom-select {
--el-border-color:transparent;
width: 100%; width: 100%;
background-color: #f5f7fa; background-color: #f5f7fa;
box-shadow: none; box-shadow: none;
border: 1px solid #e4e7ed; border: 1px solid #e4e7ed;
:deep(.el-input__wrapper) {
background-color: transparent;
box-shadow: none;
}
} }
.apply-btn { .apply-btn {

View File

@@ -2,7 +2,7 @@ import { useUserStore } from "@/store";
function permissionDirective(el: HTMLElement,binding:any) { function permissionDirective(el: HTMLElement,binding:any) {
const appStore = useUserStore(); const appStore = useUserStore();
const userPermissions = appStore.role; // 假設從store中獲取用戶權限 const userPermissions = appStore.role;
let requiredPermissions = binding.value; let requiredPermissions = binding.value;
if (typeof requiredPermissions === "string") { if (typeof requiredPermissions === "string") {
requiredPermissions = [requiredPermissions]; requiredPermissions = [requiredPermissions];

27
src/utils/permission.ts Normal file
View File

@@ -0,0 +1,27 @@
// 封装权限公用方法
import { useUserStore } from "@/store";
/**
* 使用: import { usePermission } from "@/utils/permission";
* 示例: const { checkPermission } = usePermission();
* checkPermission('permission1') OR checkPermission(['permission1', 'permission2'])
*
* */
export const usePermission = () => {
const appStore = useUserStore();
const checkPermission = (requiredPermissions: string | string[]):boolean => {
// 通过接口获取到的用户权限数据
const userPermissions = appStore.role;
const permissionArray = Array.isArray(requiredPermissions) ? requiredPermissions : [requiredPermissions];
const hasPermission = permissionArray.some((permission) =>
userPermissions.includes(permission)
);
return hasPermission;
};
return { checkPermission };
};