fix:完善字典管理功能优化全局组件
This commit is contained in:
@@ -40,7 +40,6 @@
|
||||
[x] i18n注入 语言全局化实现
|
||||
[x] 状态管理工具Pinia实现
|
||||
[x] ElementPlus 组件引入 并且实现i18n
|
||||
[] 引入unocss 样式
|
||||
[] 路由由后端控制实现动态路由
|
||||
[x] 路由由后端控制实现动态路由
|
||||
|
||||
|
||||
|
||||
6
components.d.ts
vendored
6
components.d.ts
vendored
@@ -27,6 +27,7 @@ declare module 'vue' {
|
||||
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
|
||||
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
|
||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||
ElDivider: typeof import('element-plus/es')['ElDivider']
|
||||
ElDrawer: typeof import('element-plus/es')['ElDrawer']
|
||||
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
||||
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
||||
@@ -49,6 +50,8 @@ declare module 'vue' {
|
||||
ElRow: typeof import('element-plus/es')['ElRow']
|
||||
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||
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']
|
||||
ElTable: typeof import('element-plus/es')['ElTable']
|
||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||
@@ -59,8 +62,10 @@ declare module 'vue' {
|
||||
ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
|
||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||
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']
|
||||
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']
|
||||
PageForm: typeof import('./src/components/pageForm/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']
|
||||
StandardMenu: typeof import('./src/components/standardMenu/index.vue')['default']
|
||||
StandMenu: typeof import('./src/components/standMenu/index.vue')['default']
|
||||
Xxx: typeof import('./src/components/comment/xxx.vue')['default']
|
||||
}
|
||||
export interface GlobalDirectives {
|
||||
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"dayjs": "^1.11.19",
|
||||
"sass": "^1.97.1",
|
||||
"typescript": "~5.9.3",
|
||||
"unicode-emoji-json": "^0.8.0",
|
||||
"unplugin-auto-import": "^20.3.0",
|
||||
"unplugin-vue-components": "^30.0.0",
|
||||
"vite": "^7.2.4",
|
||||
|
||||
25
pnpm-lock.yaml
generated
25
pnpm-lock.yaml
generated
@@ -51,6 +51,9 @@ importers:
|
||||
typescript:
|
||||
specifier: ~5.9.3
|
||||
version: 5.9.3
|
||||
unicode-emoji-json:
|
||||
specifier: ^0.8.0
|
||||
version: 0.8.0
|
||||
unplugin-auto-import:
|
||||
specifier: ^20.3.0
|
||||
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'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-arm-musl@2.5.1':
|
||||
resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-linux-arm64-glibc@2.5.1':
|
||||
resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-arm64-musl@2.5.1':
|
||||
resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-linux-x64-glibc@2.5.1':
|
||||
resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-x64-musl@2.5.1':
|
||||
resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-win32-arm64@2.5.1':
|
||||
resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==}
|
||||
@@ -413,67 +410,56 @@ packages:
|
||||
resolution: {integrity: sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.54.0':
|
||||
resolution: {integrity: sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.54.0':
|
||||
resolution: {integrity: sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.54.0':
|
||||
resolution: {integrity: sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-loong64-gnu@4.54.0':
|
||||
resolution: {integrity: sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.54.0':
|
||||
resolution: {integrity: sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.54.0':
|
||||
resolution: {integrity: sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.54.0':
|
||||
resolution: {integrity: sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.54.0':
|
||||
resolution: {integrity: sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.54.0':
|
||||
resolution: {integrity: sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.54.0':
|
||||
resolution: {integrity: sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-openharmony-arm64@4.54.0':
|
||||
resolution: {integrity: sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==}
|
||||
@@ -959,6 +945,9 @@ packages:
|
||||
undici-types@7.16.0:
|
||||
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
|
||||
|
||||
unicode-emoji-json@0.8.0:
|
||||
resolution: {integrity: sha512-3wDXXvp6YGoKGhS2O2H7+V+bYduOBydN1lnI0uVfr1cIdY02uFFiEH1i3kE5CCE4l6UqbLKVmEFW9USxTAMD1g==}
|
||||
|
||||
unimport@5.6.0:
|
||||
resolution: {integrity: sha512-8rqAmtJV8o60x46kBAJKtHpJDJWkA2xcBqWKPI14MgUb05o1pnpnCnXSxedUXyeq7p8fR5g3pTo2BaswZ9lD9A==}
|
||||
engines: {node: '>=18.12.0'}
|
||||
@@ -1872,6 +1861,8 @@ snapshots:
|
||||
|
||||
undici-types@7.16.0: {}
|
||||
|
||||
unicode-emoji-json@0.8.0: {}
|
||||
|
||||
unimport@5.6.0:
|
||||
dependencies:
|
||||
acorn: 8.15.0
|
||||
|
||||
@@ -66,26 +66,26 @@ export const saveDictTypeValue = (id:string,data: addDataProps) => {
|
||||
|
||||
// 删除字典类型值
|
||||
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) => {
|
||||
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) => {
|
||||
return request.get(`/auth/v1/backend/dict/type/${id}/data/${parentId}`);
|
||||
export const getNextDictMenu = (id:string,parentId:string,params: paramsProps) => {
|
||||
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`);
|
||||
}
|
||||
|
||||
// 禁用接口
|
||||
export const disableTypeDict = (id:string)=>{
|
||||
export const disableTypeDict = (typeId:string,id:string)=>{
|
||||
return request.post(`/auth/v1/backend/dict/type/${typeId}/${id}/disable`)
|
||||
}
|
||||
@@ -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>
|
||||
57
src/components/nameAvatar/index.vue
Normal file
57
src/components/nameAvatar/index.vue
Normal 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>
|
||||
@@ -1,12 +1,20 @@
|
||||
<template>
|
||||
<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">
|
||||
<template v-for="(col,index) in columns" :key="col.prop">
|
||||
<el-table
|
||||
: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
|
||||
v-if="!col.slot && col.prop !== 'actions'"
|
||||
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 v-else-if="col.slot" v-bind="col">
|
||||
@@ -22,7 +30,78 @@
|
||||
<el-table-column v-else-if="col.prop === 'actions'" v-bind="col">
|
||||
<template #default="scope">
|
||||
<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>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -54,7 +133,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from "vue";
|
||||
|
||||
import { ArrowDown } from "@element-plus/icons-vue";
|
||||
const props = defineProps({
|
||||
columns: { type: Array, required: true },
|
||||
data: { type: Array, required: true },
|
||||
@@ -64,40 +143,50 @@ const props = defineProps({
|
||||
// 是否立即请求数据
|
||||
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
|
||||
const innerLoading = ref(false);
|
||||
|
||||
// 默认按钮长度
|
||||
const MAX_BUTTON_LENGTH = 3;
|
||||
|
||||
// 标记是否是首次挂载
|
||||
let isFirstMount = true;
|
||||
|
||||
// 参数传递逻辑
|
||||
const params = computed(()=>{
|
||||
if(!props.pagination){
|
||||
return {}
|
||||
}else if(typeof props.pagination === 'object' && props.pagination !== null){
|
||||
const params = computed(() => {
|
||||
if (!props.pagination) {
|
||||
return {};
|
||||
} else if (
|
||||
typeof props.pagination === "object" &&
|
||||
props.pagination !== null
|
||||
) {
|
||||
return {
|
||||
pageNo:props.pagination.currentPage,
|
||||
pageSize:props.pagination.pageSize
|
||||
}
|
||||
}else{
|
||||
pageNo: props.pagination.currentPage,
|
||||
pageSize: props.pagination.pageSize,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
pageNo:1,
|
||||
pageSize:20
|
||||
pageNo: 1,
|
||||
pageSize: 20,
|
||||
};
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// 请求方法
|
||||
const refresh = async () => {
|
||||
if (!props.requestApi) return;
|
||||
innerLoading.value = true;
|
||||
try {
|
||||
|
||||
const res = await props.requestApi(params.value);
|
||||
emit("update:data", res?.records || []);
|
||||
emit("update:total", res?.total || 0);
|
||||
@@ -123,7 +212,9 @@ onMounted(() => {
|
||||
if (props.immediate) {
|
||||
refresh();
|
||||
}
|
||||
setTimeout(() => { isFirstMount = false; }, 0);
|
||||
setTimeout(() => {
|
||||
isFirstMount = false;
|
||||
}, 0);
|
||||
});
|
||||
|
||||
// 兼容设置了keep-alive
|
||||
@@ -142,6 +233,35 @@ const paginationConfig = computed(() => ({
|
||||
background: false,
|
||||
...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>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -152,9 +272,9 @@ const paginationConfig = computed(() => ({
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #f0f0f0;
|
||||
|
||||
:deep(.header-row-name){
|
||||
th.el-table__cell{
|
||||
background-color: #FBFCFD;
|
||||
:deep(.header-row-name) {
|
||||
th.el-table__cell {
|
||||
background-color: #fbfcfd;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,5 +322,8 @@ const paginationConfig = computed(() => ({
|
||||
color: #409eff;
|
||||
font-weight: bold;
|
||||
}
|
||||
.dropdown-menu-table{
|
||||
vertical-align: bottom;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,7 +10,7 @@ const statusDict = {
|
||||
// 字典状态颜色
|
||||
const statusDictColor = {
|
||||
1:'#66E5BE',
|
||||
0:'#ff0000'
|
||||
0:'#90A1B9'
|
||||
}
|
||||
|
||||
// 设置字典转换为目标格式
|
||||
|
||||
@@ -21,13 +21,16 @@ const app = createApp(App);
|
||||
// 导入全局的i18n文件
|
||||
const loadLocalMessages = async () => {
|
||||
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) => {
|
||||
const lang = path.match(/\.\/locales\/(.+)\.ts$/)?.[1];
|
||||
const lang = path.match(/.*\/locales\/(.+)\.ts$/)?.[1];
|
||||
if (lang && locales[path]) {
|
||||
messages[lang] = locales[path].default;
|
||||
if (!messages[lang]) {
|
||||
messages[lang] = {};
|
||||
}
|
||||
messages[lang] = { ...messages[lang], ...locales[path].default };
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
84
src/modules/Comment/emojiPicker.vue
Normal file
84
src/modules/Comment/emojiPicker.vue
Normal 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>
|
||||
539
src/modules/Comment/index.vue
Normal file
539
src/modules/Comment/index.vue
Normal 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> 回复
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
class="delete-btn"
|
||||
@click="deleteMainComment(item)"
|
||||
v-if="item.canDelete"
|
||||
>
|
||||
<el-icon><Delete /></el-icon> 删除
|
||||
</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, "<").replace(/>/g, ">");
|
||||
// 正则匹配 @用户,包裹为 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>
|
||||
7
src/modules/Comment/locales/en.ts
Normal file
7
src/modules/Comment/locales/en.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export default {
|
||||
comment: {
|
||||
placeholder: 'Enter your comment',
|
||||
send:'Send',
|
||||
reply:'Reply'
|
||||
},
|
||||
}
|
||||
7
src/modules/Comment/locales/zh.ts
Normal file
7
src/modules/Comment/locales/zh.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export default {
|
||||
comment: {
|
||||
placeholder: '输入评论',
|
||||
send:'发送',
|
||||
reply:'回复'
|
||||
},
|
||||
}
|
||||
18
src/modules/Comment/useEmoji.ts
Normal file
18
src/modules/Comment/useEmoji.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@
|
||||
<span class="userinfo-username">{{ userInfo.username }}</span>
|
||||
<span class="userinfo-role">SUPER ADMIN</span>
|
||||
</div>
|
||||
<el-avatar :size="30" :src="userInfo.avatar" />
|
||||
<name-avatar :name="userInfo.nickname" :src="userInfo.avatar" :size="30" />
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
@@ -50,6 +50,7 @@
|
||||
<script setup lang="ts">
|
||||
import { Monitor, Bell } from "@element-plus/icons-vue";
|
||||
import TokenManager from "@/utils/storage";
|
||||
import NameAvatar from "@/components/nameAvatar/index.vue";
|
||||
import { useUserStore } from "@/store";
|
||||
defineOptions({ name: "RightMenuGroup" });
|
||||
const userStore = useUserStore();
|
||||
|
||||
99
src/pages/stage/dict/dictField.scss
Normal file
99
src/pages/stage/dict/dictField.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
<el-drawer
|
||||
v-model="visible"
|
||||
title="字段配置"
|
||||
size="70%"
|
||||
:size="size || '70%'"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
destroy-on-close
|
||||
@@ -71,7 +71,7 @@
|
||||
</CommonFilter>
|
||||
</div>
|
||||
|
||||
<div class="right-actions">
|
||||
<div class="right-actions" v-if="!hasChild">
|
||||
<el-button
|
||||
type="primary"
|
||||
class="add-field-btn"
|
||||
@@ -95,7 +95,11 @@
|
||||
>
|
||||
<!-- 名称点击 -->
|
||||
<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 #status="{ row }">
|
||||
@@ -109,17 +113,16 @@
|
||||
{{ DictManage.statusDict[row.status] }}
|
||||
</div>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<el-button link type="primary" @click="handleAddNext(row)"
|
||||
>添加二级字段</el-button
|
||||
>
|
||||
<!-- <template #actions="{ row }">
|
||||
<el-button link type="primary" v-if="!hasChild" @click="handleAddNext(row)"
|
||||
>添加二级字段</el-button>
|
||||
<el-button link type="primary" @click="handleEdit(row)"
|
||||
>编辑</el-button
|
||||
>
|
||||
<el-button link type="danger" @click="handleDelete(row)"
|
||||
>删除</el-button
|
||||
>
|
||||
</template>
|
||||
</template> -->
|
||||
</CommonTable>
|
||||
<!-- 新增字段 -->
|
||||
<dictFieldLevelManage
|
||||
@@ -129,6 +132,13 @@
|
||||
:parentId="parentId"
|
||||
@confirm-success="onConfirmSuccess"
|
||||
/>
|
||||
|
||||
<!-- 当前弹层的组件-支持嵌套展开 -->
|
||||
<dict-field-config
|
||||
v-for="child in childModals"
|
||||
:key="child.key"
|
||||
:ref="(el) => setChildModalRef(el, child.key)"
|
||||
/>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
@@ -146,14 +156,6 @@ import {
|
||||
disableTypeDict,
|
||||
} from "@/api/stage/dict";
|
||||
defineOptions({ name: "DictFieldConfig" });
|
||||
interface User {
|
||||
id: number;
|
||||
date: string;
|
||||
name: string;
|
||||
address: string;
|
||||
hasChildren?: boolean;
|
||||
children?: User[];
|
||||
}
|
||||
|
||||
const dictTitle = ref("");
|
||||
const addVisible = ref(false);
|
||||
@@ -162,14 +164,18 @@ const searchQuery = ref("");
|
||||
const filterForm = reactive({
|
||||
status: "",
|
||||
});
|
||||
|
||||
const size = ref<string>(""); //抽屉大小
|
||||
const tableRef = ref(null);
|
||||
const visible = ref<boolean>(false);
|
||||
const parentId = ref<string>("");
|
||||
const total = ref(0);
|
||||
const list = ref([]);
|
||||
|
||||
const columns = [
|
||||
const hasChild = ref<boolean>(false); //是否是子级弹窗
|
||||
const childId = ref<string|number>(''); // 子集的id
|
||||
const childModals = ref([]); //子弹窗的列表
|
||||
const childModalRefs = ref({}); // 子弹窗的引用
|
||||
const onCloseCallback = ref<Function | null>(null); // 关闭回调函数
|
||||
const columns = computed(()=>[
|
||||
{
|
||||
prop: "id",
|
||||
label: "字典编码",
|
||||
@@ -201,28 +207,95 @@ const columns = [
|
||||
label: "更新时间",
|
||||
align: "center",
|
||||
showOverflowTooltip: true,
|
||||
width:200,
|
||||
formatter: (val) => {
|
||||
return val.createTime
|
||||
? dayjs(val.createTime).format("YYYY-MM-DD HH:mm")
|
||||
return val.updateTime
|
||||
? 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 = () =>{
|
||||
console.log('next')
|
||||
}
|
||||
const onLevelNext = (row) => {
|
||||
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) => {
|
||||
try {
|
||||
const response = await getDictTypeValue(parentId.value, {
|
||||
const queryParams = {
|
||||
...params,
|
||||
keyword: searchQuery.value,
|
||||
...filterForm,
|
||||
});
|
||||
}
|
||||
|
||||
const response = hasChild.value ? await getNextDictMenu(parentId.value,childId.value,queryParams) : await getDictTypeValue(parentId.value, queryParams);
|
||||
return response;
|
||||
} catch (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 = () => {
|
||||
dictTitle.value = "新增字段";
|
||||
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) => {
|
||||
addVisible.value = true;
|
||||
dictTitle.value = "添加二级字段";
|
||||
Object.assign(selectItem,{},{parentId:item.id});
|
||||
};
|
||||
// 编辑当前字段
|
||||
const handleEdit = (item) => {
|
||||
@@ -309,8 +377,17 @@ const handleDelete = async (item) => {
|
||||
|
||||
defineExpose({
|
||||
open: async (item) => {
|
||||
parentId.value = item.id;
|
||||
parentId.value = item.parentId;
|
||||
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();
|
||||
if (tableRef.value) {
|
||||
await tableRef.value.refresh();
|
||||
@@ -318,107 +395,13 @@ defineExpose({
|
||||
},
|
||||
close() {
|
||||
visible.value = false;
|
||||
if (onCloseCallback.value) {
|
||||
onCloseCallback.value();
|
||||
onCloseCallback.value = null;
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@use "./dictField.scss" as *;
|
||||
</style>
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<el-radio :value="0">停用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注:">
|
||||
<el-form-item label="备注:" prop="remark">
|
||||
<el-input
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 4, maxRows: 5 }"
|
||||
@@ -105,6 +105,7 @@ const rules = reactive({
|
||||
label: [{ required: true, message: "请输入字段名称", trigger: "blur" }],
|
||||
value: [{ 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) => {
|
||||
if (valid) {
|
||||
loading.value = true;
|
||||
console.log("获取外部的数据信息:",form,parentId)
|
||||
// 如果是一级新增字段就是parentId为0 如果是添加二级字段就是parentId为父级的id
|
||||
try {
|
||||
const response = row.id ? await updateDictTypeValue(parentId,row.id,form) : await saveDictTypeValue(parentId,form);
|
||||
ElMessage.success(row.id ? '修改成功' : '新增成功');
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<span>:</span>
|
||||
</div>
|
||||
</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 label="状态:" prop="status">
|
||||
<el-radio-group v-model="form.status">
|
||||
|
||||
@@ -12,7 +12,11 @@
|
||||
<div class="mj-filter-content">
|
||||
<div class="filter-header">
|
||||
<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
|
||||
>
|
||||
</div>
|
||||
@@ -35,7 +39,12 @@
|
||||
</el-select>
|
||||
</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>
|
||||
</CommonFilter>
|
||||
<div class="search-dict-input">
|
||||
@@ -84,7 +93,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #actions="{ row }">
|
||||
<!-- <template #actions="{ row }">
|
||||
<el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button link type="primary" @click="handlefieldsConfig(row)"
|
||||
>字段配置</el-button
|
||||
@@ -92,7 +101,7 @@
|
||||
<el-button link type="danger" @click="handleDelete(row)"
|
||||
>删除</el-button
|
||||
>
|
||||
</template>
|
||||
</template> -->
|
||||
</CommonTable>
|
||||
|
||||
<!-- 新增-编辑字典弹窗 -->
|
||||
@@ -112,7 +121,12 @@ import CommonTable from "@/components/proTable/index.vue";
|
||||
import dictFieldConfig from "./dictFieldConfig.vue";
|
||||
import dictManage from "./dictManage.vue";
|
||||
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 { formatIndex } from "@/utils/utils";
|
||||
import { ElMessage } from "element-plus";
|
||||
@@ -164,33 +178,60 @@ const columns = [
|
||||
},
|
||||
},
|
||||
{
|
||||
prop:'updateByName',
|
||||
label:'最后修改人',
|
||||
align:'center'
|
||||
prop: "updateByName",
|
||||
label: "最后修改人",
|
||||
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数据信息
|
||||
const dataValue = ref([]);
|
||||
|
||||
|
||||
// popover关闭事件
|
||||
const onPopoverHide = () =>{
|
||||
filterForm.status = '';
|
||||
}
|
||||
const onPopoverHide = () => {
|
||||
filterForm.status = "";
|
||||
};
|
||||
// 筛选重置
|
||||
const onReset = () =>{
|
||||
filterForm.status = '';
|
||||
const onReset = () => {
|
||||
filterForm.status = "";
|
||||
fetchTableData();
|
||||
}
|
||||
};
|
||||
// 获取当前的table数据信息
|
||||
const getTableData = async (params) => {
|
||||
try {
|
||||
const response = await getDictValues({
|
||||
...params,
|
||||
keyword: searchVal.value,
|
||||
...filterForm
|
||||
...filterForm,
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
@@ -201,26 +242,30 @@ const getTableData = async (params) => {
|
||||
const fetchTableData = () => {
|
||||
dictTableRef.value && dictTableRef.value.refresh();
|
||||
};
|
||||
const clearSelectItem = () => {
|
||||
Object.assign(selectItem,{id:null,name:'',key:'',status:1,remark:''})
|
||||
};
|
||||
// 新增字典信息
|
||||
const addDict = () => {
|
||||
dictVisible.value = true;
|
||||
clearSelectItem();
|
||||
};
|
||||
// 编辑字典信息
|
||||
const handleEdit = (item) => {
|
||||
addDict();
|
||||
dictVisible.value = true;
|
||||
Object.assign(selectItem, item);
|
||||
};
|
||||
|
||||
// 启用-禁用事件
|
||||
const handleDictStatus = async (row)=>{
|
||||
const handleDictStatus = async (row) => {
|
||||
try {
|
||||
row.status === 1 ? await disableDict(row.id) : await enableDict(row.id);
|
||||
ElMessage.success('操作成功');
|
||||
ElMessage.success("操作成功");
|
||||
onConfirmSuccess();
|
||||
} catch (error) {
|
||||
console.log('error',error);
|
||||
console.log("error", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 刷新Table数据信息
|
||||
const onConfirmSuccess = () => {
|
||||
@@ -228,7 +273,7 @@ const onConfirmSuccess = () => {
|
||||
};
|
||||
// TODO:字段配置
|
||||
const handlefieldsConfig = (ite) => {
|
||||
fieldsConfigRef.value.open(ite);
|
||||
fieldsConfigRef.value.open({ ...ite, parentId: ite.id });
|
||||
};
|
||||
|
||||
// 删除字典类型数据
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import Comment from "@/components/comment/index.vue";
|
||||
import Comment from "@/modules/Comment/index.vue";
|
||||
import { reactive, ref, onMounted } from "vue";
|
||||
|
||||
defineOptions({ name: "Personnel" });
|
||||
|
||||
@@ -33,6 +33,7 @@ const useUserStore = defineStore("user", {
|
||||
routes: [] as RouteMenu[],
|
||||
isRoutesLoaded: false, // 标记路由是否已加载
|
||||
isBackendUser:true, //标记是否是后台用户
|
||||
role:['edit','delete','config','add'] //当前的权限列表
|
||||
};
|
||||
},
|
||||
getters: {
|
||||
|
||||
@@ -73,7 +73,7 @@ body {
|
||||
|
||||
// 筛选框全局样式内容
|
||||
.mj-filter-content {
|
||||
width: 380px;
|
||||
min-width: 380px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
@@ -111,14 +111,11 @@ body {
|
||||
}
|
||||
|
||||
.custom-select {
|
||||
--el-border-color:transparent;
|
||||
width: 100%;
|
||||
background-color: #f5f7fa;
|
||||
box-shadow: none;
|
||||
border: 1px solid #e4e7ed;
|
||||
:deep(.el-input__wrapper) {
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.apply-btn {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useUserStore } from "@/store";
|
||||
|
||||
function permissionDirective(el: HTMLElement,binding:any) {
|
||||
const appStore = useUserStore();
|
||||
const userPermissions = appStore.role; // 假設從store中獲取用戶權限
|
||||
const userPermissions = appStore.role;
|
||||
let requiredPermissions = binding.value;
|
||||
if (typeof requiredPermissions === "string") {
|
||||
requiredPermissions = [requiredPermissions];
|
||||
|
||||
27
src/utils/permission.ts
Normal file
27
src/utils/permission.ts
Normal 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 };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user