1193 lines
67 KiB
HTML
1193 lines
67 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=1366" />
|
||
<title>Fonrey 权限管理 · 静态原型</title>
|
||
<script src="https://cdn.tailwindcss.com"></script>
|
||
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
|
||
<script>
|
||
tailwind.config = {
|
||
theme: {
|
||
extend: {
|
||
colors: {
|
||
primary: {
|
||
50: '#F0FDFA',
|
||
100: '#CCFBF1',
|
||
200: '#99F6E4',
|
||
500: '#14B8A6',
|
||
600: '#0F766E',
|
||
700: '#115E59',
|
||
800: '#134E4A'
|
||
},
|
||
neutral: {
|
||
50: '#F8FAFC',
|
||
100: '#F1F5F9',
|
||
200: '#E2E8F0',
|
||
300: '#CBD5E1',
|
||
400: '#94A3B8',
|
||
500: '#64748B',
|
||
600: '#475569',
|
||
700: '#334155',
|
||
800: '#1E293B',
|
||
900: '#0F172A'
|
||
},
|
||
success: { 50: '#F0FDF4', 600: '#16A34A' },
|
||
warning: { 50: '#FFFBEB', 600: '#D97706' },
|
||
danger: { 50: '#FEF2F2', 600: '#DC2626' },
|
||
info: { 50: '#EFF6FF', 600: '#2563EB' }
|
||
}
|
||
}
|
||
}
|
||
};
|
||
</script>
|
||
<style>
|
||
:root {
|
||
--bg-page: #F8FAFC;
|
||
--bg-card: #FFFFFF;
|
||
--bg-subtle: #F1F5F9;
|
||
--text-primary: #0F172A;
|
||
--text-secondary: #64748B;
|
||
--border: #E2E8F0;
|
||
}
|
||
|
||
body {
|
||
background: var(--bg-page);
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
[x-cloak] { display: none !important; }
|
||
.tabular-nums { font-variant-numeric: tabular-nums; }
|
||
.main-tab { color: #64748B; border-bottom: 2px solid transparent; }
|
||
.main-tab.active { color: #115E59; border-bottom-color: #115E59; font-weight: 600; }
|
||
.action-link { font-size: 12px; color: #2563EB; }
|
||
.action-link:hover { text-decoration: underline; }
|
||
|
||
::-webkit-scrollbar { width: 8px; height: 8px; }
|
||
::-webkit-scrollbar-thumb { background: #CBD5E1; border-radius: 4px; }
|
||
::-webkit-scrollbar-thumb:hover { background: #94A3B8; }
|
||
</style>
|
||
</head>
|
||
<body class="text-sm antialiased" x-data="permissionPage()" x-init="init()" @keydown.escape.window="closeAllOverlays()">
|
||
<header class="fixed top-0 left-0 right-0 h-14 z-30 bg-primary-800 flex items-center justify-between">
|
||
<div class="flex items-center gap-2 px-4 w-60 shrink-0">
|
||
<div class="w-7 h-7 rounded-md bg-primary-500 flex items-center justify-center text-white text-sm font-semibold">F</div>
|
||
<span class="text-base font-semibold text-white">Fonrey</span>
|
||
</div>
|
||
<nav class="flex items-center gap-1 flex-1 px-2" aria-label="主导航">
|
||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white">主页</a>
|
||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white">房源</a>
|
||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white">客源</a>
|
||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white">营销</a>
|
||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white">交易</a>
|
||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white">数据</a>
|
||
<a class="px-3 py-1.5 text-sm rounded-md bg-primary-600 text-white font-medium">人事</a>
|
||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white">系统</a>
|
||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700 hover:text-white">三网</a>
|
||
</nav>
|
||
<div class="flex items-center gap-1 px-4 shrink-0">
|
||
<button class="p-1.5 text-primary-200 hover:bg-primary-700 hover:text-white rounded-md" aria-label="消息">
|
||
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 0 0 5.454-1.31A8.967 8.967 0 0 1 18 9.75V9A6 6 0 0 0 6 9v.75a8.967 8.967 0 0 1-2.312 6.022 23.848 23.848 0 0 0 5.454 1.31m5.714 0a24.255 24.255 0 0 1-5.714 0m5.714 0a3 3 0 1 1-5.714 0"/></svg>
|
||
</button>
|
||
<div class="flex items-center gap-2 pl-3 ml-1 border-l border-primary-700">
|
||
<div class="w-8 h-8 rounded-full bg-primary-600 text-white flex items-center justify-center text-sm font-semibold">杜</div>
|
||
<span class="text-sm font-medium text-primary-100">杜利强</span>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<aside class="fixed left-0 top-14 h-[calc(100vh-56px)] w-60 z-20 border-r border-neutral-200 bg-white overflow-y-auto">
|
||
<nav class="p-3 space-y-0.5">
|
||
<div class="px-2 pt-2 pb-1 text-xs font-medium text-neutral-500 uppercase tracking-wide">组织人事</div>
|
||
<a class="flex items-center gap-2 px-2 py-1.5 rounded-md text-neutral-700 hover:bg-neutral-100">组织结构</a>
|
||
<a class="flex items-center gap-2 px-2 py-1.5 rounded-md text-neutral-700 hover:bg-neutral-100">员工通讯录</a>
|
||
<a class="flex items-center gap-2 px-2 py-1.5 rounded-md bg-primary-50 text-primary-700 font-medium">权限管理</a>
|
||
<a class="flex items-center gap-2 px-2 py-1.5 rounded-md text-neutral-700 hover:bg-neutral-100">系统配置</a>
|
||
</nav>
|
||
</aside>
|
||
|
||
<main class="ml-60 pt-[72px] min-h-screen px-6 py-5">
|
||
<div class="mx-auto max-w-[1760px] space-y-4">
|
||
<section class="bg-white border border-neutral-200 rounded-lg p-4">
|
||
<div class="flex items-start justify-between gap-4">
|
||
<div>
|
||
<nav class="flex items-center gap-1 text-xs text-neutral-500 mb-2" aria-label="面包屑">
|
||
<a href="#" class="hover:text-neutral-700">人事OA</a>
|
||
<span>/</span>
|
||
<a href="#" class="hover:text-neutral-700">组织人事</a>
|
||
<span>/</span>
|
||
<span class="text-neutral-900">权限管理</span>
|
||
</nav>
|
||
<h1 class="text-xl font-semibold text-neutral-900">权限管理</h1>
|
||
<p class="text-xs text-neutral-500 mt-1">支持角色权限配置、人员权限覆盖、批量角色分配与范围权限管理</p>
|
||
</div>
|
||
<button class="px-3 py-1.5 rounded-md border border-info-600 text-info-600 bg-white hover:bg-info-50" @click="notify('打开角色权限帮助(原型)')">角色权限帮助</button>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="bg-white border border-neutral-200 rounded-lg px-4">
|
||
<nav class="flex items-center gap-6 overflow-x-auto" aria-label="权限主Tab">
|
||
<button class="main-tab py-3 whitespace-nowrap" :class="{ 'active': mainTab === 'staff' }" @click="switchTab('staff')">权限管理</button>
|
||
<button class="main-tab py-3 whitespace-nowrap" :class="{ 'active': mainTab === 'role' }" @click="switchTab('role')">角色管理</button>
|
||
</nav>
|
||
</section>
|
||
|
||
<template x-if="mainTab === 'staff'">
|
||
<section class="space-y-4">
|
||
<section class="bg-white border border-neutral-200 rounded-lg p-4 space-y-3">
|
||
<div class="grid grid-cols-6 gap-3">
|
||
<input x-model.trim="staffFilters.keyword" type="text" placeholder="请输入姓名/工号" class="px-3 py-2 rounded-md border border-neutral-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-600/30" />
|
||
<select x-model="staffFilters.dept" class="px-3 py-2 rounded-md border border-neutral-300 bg-white">
|
||
<option value="">员工部门(全选)</option>
|
||
<template x-for="d in departments" :key="'dept-opt-'+d">
|
||
<option :value="d" x-text="d"></option>
|
||
</template>
|
||
</select>
|
||
<select x-model.number="staffFilters.roleId" class="px-3 py-2 rounded-md border border-neutral-300 bg-white">
|
||
<option value="">角色(全选)</option>
|
||
<template x-for="r in roles" :key="'staff-role-opt-'+r.id">
|
||
<option :value="r.id" x-text="r.name"></option>
|
||
</template>
|
||
</select>
|
||
<select x-model="staffFilters.title" class="px-3 py-2 rounded-md border border-neutral-300 bg-white">
|
||
<option value="">职务名称(全选)</option>
|
||
<template x-for="t in titles" :key="'title-opt-'+t">
|
||
<option :value="t" x-text="t"></option>
|
||
</template>
|
||
</select>
|
||
<label class="flex items-center gap-2 px-3 py-2 rounded-md border border-neutral-200 bg-neutral-50 text-xs text-neutral-600">
|
||
<input type="checkbox" class="rounded border-neutral-300" x-model="staffFilters.onlyMismatch" />
|
||
权限与角色权限不一致
|
||
</label>
|
||
<div class="flex items-center gap-2 justify-end">
|
||
<button class="px-3 py-2 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="staffPage = 1">查询</button>
|
||
<button class="px-3 py-2 rounded-md border border-neutral-300 hover:bg-neutral-50" @click="resetStaffFilters()">清空条件</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex items-center justify-between">
|
||
<div class="flex items-center gap-2">
|
||
<button class="px-3 py-1.5 rounded-md border border-neutral-300 bg-white hover:bg-neutral-50 disabled:opacity-40 disabled:cursor-not-allowed" :disabled="selectedStaffIds.length===0" @click="openBatchRoleModal()">批量设置角色</button>
|
||
<button class="px-3 py-1.5 rounded-md border border-neutral-300 bg-white hover:bg-neutral-50" @click="notify('导出中,请稍候(原型)')">导出</button>
|
||
<span class="text-xs text-neutral-500">共 <span class="tabular-nums" x-text="filteredStaff.length"></span> 条</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="border border-neutral-200 rounded-lg overflow-hidden">
|
||
<table class="min-w-full text-xs">
|
||
<thead class="bg-neutral-50 border-b border-neutral-200 text-neutral-500">
|
||
<tr>
|
||
<th class="px-3 py-2 w-10"><input type="checkbox" class="rounded border-neutral-300" :checked="isCurrentStaffPageAllSelected" @change="toggleCurrentStaffPage($event.target.checked)" /></th>
|
||
<th class="px-3 py-2 text-left">员工姓名</th>
|
||
<th class="px-3 py-2 text-left">工号</th>
|
||
<th class="px-3 py-2 text-left">部门</th>
|
||
<th class="px-3 py-2 text-left">职务</th>
|
||
<th class="px-3 py-2 text-left">角色</th>
|
||
<th class="px-3 py-2 text-left">管理范围</th>
|
||
<th class="px-3 py-2 text-left">状态</th>
|
||
<th class="px-3 py-2 text-left">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody class="divide-y divide-neutral-100 bg-white">
|
||
<template x-if="pagedStaff.length===0">
|
||
<tr>
|
||
<td colspan="9" class="px-3 py-8 text-center text-neutral-400">暂无匹配人员</td>
|
||
</tr>
|
||
</template>
|
||
<template x-for="row in pagedStaff" :key="'staff-row-'+row.id">
|
||
<tr>
|
||
<td class="px-3 py-2"><input type="checkbox" class="rounded border-neutral-300" :value="row.id" :checked="selectedStaffIds.includes(row.id)" @change="toggleStaff(row.id, $event.target.checked)" /></td>
|
||
<td class="px-3 py-2" x-text="row.name"></td>
|
||
<td class="px-3 py-2 tabular-nums" x-text="row.no"></td>
|
||
<td class="px-3 py-2" x-text="row.dept"></td>
|
||
<td class="px-3 py-2" x-text="row.title"></td>
|
||
<td class="px-3 py-2" x-text="roleNameById(row.roleId)"></td>
|
||
<td class="px-3 py-2">
|
||
<span x-text="row.scopeSummary"></span>
|
||
<button class="action-link ml-1" @click="openScopeModal(row)">详情</button>
|
||
</td>
|
||
<td class="px-3 py-2">
|
||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[11px]" :class="row.mismatch ? 'bg-warning-50 text-warning-600' : 'bg-success-50 text-success-600'" x-text="row.mismatch ? '存在个人覆盖' : '一致'"></span>
|
||
</td>
|
||
<td class="px-3 py-2 whitespace-nowrap">
|
||
<button class="action-link" @click="openStaffPermissionDrawer(row)">修改权限</button>
|
||
<button class="action-link ml-2" @click="openModifyRoleModal(row)">复制角色</button>
|
||
<button class="action-link ml-2" @click="notify('扩充部门范围流程(原型)')">扩充范围</button>
|
||
</td>
|
||
</tr>
|
||
</template>
|
||
</tbody>
|
||
</table>
|
||
|
||
<div class="px-4 py-3 border-t border-neutral-200 bg-white flex items-center justify-between">
|
||
<div class="text-xs text-neutral-500">共 <span class="tabular-nums" x-text="filteredStaff.length"></span> 条</div>
|
||
<div class="flex items-center gap-2 text-xs">
|
||
<button class="px-2 py-1 border border-neutral-300 rounded disabled:opacity-40" :disabled="staffPage<=1" @click="staffPage--">上一页</button>
|
||
<span class="px-2 py-1 border border-primary-600 text-primary-600 rounded" x-text="staffPage"></span>
|
||
<button class="px-2 py-1 border border-neutral-300 rounded disabled:opacity-40" :disabled="staffPage>=staffTotalPages" @click="staffPage++">下一页</button>
|
||
<select x-model.number="staffPageSize" class="px-2 py-1 border border-neutral-300 rounded bg-white">
|
||
<option :value="20">20条/页</option>
|
||
<option :value="50">50条/页</option>
|
||
<option :value="100">100条/页</option>
|
||
</select>
|
||
<span>跳至</span>
|
||
<input x-model.number="staffJumpPage" type="number" min="1" :max="staffTotalPages" class="w-14 px-2 py-1 border border-neutral-300 rounded text-center" />
|
||
<span>页</span>
|
||
<button class="px-2 py-1 border border-neutral-300 rounded" @click="jumpStaffPage()">确定</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</section>
|
||
</template>
|
||
|
||
<template x-if="mainTab === 'role'">
|
||
<section class="space-y-4">
|
||
<section class="bg-white border border-neutral-200 rounded-lg p-4 space-y-3">
|
||
<div class="grid grid-cols-6 gap-3">
|
||
<input x-model.trim="roleFilters.name" type="text" placeholder="请输入角色名称" class="px-3 py-2 rounded-md border border-neutral-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-600/30" />
|
||
<select x-model="roleFilters.category" class="px-3 py-2 rounded-md border border-neutral-300 bg-white">
|
||
<option value="">角色类别(全选)</option>
|
||
<template x-for="c in roleCategories" :key="'role-cat-'+c">
|
||
<option :value="c" x-text="c"></option>
|
||
</template>
|
||
</select>
|
||
<input x-model="roleFilters.from" type="date" class="px-3 py-2 rounded-md border border-neutral-300" />
|
||
<input x-model="roleFilters.to" type="date" class="px-3 py-2 rounded-md border border-neutral-300" />
|
||
<div class="col-span-2 flex items-center gap-2 justify-end">
|
||
<button class="px-3 py-2 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="rolePage=1">查询</button>
|
||
<button class="px-3 py-2 rounded-md border border-neutral-300 hover:bg-neutral-50" @click="resetRoleFilters()">清空条件</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex items-center justify-between">
|
||
<div class="flex items-center gap-2">
|
||
<button class="px-3 py-1.5 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="openAddRoleModal()">+ 新增角色</button>
|
||
<button class="px-3 py-1.5 rounded-md border border-neutral-300 bg-white hover:bg-neutral-50 disabled:opacity-40 disabled:cursor-not-allowed" :disabled="selectedRoleIds.length===0" @click="batchDeleteRoles()">批量删除角色</button>
|
||
<span class="text-xs text-neutral-500">共 <span class="tabular-nums" x-text="filteredRoles.length"></span> 条</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="border border-neutral-200 rounded-lg overflow-hidden">
|
||
<table class="min-w-full text-xs">
|
||
<thead class="bg-neutral-50 border-b border-neutral-200 text-neutral-500">
|
||
<tr>
|
||
<th class="px-3 py-2 w-10"><input type="checkbox" class="rounded border-neutral-300" :checked="isCurrentRolePageAllSelected" @change="toggleCurrentRolePage($event.target.checked)" /></th>
|
||
<th class="px-3 py-2 text-left">角色名称</th>
|
||
<th class="px-3 py-2 text-left">角色类别</th>
|
||
<th class="px-3 py-2 text-left">应用人数</th>
|
||
<th class="px-3 py-2 text-left">引用该角色职务</th>
|
||
<th class="px-3 py-2 text-left">创建时间</th>
|
||
<th class="px-3 py-2 text-left">修改时间</th>
|
||
<th class="px-3 py-2 text-left">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody class="divide-y divide-neutral-100 bg-white">
|
||
<template x-if="pagedRoles.length===0">
|
||
<tr>
|
||
<td colspan="8" class="px-3 py-8 text-center text-neutral-400">暂无匹配角色</td>
|
||
</tr>
|
||
</template>
|
||
<template x-for="row in pagedRoles" :key="'role-row-'+row.id">
|
||
<tr>
|
||
<td class="px-3 py-2"><input type="checkbox" class="rounded border-neutral-300" :value="row.id" :disabled="row.isBuiltin" :checked="selectedRoleIds.includes(row.id)" @change="toggleRole(row.id, $event.target.checked)" /></td>
|
||
<td class="px-3 py-2">
|
||
<span x-text="row.name"></span>
|
||
<span x-show="row.isBuiltin" class="ml-1 text-[10px] px-1.5 py-0.5 rounded bg-info-50 text-info-600">预设</span>
|
||
</td>
|
||
<td class="px-3 py-2" x-text="row.category"></td>
|
||
<td class="px-3 py-2"><span class="tabular-nums" x-text="row.applyCount"></span> <button class="action-link" @click="notify('查看应用人员(原型)')">查看</button></td>
|
||
<td class="px-3 py-2" x-text="row.refTitle"></td>
|
||
<td class="px-3 py-2 tabular-nums" x-text="row.createdAt"></td>
|
||
<td class="px-3 py-2 tabular-nums" x-text="row.updatedAt"></td>
|
||
<td class="px-3 py-2 whitespace-nowrap">
|
||
<button class="action-link" @click="openRolePermissionDrawer(row)">编辑</button>
|
||
<button class="action-link ml-2" @click="deleteRole(row)">删除</button>
|
||
<button class="action-link ml-2" @click="notify('打开修改日志(原型)')">修改日志</button>
|
||
</td>
|
||
</tr>
|
||
</template>
|
||
</tbody>
|
||
</table>
|
||
|
||
<div class="px-4 py-3 border-t border-neutral-200 bg-white flex items-center justify-between">
|
||
<div class="text-xs text-neutral-500">共 <span class="tabular-nums" x-text="filteredRoles.length"></span> 条</div>
|
||
<div class="flex items-center gap-2 text-xs">
|
||
<button class="px-2 py-1 border border-neutral-300 rounded disabled:opacity-40" :disabled="rolePage<=1" @click="rolePage--">上一页</button>
|
||
<span class="px-2 py-1 border border-primary-600 text-primary-600 rounded" x-text="rolePage"></span>
|
||
<button class="px-2 py-1 border border-neutral-300 rounded disabled:opacity-40" :disabled="rolePage>=roleTotalPages" @click="rolePage++">下一页</button>
|
||
<select x-model.number="rolePageSize" class="px-2 py-1 border border-neutral-300 rounded bg-white">
|
||
<option :value="20">20条/页</option>
|
||
<option :value="50">50条/页</option>
|
||
<option :value="100">100条/页</option>
|
||
</select>
|
||
<span>跳至</span>
|
||
<input x-model.number="roleJumpPage" type="number" min="1" :max="roleTotalPages" class="w-14 px-2 py-1 border border-neutral-300 rounded text-center" />
|
||
<span>页</span>
|
||
<button class="px-2 py-1 border border-neutral-300 rounded" @click="jumpRolePage()">确定</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</section>
|
||
</template>
|
||
</div>
|
||
</main>
|
||
|
||
<div x-cloak x-show="batchRoleModal.open" class="fixed inset-0 z-40">
|
||
<div class="absolute inset-0 bg-neutral-900/40" @click="closeBatchRoleModal()"></div>
|
||
<div class="absolute inset-0 flex items-center justify-center p-4 pointer-events-none">
|
||
<div class="w-full max-w-lg bg-white rounded-xl shadow-xl pointer-events-auto">
|
||
<div class="flex items-center justify-between px-5 py-4 border-b border-neutral-200">
|
||
<h3 class="text-base font-semibold">批量设置角色</h3>
|
||
<button class="p-1 text-neutral-500 hover:bg-neutral-100 rounded" @click="closeBatchRoleModal()">✕</button>
|
||
</div>
|
||
<div class="px-5 py-4 space-y-3">
|
||
<p class="text-xs text-neutral-500">已选择 <span class="tabular-nums" x-text="selectedStaffIds.length"></span> 位员工</p>
|
||
<div>
|
||
<label class="block text-sm mb-1"><span class="text-danger-600">*</span> 角色</label>
|
||
<select x-model.number="batchRoleModal.roleId" class="w-full px-3 py-2 rounded-md border border-neutral-300 bg-white">
|
||
<option value="">请选择角色</option>
|
||
<template x-for="r in roles" :key="'batch-role-'+r.id"><option :value="r.id" x-text="r.name"></option></template>
|
||
</select>
|
||
<p x-show="batchRoleModal.error" class="text-xs text-danger-600 mt-1" x-text="batchRoleModal.error"></p>
|
||
</div>
|
||
<div x-show="selectedHasCustomOverride" class="px-3 py-2 rounded-md bg-warning-50 border border-warning-600/20 text-warning-600 text-xs">
|
||
该操作将覆盖所选员工的个人自定义权限,请确认。
|
||
</div>
|
||
</div>
|
||
<div class="px-5 py-3 border-t border-neutral-200 bg-neutral-50 flex justify-end gap-2">
|
||
<button class="px-3 py-1.5 rounded-md border border-neutral-300 bg-white hover:bg-neutral-100" @click="closeBatchRoleModal()">取消</button>
|
||
<button class="px-3 py-1.5 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="submitBatchRole()">确定</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div x-cloak x-show="addRoleModal.open" class="fixed inset-0 z-40">
|
||
<div class="absolute inset-0 bg-neutral-900/40" @click="closeAddRoleModal()"></div>
|
||
<div class="absolute inset-0 flex items-center justify-center p-4 pointer-events-none">
|
||
<div class="w-full max-w-lg bg-white rounded-xl shadow-xl pointer-events-auto">
|
||
<div class="flex items-center justify-between px-5 py-4 border-b border-neutral-200">
|
||
<h3 class="text-base font-semibold">添加角色</h3>
|
||
<button class="p-1 text-neutral-500 hover:bg-neutral-100 rounded" @click="closeAddRoleModal()">✕</button>
|
||
</div>
|
||
<div class="px-5 py-4 space-y-3">
|
||
<div>
|
||
<label class="block text-sm mb-1"><span class="text-danger-600">*</span> 角色名称</label>
|
||
<input x-model.trim="addRoleModal.form.name" type="text" placeholder="请输入角色名称" class="w-full px-3 py-2 rounded-md border border-neutral-300" />
|
||
<p x-show="addRoleModal.errors.name" class="text-xs text-danger-600 mt-1" x-text="addRoleModal.errors.name"></p>
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm mb-1"><span class="text-danger-600">*</span> 角色类别</label>
|
||
<select x-model="addRoleModal.form.category" class="w-full px-3 py-2 rounded-md border border-neutral-300 bg-white">
|
||
<option value="">请选择</option>
|
||
<template x-for="c in roleCategories" :key="'add-cat-'+c"><option :value="c" x-text="c"></option></template>
|
||
</select>
|
||
<p class="text-xs text-neutral-500 mt-1">角色类别影响权限,创建后仅本人创建类别可修改。</p>
|
||
<p x-show="addRoleModal.errors.category" class="text-xs text-danger-600 mt-1" x-text="addRoleModal.errors.category"></p>
|
||
</div>
|
||
</div>
|
||
<div class="px-5 py-3 border-t border-neutral-200 bg-neutral-50 flex justify-end gap-2">
|
||
<button class="px-3 py-1.5 rounded-md border border-neutral-300 bg-white hover:bg-neutral-100" @click="closeAddRoleModal()">取消</button>
|
||
<button class="px-3 py-1.5 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="submitAddRole()">下一步:设置权限</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div x-cloak x-show="modifyRoleModal.open" class="fixed inset-0 z-40">
|
||
<div class="absolute inset-0 bg-neutral-900/40" @click="closeModifyRoleModal()"></div>
|
||
<div class="absolute inset-0 flex items-center justify-center p-4 pointer-events-none">
|
||
<div class="w-full max-w-lg bg-white rounded-xl shadow-xl pointer-events-auto">
|
||
<div class="flex items-center justify-between px-5 py-4 border-b border-neutral-200">
|
||
<h3 class="text-base font-semibold">修改角色</h3>
|
||
<button class="p-1 text-neutral-500 hover:bg-neutral-100 rounded" @click="closeModifyRoleModal()">✕</button>
|
||
</div>
|
||
<div class="px-5 py-4 space-y-3">
|
||
<p class="text-xs text-neutral-500" x-text="modifyRoleModal.staff ? `${modifyRoleModal.staff.name}(${modifyRoleModal.staff.no})` : ''"></p>
|
||
<div>
|
||
<label class="block text-sm mb-1"><span class="text-danger-600">*</span> 角色(可多选)</label>
|
||
<div class="min-h-[40px] px-2 py-2 rounded-md border border-neutral-300 flex flex-wrap gap-1">
|
||
<template x-for="id in modifyRoleModal.selectedRoleIds" :key="'tag-role-'+id">
|
||
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded bg-neutral-100 text-xs">
|
||
<span x-text="roleNameById(id)"></span>
|
||
<button @click="toggleModifyRole(id)">✕</button>
|
||
</span>
|
||
</template>
|
||
<span x-show="modifyRoleModal.selectedRoleIds.length===0" class="text-neutral-400 text-xs">请选择角色</span>
|
||
</div>
|
||
<div class="mt-2 max-h-44 overflow-y-auto border border-neutral-200 rounded-md">
|
||
<template x-for="r in roles" :key="'mod-role-list-'+r.id">
|
||
<label class="flex items-center gap-2 px-3 py-2 hover:bg-neutral-50 border-b border-neutral-100 last:border-b-0">
|
||
<input type="checkbox" class="rounded border-neutral-300" :checked="modifyRoleModal.selectedRoleIds.includes(r.id)" @change="toggleModifyRole(r.id)" />
|
||
<span x-text="r.name"></span>
|
||
</label>
|
||
</template>
|
||
</div>
|
||
<p x-show="modifyRoleModal.error" class="text-xs text-danger-600 mt-1" x-text="modifyRoleModal.error"></p>
|
||
</div>
|
||
</div>
|
||
<div class="px-5 py-3 border-t border-neutral-200 bg-neutral-50 flex justify-end gap-2">
|
||
<button class="px-3 py-1.5 rounded-md border border-neutral-300 bg-white hover:bg-neutral-100" @click="closeModifyRoleModal()">取消</button>
|
||
<button class="px-3 py-1.5 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="submitModifyRole()">确定</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div x-cloak x-show="scopeModal.open" class="fixed inset-0 z-40">
|
||
<div class="absolute inset-0 bg-neutral-900/40" @click="scopeModal.open=false"></div>
|
||
<div class="absolute inset-0 flex items-center justify-center p-4 pointer-events-none">
|
||
<div class="w-full max-w-2xl bg-white rounded-xl shadow-xl pointer-events-auto">
|
||
<div class="flex items-center justify-between px-5 py-4 border-b border-neutral-200">
|
||
<h3 class="text-base font-semibold">管理范围详情</h3>
|
||
<button class="p-1 text-neutral-500 hover:bg-neutral-100 rounded" @click="scopeModal.open=false">✕</button>
|
||
</div>
|
||
<div class="px-5 py-4 space-y-3">
|
||
<p class="text-sm text-neutral-700" x-text="scopeModal.staff ? `${scopeModal.staff.name}(${scopeModal.staff.no})` : ''"></p>
|
||
<div class="flex flex-wrap gap-2">
|
||
<template x-for="item in scopeModal.scopes" :key="'scope-item-'+item">
|
||
<span class="inline-flex px-2 py-1 rounded bg-info-50 text-info-600 text-xs" x-text="item"></span>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
<div class="px-5 py-3 border-t border-neutral-200 bg-neutral-50 flex justify-end">
|
||
<button class="px-3 py-1.5 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="scopeModal.open=false">确定</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div x-cloak x-show="staffDrawer.open" class="fixed inset-0 z-50">
|
||
<div class="absolute inset-0 bg-neutral-900/35" @click="closeStaffDrawer()"></div>
|
||
<div class="absolute right-0 top-0 h-full w-[1120px] bg-white shadow-2xl border-l border-neutral-200 flex flex-col">
|
||
<div class="px-5 py-4 border-b border-neutral-200 flex items-center justify-between">
|
||
<div>
|
||
<h3 class="text-base font-semibold">人员权限编辑</h3>
|
||
<p class="text-xs text-neutral-500" x-text="staffDrawer.staff ? `${staffDrawer.staff.name} - ${staffDrawer.staff.dept}` : ''"></p>
|
||
</div>
|
||
<div class="flex items-center gap-2">
|
||
<button class="px-3 py-1.5 rounded-md border border-neutral-300 hover:bg-neutral-50" @click="closeStaffDrawer()">取消</button>
|
||
<button class="px-3 py-1.5 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="saveStaffPermissions()">保存</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex-1 min-h-0 grid grid-cols-[220px_1fr]">
|
||
<aside class="border-r border-neutral-200 p-3 overflow-y-auto">
|
||
<input x-model.trim="staffDrawer.search" type="text" placeholder="请输入权限名称" class="w-full px-3 py-2 rounded-md border border-neutral-300" />
|
||
<nav class="mt-3 space-y-1">
|
||
<template x-for="m in staffDrawer.modules" :key="'sd-module-'+m.key">
|
||
<button class="w-full text-left px-2 py-1.5 rounded" :class="staffDrawer.currentModuleKey===m.key ? 'bg-primary-50 text-primary-700' : 'hover:bg-neutral-50 text-neutral-700'" @click="staffDrawer.currentModuleKey=m.key" x-text="m.name"></button>
|
||
</template>
|
||
</nav>
|
||
</aside>
|
||
|
||
<section class="p-4 overflow-y-auto space-y-3">
|
||
<div class="flex items-center justify-between">
|
||
<h4 class="text-sm font-semibold" x-text="currentStaffModule ? currentStaffModule.name : '-'"></h4>
|
||
<label class="inline-flex items-center gap-2 text-xs text-neutral-600">
|
||
<span>本模块开启</span>
|
||
<input type="checkbox" class="rounded border-neutral-300" :checked="currentStaffModule ? currentStaffModule.enabled : false" @change="toggleCurrentStaffModule($event.target.checked)" />
|
||
</label>
|
||
</div>
|
||
|
||
<template x-if="currentStaffModule">
|
||
<div class="space-y-4">
|
||
<template x-for="group in filteredCurrentStaffGroups" :key="'sd-group-'+group.name">
|
||
<section class="border border-neutral-200 rounded-lg overflow-hidden">
|
||
<div class="px-3 py-2 bg-neutral-50 border-b border-neutral-200 text-xs font-semibold" x-text="group.name"></div>
|
||
<table class="min-w-full text-xs">
|
||
<thead class="bg-neutral-50 border-b border-neutral-200 text-neutral-500">
|
||
<tr>
|
||
<th class="px-3 py-2 text-left">权限项</th>
|
||
<th class="px-3 py-2 text-left">设置值</th>
|
||
<th class="px-3 py-2 text-left">说明</th>
|
||
<th class="px-3 py-2 text-left">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody class="divide-y divide-neutral-100">
|
||
<template x-for="item in group.items" :key="'sd-item-'+item.code">
|
||
<tr>
|
||
<td class="px-3 py-2" x-text="item.name"></td>
|
||
<td class="px-3 py-2">
|
||
<template x-if="item.type==='boolean'">
|
||
<select x-model="item.value" class="px-2 py-1 border border-neutral-300 rounded bg-white" :disabled="!currentStaffModule.enabled">
|
||
<option value="true">开通</option>
|
||
<option value="false">关闭</option>
|
||
</select>
|
||
</template>
|
||
<template x-if="item.type==='scope'">
|
||
<select x-model="item.value" class="px-2 py-1 border border-neutral-300 rounded bg-white" :disabled="!currentStaffModule.enabled">
|
||
<template x-for="opt in scopeOptions" :key="'scope-opt-'+opt">
|
||
<option :value="opt" x-text="opt"></option>
|
||
</template>
|
||
</select>
|
||
</template>
|
||
<template x-if="item.type==='integer'">
|
||
<input x-model.number="item.value" type="number" min="0" class="w-24 px-2 py-1 border border-neutral-300 rounded" :disabled="!currentStaffModule.enabled" />
|
||
</template>
|
||
<template x-if="item.type==='enum'">
|
||
<select x-model="item.value" class="px-2 py-1 border border-neutral-300 rounded bg-white" :disabled="!currentStaffModule.enabled">
|
||
<template x-for="opt in item.options" :key="'enum-'+item.code+'-'+opt">
|
||
<option :value="opt" x-text="opt"></option>
|
||
</template>
|
||
</select>
|
||
</template>
|
||
</td>
|
||
<td class="px-3 py-2 text-neutral-500" x-text="item.desc"></td>
|
||
<td class="px-3 py-2"><button class="action-link" @click="openPermissionItemDrawer(item)">编辑</button></td>
|
||
</tr>
|
||
</template>
|
||
</tbody>
|
||
</table>
|
||
</section>
|
||
</template>
|
||
</div>
|
||
</template>
|
||
</section>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div x-cloak x-show="roleDrawer.open" class="fixed inset-0 z-50">
|
||
<div class="absolute inset-0 bg-neutral-900/35" @click="closeRoleDrawer()"></div>
|
||
<div class="absolute right-0 top-0 h-full w-[1120px] bg-white shadow-2xl border-l border-neutral-200 flex flex-col">
|
||
<div class="px-5 py-4 border-b border-neutral-200 flex items-center justify-between">
|
||
<div>
|
||
<h3 class="text-base font-semibold">角色权限配置</h3>
|
||
<p class="text-xs text-neutral-500" x-text="roleDrawer.role ? `${roleDrawer.role.name}|应用人数 ${roleDrawer.role.applyCount}` : ''"></p>
|
||
</div>
|
||
<div class="flex items-center gap-2">
|
||
<button class="px-3 py-1.5 rounded-md border border-neutral-300 hover:bg-neutral-50" @click="closeRoleDrawer()">取消</button>
|
||
<button class="px-3 py-1.5 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="saveRolePermissions()">保存</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex-1 min-h-0 grid grid-cols-[220px_1fr]">
|
||
<aside class="border-r border-neutral-200 p-3 overflow-y-auto">
|
||
<input x-model.trim="roleDrawer.search" type="text" placeholder="请输入权限名称" class="w-full px-3 py-2 rounded-md border border-neutral-300" />
|
||
<nav class="mt-3 space-y-1">
|
||
<template x-for="m in roleDrawer.modules" :key="'rd-module-'+m.key">
|
||
<button class="w-full text-left px-2 py-1.5 rounded" :class="roleDrawer.currentModuleKey===m.key ? 'bg-primary-50 text-primary-700' : 'hover:bg-neutral-50 text-neutral-700'" @click="roleDrawer.currentModuleKey=m.key" x-text="m.name"></button>
|
||
</template>
|
||
</nav>
|
||
</aside>
|
||
|
||
<section class="p-4 overflow-y-auto space-y-3">
|
||
<div class="flex items-center justify-between">
|
||
<h4 class="text-sm font-semibold" x-text="currentRoleModule ? currentRoleModule.name : '-'"></h4>
|
||
<label class="inline-flex items-center gap-2 text-xs text-neutral-600">
|
||
<span>本模块开启</span>
|
||
<input type="checkbox" class="rounded border-neutral-300" :checked="currentRoleModule ? currentRoleModule.enabled : false" @change="toggleCurrentRoleModule($event.target.checked)" />
|
||
</label>
|
||
</div>
|
||
|
||
<template x-if="currentRoleModule">
|
||
<div class="space-y-4">
|
||
<template x-for="group in filteredCurrentRoleGroups" :key="'rd-group-'+group.name">
|
||
<section class="border border-neutral-200 rounded-lg overflow-hidden">
|
||
<div class="px-3 py-2 bg-neutral-50 border-b border-neutral-200 text-xs font-semibold" x-text="group.name"></div>
|
||
<table class="min-w-full text-xs">
|
||
<thead class="bg-neutral-50 border-b border-neutral-200 text-neutral-500">
|
||
<tr>
|
||
<th class="px-3 py-2 text-left">权限项</th>
|
||
<th class="px-3 py-2 text-left">设置值</th>
|
||
<th class="px-3 py-2 text-left">说明</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody class="divide-y divide-neutral-100">
|
||
<template x-for="item in group.items" :key="'rd-item-'+item.code">
|
||
<tr>
|
||
<td class="px-3 py-2" x-text="item.name"></td>
|
||
<td class="px-3 py-2">
|
||
<template x-if="item.type==='boolean'">
|
||
<select x-model="item.value" class="px-2 py-1 border border-neutral-300 rounded bg-white" :disabled="!currentRoleModule.enabled">
|
||
<option value="true">开通</option>
|
||
<option value="false">关闭</option>
|
||
</select>
|
||
</template>
|
||
<template x-if="item.type==='scope'">
|
||
<select x-model="item.value" class="px-2 py-1 border border-neutral-300 rounded bg-white" :disabled="!currentRoleModule.enabled">
|
||
<template x-for="opt in scopeOptions" :key="'rd-scope-opt-'+opt"><option :value="opt" x-text="opt"></option></template>
|
||
</select>
|
||
</template>
|
||
<template x-if="item.type==='integer'">
|
||
<input x-model.number="item.value" type="number" min="0" class="w-24 px-2 py-1 border border-neutral-300 rounded" :disabled="!currentRoleModule.enabled" />
|
||
</template>
|
||
<template x-if="item.type==='enum'">
|
||
<select x-model="item.value" class="px-2 py-1 border border-neutral-300 rounded bg-white" :disabled="!currentRoleModule.enabled">
|
||
<template x-for="opt in item.options" :key="'rd-enum-'+item.code+'-'+opt"><option :value="opt" x-text="opt"></option></template>
|
||
</select>
|
||
</template>
|
||
</td>
|
||
<td class="px-3 py-2 text-neutral-500" x-text="item.desc"></td>
|
||
</tr>
|
||
</template>
|
||
</tbody>
|
||
</table>
|
||
</section>
|
||
</template>
|
||
</div>
|
||
</template>
|
||
</section>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div x-cloak x-show="permissionItemDrawer.open" class="fixed inset-0 z-[60]">
|
||
<div class="absolute inset-0 bg-neutral-900/35" @click="closePermissionItemDrawer()"></div>
|
||
<div class="absolute right-0 top-0 h-full w-[520px] bg-white border-l border-neutral-200 shadow-2xl flex flex-col">
|
||
<div class="px-5 py-4 border-b border-neutral-200 flex items-center justify-between">
|
||
<div>
|
||
<h3 class="text-base font-semibold" x-text="permissionItemDrawer.item ? permissionItemDrawer.item.name : '权限项' "></h3>
|
||
<p class="text-xs text-neutral-500">权限修改后将影响当前编辑对象的可见范围</p>
|
||
</div>
|
||
<button class="p-1 text-neutral-500 hover:bg-neutral-100 rounded" @click="closePermissionItemDrawer()">✕</button>
|
||
</div>
|
||
<div class="p-5 space-y-4 flex-1 overflow-y-auto">
|
||
<p class="text-xs text-neutral-500" x-text="permissionItemDrawer.item ? permissionItemDrawer.item.desc : ''"></p>
|
||
<div class="border border-neutral-200 rounded-lg p-3 space-y-3" x-show="permissionItemDrawer.item">
|
||
<div class="text-xs text-neutral-500">设置值</div>
|
||
<template x-if="permissionItemDrawer.item && permissionItemDrawer.item.type==='boolean'">
|
||
<select x-model="permissionItemDrawer.item.value" class="w-full px-3 py-2 border border-neutral-300 rounded bg-white">
|
||
<option value="true">开通</option>
|
||
<option value="false">关闭</option>
|
||
</select>
|
||
</template>
|
||
<template x-if="permissionItemDrawer.item && permissionItemDrawer.item.type==='scope'">
|
||
<select x-model="permissionItemDrawer.item.value" class="w-full px-3 py-2 border border-neutral-300 rounded bg-white">
|
||
<template x-for="opt in scopeOptions" :key="'pid-scope-'+opt"><option :value="opt" x-text="opt"></option></template>
|
||
</select>
|
||
</template>
|
||
<template x-if="permissionItemDrawer.item && permissionItemDrawer.item.type==='integer'">
|
||
<input x-model.number="permissionItemDrawer.item.value" type="number" min="0" class="w-full px-3 py-2 border border-neutral-300 rounded" />
|
||
</template>
|
||
<template x-if="permissionItemDrawer.item && permissionItemDrawer.item.type==='enum'">
|
||
<select x-model="permissionItemDrawer.item.value" class="w-full px-3 py-2 border border-neutral-300 rounded bg-white">
|
||
<template x-for="opt in permissionItemDrawer.item.options" :key="'pid-enum-'+opt"><option :value="opt" x-text="opt"></option></template>
|
||
</select>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
<div class="px-5 py-3 border-t border-neutral-200 bg-neutral-50 flex justify-end gap-2">
|
||
<button class="px-3 py-1.5 rounded-md border border-neutral-300 bg-white hover:bg-neutral-100" @click="closePermissionItemDrawer()">取消</button>
|
||
<button class="px-3 py-1.5 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="savePermissionItemDrawer()">保存</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div x-cloak x-show="toast.show" class="fixed right-6 top-20 z-[80]" x-transition>
|
||
<div class="px-3 py-2 rounded-md shadow border text-sm" :class="toast.type==='error' ? 'bg-danger-50 border-danger-600/20 text-danger-600' : 'bg-success-50 border-success-600/20 text-success-600'" x-text="toast.message"></div>
|
||
</div>
|
||
|
||
<script>
|
||
function permissionPage() {
|
||
return {
|
||
mainTab: 'staff',
|
||
scopeOptions: ['无', '本人', '本组', '本门店', '本区域', '本大区', '全公司'],
|
||
departments: ['上海豪园店二组', '静安门店', '浦东门店', '系统管理组'],
|
||
titles: ['高级业务员', '分行经理', '行政人员', '系统管理员'],
|
||
roleCategories: ['置业顾问', '店管', '区域管理', '职能'],
|
||
|
||
staffFilters: { keyword: '', dept: '', roleId: '', title: '', onlyMismatch: false },
|
||
roleFilters: { name: '', category: '', from: '', to: '' },
|
||
|
||
staffPage: 1,
|
||
staffPageSize: 20,
|
||
staffJumpPage: 1,
|
||
rolePage: 1,
|
||
rolePageSize: 20,
|
||
roleJumpPage: 1,
|
||
|
||
selectedStaffIds: [],
|
||
selectedRoleIds: [],
|
||
|
||
batchRoleModal: { open: false, roleId: '', error: '' },
|
||
addRoleModal: { open: false, form: { name: '', category: '' }, errors: {} },
|
||
modifyRoleModal: { open: false, staff: null, selectedRoleIds: [], error: '' },
|
||
scopeModal: { open: false, staff: null, scopes: [] },
|
||
|
||
staffDrawer: { open: false, staff: null, modules: [], currentModuleKey: '', search: '' },
|
||
roleDrawer: { open: false, role: null, modules: [], currentModuleKey: '', search: '' },
|
||
permissionItemDrawer: { open: false, item: null, source: null },
|
||
|
||
toast: { show: false, type: 'success', message: '' },
|
||
|
||
roles: [
|
||
{ id: 1, name: '高级业务员', category: '置业顾问', applyCount: 12, refTitle: '高级业务员', createdAt: '2026-04-10 09:20', updatedAt: '2026-04-25 10:10', isBuiltin: true, permissionProfile: [] },
|
||
{ id: 2, name: '分行经理', category: '店管', applyCount: 6, refTitle: '分行经理', createdAt: '2026-04-10 09:20', updatedAt: '2026-04-24 16:40', isBuiltin: true, permissionProfile: [] },
|
||
{ id: 3, name: '行政人员', category: '职能', applyCount: 3, refTitle: '行政人员', createdAt: '2026-04-11 12:00', updatedAt: '2026-04-23 15:10', isBuiltin: false, permissionProfile: [] },
|
||
{ id: 4, name: '最大权限角色', category: '区域管理', applyCount: 1, refTitle: '系统管理员', createdAt: '2026-04-12 08:30', updatedAt: '2026-04-26 11:20', isBuiltin: false, permissionProfile: [] },
|
||
{ id: 5, name: '刘文龙', category: '店管', applyCount: 1, refTitle: '分行经理', createdAt: '2026-04-15 10:20', updatedAt: '2026-04-27 17:30', isBuiltin: false, permissionProfile: [] }
|
||
],
|
||
|
||
staff: [
|
||
{ id: 101, name: '刘源', no: '41', dept: '上海豪园店二组', title: '高级业务员', roleId: 1, scopeSummary: '本人/本组', scopes: ['本人', '本组'], mismatch: false, hasPersonalOverride: false },
|
||
{ id: 102, name: '刘文龙', no: '17', dept: '上海豪园店二组', title: '分行经理', roleId: 2, scopeSummary: '本门店', scopes: ['本门店'], mismatch: true, hasPersonalOverride: true },
|
||
{ id: 103, name: '金怡', no: '18', dept: '静安门店', title: '行政人员', roleId: 3, scopeSummary: '本门店', scopes: ['本门店'], mismatch: false, hasPersonalOverride: false },
|
||
{ id: 104, name: '孙海鹏', no: '56', dept: '浦东门店', title: '高级业务员', roleId: 1, scopeSummary: '本人', scopes: ['本人'], mismatch: false, hasPersonalOverride: false },
|
||
{ id: 105, name: '周伟', no: '63', dept: '上海豪园店二组', title: '高级业务员', roleId: 1, scopeSummary: '本人/本组', scopes: ['本人', '本组'], mismatch: false, hasPersonalOverride: false },
|
||
{ id: 106, name: '杜利强', no: '1', dept: '系统管理组', title: '系统管理员', roleId: 4, scopeSummary: '全公司', scopes: ['全公司'], mismatch: true, hasPersonalOverride: true }
|
||
],
|
||
|
||
init() {
|
||
const base = this.buildPermissionTemplate();
|
||
this.roles = this.roles.map(r => ({ ...r, permissionProfile: this.deepClone(base) }));
|
||
},
|
||
|
||
get filteredStaff() {
|
||
return this.staff.filter(s => {
|
||
if (this.staffFilters.keyword && !(s.name.includes(this.staffFilters.keyword) || s.no.includes(this.staffFilters.keyword))) return false;
|
||
if (this.staffFilters.dept && s.dept !== this.staffFilters.dept) return false;
|
||
if (this.staffFilters.roleId && s.roleId !== this.staffFilters.roleId) return false;
|
||
if (this.staffFilters.title && s.title !== this.staffFilters.title) return false;
|
||
if (this.staffFilters.onlyMismatch && !s.mismatch) return false;
|
||
return true;
|
||
});
|
||
},
|
||
|
||
get staffTotalPages() {
|
||
return Math.max(1, Math.ceil(this.filteredStaff.length / this.staffPageSize));
|
||
},
|
||
|
||
get pagedStaff() {
|
||
if (this.staffPage > this.staffTotalPages) this.staffPage = this.staffTotalPages;
|
||
const start = (this.staffPage - 1) * this.staffPageSize;
|
||
return this.filteredStaff.slice(start, start + this.staffPageSize);
|
||
},
|
||
|
||
get isCurrentStaffPageAllSelected() {
|
||
if (!this.pagedStaff.length) return false;
|
||
return this.pagedStaff.every(r => this.selectedStaffIds.includes(r.id));
|
||
},
|
||
|
||
toggleCurrentStaffPage(checked) {
|
||
const ids = this.pagedStaff.map(r => r.id);
|
||
if (checked) {
|
||
this.selectedStaffIds = Array.from(new Set([...this.selectedStaffIds, ...ids]));
|
||
} else {
|
||
this.selectedStaffIds = this.selectedStaffIds.filter(id => !ids.includes(id));
|
||
}
|
||
},
|
||
|
||
toggleStaff(id, checked) {
|
||
if (checked) this.selectedStaffIds = Array.from(new Set([...this.selectedStaffIds, id]));
|
||
else this.selectedStaffIds = this.selectedStaffIds.filter(x => x !== id);
|
||
},
|
||
|
||
jumpStaffPage() {
|
||
const n = Number(this.staffJumpPage || 1);
|
||
this.staffPage = Math.min(this.staffTotalPages, Math.max(1, n));
|
||
},
|
||
|
||
resetStaffFilters() {
|
||
this.staffFilters = { keyword: '', dept: '', roleId: '', title: '', onlyMismatch: false };
|
||
this.staffPage = 1;
|
||
},
|
||
|
||
get filteredRoles() {
|
||
return this.roles.filter(r => {
|
||
if (this.roleFilters.name && !r.name.includes(this.roleFilters.name)) return false;
|
||
if (this.roleFilters.category && r.category !== this.roleFilters.category) return false;
|
||
if (this.roleFilters.from && r.updatedAt.slice(0, 10) < this.roleFilters.from) return false;
|
||
if (this.roleFilters.to && r.updatedAt.slice(0, 10) > this.roleFilters.to) return false;
|
||
return true;
|
||
});
|
||
},
|
||
|
||
get roleTotalPages() {
|
||
return Math.max(1, Math.ceil(this.filteredRoles.length / this.rolePageSize));
|
||
},
|
||
|
||
get pagedRoles() {
|
||
if (this.rolePage > this.roleTotalPages) this.rolePage = this.roleTotalPages;
|
||
const start = (this.rolePage - 1) * this.rolePageSize;
|
||
return this.filteredRoles.slice(start, start + this.rolePageSize);
|
||
},
|
||
|
||
get isCurrentRolePageAllSelected() {
|
||
const rows = this.pagedRoles.filter(r => !r.isBuiltin);
|
||
if (!rows.length) return false;
|
||
return rows.every(r => this.selectedRoleIds.includes(r.id));
|
||
},
|
||
|
||
toggleCurrentRolePage(checked) {
|
||
const ids = this.pagedRoles.filter(r => !r.isBuiltin).map(r => r.id);
|
||
if (checked) this.selectedRoleIds = Array.from(new Set([...this.selectedRoleIds, ...ids]));
|
||
else this.selectedRoleIds = this.selectedRoleIds.filter(id => !ids.includes(id));
|
||
},
|
||
|
||
toggleRole(id, checked) {
|
||
if (checked) this.selectedRoleIds = Array.from(new Set([...this.selectedRoleIds, id]));
|
||
else this.selectedRoleIds = this.selectedRoleIds.filter(x => x !== id);
|
||
},
|
||
|
||
jumpRolePage() {
|
||
const n = Number(this.roleJumpPage || 1);
|
||
this.rolePage = Math.min(this.roleTotalPages, Math.max(1, n));
|
||
},
|
||
|
||
resetRoleFilters() {
|
||
this.roleFilters = { name: '', category: '', from: '', to: '' };
|
||
this.rolePage = 1;
|
||
},
|
||
|
||
switchTab(tab) {
|
||
this.mainTab = tab;
|
||
},
|
||
|
||
roleNameById(id) {
|
||
const role = this.roles.find(r => r.id === id);
|
||
return role ? role.name : '-';
|
||
},
|
||
|
||
openBatchRoleModal() {
|
||
if (!this.selectedStaffIds.length) return;
|
||
this.batchRoleModal = { open: true, roleId: '', error: '' };
|
||
},
|
||
|
||
closeBatchRoleModal() {
|
||
this.batchRoleModal = { open: false, roleId: '', error: '' };
|
||
},
|
||
|
||
get selectedHasCustomOverride() {
|
||
const set = new Set(this.selectedStaffIds);
|
||
return this.staff.some(s => set.has(s.id) && s.hasPersonalOverride);
|
||
},
|
||
|
||
submitBatchRole() {
|
||
if (!this.batchRoleModal.roleId) {
|
||
this.batchRoleModal.error = '请选择角色';
|
||
return;
|
||
}
|
||
const set = new Set(this.selectedStaffIds);
|
||
this.staff = this.staff.map(s => {
|
||
if (!set.has(s.id)) return s;
|
||
return {
|
||
...s,
|
||
roleId: this.batchRoleModal.roleId,
|
||
mismatch: false,
|
||
hasPersonalOverride: false
|
||
};
|
||
});
|
||
this.closeBatchRoleModal();
|
||
this.selectedStaffIds = [];
|
||
this.notify('批量设置角色成功');
|
||
},
|
||
|
||
openScopeModal(staff) {
|
||
this.scopeModal.open = true;
|
||
this.scopeModal.staff = staff;
|
||
this.scopeModal.scopes = staff.scopes || [];
|
||
},
|
||
|
||
openAddRoleModal() {
|
||
this.addRoleModal = { open: true, form: { name: '', category: '' }, errors: {} };
|
||
},
|
||
|
||
closeAddRoleModal() {
|
||
this.addRoleModal = { open: false, form: { name: '', category: '' }, errors: {} };
|
||
},
|
||
|
||
submitAddRole() {
|
||
const errors = {};
|
||
const name = (this.addRoleModal.form.name || '').trim();
|
||
const category = this.addRoleModal.form.category;
|
||
if (!name) errors.name = '请输入角色名称';
|
||
if (!category) errors.category = '请选择角色类别';
|
||
if (name && this.roles.some(r => r.name === name)) errors.name = '角色名称已存在';
|
||
this.addRoleModal.errors = errors;
|
||
if (Object.keys(errors).length) return;
|
||
|
||
const id = Math.max(...this.roles.map(r => r.id)) + 1;
|
||
const now = new Date();
|
||
const ts = `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}-${String(now.getDate()).padStart(2,'0')} ${String(now.getHours()).padStart(2,'0')}:${String(now.getMinutes()).padStart(2,'0')}`;
|
||
const role = {
|
||
id,
|
||
name,
|
||
category,
|
||
applyCount: 0,
|
||
refTitle: '-',
|
||
createdAt: ts,
|
||
updatedAt: ts,
|
||
isBuiltin: false,
|
||
permissionProfile: this.deepClone(this.buildPermissionTemplate())
|
||
};
|
||
this.roles.unshift(role);
|
||
this.closeAddRoleModal();
|
||
this.openRolePermissionDrawer(role);
|
||
this.notify('角色创建成功,请继续配置权限');
|
||
},
|
||
|
||
openModifyRoleModal(staff) {
|
||
this.modifyRoleModal.open = true;
|
||
this.modifyRoleModal.staff = staff;
|
||
this.modifyRoleModal.selectedRoleIds = [staff.roleId];
|
||
this.modifyRoleModal.error = '';
|
||
},
|
||
|
||
closeModifyRoleModal() {
|
||
this.modifyRoleModal = { open: false, staff: null, selectedRoleIds: [], error: '' };
|
||
},
|
||
|
||
toggleModifyRole(roleId) {
|
||
if (this.modifyRoleModal.selectedRoleIds.includes(roleId)) {
|
||
this.modifyRoleModal.selectedRoleIds = this.modifyRoleModal.selectedRoleIds.filter(id => id !== roleId);
|
||
} else {
|
||
this.modifyRoleModal.selectedRoleIds = [...this.modifyRoleModal.selectedRoleIds, roleId];
|
||
}
|
||
},
|
||
|
||
submitModifyRole() {
|
||
if (!this.modifyRoleModal.selectedRoleIds.length) {
|
||
this.modifyRoleModal.error = '请至少选择一个角色';
|
||
return;
|
||
}
|
||
const primary = this.modifyRoleModal.selectedRoleIds[0];
|
||
this.staff = this.staff.map(s => {
|
||
if (s.id !== this.modifyRoleModal.staff.id) return s;
|
||
return { ...s, roleId: primary, mismatch: true, hasPersonalOverride: true };
|
||
});
|
||
this.closeModifyRoleModal();
|
||
this.notify('员工角色已更新');
|
||
},
|
||
|
||
deleteRole(role) {
|
||
if (role.isBuiltin) {
|
||
this.notify('预设角色不可删除', 'error');
|
||
return;
|
||
}
|
||
if (role.applyCount > 0) {
|
||
this.notify('该角色仍有应用人员,无法删除', 'error');
|
||
return;
|
||
}
|
||
this.roles = this.roles.filter(r => r.id !== role.id);
|
||
this.selectedRoleIds = this.selectedRoleIds.filter(id => id !== role.id);
|
||
this.notify('角色删除成功');
|
||
},
|
||
|
||
batchDeleteRoles() {
|
||
if (!this.selectedRoleIds.length) return;
|
||
const blocked = this.roles.filter(r => this.selectedRoleIds.includes(r.id) && (r.isBuiltin || r.applyCount > 0));
|
||
if (blocked.length) {
|
||
this.notify('选中角色中包含预设角色或仍有应用人数,无法批量删除', 'error');
|
||
return;
|
||
}
|
||
const set = new Set(this.selectedRoleIds);
|
||
this.roles = this.roles.filter(r => !set.has(r.id));
|
||
this.selectedRoleIds = [];
|
||
this.notify('批量删除成功');
|
||
},
|
||
|
||
openStaffPermissionDrawer(staff) {
|
||
this.staffDrawer.open = true;
|
||
this.staffDrawer.staff = staff;
|
||
this.staffDrawer.modules = this.deepClone(this.buildPermissionTemplate());
|
||
this.staffDrawer.currentModuleKey = this.staffDrawer.modules[0].key;
|
||
this.staffDrawer.search = '';
|
||
},
|
||
|
||
closeStaffDrawer() {
|
||
this.staffDrawer = { open: false, staff: null, modules: [], currentModuleKey: '', search: '' };
|
||
this.closePermissionItemDrawer();
|
||
},
|
||
|
||
get currentStaffModule() {
|
||
return this.staffDrawer.modules.find(m => m.key === this.staffDrawer.currentModuleKey);
|
||
},
|
||
|
||
get filteredCurrentStaffGroups() {
|
||
const module = this.currentStaffModule;
|
||
if (!module) return [];
|
||
if (!this.staffDrawer.search) return module.groups;
|
||
const q = this.staffDrawer.search;
|
||
return module.groups.map(g => ({
|
||
...g,
|
||
items: g.items.filter(i => i.name.includes(q) || i.desc.includes(q))
|
||
})).filter(g => g.items.length);
|
||
},
|
||
|
||
toggleCurrentStaffModule(checked) {
|
||
const module = this.currentStaffModule;
|
||
if (module) module.enabled = checked;
|
||
},
|
||
|
||
saveStaffPermissions() {
|
||
if (!this.staffDrawer.staff) return;
|
||
this.staff = this.staff.map(s => s.id === this.staffDrawer.staff.id ? { ...s, mismatch: true, hasPersonalOverride: true } : s);
|
||
this.closeStaffDrawer();
|
||
this.notify('人员权限已保存(已标记与角色不一致)');
|
||
},
|
||
|
||
openRolePermissionDrawer(role) {
|
||
this.roleDrawer.open = true;
|
||
this.roleDrawer.role = role;
|
||
this.roleDrawer.modules = this.deepClone(role.permissionProfile && role.permissionProfile.length ? role.permissionProfile : this.buildPermissionTemplate());
|
||
this.roleDrawer.currentModuleKey = this.roleDrawer.modules[0].key;
|
||
this.roleDrawer.search = '';
|
||
},
|
||
|
||
closeRoleDrawer() {
|
||
this.roleDrawer = { open: false, role: null, modules: [], currentModuleKey: '', search: '' };
|
||
},
|
||
|
||
get currentRoleModule() {
|
||
return this.roleDrawer.modules.find(m => m.key === this.roleDrawer.currentModuleKey);
|
||
},
|
||
|
||
get filteredCurrentRoleGroups() {
|
||
const module = this.currentRoleModule;
|
||
if (!module) return [];
|
||
if (!this.roleDrawer.search) return module.groups;
|
||
const q = this.roleDrawer.search;
|
||
return module.groups.map(g => ({
|
||
...g,
|
||
items: g.items.filter(i => i.name.includes(q) || i.desc.includes(q))
|
||
})).filter(g => g.items.length);
|
||
},
|
||
|
||
toggleCurrentRoleModule(checked) {
|
||
const module = this.currentRoleModule;
|
||
if (module) module.enabled = checked;
|
||
},
|
||
|
||
saveRolePermissions() {
|
||
if (!this.roleDrawer.role) return;
|
||
const now = new Date();
|
||
const ts = `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}-${String(now.getDate()).padStart(2,'0')} ${String(now.getHours()).padStart(2,'0')}:${String(now.getMinutes()).padStart(2,'0')}`;
|
||
this.roles = this.roles.map(r => {
|
||
if (r.id !== this.roleDrawer.role.id) return r;
|
||
return { ...r, permissionProfile: this.deepClone(this.roleDrawer.modules), updatedAt: ts };
|
||
});
|
||
this.closeRoleDrawer();
|
||
this.notify('角色权限已保存');
|
||
},
|
||
|
||
openPermissionItemDrawer(item) {
|
||
this.permissionItemDrawer.open = true;
|
||
this.permissionItemDrawer.item = item;
|
||
},
|
||
|
||
closePermissionItemDrawer() {
|
||
this.permissionItemDrawer = { open: false, item: null, source: null };
|
||
},
|
||
|
||
savePermissionItemDrawer() {
|
||
this.closePermissionItemDrawer();
|
||
this.notify('权限项已更新');
|
||
},
|
||
|
||
closeAllOverlays() {
|
||
if (this.permissionItemDrawer.open) return this.closePermissionItemDrawer();
|
||
if (this.staffDrawer.open) return this.closeStaffDrawer();
|
||
if (this.roleDrawer.open) return this.closeRoleDrawer();
|
||
if (this.batchRoleModal.open) return this.closeBatchRoleModal();
|
||
if (this.addRoleModal.open) return this.closeAddRoleModal();
|
||
if (this.modifyRoleModal.open) return this.closeModifyRoleModal();
|
||
if (this.scopeModal.open) this.scopeModal.open = false;
|
||
},
|
||
|
||
notify(message, type = 'success') {
|
||
this.toast = { show: true, type, message };
|
||
setTimeout(() => { this.toast.show = false; }, 2200);
|
||
},
|
||
|
||
deepClone(v) {
|
||
return JSON.parse(JSON.stringify(v));
|
||
},
|
||
|
||
buildPermissionTemplate() {
|
||
return [
|
||
{
|
||
key: 'home',
|
||
name: '首页',
|
||
enabled: true,
|
||
groups: [
|
||
{
|
||
name: '基础权限',
|
||
items: [
|
||
{ code: 'home.version', name: '查看首页版本', type: 'enum', value: '置业顾问', options: ['无', '置业顾问', '店管', '区管', '区总', '副总', '总经理', '职能人员'], desc: '控制员工查看的首页版本。' },
|
||
{ code: 'home.new_property', name: '今日新上房源', type: 'boolean', value: 'true', desc: '控制移动端是否显示。' },
|
||
{ code: 'home.rank_person', name: '个人排行榜权限', type: 'scope', value: '本人', desc: '控制个人排行榜可见范围。' },
|
||
{ code: 'home.rank_dept', name: '部门排行榜权限', type: 'scope', value: '本组', desc: '控制部门排行榜可见范围。' },
|
||
{ code: 'home.like_manage', name: '管理点赞信息和屏蔽点赞', type: 'boolean', value: 'false', desc: '开启后可删除点赞内容并屏蔽发布。' }
|
||
]
|
||
}
|
||
]
|
||
},
|
||
{
|
||
key: 'client',
|
||
name: '客源',
|
||
enabled: true,
|
||
groups: [
|
||
{
|
||
name: '私客基础权限',
|
||
items: [
|
||
{ code: 'client.private.add', name: '新增私客', type: 'boolean', value: 'true', desc: '开启后可新增私客。' },
|
||
{ code: 'client.private.limit', name: '个人私客数量上限', type: 'integer', value: 999, desc: '999=不限制,0=不允许。' },
|
||
{ code: 'client.private.view', name: '查看私客(非保护客)', type: 'scope', value: '本组', desc: '控制查看非保护客私客范围。' },
|
||
{ code: 'client.private.edit', name: '编辑私客(非保护客)', type: 'scope', value: '本人', desc: '控制可编辑私客范围。' }
|
||
]
|
||
},
|
||
{
|
||
name: '公客基础权限',
|
||
items: [
|
||
{ code: 'client.public.view', name: '公客查看范围', type: 'scope', value: '本门店', desc: '控制公客查看范围。' },
|
||
{ code: 'client.public.convert', name: '公客转私客', type: 'boolean', value: 'false', desc: '开启后允许将公客转私客。' },
|
||
{ code: 'client.public.edit', name: '编辑公客', type: 'boolean', value: 'false', desc: '开启后可编辑公客。' }
|
||
]
|
||
},
|
||
{
|
||
name: '联系人号码权限',
|
||
items: [
|
||
{ code: 'client.tel.view', name: '私客&成交客查看号码', type: 'scope', value: '本人', desc: '控制可查看号码范围。' },
|
||
{ code: 'client.tel.count', name: '联系人号码查看个数', type: 'integer', value: 999, desc: '每天查看次数上限。' }
|
||
]
|
||
}
|
||
]
|
||
},
|
||
{
|
||
key: 'property',
|
||
name: '房源',
|
||
enabled: true,
|
||
groups: [
|
||
{
|
||
name: '基础权限',
|
||
items: [
|
||
{ code: 'property.add', name: '新增房源', type: 'boolean', value: 'true', desc: '开启后可新增房源。' },
|
||
{ code: 'property.status.view', name: '状态查看范围', type: 'enum', value: '出租/出售', options: ['出租/出售', '出租/出售/暂缓', '全部状态'], desc: '控制可查看房源状态集合。' },
|
||
{ code: 'property.maintain', name: '维护房源列表', type: 'scope', value: '本门店', desc: '按维护人范围查看。' }
|
||
]
|
||
},
|
||
{
|
||
name: '管理权限',
|
||
items: [
|
||
{ code: 'property.delete', name: '删除房源', type: 'boolean', value: 'false', desc: '开启后可删除房源。' },
|
||
{ code: 'property.recover', name: '恢复已删除房源', type: 'boolean', value: 'false', desc: '开启后可恢复已删除房源。' },
|
||
{ code: 'property.related.modify', name: '批量修改相关方范围', type: 'scope', value: '本门店', desc: '控制批量修改相关方范围。' }
|
||
]
|
||
}
|
||
]
|
||
},
|
||
{
|
||
key: 'trade',
|
||
name: '交易',
|
||
enabled: false,
|
||
groups: [
|
||
{
|
||
name: '基础权限',
|
||
items: [
|
||
{ code: 'trade.view', name: '查看交易单', type: 'scope', value: '本门店', desc: '控制交易单查看范围。' },
|
||
{ code: 'trade.edit', name: '编辑交易单', type: 'boolean', value: 'false', desc: '控制交易单编辑权限。' }
|
||
]
|
||
}
|
||
]
|
||
}
|
||
];
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
</body>
|
||
</html> |