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] 状态管理工具Pinia实现
[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']
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']

View File

@@ -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
View File

@@ -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

View File

@@ -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`)
}

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>
<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>

View File

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

View File

@@ -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 };
}
});

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-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();

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
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>

View File

@@ -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 ? '修改成功' : '新增成功');

View File

@@ -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">

View File

@@ -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)=>{
try {
row.status === 1 ? await disableDict(row.id) : await enableDict(row.id);
ElMessage.success('操作成功');
onConfirmSuccess();
} catch (error) {
console.log('error',error);
}
}
const handleDictStatus = async (row) => {
try {
row.status === 1 ? await disableDict(row.id) : await enableDict(row.id);
ElMessage.success("操作成功");
onConfirmSuccess();
} catch (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 });
};
// 删除字典类型数据

View File

@@ -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" });

View File

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

View File

@@ -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 {

View File

@@ -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
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 };
};