fix:完善权限管理页面

This commit is contained in:
liangdong
2026-01-09 14:17:20 +08:00
parent a76046e2cc
commit fc84d925d6
14 changed files with 1080 additions and 228 deletions

1
components.d.ts vendored
View File

@@ -22,6 +22,7 @@ declare module 'vue' {
ElButton: typeof import('element-plus/es')['ElButton']
ElCard: typeof import('element-plus/es')['ElCard']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
ElCol: typeof import('element-plus/es')['ElCol']
ElContainer: typeof import('element-plus/es')['ElContainer']
ElDatePick: typeof import('element-plus/es')['ElDatePick']

View File

@@ -2,10 +2,14 @@
<el-drawer
v-model="drawerVisible"
size="440px"
class="member-drawer"
class="standard-ui-back-drawer"
:close-on-click-modal="false"
:close-on-press-escape="false"
destroy-on-close
:with-header="false"
modal-class="standard-overlay-dialog-flat"
>
<div class="custom-header">
<div class="customer-drawer-header">
<div class="title-row">
<span class="decorator"></span>
<span class="title">成员管理</span>
@@ -28,37 +32,36 @@
>添加成员</el-button
>
</div>
<div class="member-list" ref="listRef">
<transition-group name="list-fade">
<div
v-for="item in filteredMembers"
:key="item.id"
class="member-item"
@click="selectMember(item)"
>
<name-avatar
:size="30"
:name="item.name"
:bgColor="'var(--mj-avatar-bg)'"
:avatarTextColor="'var(--mj-text-color)'"
/>
<div class="member-info">
<div class="name">{{ item.name }}</div>
<div class="dept">{{ item.dept }}</div>
<div class="member-list" ref="listRef">
<transition-group name="list-fade">
<div
v-for="item in filteredMembers"
:key="item.id"
class="member-item"
@click="selectMember(item)"
>
<name-avatar
:size="30"
:name="item.name"
:bgColor="'var(--mj-avatar-bg)'"
:avatarTextColor="'var(--mj-text-color)'"
/>
<div class="member-info">
<div class="name">{{ item.name }}</div>
<div class="dept">{{ item.dept }}</div>
</div>
<div class="member-action">
<el-button link type="info" @click.stop="handleRemove(item.id)"
>移除</el-button
>
</div>
</div>
<div class="member-action">
<el-button link type="info" @click.stop="handleRemove(item.id)"
>移除</el-button
>
</div>
</div>
</transition-group>
</div>
</transition-group>
</div>
</div>
<template #footer>
<div class="drawer-footer">
<div class="custom-flat-drawer-footer">
<div class="stats-info">
<span class="label">统计信息</span>
<span class="count"> {{ memberList.length }} 名成员</span>
@@ -144,53 +147,8 @@ const selectMember = (item) => {
item.active = !item.active;
};
</script>
<style lang="scss">
.member-drawer {
.el-drawer__body {
padding: 0 !important; // 必须覆盖默认内边距
display: flex;
flex-direction: column;
overflow: hidden;
}
// 自定义头部
.custom-header {
padding: 20px 24px 15px;
border-bottom: 1px solid #f0f0f0;
.title-row {
display: flex;
align-items: center;
.decorator {
width: 4px;
height: 16px;
background: #409eff;
border-radius: 2px;
margin-right: 8px;
}
.title {
font-size: 16px;
font-weight: bold;
flex: 1;
}
.close-icon {
cursor: pointer;
color: #909399;
&:hover {
color: #409eff;
}
}
}
.sub-title {
margin: 8px 0 0 12px;
font-size: 13px;
color: #909399;
span {
color: #409eff;
}
}
}
<style lang="scss" scoped>
.standard-ui-back-drawer {
.drawer-body-container {
flex: 1;
display: flex;
@@ -268,14 +226,9 @@ const selectMember = (item) => {
}
.el-drawer__footer {
border-top: 1px solid #f0f0f0;
padding: 16px 24px;
.drawer-footer {
display: flex;
justify-content: space-between;
align-items: center;
.custom-flat-drawer-footer {
.stats-info {
display: flex;
flex-direction: column;
@@ -290,16 +243,6 @@ const selectMember = (item) => {
font-weight: bold;
}
}
.btn-confirm {
background-color: #1d2635;
border-color: #1d2635;
padding: 10px 24px;
&:hover {
background-color: #2b364a;
border-color: #2b364a;
}
}
}
}
}

View File

@@ -24,8 +24,6 @@ const props = defineProps({
avatarTextColor: { type: String, default: "" },
});
console.log(1111,props.bgColor)
const displayText = computed(() => {
return props.name ? props.name.charAt(0) : "";
});

View File

@@ -109,9 +109,9 @@
</template>
</el-table>
<div class="pro-table-footer">
<div class="pro-table-footer" v-if="pagination">
<slot name="footer">
<div class="mj-footer-content" v-if="pagination">
<div class="mj-footer-content">
<div class="footer-left">
<span class="total-text">共 {{ total || data.length }} 个条目</span>
</div>
@@ -134,6 +134,7 @@
<script setup lang="ts">
import { ref, computed } from "vue";
import { debounce } from 'lodash-es';
const props = defineProps({
columns: { type: Array, required: true },
data: { type: Array, required: true },
@@ -164,20 +165,21 @@ const tableContainerRef = ref(null);
const containerHeight = ref(0);
const minHeight = 400; // 最小高度值
// 监听窗口大小变化
const handleResize = () => {
const handleResize = debounce(() => {
updateContainerHeight();
};
},100);
// 动态计算表格容器高度
const updateContainerHeight = async () => {
await nextTick();
if (tableContainerRef.value) {
// 获取容器相对于视口的位置
const rect = tableContainerRef.value.getBoundingClientRect();
const element = tableContainerRef.value;
const rect = element.getBoundingClientRect();
const containerTop = rect.top;
// 计算从容器位置到浏览器底部的可用高度
const availableHeight = window.innerHeight - containerTop;
const availableHeight = window.innerHeight - containerTop -20;
containerHeight.value = Math.max(availableHeight, minHeight);
} else {
// 如果元素尚未渲染,使用默认偏移值
@@ -186,8 +188,9 @@ const updateContainerHeight = async () => {
};
const tableHeight = computed(() => {
// 为页码预留空间大约60px
return (containerHeight.value - 60) + 'px';
// 为页码预留空间
const paginationHeight = props.pagination ? 38 : 0;
return (containerHeight.value - paginationHeight) + 'px';
});
// 标记是否是首次挂载
@@ -316,11 +319,15 @@ const getButtonProps = (button) => {
background-color: #fbfcfd;
}
}
:deep(.el-table__inner-wrapper:before){
--el-table-border-color:transparent;
}
/* 底部容器样式对应图片中的布局 */
.pro-table-footer {
padding: 3px 24px;
background-color: #fcfdfe;
border-top: 1px solid #E2E8F0;
}
.mj-footer-content {

View File

@@ -524,7 +524,8 @@ const updateUIAfterSend = (type, params, response) => {
const newComment = {
id: response,
employee: {
...currentUser.value,
userId:currentUser.value.id,
username:currentUser.value.name,
},
replyId: params.replyUserId,
replyUser: {

View File

@@ -229,8 +229,8 @@ const getTableData = async (params) => {
try {
const response = await getDictValues({
...params,
keyword: searchVal.value,
...filterForm,
...(searchVal.value && { keyword: searchVal.value }),
...(filterForm.status && { status: filterForm.status })
});
return response;
} catch (error) {

View File

@@ -3,7 +3,8 @@
v-model="dialogVisible"
title="新增系统角色"
width="500px"
class="custom-role-dialog"
class="standard-ui-flat-dialog"
modal-class="standard-overlay-dialog-flat"
destroy-on-close
>
<el-form :model="form" label-position="top" class="role-form">
@@ -17,11 +18,9 @@
</div>
<el-form-item label="角色类型">
<el-radio-group v-model="form.type" class="full-width-radio">
<el-radio-button label="实例角色" />
<el-radio-button label="默认角色" />
<el-radio-button label="系统角色" />
</el-radio-group>
<div class="full-width-radio">
<BaseSegmented v-model="roleType" :options="roleOptions" />
</div>
</el-form-item>
<div class="feature-card light-green">
@@ -55,6 +54,7 @@
v-model="form.remark"
type="textarea"
:rows="3"
resize="none"
placeholder="请输入备注说明..."
/>
</el-form-item>
@@ -62,7 +62,7 @@
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button @click="dialogVisible = false" text>取消</el-button>
<el-button type="primary" @click="handleSubmit">确认创建</el-button>
</div>
</template>
@@ -70,9 +70,16 @@
</template>
<script lang="ts" setup>
import BaseSegmented from "./baseSegmented.vue";
defineOptions({ name: "addRoles" });
const dialogVisible = defineModel('visible',{type: Boolean, default: false})
const dialogVisible = defineModel("visible", { type: Boolean, default: false });
const roleType = ref('system'); // 初始值
const roleOptions = [
{ label: '实例角色', value: 'instance' },
{ label: '默认角色', value: 'default' },
{ label: '系统角色', value: 'system' }
];
const form = reactive({
name: "",
code: "",
@@ -82,25 +89,20 @@ const form = reactive({
status: true,
remark: "",
});
const handleSubmit = () => {
dialogVisible.value = false;
};
</script>
<style lang="scss" scoped>
/* 样式穿透修改 Element Plus 默认外观 */
.custom-role-dialog {
border-radius: 8px;
.standard-ui-flat-dialog {
.el-dialog__header {
margin-right: 0;
padding: 20px 24px;
border-bottom: 1px solid #f0f2f5;
.el-dialog__title {
font-size: 16px;
font-weight: 600;
position: relative;
padding-left: 12px;
padding-left: 10px;
font-size: 14px;
// 标题前面的蓝色小方块
&::before {
content: "";
position: absolute;
@@ -109,14 +111,14 @@ const form = reactive({
transform: translateY(-50%);
width: 4px;
height: 16px;
background-color: #1661ff;
background-color: var(--blue-block-color);
border-radius: 2px;
}
}
}
.el-dialog__body {
padding: 24px;
padding: 32px;
}
// 表单标签样式
@@ -134,31 +136,8 @@ const form = reactive({
flex: 1;
}
}
// 选项卡风格的单选框
.full-width-radio {
display: flex;
.full-width-radio{
width: 100%;
background: #f2f3f5;
padding: 4px;
border-radius: 4px;
.el-radio-button {
flex: 1;
.el-radio-button__inner {
width: 100%;
border: none;
background: transparent;
box-shadow: none;
border-radius: 4px;
color: #4e5969;
}
&.is-active .el-radio-button__inner {
background: #fff;
color: #1661ff;
font-weight: 500;
}
}
}
// 绿色和蓝色的特色功能卡片
@@ -192,17 +171,5 @@ const form = reactive({
border: 1px solid #e8f0f9;
}
}
.el-dialog__footer {
padding: 16px 24px 24px;
text-align: right;
.el-button {
padding: 10px 24px;
&--primary {
background-color: #1661ff;
}
}
}
}
</style>

View File

@@ -0,0 +1,165 @@
<template>
<div class="permission-scroll-area">
<div v-for="group in permissions" :key="group.id" class="permission-group">
<div class="group-header">
<el-checkbox
v-model="group.allSelected"
:indeterminate="group.isIndeterminate"
@change="(val) => handleGroupCheckAll(group, val)"
>
<span class="group-title">{{ group.name }}</span>
<span class="group-count"
>({{ getCheckedCount(group) }}/{{ group.children.length }})</span
>
</el-checkbox>
</div>
<div class="group-body">
<div v-for="row in group.children" :key="row.id" class="permission-row">
<div class="row-label">
<el-checkbox
v-model="row.checked"
@change="() => handleRowChange(group, row)"
>
{{ row.name }}
</el-checkbox>
</div>
<div class="row-actions">
<el-checkbox-group
v-model="row.actions"
:disabled="!row.checked"
@change="() => handleActionChange(group, row)"
>
<el-checkbox value="add">新增</el-checkbox>
<el-checkbox value="delete" class="is-danger">删除</el-checkbox>
<el-checkbox value="import">导入</el-checkbox>
<el-checkbox value="export">导出</el-checkbox>
</el-checkbox-group>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, ref, onMounted } from "vue";
defineOptions({name: "baseSegmentedMenu"});
// 模拟数据结构
const permissions = reactive([
{
id: 1,
name: "项目管理",
allSelected: false,
isIndeterminate: true,
children: [
{
id: 11,
name: "需求管理",
checked: true,
actions: ["add", "delete", "import", "export"],
},
{ id: 12, name: "项目管理", checked: false, actions: [] },
{ id: 13, name: "任务管理", checked: false, actions: [] },
],
},
{
id: 2,
name: "招聘管理",
allSelected: false,
isIndeterminate: false,
children: [
{ id: 21, name: "简历管理", checked: false, actions: [] },
{ id: 22, name: "推送管理", checked: false, actions: [] },
],
},
{
id: 2,
name: "招聘管理",
allSelected: false,
isIndeterminate: false,
children: [
{ id: 21, name: "简历管理", checked: false, actions: [] },
{ id: 22, name: "推送管理", checked: false, actions: [] },
],
},
{
id: 2,
name: "招聘管理",
allSelected: false,
isIndeterminate: false,
children: [
{ id: 21, name: "简历管理", checked: false, actions: [] },
{ id: 22, name: "推送管理", checked: false, actions: [] },
],
},
{
id: 2,
name: "招聘管理",
allSelected: false,
isIndeterminate: false,
children: [
{ id: 21, name: "简历管理", checked: false, actions: [] },
{ id: 22, name: "推送管理", checked: false, actions: [] },
],
},
{
id: 2,
name: "招聘管理",
allSelected: false,
isIndeterminate: false,
children: [
{ id: 21, name: "简历管理", checked: false, actions: [] },
{ id: 22, name: "推送管理", checked: false, actions: [] },
],
},
{
id: 2,
name: "招聘管理",
allSelected: false,
isIndeterminate: false,
children: [
{ id: 21, name: "简历管理", checked: false, actions: [] },
{ id: 22, name: "推送管理", checked: false, actions: [] },
],
},
]);
// 获取已选中的子项数量
const getCheckedCount = (group) =>
group.children.filter((item) => item.checked).length;
// 处理一级全选
const handleGroupCheckAll = (group, val) => {
group.children.forEach((row) => {
row.checked = val;
row.actions = val ? ["add", "delete", "import", "export"] : [];
});
group.isIndeterminate = false;
};
// 处理二级勾选
const handleRowChange = (group, row) => {
if (!row.checked) row.actions = [];
updateGroupStatus(group);
};
// 处理三级按钮勾选
const handleActionChange = (group, row) => {
if (row.actions.length > 0) row.checked = true;
updateGroupStatus(group);
};
// 更新父级的半选/全选状态
const updateGroupStatus = (group) => {
const checkedCount = getCheckedCount(group);
group.allSelected = checkedCount === group.children.length;
group.isIndeterminate =
checkedCount > 0 && checkedCount < group.children.length;
};
</script>
<style lang="scss" scoped>
@use './baseSegmentedPermission.scss' as *;
</style>

View File

@@ -0,0 +1,102 @@
<template>
<div class="base-segmented-flat">
<div v-for="item in options" :key="item.value" class="segmented-item">
<input
type="radio"
:id="uid + item.value"
:name="uid"
:value="item.value"
:checked="modelValue === item.value"
@change="handleChange(item.value)"
/>
<label
:for="uid + item.value"
:class="{ 'is-active': modelValue === item.value }"
>
{{ item.label }}
</label>
</div>
</div>
</template>
<script setup>
import { inject } from "vue";
import { computed } from "vue";
import { useFormItem } from "element-plus";
const props = defineProps({
modelValue: [String, Number],
options: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(["update:modelValue", "change"]);
// 生成唯一ID防止页面存在多个组件时 name 冲突
const uid = `seg-${Math.random().toString(36).substring(2, 8)}`;
const { formItem } = useFormItem();
const handleChange = (val) => {
// 2. 更新父组件的值
emit("update:modelValue", val);
emit("change", val);
// 3. 关键:通知 el-form-item 进行校验
if (formItem) {
formItem.validate("change").catch(() => {});
}
};
</script>
<style lang="scss" scoped>
$bg-color: #f2f3f5;
$active-bg: #ffffff;
$primary-color: #1661ff;
$text-secondary: #86909c;
.base-segmented-flat {
display: flex;
align-items: center;
width: 100%;
background-color: $bg-color;
padding: 4px;
border-radius: 4px;
box-sizing: border-box;
.segmented-item {
flex: 1;
display: flex;
input[type="radio"] {
display: none;
}
label {
flex: 1;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
color: $text-secondary;
cursor: pointer;
transition: all 0.2s ease-in-out;
border-radius: 4px;
user-select: none;
&:hover {
color: darken($text-secondary, 15%);
}
// 选中状态样式
&.is-active {
background-color: $active-bg;
color: $primary-color;
font-weight: 500;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
}
}
}
}
</style>

View File

@@ -0,0 +1,106 @@
.permission-container {
--primary-blue: #1661ff;
--danger-red: #f53f3f;
--text-main: #1d2129;
--text-grey: #86909c;
--border-color: #f2f3f5;
--common-padding:24px;
.custom-permission-tabs {
--el-tabs-header-height:50px;
--el-border-color-light:transparent;
:deep(.el-tabs__header){
padding: 0 var(--common-padding);
background-color: #FBFCFD;
margin-bottom: 0;
}
:deep(.el-tabs__content){
border-top: 1px solid var(--border-color);
}
:deep(.el-tabs__item) {
font-size: 14px;
&.is-active {
color: var(--primary-blue);
}
}
}
.permission-scroll-area{
padding: 0 var(--common-padding);
box-sizing: border-box;
height: calc(100vh - 190px); //设置固定的高度
overflow-y: auto;
}
.permission-group {
padding: 20px 0;
border-bottom: 1px solid var(--border-color);
.group-header {
margin-bottom: 16px;
.group-title {
font-weight: 600;
color: var(--text-main);
font-size: 15px;
}
.group-count {
color: var(--text-grey);
margin-left: 8px;
font-size: 12px;
}
/* 图二中:半选状态的蓝色方块样式 */
:deep(.el-checkbox__input.is-indeterminate .el-checkbox__inner) {
background-color: var(--primary-blue);
border-color: var(--primary-blue);
&::before {
height: 3px;
top: 6px;
}
}
}
.group-body {
padding-left: 28px;
.permission-row {
display: flex;
align-items: center;
margin-bottom: 12px;
.row-label {
width: 180px;
:deep(.el-checkbox__label) {
color: var(--text-grey);
}
}
.row-actions {
flex: 1;
display: flex;
justify-content: flex-end;
:deep(.el-checkbox) {
margin-right: 32px;
/* 默认选中文字也保持蓝色 */
&.is-checked .el-checkbox__label {
color: var(--primary-blue);
}
/* 特殊处理:删除 按钮 */
&.is-danger.is-checked .el-checkbox__label {
color: var(--danger-red);
}
}
}
}
}
}
/* 禁用时的文字颜色微调 */
:deep(.el-checkbox.is-disabled .el-checkbox__label) {
color: #c0c4cc;
}
}

View File

@@ -0,0 +1,322 @@
<template>
<div class="field-permission-manager">
<div class="module-grid">
<div
v-for="mod in modules"
:key="mod.id"
class="module-card"
:class="{ 'is-active': modelValue === mod.id }"
@click="handleModuleChange(mod.id)"
>
{{ mod.name }}
</div>
</div>
<div class="field-group-list">
<div
v-for="(group, index) in fieldGroups"
:key="group.groupId"
class="group-container"
:class="{ 'is-collapsed': group.collapsed }"
>
<div class="group-header" :class="{ 'is-collapsed': group.collapsed }">
<div class="header-left">
<el-icon><List /></el-icon>
<span class="group-title">{{ group.groupName }}</span>
</div>
<div class="header-right">
<span class="quick-set-label">快速设置</span>
<div class="quick-actions">
<span
v-for="opt in quickOptions"
:key="opt.value"
class="quick-btn"
:class="{ 'is-disabled': isProcessing }"
@click.stop="batchSetPermissionChunked(group, opt.value)"
>
{{ opt.label }}
</span>
</div>
<el-icon class="collapse-icon" @click.stop="toggleGroup(index)"
><ArrowDown
/></el-icon>
</div>
</div>
<div class="field-rows-collapse">
<div class="field-rows-inner">
<div
v-for="field in group.fields"
:key="field.id"
class="field-item"
>
<div class="field-info">
<i class="tree-line-icon"></i>
<span class="field-label">{{ field.name }}</span>
</div>
<div class="field-ctrl">
<el-select v-model="field.permission">
<el-option label="可查看" value="read" />
<el-option label="可编辑" value="edit" />
<el-option label="不可查看" value="none" />
</el-select>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from "vue";
import { List, Document, ArrowDown } from "@element-plus/icons-vue";
const props = defineProps({
// 模块列表
modules: { type: Array, default: () => [] },
// 初始选中的模块ID
modelValue: [String, Number],
fieldGroups: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(["update:modelValue", "update:fieldGroups", "change"]);
const isProcessing = ref(false);
const processingGroupId = ref(null);
// 快速设置选项
const quickOptions = [
{ label: "可查看", value: "read" },
{ label: "可编辑", value: "edit" },
{ label: "不可查看", value: "none" },
];
const batchSetPermissionChunked = (group, type) => {
if (isProcessing.value) return;
const fields = group.fields;
const total = fields.length;
const chunkSize = 40;
let currentIndex = 0;
isProcessing.value = true;
processingGroupId.value = group.groupId;
const runChunk = () => {
const nextLimit = Math.min(currentIndex + chunkSize, total);
for (let i = currentIndex; i < nextLimit; i++) {
fields[i].permission = type;
}
currentIndex = nextLimit;
if (currentIndex < total) {
// 关键:将控制权交还给浏览器渲染线程,下一帧继续
requestAnimationFrame(runChunk);
} else {
isProcessing.value = false;
processingGroupId.value = null;
emit("update:fieldGroups", [...props.fieldGroups]);
}
};
runChunk();
};
// 切换折叠逻辑
const toggleGroup = (index) => {
console.log("");
props.fieldGroups[index].collapsed = !props.fieldGroups[index].collapsed;
};
const handleModuleChange = (id) => {
emit("update:modelValue", id);
emit("change", id);
};
// 批量设置权限逻辑
const batchSetPermission = (group, type) => {
group.fields.forEach((field) => {
field.permission = type;
});
emit("update:fieldGroups", [...props.fieldGroups]);
};
</script>
<style lang="scss" scoped>
.field-permission-manager {
--primary-blue: #1661ff;
--bg-light: #f2f3f5;
--border-color: #e5e6eb;
--transition-speed: 0.3s;
overflow-y: auto;
height: calc(100vh - 190px);
box-sizing: border-box;
.module-grid {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 24px;
padding: 24px 24px 0;
user-select: none;
.module-card {
width: calc((100% - 48px) / 5);
min-width: 100px;
height: 40px;
border: 1px solid var(--border-color);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 14px;
color: #4e5969;
box-sizing: border-box;
transition: all 0.2s;
&:hover {
border-color: var(--primary-blue);
color: var(--primary-blue);
}
&.is-active {
background: var(--primary-blue);
border-color: var(--primary-blue);
color: #fff;
font-weight: 500;
}
}
}
.field-group-list {
padding: 0 24px;
}
// 2. 字段卡片样式
.group-container {
border: 1px solid var(--bg-light);
border-radius: 6px;
margin-bottom: 16px;
overflow: hidden;
.group-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 16px;
height: 48px;
background: #fff;
border-bottom: 1px solid var(--bg-light);
.header-left {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
.el-icon {
color: #86909c;
font-size: 18px;
}
}
.header-right {
display: flex;
align-items: center;
.quick-set-label {
font-size: 12px;
color: #86909c;
margin-right: 8px;
}
.quick-actions {
display: flex;
gap: 8px;
margin-right: 16px;
.quick-btn {
padding: 2px 10px;
background: #f7f8fa;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 12px;
color: var(--primary-blue);
cursor: pointer;
&:hover {
background: #eff4ff;
}
}
}
.collapse-icon {
color: #c9cdd4;
cursor: pointer;
}
}
}
.field-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 16px 10px 32px;
border-bottom: 1px solid #fafafa;
&:last-child {
border-bottom: none;
}
.field-info {
display: flex;
align-items: center;
.tree-line-icon {
width: 12px;
height: 12px;
border-left: 1px solid var(--border-color);
border-bottom: 1px solid var(--border-color);
margin-right: 8px;
margin-top: -6px;
}
.field-label {
font-size: 14px;
color: #4e5969;
}
}
.field-ctrl {
flex: 0 0 120px;
}
}
.field-rows-collapse {
max-height: 6000px;
overflow: hidden;
transition: max-height var(--transition-speed)
cubic-bezier(0.4, 0, 0.2, 1),
opacity var(--transition-speed);
opacity: 1;
}
// 折叠后的状态
&.is-collapsed {
.group-header {
border-bottom-color: transparent;
}
.field-rows-collapse {
max-height: 0;
opacity: 0;
transition: max-height var(--transition-speed) cubic-bezier(0, 1, 0, 1),
opacity var(--transition-speed);
}
.collapse-icon {
transform: rotate(-90deg);
}
}
.collapse-icon {
transition: transform var(--transition-speed);
}
}
}
</style>

View File

@@ -16,7 +16,13 @@
></el-input>
</div>
<div class="mj-dict-actions-right">
<el-button :icon="'Plus'" type="primary" plain @click="addRolesClick">{{ checkRolesText.btnText }}</el-button>
<el-button
:icon="'Plus'"
type="primary"
plain
@click="addBtnClick"
>{{ checkRolesText.btnText }}</el-button
>
</div>
</div>
</template>
@@ -31,32 +37,42 @@
pagination
:request-api="getTableData"
>
<!-- 状态 -->
<template #status="{ row }">
<el-tag>{{ row.status }}</el-tag>
</template>
<!-- 状态 -->
<template #status="{row}">
<el-tag>{{ row.status }}</el-tag>
</template>
<!-- 成员数量 -->
<template #member="{ row }">
<el-tag @click.stop="addMember">{{ row.member }}</el-tag>
</template>
</CommonTable>
</div>
<!-- 成员管理 -->
<!-- <member-selector v-model:visible="showMember" /> -->
<member-selector v-model:visible="showMember" />
<!-- 新增角色 -->
<add-roles v-model:visible="showMember" />
<!-- 新增角色 -->
<add-roles v-model:visible="showRoles" />
<!-- 权限抽屉 -->
<permission-drawer v-model:visible="showPermission" />
</template>
<script setup lang="ts">
import dayjs from "dayjs";
import CommonTable from "@/components/proTable/index.vue";
import memberSelector from '@/components/memberSelector/index.vue';
import memberSelector from "@/components/memberSelector/index.vue";
import addRoles from "./addRoles.vue";
import permissionDrawer from "./permissionDrawer.vue";
defineOptions({ name: "PermissionManagement" });
const activeTab = ref(1);
const dictTableRef = ref(null);
const searchVal = ref("");
const dataList = ref([]);
const total = ref(0);
const showMember = ref(true);
const showMember = ref(false);
const showRoles = ref(false);
const showPermission = ref(false);
const tabList = [
{
label: "角色与权限",
@@ -76,9 +92,10 @@ const columns = [
{ prop: "id", label: "编号", width: "80", align: "center", slot: "number" },
{ prop: "name", label: "字典名称", align: "center", slot: "name" },
{
prop: "number",
prop: "member",
label: "成员数量",
align: "center",
slot: "member",
},
{
prop: "key",
@@ -130,75 +147,85 @@ const columns = [
type: "primary",
link: true,
permission: ["edit"],
onClick: (row) => handleEdit(row),
onClick: (row) => {
showPermission.value = true;
},
},
{
label: "复制",
type: "default",
link: true,
permission: ["edit"],
onClick: (row) => handleEdit(row),
onClick: (row) => {},
},
{
label: "编辑",
type: "default",
link: true,
permission: ["config"],
onClick: (row) => handlefieldsConfig(row),
onClick: (row) => {},
},
{
label: "删除",
type: "danger",
link: true,
permission: ["delete"],
onClick: (row) => handleDelete(row),
onClick: (row) => {},
},
],
},
];
const getTableData = async (params) => {
await new Promise(resolve => setTimeout(resolve, 500))
return {
records:[
{
id:1,
name:'11111',
number:'2222',
status:1,
key:'',
roleType:1,
remark:'',
createTime:1767878508824,
updateByName:'111'
}
]
}
// 当前成员数量
const addMember = () => {
showMember.value = true;
};
const getTableData = async (params) => {
await new Promise((resolve) => setTimeout(resolve, 500));
return {
records: [
{
id: 1,
name: "11111",
member: "2222",
status: 1,
key: "",
roleType: 1,
remark: "",
createTime: 1767878508824,
updateByName: "111",
},
],
};
};
const checkRolesText = computed(() => {
const btnText = {
1:'新增角色',
2:'新增用户'
}[activeTab.value]
1: "新增角色",
2: "新增用户",
}[activeTab.value];
const placeholder = {
1:'搜索角色名称...',
2:'搜索用户姓名/账号...'
}[activeTab.value]
1: "搜索角色名称...",
2: "搜索用户姓名/账号...",
}[activeTab.value];
return {
btnText,
placeholder
}
placeholder,
};
});
// 请求数据信息
const fetchTableData = () => {};
// 新增角色
const addRolesClick = () => {};
const addBtnClick = () => {
if (activeTab.value === 1) {
showRoles.value = true;
}
};
</script>
<style lang="scss" scoped>
.mj-permission-management {
@@ -211,4 +238,4 @@ const addRolesClick = () => {};
gap: 10px;
}
}
</style>
</style>

View File

@@ -0,0 +1,103 @@
<template>
<el-drawer
v-model="drawerVisible"
size="40%"
title="测试评论"
:close-on-click-modal="false"
:close-on-press-escape="false"
destroy-on-close
:with-header="false"
modal-class="standard-overlay-dialog-flat"
class="standard-ui-back-drawer"
>
<div class="permission-drawer-container">
<!-- header -->
<div class="customer-drawer-header">
<div class="title-row">
<span class="title">权限配置</span>
<el-icon class="close-icon" @click="drawerVisible = false"
><Close
/></el-icon>
</div>
<div class="sub-title">
正在为<span class="sub-roles">[超级管理员]&nbsp;</span>分配系统访问权限
</div>
</div>
<!-- 内容 -->
<div class="permission-drawer-content">
<div class="permission-container">
<el-tabs v-model="activeTab" class="custom-permission-tabs">
<el-tab-pane label="菜单权限" name="menu">
<base-segment-menu></base-segment-menu>
</el-tab-pane>
<el-tab-pane label="字段权限" name="field">
<fieldPermissionManager
v-model="currentModule"
:modules="moduleList"
v-model:field-groups="fieldsList"
@change="onModuleTabChange"
/>
</el-tab-pane>
</el-tabs>
</div>
</div>
</div>
<!-- 底部footer -->
<template #footer>
<div class="custom-flat-drawer-footer">
<div class="stats-info"></div>
<div class="actions">
<el-button link @click="drawerVisible = false">取消</el-button>
<el-button type="primary" class="btn-confirm">确认应用</el-button>
</div>
</div>
</template>
</el-drawer>
</template>
<script setup lang="ts">
import { Close } from "@element-plus/icons-vue";
import baseSegmentMenu from "./baseSegmentMenu.vue";
import fieldPermissionManager from "./fieldPermissionManager.vue";
defineOptions({ name: "PermissionDrawer" });
const drawerVisible = defineModel("visible", { type: Boolean, default: false });
const activeTab = ref("menu");
const currentModule = ref(4); // 默认选中“商机详情”
const fieldsList = ref([
{
groupId: "g1",
groupName: "文本",
fields: [
{ id: "f1", name: "商机名称", permission: "read", collapsed: false },
{ id: "f2", name: "商机需求描述", permission: "read", collapsed: false },
],
},
{
groupId: "g2",
groupName: "选择",
fields: [
{ id: "f3", name: "合作类型", permission: "read", collapsed: false },
{ id: "f4", name: "需求品类", permission: "read", collapsed: false },
{ id: "f5", name: "线索/商机渠道", permission: "read", collapsed: false },
],
},
]);
const moduleList = [
{ id: 1, name: "线索详情" },
{ id: 2, name: "客户详情" },
{ id: 3, name: "工作室详情" },
{ id: 4, name: "商机详情" },
{ id: 5, name: "合同详情" },
{ id: 6, name: "项目详情" },
// ...以此类推
];
const onModuleTabChange = (id) => {
console.log("切换到模块:", id);
// 这里可以调用接口更新 currentFields 的数据
};
</script>
<style lang="scss" scoped>
@use "./baseSegmentedPermission.scss" as *;
</style>

View File

@@ -1,15 +1,14 @@
// 当前样式表修改element 全局的样式
// 标砖抽屉样式
.standard-ui-drawer{
.el-drawer__header{
.standard-ui-drawer {
.el-drawer__header {
position: relative;
margin-bottom: 0;
padding-bottom: var(--el-drawer-padding-primary);
&::after{
content:'';
&::after {
content: '';
position: absolute;
left: var(--el-drawer-padding-primary);
bottom: 0;
@@ -20,51 +19,162 @@
}
}
.standard-ui-back-drawer{
.standard-ui-back-drawer {
@extend .standard-ui-drawer;
.el-drawer__header{
.el-drawer__header {
background-color: #FBFCFD;
&::after{
&::after {
width: 100%;
left: 0;
}
}
.el-drawer__body{
.customer-drawer-header {
padding: 20px 24px 15px;
border-bottom: 1px solid #f0f0f0;
position: relative;
&::before {
content: "";
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 16px;
background-color: #409eff;
border-radius: 2px;
}
.title-row {
display: flex;
align-items: center;
margin-left: 20px;
.title {
font-size: 16px;
font-weight: bold;
flex: 1;
}
.close-icon {
cursor: pointer;
color: #909399;
&:hover {
color: #409eff;
}
}
}
.sub-title {
margin: 2px 0 0 20px;
font-size: 13px;
color: #909399;
.sub-roles{
color: var(--el-color-primary);
}
}
}
.el-drawer__body {
padding: 0;
}
.el-drawer__footer{
.el-drawer__footer {
background-color: #FBFCFD;
border-top: 1px solid #E5E7EB;
.custom-flat-drawer-footer {
--black-bg-hover-color:#2b364a;
--black-bg-color:#1d2635;
display: flex;
justify-content: space-between;
align-items: center;
.btn-confirm {
background-color: var(--black-bg-color);
border-color: var(--black-bg-color);
padding: 10px 24px;
&:hover {
background-color: var(--black-bg-hover-color);
border-color: var(--black-bg-hover-color);
}
}
}
}
}
// 标注弹窗样式
.standard-ui-dialog{
&.el-dialog{
--el-dialog-inset-padding-primary:16px;
--el-dialog-padding-primary:0;
--el-dialog-border-radius:16px;
--el-dialog-bg-header-footer:#FBFCFD;
--el-dialog-border-header-footer-color:#E5E7EB;
.standard-ui-dialog {
&.el-dialog {
--el-dialog-inset-padding-primary: 16px;
--el-dialog-padding-primary: 0;
--el-dialog-border-radius: 16px;
--el-dialog-bg-header-footer: #FBFCFD;
--el-dialog-border-header-footer-color: #E5E7EB;
padding: var(--el-dialog-padding-primary);
}
.el-dialog__header{
.el-dialog__header {
border-bottom: 1px solid var(--el-dialog-border-header-footer-color);
background-color:var(--el-dialog-bg-header-footer);
background-color: var(--el-dialog-bg-header-footer);
padding: var(--el-dialog-inset-padding-primary);
border-radius: var(--el-dialog-border-radius) var(--el-dialog-border-radius) 0 0;
}
.el-dialog__headerbtn{
.el-dialog__headerbtn {
height: 60px;
}
.el-dialog__body{
.el-dialog__body {
padding: var(--el-dialog-inset-padding-primary);
}
.el-dialog__footer{
.el-dialog__footer {
padding: var(--el-dialog-inset-padding-primary);
background-color: var(--el-dialog-bg-header-footer);
border-top: 1px solid var(--el-dialog-border-header-footer-color);
border-radius: 0 0 var(--el-dialog-border-radius) var(--el-dialog-border-radius);
}
}
// 扁平化dialog样式
.standard-ui-flat-dialog {
&.el-dialog {
--el-dialog-inset-padding-primary: 16px;
--el-dialog-padding-primary: 0;
--el-dialog-border-radius: 4px;
--el-dialog-bg-header-footer: #fcfdfe;
--el-dialog-border-header-footer-color: #f0f2f5;
--blue-block-color: #1661ff;
padding: var(--el-dialog-padding-primary);
}
.el-dialog__header {
padding: var(--el-dialog-inset-padding-primary);
border-bottom: 1px solid var(--el-dialog-border-header-footer-color);
}
.el-dialog__body {
padding: 32px;
}
.el-dialog__footer {
text-align: right;
background-color: var(--el-dialog-bg-header-footer);
border-top: 1px solid var(--el-dialog-border-header-footer-color);
border-radius: 0 0 var(--el-dialog-border-radius) var(--el-dialog-border-radius);
padding: var(--el-dialog-inset-padding-primary);
}
}
// 扁平化遮罩层样式 (drawer-dialog 的遮罩层样式)
.standard-overlay-dialog-flat {
--el-overlay-color-lighter: rgba(0, 0, 0, .12);
backdrop-filter: blur(8px);
}