1936 lines
104 KiB
HTML
1936 lines
104 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; }
|
||
|
||
.bg-surface { background: var(--bg-card); }
|
||
.bg-subtle { background: var(--bg-subtle); }
|
||
.border-surface { border-color: var(--border); }
|
||
.text-surface { color: var(--text-primary); }
|
||
.text-muted { color: var(--text-secondary); }
|
||
|
||
.main-tab {
|
||
color: #64748B;
|
||
border-bottom: 2px solid transparent;
|
||
}
|
||
.main-tab.active {
|
||
color: #0F766E;
|
||
border-bottom-color: #0F766E;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.action-link {
|
||
font-size: 12px;
|
||
color: #2563EB;
|
||
}
|
||
.action-link:hover { text-decoration: underline; }
|
||
|
||
.badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
border-radius: 999px;
|
||
padding: 2px 8px;
|
||
font-size: 12px;
|
||
line-height: 1.4;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.badge-success { background: #F0FDF4; color: #16A34A; }
|
||
.badge-warning { background: #FFFBEB; color: #D97706; }
|
||
.badge-danger { background: #FEF2F2; color: #DC2626; }
|
||
.badge-info { background: #EFF6FF; color: #2563EB; }
|
||
|
||
::-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="orgPage()" x-init="init()" @keydown.escape.window="closeAllPanels()">
|
||
<!-- Top Bar -->
|
||
<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>
|
||
|
||
<!-- Side Bar -->
|
||
<aside class="fixed left-0 top-14 h-[calc(100vh-56px)] w-60 z-20 border-r border-surface bg-surface overflow-y-auto">
|
||
<nav class="p-3 space-y-0.5">
|
||
<div class="px-2 pt-2 pb-1 text-xs font-medium text-muted uppercase tracking-wide">组织人事</div>
|
||
<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>
|
||
<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>
|
||
</nav>
|
||
</aside>
|
||
|
||
<!-- Main -->
|
||
<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-surface border border-surface rounded-lg p-4">
|
||
<div class="flex items-start justify-between gap-4">
|
||
<div>
|
||
<nav class="flex items-center gap-1 text-xs text-muted 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-surface" x-text="mainTab === 'org' ? '组织结构' : (mainTab === 'chart' ? '部门架构图' : '员工通讯录')"></span>
|
||
</nav>
|
||
<h1 class="text-xl font-semibold text-surface">组织人事</h1>
|
||
</div>
|
||
<button class="px-3 py-1.5 rounded-md border border-danger-600 text-danger-600 bg-white hover:bg-danger-50" @click="notify('进入员工入职黑名单(原型)')">员工入职黑名单</button>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="bg-surface border border-surface rounded-lg px-4">
|
||
<nav class="flex items-center gap-6 overflow-x-auto" aria-label="组织人事视图切换">
|
||
<button class="main-tab py-3 whitespace-nowrap" :class="{ 'active': mainTab === 'org' }" @click="switchMainTab('org')">组织结构</button>
|
||
<button class="main-tab py-3 whitespace-nowrap" :class="{ 'active': mainTab === 'chart' }" @click="switchMainTab('chart')">部门架构图</button>
|
||
<button class="main-tab py-3 whitespace-nowrap" :class="{ 'active': mainTab === 'contacts' }" @click="switchMainTab('contacts')">员工通讯录</button>
|
||
</nav>
|
||
</section>
|
||
|
||
<!-- 组织结构 -->
|
||
<template x-if="mainTab === 'org'">
|
||
<section class="grid grid-cols-[320px_1fr] gap-4">
|
||
<!-- 左侧组织树 -->
|
||
<section class="bg-surface border border-surface rounded-lg p-3 space-y-3">
|
||
<button class="w-full px-3 py-2 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="openDeptModal('add', selectedDeptId)">+ 新增部门</button>
|
||
|
||
<input x-model.trim="deptTreeSearch" type="text" placeholder="搜索部门" class="w-full px-3 py-2 rounded-md border border-surface bg-white focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-600/40" />
|
||
|
||
<label class="inline-flex items-center gap-2 text-xs text-muted">
|
||
<input type="checkbox" class="rounded border-surface" x-model="showClosedDepartments" />
|
||
<span>显示已关闭部门</span>
|
||
</label>
|
||
|
||
<div class="border border-surface rounded-md p-2 max-h-[680px] overflow-y-auto">
|
||
<template x-for="row in deptTreeRows" :key="'dept-tree-' + row.id">
|
||
<div class="group flex items-center gap-1 py-1 rounded-md" :class="selectedDeptId === row.id ? 'bg-primary-50' : 'hover:bg-neutral-50'" :style="`padding-left:${row.depth * 14}px`">
|
||
<button class="w-5 h-5 text-neutral-400 hover:text-neutral-700" @click="toggleDeptExpand(row.id)" :aria-label="deptExpanded[row.id] ? '收起节点' : '展开节点'">
|
||
<template x-if="row.hasChildren">
|
||
<span x-text="deptExpanded[row.id] ? '▾' : '▸'"></span>
|
||
</template>
|
||
<template x-if="!row.hasChildren">
|
||
<span class="opacity-40">•</span>
|
||
</template>
|
||
</button>
|
||
|
||
<button class="flex-1 text-left" @click="selectedDeptId = row.id">
|
||
<span class="text-[13px]" :class="selectedDeptId === row.id ? 'text-primary-700 font-medium' : 'text-surface'" x-text="row.name"></span>
|
||
<span class="text-[11px] text-muted ml-1" x-text="`(${row.employeeCount}人)`"></span>
|
||
<span x-show="!row.isActive" class="ml-1 text-[10px] text-danger-600">已关闭</span>
|
||
</button>
|
||
|
||
<div class="hidden group-hover:flex items-center gap-2 pr-1">
|
||
<button class="text-[11px] text-info-600" @click="openDeptModal('edit', row.id)">编辑</button>
|
||
<button x-show="row.id !== rootDeptId" class="text-[11px] text-danger-600" @click="deleteDepartment(row.id)">删除</button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 右侧员工区域 -->
|
||
<section class="space-y-4">
|
||
<section class="bg-surface border border-surface rounded-lg p-4">
|
||
<div class="flex items-start justify-between gap-3">
|
||
<div>
|
||
<h2 class="text-base font-semibold" x-text="selectedDepartment ? selectedDepartment.name : '-' "></h2>
|
||
<p class="text-xs text-muted mt-1">部门详情(组织结构维护)</p>
|
||
</div>
|
||
<div class="flex items-center gap-2">
|
||
<button class="px-3 py-1.5 rounded-md border border-warning-600 text-warning-600 bg-white hover:bg-warning-50" @click="notify('发起入职邀请(原型)')">入职邀请</button>
|
||
<button class="px-3 py-1.5 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="openDeptModal('edit', selectedDeptId)">编辑</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-2 gap-x-8 gap-y-2 mt-3 text-sm">
|
||
<div class="flex"><span class="w-24 text-muted">部门名称</span><span x-text="selectedDepartment ? selectedDepartment.name : '-' "></span></div>
|
||
<div class="flex"><span class="w-24 text-muted">上级部门</span><span x-text="departmentNameById(selectedDepartment ? selectedDepartment.parentId : null) || '-' "></span></div>
|
||
<div class="flex"><span class="w-24 text-muted">部门级别</span><span x-text="levelLabel(selectedDepartment ? selectedDepartment.level : '')"></span></div>
|
||
<div class="flex"><span class="w-24 text-muted">部门属性</span><span x-text="selectedDepartment && selectedDepartment.attribute ? selectedDepartment.attribute : '-' "></span></div>
|
||
<div class="flex"><span class="w-24 text-muted">部门电话</span><span x-text="selectedDepartment && selectedDepartment.phone ? selectedDepartment.phone : '-' "></span></div>
|
||
<div class="flex"><span class="w-24 text-muted">部门状态</span><span x-text="selectedDepartment && selectedDepartment.isActive ? '启用' : '关闭' "></span></div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="bg-warning-50 border border-warning-600/20 rounded-lg px-4 py-2 text-xs text-warning-600">
|
||
账号数上限 17,已使用账号数 17,其中入职审批中员工 1(不包含离职、共享账号、已删除账号)
|
||
</section>
|
||
|
||
<section class="bg-warning-50 border border-danger-600/20 rounded-lg px-4 py-2 text-xs text-danger-600 flex items-center justify-between">
|
||
<span>经公安系统校验,有 1 个员工的身份证和真实姓名不匹配,请将这些员工的证件正确录入。</span>
|
||
<button class="action-link" @click="notify('已筛选证件异常员工(原型)')">立即筛选数据</button>
|
||
</section>
|
||
|
||
<section class="bg-surface border border-surface rounded-lg p-4 space-y-3">
|
||
<div class="grid grid-cols-5 gap-3">
|
||
<input x-model.trim="employeeFilters.keyword" type="text" placeholder="姓名/工号/电话" class="px-3 py-2 rounded-md border border-surface focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-600/40" />
|
||
<select x-model="employeeFilters.jobTitle" class="px-3 py-2 rounded-md border border-surface bg-white">
|
||
<option value="">请选择职务</option>
|
||
<template x-for="opt in jobOptions" :key="'filter-job-'+opt.title">
|
||
<option :value="opt.title" x-text="opt.title"></option>
|
||
</template>
|
||
</select>
|
||
<select x-model="employeeFilters.status" class="px-3 py-2 rounded-md border border-surface bg-white">
|
||
<option value="">员工状态(全选)</option>
|
||
<option value="active">正式</option>
|
||
<option value="probation">试用</option>
|
||
<option value="frozen">冻结</option>
|
||
<option value="resigned">离职</option>
|
||
</select>
|
||
<select x-model="employeeFilters.approval" class="px-3 py-2 rounded-md border border-surface bg-white">
|
||
<option value="">审批状态(全选)</option>
|
||
<option value="入职审">入职审</option>
|
||
<option value="通过">通过</option>
|
||
<option value="驳回">驳回</option>
|
||
</select>
|
||
<div class="flex items-center gap-2">
|
||
<button class="px-3 py-2 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="employeePage = 1">查询</button>
|
||
<button class="px-3 py-2 rounded-md border border-surface hover:bg-neutral-50" @click="resetEmployeeFilters()">清空条件</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-4 gap-3">
|
||
<select x-model="employeeFilters.level" class="px-3 py-2 rounded-md border border-surface bg-white">
|
||
<option value="">部门级别(全选)</option>
|
||
<template x-for="lv in levelOptions" :key="'lv-filter-'+lv.value">
|
||
<option :value="lv.value" x-text="lv.label"></option>
|
||
</template>
|
||
</select>
|
||
<select x-model="employeeFilters.systemAdmin" class="px-3 py-2 rounded-md border border-surface bg-white">
|
||
<option value="">系统管理员(不限)</option>
|
||
<option value="yes">是</option>
|
||
<option value="no">否</option>
|
||
</select>
|
||
<input x-model="employeeFilters.joinedFrom" type="date" class="px-3 py-2 rounded-md border border-surface bg-white" />
|
||
<input x-model="employeeFilters.joinedTo" type="date" class="px-3 py-2 rounded-md border border-surface bg-white" />
|
||
</div>
|
||
|
||
<label class="inline-flex items-center gap-2 text-xs text-muted">
|
||
<input type="checkbox" class="rounded border-surface" x-model="employeeFilters.includeChildren" />
|
||
<span>显示下属部门员工</span>
|
||
</label>
|
||
</section>
|
||
|
||
<section class="bg-surface border border-surface rounded-lg p-3 flex items-center justify-between gap-3">
|
||
<div class="flex items-center gap-2 flex-wrap">
|
||
<button class="px-3 py-1.5 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="openOnboardDrawer()">新增员工</button>
|
||
<button class="px-3 py-1.5 rounded-md border border-surface hover:bg-neutral-50" @click="notify('导出员工(原型)')">导出员工</button>
|
||
<button class="px-3 py-1.5 rounded-md border border-surface" :class="selectedEmployeeCount===0 ? 'opacity-50 cursor-not-allowed' : 'hover:bg-neutral-50'" :disabled="selectedEmployeeCount===0" @click="openTransferDrawerByBatch()">批量调动员工</button>
|
||
<button class="px-3 py-1.5 rounded-md border border-surface" :class="selectedEmployeeCount===0 ? 'opacity-50 cursor-not-allowed' : 'hover:bg-neutral-50'" :disabled="selectedEmployeeCount===0" @click="notify('批量设置员工上级(原型)')">批量设置员工上级</button>
|
||
<button class="px-3 py-1.5 rounded-md border border-surface" :class="selectedEmployeeCount===0 ? 'opacity-50 cursor-not-allowed' : 'hover:bg-neutral-50'" :disabled="selectedEmployeeCount===0" @click="notify('更多批量操作(原型)')">更多</button>
|
||
</div>
|
||
<button class="action-link" @click="openTransferLogsModal()">员工异动记录</button>
|
||
</section>
|
||
|
||
<section class="bg-surface border border-surface rounded-lg overflow-hidden">
|
||
<div class="overflow-x-auto">
|
||
<table class="min-w-full text-sm">
|
||
<thead class="bg-subtle border-b border-surface">
|
||
<tr class="text-left">
|
||
<th scope="col" class="px-3 py-2 w-10">
|
||
<input type="checkbox" class="rounded border-surface" :checked="employeeAllOnPageSelected" @click.prevent="toggleEmployeePageSelect(!employeeAllOnPageSelected)" aria-label="全选当前页员工" />
|
||
</th>
|
||
<th scope="col" class="px-3 py-2">姓名/昵称</th>
|
||
<th scope="col" class="px-3 py-2">员工号</th>
|
||
<th scope="col" class="px-3 py-2">职务</th>
|
||
<th scope="col" class="px-3 py-2">部门</th>
|
||
<th scope="col" class="px-3 py-2">部门级别</th>
|
||
<th scope="col" class="px-3 py-2">上级</th>
|
||
<th scope="col" class="px-3 py-2">电话</th>
|
||
<th scope="col" class="px-3 py-2">入职时间</th>
|
||
<th scope="col" class="px-3 py-2">审批状态</th>
|
||
<th scope="col" class="px-3 py-2">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<template x-if="employeePagedRows.length === 0">
|
||
<tr>
|
||
<td colspan="11" class="px-4 py-12 text-center">
|
||
<p class="text-base font-medium text-surface">暂无匹配数据</p>
|
||
<p class="text-xs text-muted mt-1">请尝试调整筛选条件</p>
|
||
</td>
|
||
</tr>
|
||
</template>
|
||
<template x-for="row in employeePagedRows" :key="'emp-row-'+row.id">
|
||
<tr class="border-b border-surface hover:bg-neutral-50/50">
|
||
<td class="px-3 py-2 align-top">
|
||
<input type="checkbox" class="rounded border-surface" :checked="isEmployeeSelected(row.id)" @click.prevent="toggleEmployeeSelect(row.id, !isEmployeeSelected(row.id))" aria-label="选择员工" />
|
||
</td>
|
||
<td class="px-3 py-2 align-top">
|
||
<div class="flex items-center gap-2">
|
||
<span class="w-7 h-7 rounded-full bg-neutral-100 text-neutral-600 flex items-center justify-center text-xs font-semibold" x-text="row.name.slice(0,1)"></span>
|
||
<div>
|
||
<button class="text-info-600 hover:underline" @click="openEmployeeDetail(row.id)" x-text="row.name"></button>
|
||
<div class="text-xs text-muted" x-text="row.nickname || '-' "></div>
|
||
</div>
|
||
<span x-show="row.idRisk" class="text-danger-600" title="证件信息不匹配">⚠</span>
|
||
</div>
|
||
</td>
|
||
<td class="px-3 py-2 align-top" x-text="row.employeeNo"></td>
|
||
<td class="px-3 py-2 align-top" x-text="row.jobTitle"></td>
|
||
<td class="px-3 py-2 align-top" x-text="departmentNameById(row.orgUnitId)"></td>
|
||
<td class="px-3 py-2 align-top" x-text="levelLabel(departmentLevelById(row.orgUnitId))"></td>
|
||
<td class="px-3 py-2 align-top" x-text="staffNameById(row.supervisorId) || '-' "></td>
|
||
<td class="px-3 py-2 align-top tabular-nums" x-text="maskPhone(row.phone)"></td>
|
||
<td class="px-3 py-2 align-top tabular-nums" x-text="row.joinedAt"></td>
|
||
<td class="px-3 py-2 align-top">
|
||
<span class="badge" :class="row.approval==='通过' ? 'badge-success' : (row.approval==='驳回' ? 'badge-danger' : 'badge-warning')" x-text="row.approval"></span>
|
||
</td>
|
||
<td class="px-3 py-2 align-top">
|
||
<div class="flex items-center gap-3 text-xs">
|
||
<button class="action-link" @click="openEmployeeDetail(row.id)">查看</button>
|
||
<button class="action-link" @click="openTransferDrawer(row.id)">调动</button>
|
||
<button class="action-link" @click="openLeaveModal(row.id)">离职</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
</template>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="px-4 py-3 border-t border-surface bg-white flex items-center justify-between">
|
||
<div class="text-xs text-muted" x-text="`共 ${employeeFilteredRows.length} 条`"></div>
|
||
<div class="flex items-center gap-1">
|
||
<button class="px-2 py-1 rounded border border-surface text-xs" :disabled="employeePage===1" :class="employeePage===1 ? 'opacity-40 cursor-not-allowed' : 'hover:bg-neutral-50'" @click="employeePage = Math.max(1, employeePage-1)">上一页</button>
|
||
<span class="px-2 py-1 text-xs" x-text="employeePage"></span>
|
||
<button class="px-2 py-1 rounded border border-surface text-xs" :disabled="employeePage>=employeeTotalPages" :class="employeePage>=employeeTotalPages ? 'opacity-40 cursor-not-allowed' : 'hover:bg-neutral-50'" @click="employeePage = Math.min(employeeTotalPages, employeePage+1)">下一页</button>
|
||
</div>
|
||
<div class="text-xs text-muted flex items-center gap-2">
|
||
<span>20 条/页</span>
|
||
<span>跳至</span>
|
||
<input type="number" min="1" :max="employeeTotalPages" x-model.number="employeeJumpPage" class="w-14 px-2 py-1 rounded border border-surface text-center" />
|
||
<span>页</span>
|
||
<button class="px-2 py-1 rounded border border-surface hover:bg-neutral-50" @click="jumpEmployeePage()">确定</button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</section>
|
||
</section>
|
||
</template>
|
||
|
||
<!-- 部门架构图 -->
|
||
<template x-if="mainTab === 'chart'">
|
||
<section class="space-y-4">
|
||
<section class="bg-surface border border-surface rounded-lg p-4 flex items-center justify-between gap-3">
|
||
<div class="flex items-center gap-3">
|
||
<label class="text-xs text-muted">部门</label>
|
||
<select x-model.number="chartFilters.deptId" class="px-3 py-2 rounded-md border border-surface bg-white">
|
||
<option value="">请选择</option>
|
||
<template x-for="d in activeDepartmentOptions" :key="'chart-dept-'+d.id">
|
||
<option :value="d.id" x-text="d.name"></option>
|
||
</template>
|
||
</select>
|
||
<label class="inline-flex items-center gap-2 text-xs text-muted">
|
||
<input type="checkbox" class="rounded border-surface" x-model="chartFilters.showClosed" />
|
||
<span>显示已关闭部门</span>
|
||
</label>
|
||
</div>
|
||
<div class="flex items-center gap-2">
|
||
<button class="w-8 h-8 rounded border border-surface hover:bg-neutral-50" @click="notify('放大架构图(原型)')" aria-label="放大">+</button>
|
||
<button class="w-8 h-8 rounded border border-surface hover:bg-neutral-50" @click="notify('缩小架构图(原型)')" aria-label="缩小">-</button>
|
||
<button class="w-8 h-8 rounded border border-surface hover:bg-neutral-50" @click="notify('导出架构图(原型)')" aria-label="导出">⤓</button>
|
||
<button class="w-8 h-8 rounded border border-surface hover:bg-neutral-50" @click="notify('适应窗口(原型)')" aria-label="适应">□</button>
|
||
<button class="w-8 h-8 rounded border border-surface hover:bg-neutral-50" @click="notify('重置视图(原型)')" aria-label="重置">↻</button>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="bg-info-50 border border-info-600/20 rounded-lg px-4 py-2 text-xs text-info-600">
|
||
*最多 8 个层级数量,可对下图进行拖拽/缩放操作
|
||
</section>
|
||
|
||
<section class="bg-surface border border-surface rounded-lg p-6 overflow-auto min-h-[520px]">
|
||
<div class="min-w-[980px]">
|
||
<div class="flex justify-center">
|
||
<div class="w-72 bg-white border border-surface rounded-lg p-3 text-center shadow-sm">
|
||
<p class="text-sm font-semibold" x-text="chartRootNode.name"></p>
|
||
<p class="text-xs text-muted mt-1" x-text="`${chartNodeEmployeeCount(chartRootNode.id)} 人`"></p>
|
||
<button class="mt-2 text-xs text-info-600" @click="toggleChartExpand(chartRootNode.id)" x-text="isChartExpanded(chartRootNode.id) ? '收起' : '展开'"></button>
|
||
</div>
|
||
</div>
|
||
|
||
<div x-show="isChartExpanded(chartRootNode.id)" class="mt-10 flex items-start justify-center gap-6">
|
||
<template x-for="child in chartChildren(chartRootNode.id)" :key="'chart-child-'+child.id">
|
||
<div class="w-64">
|
||
<div class="bg-white border border-surface rounded-lg p-3 shadow-sm">
|
||
<div class="flex items-center justify-between">
|
||
<p class="text-sm font-semibold" x-text="child.name"></p>
|
||
<span class="badge badge-info" x-text="levelLabel(child.level)"></span>
|
||
</div>
|
||
<p class="text-xs text-muted mt-2" x-text="`负责人:${child.manager || '未设置部门负责人'}`"></p>
|
||
<p class="text-xs text-muted mt-1" x-text="`部门人数:${chartNodeEmployeeCount(child.id)}`"></p>
|
||
<p class="text-xs text-muted mt-1" x-text="`直属下级:${chartChildren(child.id).length}`"></p>
|
||
<button x-show="chartChildren(child.id).length > 0" class="mt-2 text-xs text-info-600" @click="toggleChartExpand(child.id)" x-text="isChartExpanded(child.id) ? '收起下级' : '展开下级'"></button>
|
||
</div>
|
||
|
||
<div x-show="isChartExpanded(child.id) && chartChildren(child.id).length > 0" class="mt-3 space-y-2">
|
||
<template x-for="sub in chartChildren(child.id)" :key="'chart-sub-'+sub.id">
|
||
<div class="bg-neutral-50 border border-surface rounded-md p-2">
|
||
<div class="flex items-center justify-between">
|
||
<span class="text-xs font-medium" x-text="sub.name"></span>
|
||
<span class="text-[11px] text-muted" x-text="levelLabel(sub.level)"></span>
|
||
</div>
|
||
<p class="text-[11px] text-muted mt-1" x-text="`人数:${chartNodeEmployeeCount(sub.id)} | 下级:${chartChildren(sub.id).length}`"></p>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</section>
|
||
</template>
|
||
|
||
<!-- 员工通讯录 -->
|
||
<template x-if="mainTab === 'contacts'">
|
||
<section class="space-y-4">
|
||
<section class="bg-surface border border-surface rounded-lg p-4">
|
||
<div class="grid grid-cols-5 gap-3">
|
||
<select x-model.number="contactFilters.deptId" class="px-3 py-2 rounded-md border border-surface bg-white">
|
||
<option value="">部门(请选择)</option>
|
||
<template x-for="d in activeDepartmentOptions" :key="'contact-dept-'+d.id">
|
||
<option :value="d.id" x-text="d.name"></option>
|
||
</template>
|
||
</select>
|
||
<select x-model="contactFilters.jobTitle" class="px-3 py-2 rounded-md border border-surface bg-white">
|
||
<option value="">职务(请选择)</option>
|
||
<template x-for="opt in jobOptions" :key="'contact-job-'+opt.title">
|
||
<option :value="opt.title" x-text="opt.title"></option>
|
||
</template>
|
||
</select>
|
||
<select x-model="contactFilters.birthday" class="px-3 py-2 rounded-md border border-surface bg-white">
|
||
<option value="">生日(不限)</option>
|
||
<option value="thisMonth">本月生日</option>
|
||
</select>
|
||
<input x-model.trim="contactFilters.keyword" type="text" placeholder="姓名/电话/分机/邮件" class="px-3 py-2 rounded-md border border-surface" />
|
||
<div class="flex items-center gap-2">
|
||
<button class="px-3 py-2 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="contactPage = 1">查询</button>
|
||
<button class="px-3 py-2 rounded-md border border-surface hover:bg-neutral-50" @click="resetContactFilters()">清除条件</button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="bg-surface border border-surface rounded-lg overflow-hidden">
|
||
<div class="overflow-x-auto">
|
||
<table class="min-w-full text-sm">
|
||
<thead class="bg-subtle border-b border-surface">
|
||
<tr class="text-left">
|
||
<th scope="col" class="px-3 py-2">部门</th>
|
||
<th scope="col" class="px-3 py-2">姓名</th>
|
||
<th scope="col" class="px-3 py-2">职务</th>
|
||
<th scope="col" class="px-3 py-2">性别</th>
|
||
<th scope="col" class="px-3 py-2">生日</th>
|
||
<th scope="col" class="px-3 py-2">电话</th>
|
||
<th scope="col" class="px-3 py-2">分机</th>
|
||
<th scope="col" class="px-3 py-2">邮箱</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<template x-if="contactPagedRows.length === 0">
|
||
<tr>
|
||
<td colspan="8" class="px-4 py-12 text-center text-muted">暂无匹配数据</td>
|
||
</tr>
|
||
</template>
|
||
<template x-for="row in contactPagedRows" :key="'contact-row-'+row.id">
|
||
<tr class="border-b border-surface hover:bg-neutral-50/50">
|
||
<td class="px-3 py-2" x-text="departmentNameById(row.orgUnitId)"></td>
|
||
<td class="px-3 py-2">
|
||
<div class="flex items-center gap-2">
|
||
<span class="w-6 h-6 rounded-full bg-neutral-100 text-neutral-600 flex items-center justify-center text-xs" x-text="row.name.slice(0,1)"></span>
|
||
<span x-text="row.name"></span>
|
||
</div>
|
||
</td>
|
||
<td class="px-3 py-2" x-text="row.jobTitle"></td>
|
||
<td class="px-3 py-2" x-text="row.gender || '-' "></td>
|
||
<td class="px-3 py-2 tabular-nums" x-text="birthdayMonthDay(row.birthday)"></td>
|
||
<td class="px-3 py-2 tabular-nums">
|
||
<span x-text="isPhoneVisible(row.id) ? row.phone : maskPhone(row.phone)"></span>
|
||
<button class="action-link ml-2" @click="notify(`拨打 ${row.name}(原型)`)">拨打</button>
|
||
<button class="action-link ml-1" @click="togglePhoneVisible(row.id)">查看号码</button>
|
||
</td>
|
||
<td class="px-3 py-2" x-text="row.extension || '-' "></td>
|
||
<td class="px-3 py-2" x-text="row.email || '-' "></td>
|
||
</tr>
|
||
</template>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="px-4 py-3 border-t border-surface bg-white flex items-center justify-between">
|
||
<div class="text-xs text-muted" x-text="`共 ${contactFilteredRows.length} 条`"></div>
|
||
<div class="flex items-center gap-1">
|
||
<button class="px-2 py-1 rounded border border-surface text-xs" :disabled="contactPage===1" :class="contactPage===1 ? 'opacity-40 cursor-not-allowed' : 'hover:bg-neutral-50'" @click="contactPage = Math.max(1, contactPage-1)">上一页</button>
|
||
<span class="px-2 py-1 text-xs" x-text="contactPage"></span>
|
||
<button class="px-2 py-1 rounded border border-surface text-xs" :disabled="contactPage>=contactTotalPages" :class="contactPage>=contactTotalPages ? 'opacity-40 cursor-not-allowed' : 'hover:bg-neutral-50'" @click="contactPage = Math.min(contactTotalPages, contactPage+1)">下一页</button>
|
||
</div>
|
||
<div class="text-xs text-muted">20 条/页</div>
|
||
</div>
|
||
</section>
|
||
</section>
|
||
</template>
|
||
</div>
|
||
</main>
|
||
|
||
<!-- 部门新增/编辑 Modal -->
|
||
<div x-cloak x-show="deptModal.open" class="fixed inset-0 z-40">
|
||
<div class="absolute inset-0 bg-neutral-900/40" @click="closeDeptModal()"></div>
|
||
<div class="absolute inset-0 flex items-center justify-center p-4 pointer-events-none">
|
||
<div class="w-full max-w-3xl bg-white rounded-xl shadow-xl pointer-events-auto flex flex-col max-h-[92vh]">
|
||
<div class="flex items-center justify-between px-5 py-4 border-b border-surface">
|
||
<h3 class="text-base font-semibold" x-text="deptModal.mode === 'add' ? '部门新增' : '部门编辑'"></h3>
|
||
<button class="p-1 text-muted hover:bg-neutral-100 rounded" @click="closeDeptModal()">✕</button>
|
||
</div>
|
||
|
||
<div class="flex-1 overflow-y-auto px-5 py-4 space-y-4">
|
||
<div class="bg-info-50 border border-info-600/20 rounded-lg px-3 py-2 text-xs text-info-600">
|
||
1. 店组级别部门必须挂在门店下;2. 经纪人/店管的所属部门只能是门店/店组;3. 经纪人是职务类别为置业顾问的员工。
|
||
</div>
|
||
|
||
<h4 class="text-sm font-semibold">部门基本信息</h4>
|
||
<div class="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<label class="block text-xs text-muted mb-1"><span class="text-danger-600">*</span> 部门名称</label>
|
||
<input x-model.trim="deptModal.form.name" type="text" placeholder="请输入部门名称" class="w-full px-3 py-2 rounded-md border border-surface" />
|
||
<p x-show="deptModal.errors.name" class="text-xs text-danger-600 mt-1" x-text="deptModal.errors.name"></p>
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs text-muted mb-1"><span class="text-danger-600">*</span> 上级部门</label>
|
||
<select x-model.number="deptModal.form.parentId" class="w-full px-3 py-2 rounded-md border border-surface bg-white">
|
||
<option value="">请选择</option>
|
||
<template x-for="d in parentDepartmentOptions(deptModal.form.id)" :key="'parent-opt-'+d.id">
|
||
<option :value="d.id" x-text="d.name"></option>
|
||
</template>
|
||
</select>
|
||
<p x-show="deptModal.errors.parentId" class="text-xs text-danger-600 mt-1" x-text="deptModal.errors.parentId"></p>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-xs text-muted mb-1"><span class="text-danger-600">*</span> 部门级别</label>
|
||
<select x-model="deptModal.form.level" class="w-full px-3 py-2 rounded-md border border-surface bg-white">
|
||
<option value="">请选择部门级别</option>
|
||
<template x-for="lv in levelOptions" :key="'lv-opt-'+lv.value">
|
||
<option :value="lv.value" x-text="lv.label"></option>
|
||
</template>
|
||
</select>
|
||
<p x-show="deptModal.errors.level" class="text-xs text-danger-600 mt-1" x-text="deptModal.errors.level"></p>
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs text-muted mb-1">部门属性</label>
|
||
<select x-model="deptModal.form.attribute" class="w-full px-3 py-2 rounded-md border border-surface bg-white">
|
||
<option value="">请选择</option>
|
||
<option value="直营">直营</option>
|
||
<option value="加盟">加盟</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-xs text-muted mb-1">部门地址</label>
|
||
<input x-model.trim="deptModal.form.address" type="text" placeholder="请输入部门地址" class="w-full px-3 py-2 rounded-md border border-surface" />
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs text-muted mb-1">部门负责人</label>
|
||
<select x-model.number="deptModal.form.managerId" class="w-full px-3 py-2 rounded-md border border-surface bg-white">
|
||
<option value="">请选择</option>
|
||
<template x-for="e in activeEmployees" :key="'mgr-'+e.id">
|
||
<option :value="e.id" x-text="e.name"></option>
|
||
</template>
|
||
</select>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-xs text-muted mb-1">部门电话</label>
|
||
<input x-model.trim="deptModal.form.phone" type="text" placeholder="请输入部门电话" class="w-full px-3 py-2 rounded-md border border-surface" />
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs text-muted mb-1">部门状态</label>
|
||
<select x-model="deptModal.form.isActive" class="w-full px-3 py-2 rounded-md border border-surface bg-white">
|
||
<option :value="true">启用</option>
|
||
<option :value="false">关闭</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<p x-show="deptModal.errors.rule" class="text-xs text-danger-600" x-text="deptModal.errors.rule"></p>
|
||
</div>
|
||
|
||
<div class="px-5 py-3 border-t border-surface bg-neutral-50 flex items-center justify-end gap-2">
|
||
<button class="px-4 py-1.5 rounded-md border border-surface hover:bg-white" @click="closeDeptModal()">取消</button>
|
||
<button class="px-4 py-1.5 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="saveDepartment()">保存</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 新增员工 Drawer -->
|
||
<div x-cloak x-show="onboardDrawer.open" class="fixed inset-0 z-40">
|
||
<div class="absolute inset-0 bg-neutral-900/40" @click="closeOnboardDrawer()"></div>
|
||
<div class="absolute right-0 top-0 h-full w-[640px] bg-white shadow-xl flex flex-col border-l border-surface">
|
||
<div class="flex items-center justify-between px-5 py-4 border-b border-surface">
|
||
<h3 class="text-base font-semibold">新增员工 / 办理入职</h3>
|
||
<button class="p-1 text-muted hover:bg-neutral-100 rounded" @click="closeOnboardDrawer()">✕</button>
|
||
</div>
|
||
|
||
<div class="flex-1 overflow-y-auto px-5 py-4 space-y-4">
|
||
<div class="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<label class="block text-xs text-muted mb-1"><span class="text-danger-600">*</span> 姓名</label>
|
||
<input data-test="input-onboard-name" x-model.trim="onboardDrawer.form.name" type="text" placeholder="请输入姓名" class="w-full px-3 py-2 rounded-md border border-surface" />
|
||
<p x-show="onboardDrawer.errors.name" class="text-xs text-danger-600 mt-1" x-text="onboardDrawer.errors.name"></p>
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs text-muted mb-1">昵称</label>
|
||
<input x-model.trim="onboardDrawer.form.nickname" type="text" placeholder="请输入昵称" class="w-full px-3 py-2 rounded-md border border-surface" />
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-xs text-muted mb-1"><span class="text-danger-600">*</span> 手机号</label>
|
||
<input data-test="input-onboard-phone" x-model.trim="onboardDrawer.form.phone" type="text" placeholder="请输入11位手机号" class="w-full px-3 py-2 rounded-md border border-surface" />
|
||
<p x-show="onboardDrawer.errors.phone" class="text-xs text-danger-600 mt-1" x-text="onboardDrawer.errors.phone"></p>
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs text-muted mb-1">入职日期</label>
|
||
<input x-model="onboardDrawer.form.joinedAt" type="date" class="w-full px-3 py-2 rounded-md border border-surface bg-white" />
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-xs text-muted mb-1"><span class="text-danger-600">*</span> 所属门店/店组</label>
|
||
<select data-test="select-onboard-dept" x-model.number="onboardDrawer.form.orgUnitId" class="w-full px-3 py-2 rounded-md border border-surface bg-white">
|
||
<option value="">请选择</option>
|
||
<template x-for="d in storeAndGroupDepartments" :key="'onboard-dept-'+d.id">
|
||
<option :value="d.id" x-text="`${d.name}(${levelLabel(d.level)})`"></option>
|
||
</template>
|
||
</select>
|
||
<p x-show="onboardDrawer.errors.orgUnitId" class="text-xs text-danger-600 mt-1" x-text="onboardDrawer.errors.orgUnitId"></p>
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs text-muted mb-1"><span class="text-danger-600">*</span> 职务</label>
|
||
<select data-test="select-onboard-job" x-model="onboardDrawer.form.jobTitle" class="w-full px-3 py-2 rounded-md border border-surface bg-white" @change="syncOnboardJobMeta()">
|
||
<option value="">请选择</option>
|
||
<template x-for="j in jobOptions" :key="'onboard-job-'+j.title">
|
||
<option :value="j.title" x-text="j.title"></option>
|
||
</template>
|
||
</select>
|
||
<p x-show="onboardDrawer.errors.jobTitle" class="text-xs text-danger-600 mt-1" x-text="onboardDrawer.errors.jobTitle"></p>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-xs text-muted mb-1">职务类别(联动)</label>
|
||
<input x-model="onboardDrawer.form.jobCategory" type="text" readonly class="w-full px-3 py-2 rounded-md border border-surface bg-neutral-50 text-muted" />
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs text-muted mb-1">默认角色(联动)</label>
|
||
<input x-model="onboardDrawer.form.role" type="text" readonly class="w-full px-3 py-2 rounded-md border border-surface bg-neutral-50 text-muted" />
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-xs text-muted mb-1">直属上级</label>
|
||
<select x-model.number="onboardDrawer.form.supervisorId" class="w-full px-3 py-2 rounded-md border border-surface bg-white">
|
||
<option value="">请选择</option>
|
||
<template x-for="e in activeEmployees" :key="'onboard-super-'+e.id">
|
||
<option :value="e.id" x-text="`${departmentNameById(e.orgUnitId)}-${e.name}`"></option>
|
||
</template>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs text-muted mb-1">状态</label>
|
||
<select x-model="onboardDrawer.form.status" class="w-full px-3 py-2 rounded-md border border-surface bg-white">
|
||
<option value="probation">试用</option>
|
||
<option value="active">正式</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<p x-show="onboardDrawer.errors.rule" class="text-xs text-danger-600" x-text="onboardDrawer.errors.rule"></p>
|
||
|
||
<div x-show="onboardDrawer.createdAccount" class="bg-success-50 border border-success-600/20 rounded-lg p-3 text-xs">
|
||
<p class="font-semibold text-success-600">账号创建成功</p>
|
||
<p class="mt-1">登录账号:<span class="tabular-nums" x-text="onboardDrawer.createdAccount.username"></span></p>
|
||
<p>初始密码:<span class="tabular-nums" x-text="onboardDrawer.createdAccount.password"></span></p>
|
||
<p class="text-muted mt-1">已模拟发送给员工,可立即登录系统(原型)。</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="px-5 py-3 border-t border-surface bg-neutral-50 flex items-center justify-end gap-2">
|
||
<button class="px-4 py-1.5 rounded-md border border-surface hover:bg-white" @click="closeOnboardDrawer()">取消</button>
|
||
<button data-test="btn-onboard-submit" class="px-4 py-1.5 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="submitOnboard()">提交</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 员工离职 Modal -->
|
||
<div x-cloak x-show="leaveModal.open" class="fixed inset-0 z-40">
|
||
<div class="absolute inset-0 bg-neutral-900/40" @click="closeLeaveModal()"></div>
|
||
<div class="absolute inset-0 flex items-center justify-center p-4 pointer-events-none">
|
||
<div class="w-full max-w-xl bg-white rounded-xl shadow-xl pointer-events-auto">
|
||
<div class="flex items-center justify-between px-5 py-4 border-b border-surface">
|
||
<h3 class="text-base font-semibold">员工离职</h3>
|
||
<button class="p-1 text-muted hover:bg-neutral-100 rounded" @click="closeLeaveModal()">✕</button>
|
||
</div>
|
||
<div class="px-5 py-4 space-y-4">
|
||
<div class="text-sm font-medium" x-text="`${leaveModal.employee ? leaveModal.employee.name : '-'} 的业务信息统计`"></div>
|
||
<div class="grid grid-cols-3 gap-3 text-xs">
|
||
<div class="bg-subtle rounded-md px-3 py-2"><span class="text-muted">房源数量</span><p class="tabular-nums mt-1" x-text="leaveModal.stats.house"></p></div>
|
||
<div class="bg-subtle rounded-md px-3 py-2"><span class="text-muted">客源数量</span><p class="tabular-nums mt-1" x-text="leaveModal.stats.client"></p></div>
|
||
<div class="bg-subtle rounded-md px-3 py-2"><span class="text-muted">营销客数量</span><p class="tabular-nums mt-1" x-text="leaveModal.stats.market"></p></div>
|
||
</div>
|
||
<p class="text-xs text-danger-600">注:若不转给任何账号,则离职成功后业务信息仍属于该离职员工 <button class="action-link ml-1" @click="notify('进入转移业务归属(原型)')">转移业务归属</button></p>
|
||
|
||
<div>
|
||
<label class="block text-xs text-muted mb-1"><span class="text-danger-600">*</span> 离职日期</label>
|
||
<input data-test="input-leave-date" x-model="leaveModal.form.resignedAt" type="date" class="w-full px-3 py-2 rounded-md border border-surface bg-white" />
|
||
<p x-show="leaveModal.errors.resignedAt" class="text-xs text-danger-600 mt-1" x-text="leaveModal.errors.resignedAt"></p>
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs text-muted mb-1"><span class="text-danger-600">*</span> 离职类型</label>
|
||
<select data-test="select-leave-type" x-model="leaveModal.form.type" class="w-full px-3 py-2 rounded-md border border-surface bg-white">
|
||
<option value="">请选择</option>
|
||
<option value="自离">自离</option>
|
||
<option value="协商离职">协商离职</option>
|
||
<option value="辞退">辞退</option>
|
||
</select>
|
||
<p x-show="leaveModal.errors.type" class="text-xs text-danger-600 mt-1" x-text="leaveModal.errors.type"></p>
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs text-muted mb-1">备注</label>
|
||
<textarea x-model.trim="leaveModal.form.remark" rows="3" maxlength="50" placeholder="50字以内" class="w-full px-3 py-2 rounded-md border border-surface"></textarea>
|
||
</div>
|
||
</div>
|
||
<div class="px-5 py-3 border-t border-surface bg-neutral-50 flex items-center justify-end gap-2">
|
||
<button class="px-4 py-1.5 rounded-md border border-surface hover:bg-white" @click="closeLeaveModal()">取消</button>
|
||
<button data-test="btn-leave-submit" class="px-4 py-1.5 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="submitLeave()">确定</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 员工调动 Drawer -->
|
||
<div x-cloak x-show="transferDrawer.open" class="fixed inset-0 z-40">
|
||
<div class="absolute inset-0 bg-neutral-900/40" @click="closeTransferDrawer()"></div>
|
||
<div class="absolute right-0 top-0 h-full w-[760px] bg-white shadow-xl flex flex-col border-l border-surface">
|
||
<div class="flex items-center justify-between px-5 py-4 border-b border-surface">
|
||
<h3 class="text-base font-semibold">员工调动</h3>
|
||
<button class="p-1 text-muted hover:bg-neutral-100 rounded" @click="closeTransferDrawer()">✕</button>
|
||
</div>
|
||
|
||
<div class="flex-1 overflow-y-auto px-5 py-4 space-y-4">
|
||
<div class="bg-warning-50 border border-warning-600/20 rounded-lg p-3 text-xs">
|
||
<p class="font-semibold" x-text="`${transferDrawer.employee ? transferDrawer.employee.name : '-'} 的业务信息统计`"></p>
|
||
<p class="mt-1">房源数量:<span class="tabular-nums" x-text="transferDrawer.stats.house"></span></p>
|
||
<p class="mt-1 text-danger-600">注:若不转给任何账号,则业务信息跟随到新部门 <button class="action-link ml-1" @click="notify('进入转移业务归属(原型)')">转移业务归属</button></p>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-xs text-muted mb-1"><span class="text-danger-600">*</span> 调动日期</label>
|
||
<input data-test="input-transfer-date" x-model="transferDrawer.form.transferDate" type="date" class="w-full px-3 py-2 rounded-md border border-surface bg-white" />
|
||
<p class="text-[11px] text-muted mt-1">若日期为今日之前的日期,若当天有已提交日报,当天之后的日报将进行调动。</p>
|
||
<p x-show="transferDrawer.errors.transferDate" class="text-xs text-danger-600 mt-1" x-text="transferDrawer.errors.transferDate"></p>
|
||
</div>
|
||
|
||
<div class="overflow-hidden border border-surface rounded-lg">
|
||
<table class="min-w-full text-xs">
|
||
<thead class="bg-subtle border-b border-surface">
|
||
<tr>
|
||
<th class="px-3 py-2 text-left w-28">字段</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-surface">
|
||
<tr>
|
||
<td class="px-3 py-2"><span class="text-danger-600">*</span> 部门</td>
|
||
<td class="px-3 py-2" x-text="departmentNameById(transferDrawer.employee ? transferDrawer.employee.orgUnitId : null)"></td>
|
||
<td class="px-3 py-2">
|
||
<select data-test="select-transfer-dept" x-model.number="transferDrawer.form.orgUnitId" class="w-full px-2 py-1.5 rounded border border-surface bg-white text-xs">
|
||
<option value="">请选择</option>
|
||
<template x-for="d in storeAndGroupDepartments" :key="'transfer-dept-'+d.id">
|
||
<option :value="d.id" x-text="`${d.name}(${levelLabel(d.level)})`"></option>
|
||
</template>
|
||
</select>
|
||
<p x-show="transferDrawer.errors.orgUnitId" class="text-xs text-danger-600 mt-1" x-text="transferDrawer.errors.orgUnitId"></p>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td class="px-3 py-2">部门级别</td>
|
||
<td class="px-3 py-2" x-text="levelLabel(departmentLevelById(transferDrawer.employee ? transferDrawer.employee.orgUnitId : null))"></td>
|
||
<td class="px-3 py-2 text-muted" x-text="levelLabel(departmentLevelById(transferDrawer.form.orgUnitId)) || '-' "></td>
|
||
</tr>
|
||
<tr>
|
||
<td class="px-3 py-2"><span class="text-danger-600">*</span> 职务</td>
|
||
<td class="px-3 py-2" x-text="transferDrawer.employee ? transferDrawer.employee.jobTitle : '-' "></td>
|
||
<td class="px-3 py-2">
|
||
<select data-test="select-transfer-job" x-model="transferDrawer.form.jobTitle" class="w-full px-2 py-1.5 rounded border border-surface bg-white text-xs" @change="syncTransferJobMeta()">
|
||
<option value="">请选择</option>
|
||
<template x-for="j in jobOptions" :key="'transfer-job-'+j.title"><option :value="j.title" x-text="j.title"></option></template>
|
||
</select>
|
||
<p x-show="transferDrawer.errors.jobTitle" class="text-xs text-danger-600 mt-1" x-text="transferDrawer.errors.jobTitle"></p>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td class="px-3 py-2">职务类别</td>
|
||
<td class="px-3 py-2" x-text="transferDrawer.employee ? transferDrawer.employee.jobCategory : '-' "></td>
|
||
<td class="px-3 py-2 text-muted" x-text="transferDrawer.form.jobCategory || '-' "></td>
|
||
</tr>
|
||
<tr>
|
||
<td class="px-3 py-2"><span class="text-danger-600">*</span> 角色</td>
|
||
<td class="px-3 py-2" x-text="transferDrawer.employee ? transferDrawer.employee.role : '-' "></td>
|
||
<td class="px-3 py-2">
|
||
<input data-test="input-transfer-role" x-model.trim="transferDrawer.form.role" type="text" class="w-full px-2 py-1.5 rounded border border-surface text-xs" placeholder="请输入角色" />
|
||
<p x-show="transferDrawer.errors.role" class="text-xs text-danger-600 mt-1" x-text="transferDrawer.errors.role"></p>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td class="px-3 py-2"><span class="text-danger-600">*</span> 直属上级</td>
|
||
<td class="px-3 py-2" x-text="staffNameById(transferDrawer.employee ? transferDrawer.employee.supervisorId : null) || '-' "></td>
|
||
<td class="px-3 py-2">
|
||
<select x-model.number="transferDrawer.form.supervisorId" :disabled="transferDrawer.form.noSupervisor" class="w-full px-2 py-1.5 rounded border border-surface bg-white text-xs disabled:bg-neutral-100 disabled:text-muted">
|
||
<option value="">请选择</option>
|
||
<template x-for="e in activeEmployees" :key="'transfer-super-'+e.id"><option :value="e.id" x-text="`${departmentNameById(e.orgUnitId)}-${e.name}`"></option></template>
|
||
</select>
|
||
<label class="inline-flex items-center gap-1 mt-1">
|
||
<input type="checkbox" class="rounded border-surface" x-model="transferDrawer.form.noSupervisor" />
|
||
<span class="text-[11px] text-muted">无直属上级</span>
|
||
</label>
|
||
<p x-show="transferDrawer.errors.supervisorId" class="text-xs text-danger-600 mt-1" x-text="transferDrawer.errors.supervisorId"></p>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-xs text-muted mb-1">备注(此次调动为:<span class="text-danger-600" x-text="transferCategoryText"></span>)</label>
|
||
<textarea x-model.trim="transferDrawer.form.remark" rows="3" maxlength="30" placeholder="备注内容不超过30个字符" class="w-full px-3 py-2 rounded-md border border-surface"></textarea>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="px-5 py-3 border-t border-surface bg-neutral-50 flex items-center justify-end gap-2">
|
||
<button class="px-4 py-1.5 rounded-md border border-surface hover:bg-white" @click="closeTransferDrawer()">取消</button>
|
||
<button data-test="btn-transfer-submit" class="px-4 py-1.5 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="submitTransfer()">提交</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 员工详情 Drawer -->
|
||
<div x-cloak x-show="employeeDetail.open" class="fixed inset-0 z-40">
|
||
<div class="absolute inset-0 bg-neutral-900/40" @click="closeEmployeeDetail()"></div>
|
||
<div class="absolute right-0 top-0 h-full w-[920px] bg-white shadow-xl flex flex-col border-l border-surface">
|
||
<div class="flex items-center justify-between px-5 py-4 border-b border-surface">
|
||
<h3 class="text-base font-semibold">员工详情</h3>
|
||
<button class="p-1 text-muted hover:bg-neutral-100 rounded" @click="closeEmployeeDetail()">✕</button>
|
||
</div>
|
||
|
||
<div class="flex-1 overflow-y-auto p-5">
|
||
<div class="grid grid-cols-[240px_1fr] gap-4">
|
||
<aside class="space-y-3">
|
||
<div class="border border-surface rounded-lg p-3 text-center">
|
||
<div class="w-16 h-16 mx-auto rounded-full bg-neutral-100 text-neutral-600 flex items-center justify-center text-lg font-semibold" x-text="detailEmployee ? detailEmployee.name.slice(0,1) : '-' "></div>
|
||
<p class="font-semibold mt-2" x-text="detailEmployee ? detailEmployee.name : '-' "></p>
|
||
<p class="text-xs text-muted mt-1" x-text="detailEmployee ? departmentNameById(detailEmployee.orgUnitId) : '-' "></p>
|
||
<div class="grid grid-cols-2 gap-2 mt-3 text-xs">
|
||
<div class="bg-subtle rounded px-2 py-1"><span class="text-muted">职务</span><p x-text="detailEmployee ? detailEmployee.jobTitle : '-' "></p></div>
|
||
<div class="bg-subtle rounded px-2 py-1"><span class="text-muted">工号</span><p x-text="detailEmployee ? detailEmployee.employeeNo : '-' "></p></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="border border-surface rounded-lg p-2 space-y-1 text-sm">
|
||
<button class="w-full text-left px-2 py-1.5 rounded" :class="employeeDetail.tab==='basic' ? 'bg-primary-50 text-primary-700' : 'hover:bg-neutral-50'" @click="employeeDetail.tab='basic'">员工基本信息</button>
|
||
<button class="w-full text-left px-2 py-1.5 rounded" :class="employeeDetail.tab==='transfer' ? 'bg-primary-50 text-primary-700' : 'hover:bg-neutral-50'" @click="employeeDetail.tab='transfer'">异动记录</button>
|
||
<button class="w-full text-left px-2 py-1.5 rounded" :class="employeeDetail.tab==='account' ? 'bg-primary-50 text-primary-700' : 'hover:bg-neutral-50'" @click="employeeDetail.tab='account'">账号信息</button>
|
||
</div>
|
||
</aside>
|
||
|
||
<section class="border border-surface rounded-lg p-4">
|
||
<div class="flex items-center justify-between mb-4">
|
||
<h4 class="text-base font-semibold" x-text="employeeDetail.tab==='basic' ? '员工基本信息' : (employeeDetail.tab==='transfer' ? '异动记录' : '账号信息')"></h4>
|
||
<button x-show="employeeDetail.tab==='basic'" class="px-3 py-1.5 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="notify('进入员工信息编辑(原型)')">编辑</button>
|
||
</div>
|
||
|
||
<template x-if="employeeDetail.tab==='basic'">
|
||
<div class="space-y-4 text-sm">
|
||
<div>
|
||
<h5 class="font-semibold mb-2">任职信息</h5>
|
||
<div class="grid grid-cols-2 gap-x-8 gap-y-2">
|
||
<div class="flex"><span class="w-24 text-muted">昵称</span><span x-text="detailEmployee ? detailEmployee.nickname || '-' : '-' "></span></div>
|
||
<div class="flex"><span class="w-24 text-muted">工号</span><span x-text="detailEmployee ? detailEmployee.employeeNo : '-' "></span></div>
|
||
<div class="flex"><span class="w-24 text-muted">入职日期</span><span x-text="detailEmployee ? detailEmployee.joinedAt : '-' "></span></div>
|
||
<div class="flex"><span class="w-24 text-muted">状态</span><span x-text="detailEmployee ? employeeStatusLabel(detailEmployee.status) : '-' "></span></div>
|
||
<div class="flex"><span class="w-24 text-muted">职务</span><span x-text="detailEmployee ? detailEmployee.jobTitle : '-' "></span></div>
|
||
<div class="flex"><span class="w-24 text-muted">职务类别</span><span x-text="detailEmployee ? detailEmployee.jobCategory : '-' "></span></div>
|
||
<div class="flex"><span class="w-24 text-muted">部门</span><span x-text="detailEmployee ? departmentNameById(detailEmployee.orgUnitId) : '-' "></span></div>
|
||
<div class="flex"><span class="w-24 text-muted">直属上级</span><span x-text="detailEmployee ? (staffNameById(detailEmployee.supervisorId) || '-') : '-' "></span></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<h5 class="font-semibold mb-2">联系方式</h5>
|
||
<div class="grid grid-cols-2 gap-x-8 gap-y-2">
|
||
<div class="flex"><span class="w-24 text-muted">手机号</span><span x-text="detailEmployee ? maskPhone(detailEmployee.phone) : '-' "></span></div>
|
||
<div class="flex"><span class="w-24 text-muted">通讯录号码</span><span>不隐藏</span></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<h5 class="font-semibold mb-2">个人信息</h5>
|
||
<div class="grid grid-cols-2 gap-x-8 gap-y-2">
|
||
<div class="flex"><span class="w-24 text-muted">真实姓名</span><span x-text="detailEmployee ? detailEmployee.name : '-' "></span></div>
|
||
<div class="flex"><span class="w-24 text-muted">性别</span><span x-text="detailEmployee ? detailEmployee.gender || '-' : '-' "></span></div>
|
||
<div class="flex"><span class="w-24 text-muted">证件类型</span><span>身份证</span></div>
|
||
<div class="flex"><span class="w-24 text-muted">证件号码</span><span x-text="detailEmployee && detailEmployee.idRisk ? '410*********3037(未匹配)' : '410*********3037(已认证)'"></span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<template x-if="employeeDetail.tab==='transfer'">
|
||
<div class="overflow-x-auto">
|
||
<table class="min-w-full text-xs">
|
||
<thead class="bg-subtle border border-surface">
|
||
<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>
|
||
<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-surface">
|
||
<template x-for="log in detailEmployeeLogs" :key="'detail-log-'+log.id">
|
||
<tr>
|
||
<td class="px-3 py-2" x-text="log.transferDate"></td>
|
||
<td class="px-3 py-2" x-text="log.typeLabel"></td>
|
||
<td class="px-3 py-2" x-text="log.oldValue"></td>
|
||
<td class="px-3 py-2" x-text="log.newValue"></td>
|
||
<td class="px-3 py-2" x-text="log.remark || '-' "></td>
|
||
<td class="px-3 py-2" x-text="log.operator"></td>
|
||
</tr>
|
||
</template>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</template>
|
||
|
||
<template x-if="employeeDetail.tab==='account'">
|
||
<div class="space-y-4 text-sm">
|
||
<div class="border border-surface rounded p-3">
|
||
<h5 class="font-semibold mb-2">员工登录系统账号信息</h5>
|
||
<div class="grid grid-cols-2 gap-x-8 gap-y-2">
|
||
<div class="flex"><span class="w-24 text-muted">账号</span><span x-text="detailEmployee ? detailEmployee.phone : '-' "></span></div>
|
||
<div class="flex"><span class="w-24 text-muted">状态</span><span x-text="detailEmployee ? (detailEmployee.status==='frozen' ? '冻结' : '启用') : '-' "></span></div>
|
||
<div class="flex"><span class="w-24 text-muted">原登录账号</span><span x-text="detailEmployee ? `${departmentNameById(detailEmployee.orgUnitId)} ${detailEmployee.name}` : '-' "></span></div>
|
||
<div class="flex"><span class="w-24 text-muted">公众号绑定</span><span>未绑定</span></div>
|
||
</div>
|
||
</div>
|
||
<div class="border border-surface rounded p-3">
|
||
<h5 class="font-semibold mb-2">中国网络经纪人账号</h5>
|
||
<table class="min-w-full text-xs">
|
||
<thead class="bg-subtle border border-surface"><tr><th class="px-2 py-1 text-left">账号</th><th class="px-2 py-1 text-left">手机号</th><th class="px-2 py-1 text-left">实名信息是否一致</th></tr></thead>
|
||
<tbody><tr><td class="px-2 py-2">-</td><td class="px-2 py-2">-</td><td class="px-2 py-2">-</td></tr></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</section>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 异动记录汇总 Modal -->
|
||
<div x-cloak x-show="transferLogsModal.open" class="fixed inset-0 z-40">
|
||
<div class="absolute inset-0 bg-neutral-900/40" @click="closeTransferLogsModal()"></div>
|
||
<div class="absolute inset-0 flex items-center justify-center p-4 pointer-events-none">
|
||
<div class="w-full max-w-6xl bg-white rounded-xl shadow-xl pointer-events-auto flex flex-col max-h-[92vh]">
|
||
<div class="flex items-center justify-between px-5 py-4 border-b border-surface">
|
||
<h3 class="text-base font-semibold">异动记录</h3>
|
||
<button class="p-1 text-muted hover:bg-neutral-100 rounded" @click="closeTransferLogsModal()">✕</button>
|
||
</div>
|
||
|
||
<div class="px-5 py-4 space-y-3 overflow-y-auto">
|
||
<div class="grid grid-cols-6 gap-3">
|
||
<select x-model="transferLogsModal.filters.type" class="px-3 py-2 rounded-md border border-surface bg-white">
|
||
<option value="">请选择类型</option>
|
||
<option value="入职">入职</option>
|
||
<option value="员工调动">员工调动</option>
|
||
<option value="离职">离职</option>
|
||
</select>
|
||
<input x-model="transferLogsModal.filters.from" type="date" class="px-3 py-2 rounded-md border border-surface" />
|
||
<input x-model="transferLogsModal.filters.to" type="date" class="px-3 py-2 rounded-md border border-surface" />
|
||
<select x-model.number="transferLogsModal.filters.deptId" class="px-3 py-2 rounded-md border border-surface bg-white">
|
||
<option value="">请选择部门</option>
|
||
<template x-for="d in activeDepartmentOptions" :key="'log-dept-'+d.id"><option :value="d.id" x-text="d.name"></option></template>
|
||
</select>
|
||
<input x-model.trim="transferLogsModal.filters.keyword" type="text" placeholder="姓名/员工编号" class="px-3 py-2 rounded-md border border-surface" />
|
||
<div class="flex items-center gap-2">
|
||
<button class="px-3 py-2 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="noop()">查询</button>
|
||
<button class="px-3 py-2 rounded-md border border-surface hover:bg-neutral-50" @click="resetTransferLogFilters()">清空条件</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-surface hover:bg-neutral-50" @click="notify('新增异动记录(原型)')">新增异动记录</button>
|
||
<button class="px-3 py-1.5 rounded-md border border-surface hover:bg-neutral-50" @click="notify('报表导出中,请稍候(原型)')">报表导出</button>
|
||
</div>
|
||
<div class="text-xs text-muted" x-text="`共 ${filteredTransferLogs.length} 条`"></div>
|
||
</div>
|
||
|
||
<div class="overflow-auto border border-surface rounded-lg">
|
||
<table class="min-w-full text-xs">
|
||
<thead class="bg-subtle border-b border-surface">
|
||
<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>
|
||
<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-surface">
|
||
<template x-for="log in filteredTransferLogs" :key="'log-row-'+log.id">
|
||
<tr>
|
||
<td class="px-3 py-2" x-text="log.department"></td>
|
||
<td class="px-3 py-2" x-text="log.staffName"></td>
|
||
<td class="px-3 py-2" x-text="log.employeeNo"></td>
|
||
<td class="px-3 py-2" x-text="log.staffStatus"></td>
|
||
<td class="px-3 py-2" x-text="log.jobTitle"></td>
|
||
<td class="px-3 py-2" x-text="log.typeLabel"></td>
|
||
<td class="px-3 py-2" x-text="log.oldValue"></td>
|
||
<td class="px-3 py-2" x-text="log.newValue"></td>
|
||
<td class="px-3 py-2" x-text="log.remark || '-' "></td>
|
||
<td class="px-3 py-2" x-text="log.operator"></td>
|
||
<td class="px-3 py-2" x-text="log.transferDate"></td>
|
||
</tr>
|
||
</template>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Toast -->
|
||
<div x-cloak x-show="toast.show" class="fixed right-6 top-20 z-50" 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 orgPage() {
|
||
return {
|
||
mainTab: 'org',
|
||
rootDeptId: 1,
|
||
deptTreeSearch: '',
|
||
showClosedDepartments: false,
|
||
|
||
departments: [
|
||
{ id: 1, name: '上海沪房房地产经纪事务所', parentId: null, level: 'company', attribute: '直营', isActive: true, managerId: null, phone: '' },
|
||
{ id: 2, name: '沪居地产', parentId: 1, level: 'division', attribute: '直营', isActive: true, managerId: null, phone: '' },
|
||
{ id: 3, name: '系统管理组', parentId: 1, level: 'functional', attribute: '直营', isActive: true, managerId: null, phone: '' },
|
||
{ id: 4, name: '上海大区', parentId: 2, level: 'region', attribute: '直营', isActive: true, managerId: null, phone: '' },
|
||
{ id: 5, name: '静安门店', parentId: 4, level: 'store', attribute: '直营', isActive: true, managerId: null, phone: '021-66778899' },
|
||
{ id: 6, name: '上海豪园店二组', parentId: 5, level: 'group', attribute: '直营', isActive: true, managerId: null, phone: '' },
|
||
{ id: 7, name: '浦东门店', parentId: 4, level: 'store', attribute: '直营', isActive: true, managerId: null, phone: '' },
|
||
{ id: 8, name: '历史停用门店', parentId: 4, level: 'store', attribute: '直营', isActive: false, managerId: null, phone: '' }
|
||
],
|
||
|
||
deptExpanded: {},
|
||
selectedDeptId: 2,
|
||
|
||
jobOptions: [
|
||
{ title: '高级业务员', category: '置业顾问', role: '高级业务员' },
|
||
{ title: '分行经理', category: '店管', role: '分行经理' },
|
||
{ title: '行政人员', category: '其他职能', role: '行政人员' },
|
||
{ title: '系统管理员', category: '其他职能', role: '管理员' }
|
||
],
|
||
|
||
levelOptions: [
|
||
{ value: 'division', label: '事业部' },
|
||
{ value: 'region', label: '大区' },
|
||
{ value: 'area', label: '区域' },
|
||
{ value: 'district', label: '片区' },
|
||
{ value: 'store', label: '门店' },
|
||
{ value: 'group', label: '店组' },
|
||
{ value: 'functional', label: '职能' }
|
||
],
|
||
|
||
employees: [
|
||
{ id: 101, name: '刘源', nickname: '源源', employeeNo: '41', orgUnitId: 6, supervisorId: 102, jobTitle: '高级业务员', jobCategory: '置业顾问', role: '高级业务员', status: 'active', phone: '15901850696', joinedAt: '2026-03-09', approval: '通过', idRisk: false, gender: '男', birthday: '1998-10-02', extension: '801', email: 'liuyuan@fonrey.com', isSystemAdmin: false },
|
||
{ id: 102, name: '刘文龙', nickname: '', employeeNo: '17', orgUnitId: 6, supervisorId: null, jobTitle: '分行经理', jobCategory: '店管', role: '分行经理', status: 'active', phone: '13800001234', joinedAt: '2025-07-18', approval: '通过', idRisk: false, gender: '男', birthday: '1989-07-18', extension: '802', email: 'liuwenlong@fonrey.com', isSystemAdmin: false },
|
||
{ id: 103, name: '金怡', nickname: '', employeeNo: '18', orgUnitId: 5, supervisorId: null, jobTitle: '行政人员', jobCategory: '其他职能', role: '行政人员', status: 'active', phone: '13712345678', joinedAt: '2024-05-11', approval: '通过', idRisk: true, gender: '女', birthday: '1993-10-09', extension: '803', email: 'jinyi@fonrey.com', isSystemAdmin: false },
|
||
{ id: 104, name: '孙海鹏', nickname: '', employeeNo: '56', orgUnitId: 7, supervisorId: null, jobTitle: '高级业务员', jobCategory: '置业顾问', role: '高级业务员', status: 'probation', phone: '13611112222', joinedAt: '2026-04-03', approval: '入职审', idRisk: false, gender: '男', birthday: '2000-02-17', extension: '', email: '', isSystemAdmin: false },
|
||
{ id: 105, name: '周伟', nickname: '', employeeNo: '63', orgUnitId: 6, supervisorId: 102, jobTitle: '高级业务员', jobCategory: '置业顾问', role: '高级业务员', status: 'active', phone: '13566667777', joinedAt: '2026-01-14', approval: '通过', idRisk: false, gender: '男', birthday: '1997-04-24', extension: '805', email: 'zhouwei@fonrey.com', isSystemAdmin: false },
|
||
{ id: 106, name: '杜利强', nickname: '', employeeNo: '1', orgUnitId: 3, supervisorId: null, jobTitle: '系统管理员', jobCategory: '其他职能', role: '管理员', status: 'active', phone: '13988889999', joinedAt: '2023-01-01', approval: '通过', idRisk: false, gender: '男', birthday: '1988-01-01', extension: '', email: 'admin@fonrey.com', isSystemAdmin: true }
|
||
],
|
||
|
||
employeeFilters: {
|
||
keyword: '',
|
||
jobTitle: '',
|
||
status: '',
|
||
approval: '',
|
||
includeChildren: true,
|
||
level: '',
|
||
systemAdmin: '',
|
||
joinedFrom: '',
|
||
joinedTo: ''
|
||
},
|
||
|
||
selectedEmployeeIds: [],
|
||
employeePage: 1,
|
||
employeeJumpPage: 1,
|
||
|
||
contactFilters: {
|
||
deptId: '',
|
||
jobTitle: '',
|
||
birthday: '',
|
||
keyword: ''
|
||
},
|
||
contactPage: 1,
|
||
visibleContactPhones: [],
|
||
|
||
chartFilters: {
|
||
deptId: '',
|
||
showClosed: false
|
||
},
|
||
chartExpanded: {},
|
||
|
||
deptModal: {
|
||
open: false,
|
||
mode: 'add',
|
||
form: { id: null, name: '', parentId: '', level: '', attribute: '直营', managerId: '', phone: '', address: '', isActive: true },
|
||
errors: {}
|
||
},
|
||
|
||
onboardDrawer: {
|
||
open: false,
|
||
form: { name: '', nickname: '', phone: '', orgUnitId: '', jobTitle: '', jobCategory: '', role: '', supervisorId: '', joinedAt: '', status: 'probation' },
|
||
errors: {},
|
||
createdAccount: null
|
||
},
|
||
|
||
leaveModal: {
|
||
open: false,
|
||
employee: null,
|
||
stats: { house: 0, client: 0, market: 0 },
|
||
form: { resignedAt: '', type: '', remark: '' },
|
||
errors: {}
|
||
},
|
||
|
||
transferDrawer: {
|
||
open: false,
|
||
employee: null,
|
||
stats: { house: 0 },
|
||
form: { transferDate: '', orgUnitId: '', jobTitle: '', jobCategory: '', role: '', supervisorId: '', noSupervisor: false, remark: '' },
|
||
errors: {}
|
||
},
|
||
|
||
employeeDetail: {
|
||
open: false,
|
||
employeeId: null,
|
||
tab: 'basic'
|
||
},
|
||
|
||
transferLogsModal: {
|
||
open: false,
|
||
filters: { type: '', from: '', to: '', deptId: '', keyword: '' }
|
||
},
|
||
|
||
transferLogs: [
|
||
{ id: 1, staffId: 104, staffName: '孙海鹏', employeeNo: '56', department: '浦东门店', staffStatus: '试用', jobTitle: '高级业务员', type: 'onboard', typeLabel: '入职', oldValue: '-', newValue: '浦东门店', remark: '', operator: '金怡-静安门店', transferDate: '2026-04-03' },
|
||
{ id: 2, staffId: 105, staffName: '周伟', employeeNo: '63', department: '上海豪园店二组', staffStatus: '正式', jobTitle: '高级业务员', type: 'transfer', typeLabel: '员工调动', oldValue: '静安门店', newValue: '上海豪园店二组', remark: '平调', operator: '杜利强-系统管理组', transferDate: '2026-02-12' },
|
||
{ id: 3, staffId: 103, staffName: '金怡', employeeNo: '18', department: '静安门店', staffStatus: '正式', jobTitle: '行政人员', type: 'onboard', typeLabel: '入职', oldValue: '-', newValue: '静安门店', remark: '', operator: '杜利强-系统管理组', transferDate: '2024-05-11' }
|
||
],
|
||
|
||
toast: { show: false, message: '', type: 'success' },
|
||
|
||
init() {
|
||
this.departments.forEach((d) => {
|
||
if (d.parentId === null || d.parentId === 1 || d.parentId === 2 || d.parentId === 4) {
|
||
this.deptExpanded[d.id] = true;
|
||
}
|
||
this.chartExpanded[d.id] = true;
|
||
});
|
||
this.employeeJumpPage = this.employeePage;
|
||
},
|
||
|
||
noop() {},
|
||
|
||
switchMainTab(tab) {
|
||
this.mainTab = tab;
|
||
},
|
||
|
||
levelLabel(level) {
|
||
const map = {
|
||
company: '公司',
|
||
division: '事业部',
|
||
region: '大区',
|
||
area: '区域',
|
||
district: '片区',
|
||
store: '门店',
|
||
group: '店组',
|
||
functional: '职能'
|
||
};
|
||
return map[level] || '-';
|
||
},
|
||
|
||
employeeStatusLabel(status) {
|
||
const map = { active: '正式', probation: '试用', frozen: '冻结', resigned: '离职' };
|
||
return map[status] || status;
|
||
},
|
||
|
||
maskPhone(phone) {
|
||
if (!phone) return '-';
|
||
return String(phone).replace(/^(\d{3})\d{4}(\d{2,4})$/, '$1******$2');
|
||
},
|
||
|
||
birthdayMonthDay(dateStr) {
|
||
if (!dateStr || !dateStr.includes('-')) return '-';
|
||
const parts = dateStr.split('-');
|
||
return `${parts[1]}-${parts[2]}`;
|
||
},
|
||
|
||
departmentById(id) {
|
||
return this.departments.find(d => d.id === Number(id));
|
||
},
|
||
|
||
departmentNameById(id) {
|
||
const d = this.departmentById(id);
|
||
return d ? d.name : '';
|
||
},
|
||
|
||
departmentLevelById(id) {
|
||
const d = this.departmentById(id);
|
||
return d ? d.level : '';
|
||
},
|
||
|
||
staffById(id) {
|
||
return this.employees.find(e => e.id === Number(id));
|
||
},
|
||
|
||
staffNameById(id) {
|
||
const s = this.staffById(id);
|
||
return s ? s.name : '';
|
||
},
|
||
|
||
get activeEmployees() {
|
||
return this.employees.filter(e => e.status !== 'resigned');
|
||
},
|
||
|
||
get activeDepartmentOptions() {
|
||
return this.departments.filter(d => d.id !== this.rootDeptId && (d.isActive || this.showClosedDepartments));
|
||
},
|
||
|
||
get storeAndGroupDepartments() {
|
||
return this.departments.filter(d => (d.level === 'store' || d.level === 'group') && d.isActive);
|
||
},
|
||
|
||
parentDepartmentOptions(currentId = null) {
|
||
return this.departments.filter(d => d.id !== Number(currentId) && (d.isActive || this.showClosedDepartments));
|
||
},
|
||
|
||
get selectedDepartment() {
|
||
return this.departmentById(this.selectedDeptId);
|
||
},
|
||
|
||
employeeCountByDept(id) {
|
||
return this.employees.filter(e => e.orgUnitId === id && e.status !== 'resigned').length;
|
||
},
|
||
|
||
getChildren(parentId) {
|
||
return this.departments.filter(d => d.parentId === parentId && (this.showClosedDepartments || d.isActive));
|
||
},
|
||
|
||
get deptTreeRows() {
|
||
const rows = [];
|
||
const search = this.deptTreeSearch.toLowerCase();
|
||
|
||
const travel = (id, depth) => {
|
||
const node = this.departmentById(id);
|
||
if (!node) return;
|
||
const children = this.getChildren(id);
|
||
const hasChildren = children.length > 0;
|
||
|
||
const hitSelf = !search || node.name.toLowerCase().includes(search);
|
||
const hitChild = children.some(c => c.name.toLowerCase().includes(search));
|
||
const visible = (!search || hitSelf || hitChild || depth === 0);
|
||
|
||
if (visible) {
|
||
rows.push({
|
||
id: node.id,
|
||
name: node.name,
|
||
depth,
|
||
hasChildren,
|
||
employeeCount: this.employeeCountByDept(node.id),
|
||
isActive: node.isActive
|
||
});
|
||
}
|
||
|
||
if (!hasChildren) return;
|
||
|
||
const expanded = !!this.deptExpanded[node.id] || !!search;
|
||
if (!expanded) return;
|
||
children.forEach(child => travel(child.id, depth + 1));
|
||
};
|
||
|
||
travel(this.rootDeptId, 0);
|
||
return rows;
|
||
},
|
||
|
||
toggleDeptExpand(id) {
|
||
this.deptExpanded[id] = !this.deptExpanded[id];
|
||
},
|
||
|
||
subtreeIds(rootId) {
|
||
const ids = [Number(rootId)];
|
||
const loop = (pid) => {
|
||
this.departments.filter(d => d.parentId === pid).forEach((child) => {
|
||
ids.push(child.id);
|
||
loop(child.id);
|
||
});
|
||
};
|
||
loop(Number(rootId));
|
||
return ids;
|
||
},
|
||
|
||
get employeeFilteredRows() {
|
||
const f = this.employeeFilters;
|
||
const keyword = f.keyword.toLowerCase();
|
||
|
||
const targetDeptIds = (() => {
|
||
const baseId = this.selectedDeptId;
|
||
if (!baseId) return [];
|
||
return f.includeChildren ? this.subtreeIds(baseId) : [baseId];
|
||
})();
|
||
|
||
return this.employees.filter((e) => {
|
||
if (!targetDeptIds.includes(e.orgUnitId)) return false;
|
||
if (keyword && !(`${e.name}${e.employeeNo}${e.phone}`.toLowerCase().includes(keyword))) return false;
|
||
if (f.jobTitle && e.jobTitle !== f.jobTitle) return false;
|
||
if (f.status && e.status !== f.status) return false;
|
||
if (f.approval && e.approval !== f.approval) return false;
|
||
if (f.level && this.departmentLevelById(e.orgUnitId) !== f.level) return false;
|
||
if (f.systemAdmin === 'yes' && !e.isSystemAdmin) return false;
|
||
if (f.systemAdmin === 'no' && e.isSystemAdmin) return false;
|
||
if (f.joinedFrom && e.joinedAt < f.joinedFrom) return false;
|
||
if (f.joinedTo && e.joinedAt > f.joinedTo) return false;
|
||
return true;
|
||
});
|
||
},
|
||
|
||
get employeeTotalPages() {
|
||
return Math.max(1, Math.ceil(this.employeeFilteredRows.length / 20));
|
||
},
|
||
|
||
get employeePagedRows() {
|
||
const start = (this.employeePage - 1) * 20;
|
||
return this.employeeFilteredRows.slice(start, start + 20);
|
||
},
|
||
|
||
jumpEmployeePage() {
|
||
const n = Number(this.employeeJumpPage) || 1;
|
||
this.employeePage = Math.min(this.employeeTotalPages, Math.max(1, n));
|
||
},
|
||
|
||
resetEmployeeFilters() {
|
||
this.employeeFilters = {
|
||
keyword: '',
|
||
jobTitle: '',
|
||
status: '',
|
||
approval: '',
|
||
includeChildren: true,
|
||
level: '',
|
||
systemAdmin: '',
|
||
joinedFrom: '',
|
||
joinedTo: ''
|
||
};
|
||
this.employeePage = 1;
|
||
},
|
||
|
||
isEmployeeSelected(id) {
|
||
return this.selectedEmployeeIds.includes(id);
|
||
},
|
||
|
||
toggleEmployeeSelect(id, checked) {
|
||
if (checked) {
|
||
if (!this.selectedEmployeeIds.includes(id)) this.selectedEmployeeIds.push(id);
|
||
} else {
|
||
this.selectedEmployeeIds = this.selectedEmployeeIds.filter(x => x !== id);
|
||
}
|
||
},
|
||
|
||
get employeeAllOnPageSelected() {
|
||
if (this.employeePagedRows.length === 0) return false;
|
||
return this.employeePagedRows.every(r => this.selectedEmployeeIds.includes(r.id));
|
||
},
|
||
|
||
toggleEmployeePageSelect(checked) {
|
||
const ids = this.employeePagedRows.map(r => r.id);
|
||
if (checked) {
|
||
this.selectedEmployeeIds = [...new Set([...this.selectedEmployeeIds, ...ids])];
|
||
} else {
|
||
this.selectedEmployeeIds = this.selectedEmployeeIds.filter(id => !ids.includes(id));
|
||
}
|
||
},
|
||
|
||
get selectedEmployeeCount() {
|
||
return this.selectedEmployeeIds.length;
|
||
},
|
||
|
||
openDeptModal(mode, deptId = null) {
|
||
this.deptModal.open = true;
|
||
this.deptModal.mode = mode;
|
||
this.deptModal.errors = {};
|
||
|
||
if (mode === 'edit' && deptId) {
|
||
const d = this.departmentById(deptId);
|
||
if (!d) return;
|
||
this.deptModal.form = {
|
||
id: d.id,
|
||
name: d.name,
|
||
parentId: d.parentId,
|
||
level: d.level,
|
||
attribute: d.attribute || '直营',
|
||
managerId: d.managerId || '',
|
||
phone: d.phone || '',
|
||
address: d.address || '',
|
||
isActive: d.isActive
|
||
};
|
||
} else {
|
||
this.deptModal.form = {
|
||
id: null,
|
||
name: '',
|
||
parentId: Number(deptId) || this.selectedDeptId || this.rootDeptId,
|
||
level: '',
|
||
attribute: '直营',
|
||
managerId: '',
|
||
phone: '',
|
||
address: '',
|
||
isActive: true
|
||
};
|
||
}
|
||
},
|
||
|
||
closeDeptModal() {
|
||
this.deptModal.open = false;
|
||
this.deptModal.errors = {};
|
||
},
|
||
|
||
saveDepartment() {
|
||
const f = this.deptModal.form;
|
||
this.deptModal.errors = {};
|
||
|
||
if (!f.name) this.deptModal.errors.name = '请输入部门名称';
|
||
if (!f.parentId && f.parentId !== 0) this.deptModal.errors.parentId = '请选择上级部门';
|
||
if (!f.level) this.deptModal.errors.level = '请选择部门级别';
|
||
|
||
const parent = this.departmentById(f.parentId);
|
||
if (f.level === 'group' && parent && parent.level !== 'store') {
|
||
this.deptModal.errors.rule = '店组级别部门必须挂在门店下';
|
||
}
|
||
|
||
if (Object.keys(this.deptModal.errors).length > 0) return;
|
||
|
||
if (this.deptModal.mode === 'add') {
|
||
const nextId = Math.max(...this.departments.map(d => d.id)) + 1;
|
||
this.departments.push({
|
||
id: nextId,
|
||
name: f.name,
|
||
parentId: Number(f.parentId),
|
||
level: f.level,
|
||
attribute: f.attribute,
|
||
isActive: f.isActive,
|
||
managerId: Number(f.managerId) || null,
|
||
phone: f.phone || '',
|
||
address: f.address || ''
|
||
});
|
||
this.deptExpanded[Number(f.parentId)] = true;
|
||
this.selectedDeptId = nextId;
|
||
this.notify('新增部门成功(原型)');
|
||
} else {
|
||
this.departments = this.departments.map((d) => d.id === f.id ? {
|
||
...d,
|
||
name: f.name,
|
||
parentId: Number(f.parentId),
|
||
level: f.level,
|
||
attribute: f.attribute,
|
||
isActive: f.isActive,
|
||
managerId: Number(f.managerId) || null,
|
||
phone: f.phone || '',
|
||
address: f.address || ''
|
||
} : d);
|
||
this.notify('编辑部门成功(原型)');
|
||
}
|
||
|
||
this.closeDeptModal();
|
||
},
|
||
|
||
deleteDepartment(deptId) {
|
||
const ids = this.subtreeIds(deptId);
|
||
const hasEmployee = this.employees.some(e => ids.includes(e.orgUnitId) && e.status !== 'resigned');
|
||
if (hasEmployee) {
|
||
this.notify('该部门存在员工,无法删除', 'error');
|
||
return;
|
||
}
|
||
|
||
this.departments = this.departments.filter(d => !ids.includes(d.id));
|
||
if (!this.departmentById(this.selectedDeptId)) this.selectedDeptId = this.rootDeptId;
|
||
this.notify('部门删除成功(原型)');
|
||
},
|
||
|
||
openOnboardDrawer() {
|
||
this.onboardDrawer.open = true;
|
||
this.onboardDrawer.errors = {};
|
||
this.onboardDrawer.createdAccount = null;
|
||
this.onboardDrawer.form = {
|
||
name: '',
|
||
nickname: '',
|
||
phone: '',
|
||
orgUnitId: '',
|
||
jobTitle: '',
|
||
jobCategory: '',
|
||
role: '',
|
||
supervisorId: '',
|
||
joinedAt: new Date().toISOString().slice(0, 10),
|
||
status: 'probation'
|
||
};
|
||
},
|
||
|
||
closeOnboardDrawer() {
|
||
this.onboardDrawer.open = false;
|
||
this.onboardDrawer.errors = {};
|
||
},
|
||
|
||
syncOnboardJobMeta() {
|
||
const found = this.jobOptions.find(j => j.title === this.onboardDrawer.form.jobTitle);
|
||
this.onboardDrawer.form.jobCategory = found ? found.category : '';
|
||
this.onboardDrawer.form.role = found ? found.role : '';
|
||
},
|
||
|
||
randomPassword() {
|
||
return Math.random().toString(36).slice(-8);
|
||
},
|
||
|
||
submitOnboard() {
|
||
const f = this.onboardDrawer.form;
|
||
this.onboardDrawer.errors = {};
|
||
|
||
if (!f.name) this.onboardDrawer.errors.name = '请输入姓名';
|
||
if (!f.phone) this.onboardDrawer.errors.phone = '请输入手机号';
|
||
if (f.phone && !/^1\d{10}$/.test(f.phone)) this.onboardDrawer.errors.phone = '手机号格式不正确';
|
||
if (!f.orgUnitId) this.onboardDrawer.errors.orgUnitId = '请选择所属门店/店组';
|
||
if (!f.jobTitle) this.onboardDrawer.errors.jobTitle = '请选择职务';
|
||
|
||
const selectedDept = this.departmentById(f.orgUnitId);
|
||
if (selectedDept && !(selectedDept.level === 'store' || selectedDept.level === 'group')) {
|
||
this.onboardDrawer.errors.rule = '经纪人/店管所属部门只能是门店/店组';
|
||
}
|
||
|
||
if ((f.jobCategory === '置业顾问' || f.jobCategory === '店管') && selectedDept && !(selectedDept.level === 'store' || selectedDept.level === 'group')) {
|
||
this.onboardDrawer.errors.rule = '当前职务类别仅可归属门店/店组';
|
||
}
|
||
|
||
if (Object.keys(this.onboardDrawer.errors).length > 0) return;
|
||
|
||
const nextId = Math.max(...this.employees.map(e => e.id)) + 1;
|
||
const nextNo = String(Math.max(...this.employees.map(e => Number(e.employeeNo))) + 1);
|
||
this.employees.unshift({
|
||
id: nextId,
|
||
name: f.name,
|
||
nickname: f.nickname,
|
||
employeeNo: nextNo,
|
||
orgUnitId: Number(f.orgUnitId),
|
||
supervisorId: Number(f.supervisorId) || null,
|
||
jobTitle: f.jobTitle,
|
||
jobCategory: f.jobCategory,
|
||
role: f.role,
|
||
status: f.status,
|
||
phone: f.phone,
|
||
joinedAt: f.joinedAt,
|
||
approval: '入职审',
|
||
idRisk: false,
|
||
gender: '-',
|
||
birthday: '',
|
||
extension: '',
|
||
email: '',
|
||
isSystemAdmin: false
|
||
});
|
||
|
||
const pwd = this.randomPassword();
|
||
this.onboardDrawer.createdAccount = { username: f.phone, password: pwd };
|
||
|
||
this.transferLogs.unshift({
|
||
id: Math.max(...this.transferLogs.map(l => l.id)) + 1,
|
||
staffId: nextId,
|
||
staffName: f.name,
|
||
employeeNo: nextNo,
|
||
department: this.departmentNameById(f.orgUnitId),
|
||
staffStatus: this.employeeStatusLabel(f.status),
|
||
jobTitle: f.jobTitle,
|
||
type: 'onboard',
|
||
typeLabel: '入职',
|
||
oldValue: '-',
|
||
newValue: this.departmentNameById(f.orgUnitId),
|
||
remark: '',
|
||
operator: '杜利强-系统管理组',
|
||
transferDate: f.joinedAt
|
||
});
|
||
|
||
this.notify('入职办理成功,账号已创建(原型)');
|
||
this.employeePage = 1;
|
||
},
|
||
|
||
openLeaveModal(employeeId) {
|
||
const emp = this.staffById(employeeId);
|
||
if (!emp) return;
|
||
this.leaveModal.open = true;
|
||
this.leaveModal.employee = emp;
|
||
this.leaveModal.errors = {};
|
||
this.leaveModal.form = { resignedAt: '', type: '', remark: '' };
|
||
this.leaveModal.stats = {
|
||
house: emp.id === 104 ? 564 : (emp.id === 105 ? 13 : 21),
|
||
client: emp.id === 104 ? 21 : 9,
|
||
market: emp.id === 104 ? 3 : 1
|
||
};
|
||
},
|
||
|
||
closeLeaveModal() {
|
||
this.leaveModal.open = false;
|
||
this.leaveModal.errors = {};
|
||
},
|
||
|
||
submitLeave() {
|
||
const f = this.leaveModal.form;
|
||
this.leaveModal.errors = {};
|
||
if (!f.resignedAt) this.leaveModal.errors.resignedAt = '请选择离职日期';
|
||
if (!f.type) this.leaveModal.errors.type = '请选择离职类型';
|
||
if (Object.keys(this.leaveModal.errors).length > 0) return;
|
||
|
||
const emp = this.leaveModal.employee;
|
||
this.employees = this.employees.map(e => e.id === emp.id ? { ...e, status: 'resigned', approval: '通过', resignedAt: f.resignedAt } : e);
|
||
|
||
this.transferLogs.unshift({
|
||
id: Math.max(...this.transferLogs.map(l => l.id)) + 1,
|
||
staffId: emp.id,
|
||
staffName: emp.name,
|
||
employeeNo: emp.employeeNo,
|
||
department: this.departmentNameById(emp.orgUnitId),
|
||
staffStatus: '离职',
|
||
jobTitle: emp.jobTitle,
|
||
type: 'resign',
|
||
typeLabel: '离职',
|
||
oldValue: this.departmentNameById(emp.orgUnitId),
|
||
newValue: '离职',
|
||
remark: `${f.type}${f.remark ? ':' + f.remark : ''}`,
|
||
operator: '杜利强-系统管理组',
|
||
transferDate: f.resignedAt
|
||
});
|
||
|
||
this.closeLeaveModal();
|
||
this.notify('离职办理成功(原型)');
|
||
},
|
||
|
||
openTransferDrawer(employeeId) {
|
||
const emp = this.staffById(employeeId);
|
||
if (!emp) return;
|
||
this.transferDrawer.open = true;
|
||
this.transferDrawer.employee = emp;
|
||
this.transferDrawer.errors = {};
|
||
this.transferDrawer.stats = { house: emp.id === 105 ? 13 : 9 };
|
||
this.transferDrawer.form = {
|
||
transferDate: new Date().toISOString().slice(0, 10),
|
||
orgUnitId: emp.orgUnitId,
|
||
jobTitle: emp.jobTitle,
|
||
jobCategory: emp.jobCategory,
|
||
role: emp.role,
|
||
supervisorId: emp.supervisorId || '',
|
||
noSupervisor: !emp.supervisorId,
|
||
remark: ''
|
||
};
|
||
},
|
||
|
||
openTransferDrawerByBatch() {
|
||
if (this.selectedEmployeeIds.length === 0) return;
|
||
this.openTransferDrawer(this.selectedEmployeeIds[0]);
|
||
},
|
||
|
||
closeTransferDrawer() {
|
||
this.transferDrawer.open = false;
|
||
this.transferDrawer.errors = {};
|
||
},
|
||
|
||
syncTransferJobMeta() {
|
||
const found = this.jobOptions.find(j => j.title === this.transferDrawer.form.jobTitle);
|
||
this.transferDrawer.form.jobCategory = found ? found.category : '';
|
||
if (found && !this.transferDrawer.form.role) this.transferDrawer.form.role = found.role;
|
||
},
|
||
|
||
get transferCategoryText() {
|
||
const emp = this.transferDrawer.employee;
|
||
if (!emp) return '-';
|
||
const before = this.departmentLevelById(emp.orgUnitId);
|
||
const after = this.departmentLevelById(this.transferDrawer.form.orgUnitId);
|
||
const order = ['division', 'region', 'area', 'district', 'store', 'group'];
|
||
const b = order.indexOf(before);
|
||
const a = order.indexOf(after);
|
||
if (a === -1 || b === -1 || a === b) return '平调';
|
||
return a > b ? '降职' : '晋升';
|
||
},
|
||
|
||
submitTransfer() {
|
||
const f = this.transferDrawer.form;
|
||
const emp = this.transferDrawer.employee;
|
||
this.transferDrawer.errors = {};
|
||
if (!f.transferDate) this.transferDrawer.errors.transferDate = '请选择调动日期';
|
||
if (!f.orgUnitId) this.transferDrawer.errors.orgUnitId = '请选择调动后部门';
|
||
if (!f.jobTitle) this.transferDrawer.errors.jobTitle = '请选择调动后职务';
|
||
if (!f.role) this.transferDrawer.errors.role = '请输入角色';
|
||
if (!f.noSupervisor && !f.supervisorId) this.transferDrawer.errors.supervisorId = '请选择直属上级或勾选无直属上级';
|
||
if (Object.keys(this.transferDrawer.errors).length > 0) return;
|
||
|
||
const oldDept = this.departmentNameById(emp.orgUnitId);
|
||
const newDept = this.departmentNameById(f.orgUnitId);
|
||
|
||
this.employees = this.employees.map((e) => e.id === emp.id ? {
|
||
...e,
|
||
orgUnitId: Number(f.orgUnitId),
|
||
jobTitle: f.jobTitle,
|
||
jobCategory: f.jobCategory,
|
||
role: f.role,
|
||
supervisorId: f.noSupervisor ? null : Number(f.supervisorId) || null
|
||
} : e);
|
||
|
||
this.transferLogs.unshift({
|
||
id: Math.max(...this.transferLogs.map(l => l.id)) + 1,
|
||
staffId: emp.id,
|
||
staffName: emp.name,
|
||
employeeNo: emp.employeeNo,
|
||
department: newDept,
|
||
staffStatus: this.employeeStatusLabel(emp.status),
|
||
jobTitle: f.jobTitle,
|
||
type: 'transfer',
|
||
typeLabel: '员工调动',
|
||
oldValue: oldDept,
|
||
newValue: newDept,
|
||
remark: f.remark || this.transferCategoryText,
|
||
operator: '杜利强-系统管理组',
|
||
transferDate: f.transferDate
|
||
});
|
||
|
||
this.closeTransferDrawer();
|
||
this.notify('员工调动成功(原型)');
|
||
},
|
||
|
||
openEmployeeDetail(employeeId) {
|
||
this.employeeDetail.open = true;
|
||
this.employeeDetail.employeeId = employeeId;
|
||
this.employeeDetail.tab = 'basic';
|
||
},
|
||
|
||
closeEmployeeDetail() {
|
||
this.employeeDetail.open = false;
|
||
},
|
||
|
||
get detailEmployee() {
|
||
return this.staffById(this.employeeDetail.employeeId);
|
||
},
|
||
|
||
get detailEmployeeLogs() {
|
||
const emp = this.detailEmployee;
|
||
if (!emp) return [];
|
||
return this.transferLogs.filter(l => l.staffId === emp.id);
|
||
},
|
||
|
||
openTransferLogsModal() {
|
||
this.transferLogsModal.open = true;
|
||
},
|
||
|
||
closeTransferLogsModal() {
|
||
this.transferLogsModal.open = false;
|
||
},
|
||
|
||
resetTransferLogFilters() {
|
||
this.transferLogsModal.filters = { type: '', from: '', to: '', deptId: '', keyword: '' };
|
||
},
|
||
|
||
get filteredTransferLogs() {
|
||
const f = this.transferLogsModal.filters;
|
||
const kw = (f.keyword || '').toLowerCase();
|
||
return this.transferLogs.filter((l) => {
|
||
if (f.type && l.typeLabel !== f.type) return false;
|
||
if (f.from && l.transferDate < f.from) return false;
|
||
if (f.to && l.transferDate > f.to) return false;
|
||
if (f.deptId && this.departmentNameById(f.deptId) !== l.department) return false;
|
||
if (kw && !(`${l.staffName}${l.employeeNo}`.toLowerCase().includes(kw))) return false;
|
||
return true;
|
||
});
|
||
},
|
||
|
||
get chartRootNode() {
|
||
const id = Number(this.chartFilters.deptId) || this.rootDeptId;
|
||
return this.departmentById(id) || this.departmentById(this.rootDeptId);
|
||
},
|
||
|
||
chartChildren(parentId) {
|
||
return this.departments.filter(d => d.parentId === parentId && (this.chartFilters.showClosed || d.isActive));
|
||
},
|
||
|
||
chartNodeEmployeeCount(id) {
|
||
return this.employees.filter(e => e.orgUnitId === id && e.status !== 'resigned').length;
|
||
},
|
||
|
||
isChartExpanded(id) {
|
||
return !!this.chartExpanded[id];
|
||
},
|
||
|
||
toggleChartExpand(id) {
|
||
this.chartExpanded[id] = !this.chartExpanded[id];
|
||
},
|
||
|
||
get contactFilteredRows() {
|
||
const f = this.contactFilters;
|
||
const kw = f.keyword.toLowerCase();
|
||
return this.employees.filter((e) => {
|
||
if (e.status === 'resigned') return false;
|
||
if (f.deptId && e.orgUnitId !== Number(f.deptId)) return false;
|
||
if (f.jobTitle && e.jobTitle !== f.jobTitle) return false;
|
||
if (f.birthday === 'thisMonth' && (!e.birthday || e.birthday.slice(5, 7) !== new Date().toISOString().slice(5, 7))) return false;
|
||
if (kw && !(`${e.name}${e.phone}${e.extension || ''}${e.email || ''}`.toLowerCase().includes(kw))) return false;
|
||
return true;
|
||
});
|
||
},
|
||
|
||
get contactTotalPages() {
|
||
return Math.max(1, Math.ceil(this.contactFilteredRows.length / 20));
|
||
},
|
||
|
||
get contactPagedRows() {
|
||
const start = (this.contactPage - 1) * 20;
|
||
return this.contactFilteredRows.slice(start, start + 20);
|
||
},
|
||
|
||
resetContactFilters() {
|
||
this.contactFilters = { deptId: '', jobTitle: '', birthday: '', keyword: '' };
|
||
this.contactPage = 1;
|
||
},
|
||
|
||
isPhoneVisible(id) {
|
||
return this.visibleContactPhones.includes(id);
|
||
},
|
||
|
||
togglePhoneVisible(id) {
|
||
if (this.visibleContactPhones.includes(id)) {
|
||
this.visibleContactPhones = this.visibleContactPhones.filter(x => x !== id);
|
||
} else {
|
||
this.visibleContactPhones.push(id);
|
||
}
|
||
},
|
||
|
||
closeAllPanels() {
|
||
this.closeDeptModal();
|
||
this.closeOnboardDrawer();
|
||
this.closeLeaveModal();
|
||
this.closeTransferDrawer();
|
||
this.closeEmployeeDetail();
|
||
this.closeTransferLogsModal();
|
||
},
|
||
|
||
notify(message, type = 'success') {
|
||
this.toast = { show: true, message, type };
|
||
setTimeout(() => {
|
||
this.toast.show = false;
|
||
}, 1800);
|
||
}
|
||
};
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|