Files
X-Financial/web/src/views/EmployeeManagementView.vue

572 lines
22 KiB
Vue
Raw Normal View History

<template>
<section class="employee-center">
<Transition name="employee-view" mode="out-in">
<article v-if="selectedEmployee" key="detail" class="employee-detail">
<div class="detail-scroll">
<section class="detail-hero panel">
<div class="hero-profile">
<div class="hero-avatar">{{ selectedEmployee.avatar }}</div>
<div class="hero-copy">
<div class="hero-tag">{{ selectedEmployee.employeeNo }}</div>
<h2>{{ selectedEmployee.name }}</h2>
<p>
{{ selectedEmployee.department }} / {{ selectedEmployee.position }} /
{{ selectedEmployee.grade }}
</p>
</div>
</div>
<div class="hero-stats">
<div class="hero-stat">
<span>账号状态</span>
<strong>{{ selectedEmployee.status }}</strong>
</div>
<div class="hero-stat">
<span>直属上级</span>
<strong>{{ selectedEmployee.manager }}</strong>
</div>
<div class="hero-stat">
<span>财务归口</span>
<strong>{{ selectedEmployee.financeOwner }}</strong>
</div>
<div class="hero-stat">
<span>角色数量</span>
<strong>{{ roleCount }}</strong>
</div>
</div>
</section>
<div class="detail-grid">
<section class="detail-main">
<article class="detail-card panel">
<div class="card-head">
<div>
<h3>基础信息</h3>
<p>维护员工编号联系方式入职日期与常用档案信息</p>
</div>
</div>
<div class="form-grid">
<label class="field">
<span>员工姓名</span>
<input v-model="employeeForm.name" />
</label>
<label class="field">
<span>员工编号</span>
<input v-model="employeeForm.employeeNo" readonly />
</label>
<label class="field">
<span>性别</span>
<input v-model="employeeForm.gender" />
</label>
<label class="field">
<span>年龄</span>
<input :value="detailAge" readonly />
</label>
<label class="field">
<span>出生日期</span>
<input v-model="employeeForm.birthDate" type="date" />
</label>
<label class="field">
<span>手机号</span>
<input v-model="employeeForm.phone" />
</label>
<label class="field">
<span>邮箱</span>
<input v-model="employeeForm.email" type="email" />
</label>
<label class="field">
<span>密码设置</span>
<input
v-model="employeeForm.password"
type="password"
autocomplete="new-password"
placeholder="留空则不修改"
/>
</label>
<label class="field">
<span>入职日期</span>
<input v-model="employeeForm.joinDate" type="date" />
</label>
<label class="field">
<span>办公地点</span>
<input v-model="employeeForm.location" />
</label>
</div>
</article>
<article class="detail-card panel">
<div class="card-head">
<div>
<h3>组织与岗位</h3>
<p>配置部门岗位职级和管理归属决定审批链路和数据访问边界</p>
</div>
</div>
<div class="form-grid">
<label class="field">
<span>所属部门</span>
<input v-model="employeeForm.department" readonly />
</label>
<label class="field">
<span>岗位</span>
<input v-model="employeeForm.position" />
</label>
<label class="field">
<span>职级</span>
<input v-model="employeeForm.grade" />
</label>
<label class="field">
<span>直属上级</span>
<input v-model="employeeForm.manager" readonly />
</label>
<label class="field">
<span>财务归口</span>
<input v-model="employeeForm.financeOwner" />
</label>
<label class="field">
<span>成本中心</span>
<input v-model="employeeForm.costCenter" />
</label>
</div>
</article>
<article class="detail-card panel">
<div class="card-head">
<div>
<h3>系统角色分配</h3>
<p>为员工分配管理员财务人员使用者高级管理人员等业务角色</p>
</div>
<span class="count-badge">{{ roleCount }} 个角色</span>
</div>
<div class="role-grid">
<label
v-for="role in roleOptions"
:key="role.id"
class="role-card"
:class="{ active: employeeForm.roleCodes.includes(role.code) }"
>
<input v-model="employeeForm.roleCodes" type="checkbox" :value="role.code" />
<div class="role-copy">
<strong>{{ role.label }}</strong>
<p>{{ role.desc }}</p>
</div>
</label>
</div>
</article>
</section>
<aside class="detail-side">
<article class="side-card panel">
<div class="card-head">
<div>
<h3>当前权限摘要</h3>
<p>角色组合带来的系统可见范围</p>
</div>
</div>
<div class="tag-list">
<span v-for="role in selectedRoleLabels" :key="role">{{ role }}</span>
</div>
<ul class="bullet-list">
<li v-for="item in selectedEmployee.permissions" :key="item">{{ item }}</li>
</ul>
</article>
<article class="side-card panel">
<div class="card-head">
<div>
<h3>最近变更</h3>
<p>查看角色与档案调整记录</p>
</div>
</div>
<div class="history-list">
<div v-for="item in selectedEmployee.history" :key="item.time" class="history-row">
<strong>{{ item.action }}</strong>
<span>{{ item.owner }}</span>
<small>{{ item.time }}</small>
</div>
</div>
</article>
<article class="side-card panel publish-card">
<div>
<h3>生效状态</h3>
<p>当前修改将在保存后同步到审批报销知识和权限模块</p>
</div>
<div class="publish-summary">
<span>上次同步{{ selectedEmployee.lastSync }}</span>
<strong>{{ selectedEmployee.syncState }}</strong>
</div>
</article>
</aside>
</div>
</div>
<footer class="detail-actions">
<button class="back-action" type="button" @click="closeEmployeeDetail">
<i class="mdi mdi-arrow-left"></i>
<span>返回员工列表</span>
</button>
<div class="detail-action-group">
<button class="minor-action" type="button" :disabled="disableActionDisabled" @click="disableEmployeeAccount">
<i class="mdi mdi-account-cancel-outline"></i>
<span>{{ selectedEmployee.status === '停用' ? '账号已停用' : actionState === 'disable' ? '停用中...' : '停用账号' }}</span>
</button>
<button class="major-action" type="button" :disabled="actionBusy" @click="saveEmployeeChanges">
<i class="mdi mdi-check-circle-outline"></i>
<span>{{ actionState === 'save' ? '保存中...' : '保存并生效' }}</span>
</button>
</div>
</footer>
</article>
<article v-else key="list" class="employee-list panel">
<nav class="status-tabs" aria-label="员工状态筛选">
<button
v-for="tab in tabs"
:key="tab.label"
type="button"
:class="{ active: activeTab === tab.label }"
@click="activeTab = tab.label"
>
<span>{{ tab.label }}</span>
<small>{{ tab.count }}</small>
</button>
</nav>
<div class="list-toolbar">
<div class="filter-set">
<div class="list-search">
<i class="mdi mdi-magnify"></i>
<input
v-model="searchKeyword"
type="search"
placeholder="搜索姓名、工号、部门、岗位"
/>
</div>
<div class="picker-filter" :class="{ open: activeFilterPopover === 'department' }">
<button
class="picker-trigger"
type="button"
:aria-expanded="activeFilterPopover === 'department'"
aria-haspopup="dialog"
@click="toggleFilterPopover('department')"
>
<span class="picker-label">{{ selectedDepartment || '组织部门' }}</span>
<i class="mdi mdi-chevron-down"></i>
</button>
<div
v-if="activeFilterPopover === 'department'"
class="picker-popover"
role="dialog"
aria-label="选择组织部门"
>
<header>
<strong>选择组织部门</strong>
<button type="button" aria-label="关闭组织部门选择" @click="closeFilterPopover">
<i class="mdi mdi-close"></i>
</button>
</header>
<div class="picker-option-list">
<button
type="button"
class="picker-option"
:class="{ active: !selectedDepartment }"
@click="selectFilter('department', '')"
>
全部部门
</button>
<button
v-for="department in departmentOptions"
:key="department"
type="button"
class="picker-option"
:class="{ active: selectedDepartment === department }"
@click="selectFilter('department', department)"
>
{{ department }}
</button>
</div>
</div>
</div>
<div class="picker-filter" :class="{ open: activeFilterPopover === 'grade' }">
<button
class="picker-trigger"
type="button"
:aria-expanded="activeFilterPopover === 'grade'"
aria-haspopup="dialog"
@click="toggleFilterPopover('grade')"
>
<span class="picker-label">{{ selectedGrade || '职级' }}</span>
<i class="mdi mdi-chevron-down"></i>
</button>
<div
v-if="activeFilterPopover === 'grade'"
class="picker-popover"
role="dialog"
aria-label="选择职级"
>
<header>
<strong>选择职级</strong>
<button type="button" aria-label="关闭职级选择" @click="closeFilterPopover">
<i class="mdi mdi-close"></i>
</button>
</header>
<div class="picker-option-list">
<button
type="button"
class="picker-option"
:class="{ active: !selectedGrade }"
@click="selectFilter('grade', '')"
>
全部职级
</button>
<button
v-for="grade in gradeOptions"
:key="grade"
type="button"
class="picker-option"
:class="{ active: selectedGrade === grade }"
@click="selectFilter('grade', grade)"
>
{{ grade }}
</button>
</div>
</div>
</div>
<div class="picker-filter" :class="{ open: activeFilterPopover === 'role' }">
<button
class="picker-trigger"
type="button"
:aria-expanded="activeFilterPopover === 'role'"
aria-haspopup="dialog"
@click="toggleFilterPopover('role')"
>
<span class="picker-label">{{ selectedRole || '系统角色' }}</span>
<i class="mdi mdi-chevron-down"></i>
</button>
<div
v-if="activeFilterPopover === 'role'"
class="picker-popover"
role="dialog"
aria-label="选择系统角色"
>
<header>
<strong>选择系统角色</strong>
<button type="button" aria-label="关闭系统角色选择" @click="closeFilterPopover">
<i class="mdi mdi-close"></i>
</button>
</header>
<div class="picker-option-list">
<button
type="button"
class="picker-option"
:class="{ active: !selectedRole }"
@click="selectFilter('role', '')"
>
全部角色
</button>
<button
v-for="role in roleFilterOptions"
:key="role"
type="button"
class="picker-option"
:class="{ active: selectedRole === role }"
@click="selectFilter('role', role)"
>
{{ role }}
</button>
</div>
</div>
</div>
</div>
<div class="toolbar-actions">
<button v-if="hasActiveFilters" class="ghost-filter-btn" type="button" @click="resetFilters">
<i class="mdi mdi-filter-remove-outline"></i>
<span>清空筛选</span>
</button>
<button class="create-btn" type="button">
<i class="mdi mdi-plus"></i>
<span>新增员工</span>
</button>
</div>
</div>
<p class="hint">
<i class="mdi mdi-information-outline"></i>
点击任意员工行可进入基础信息与角色权限编辑界面
</p>
<div v-if="activeFilterTokens.length" class="active-filter-strip">
<span v-for="token in activeFilterTokens" :key="token" class="active-filter-chip">
{{ token }}
</span>
</div>
<div class="table-wrap">
<div v-if="loading" class="table-state">
<i class="mdi mdi-loading mdi-spin"></i>
<p>正在加载员工数据...</p>
</div>
<div v-else-if="errorMessage" class="table-state error">
<i class="mdi mdi-alert-circle-outline"></i>
<p>{{ errorMessage }}</p>
<button type="button" class="state-action" @click="loadEmployees">重新加载</button>
</div>
<div v-else-if="!visibleEmployees.length" class="table-state empty">
<i class="mdi mdi-account-search-outline"></i>
<p>没有匹配的员工数据</p>
</div>
<table v-else>
<colgroup>
<col class="col-employee">
<col class="col-employee-no">
<col class="col-department">
<col class="col-position">
<col class="col-grade">
<col class="col-role">
<col class="col-status">
<col class="col-updated">
</colgroup>
<thead>
<tr>
<th>员工</th>
<th>工号</th>
<th>部门</th>
<th>岗位</th>
<th>职级</th>
<th>系统角色</th>
<th>状态</th>
<th>最近更新</th>
</tr>
</thead>
<tbody>
<tr
v-for="employee in visibleEmployees"
:key="employee.id"
:class="{ spotlight: employee.spotlight }"
@click="openEmployeeDetail(employee)"
>
<td>
<div class="employee-cell">
<span class="employee-avatar">{{ employee.avatar }}</span>
<div>
<strong>{{ employee.name }}</strong>
<span>{{ employee.email }}</span>
</div>
</div>
</td>
<td>{{ employee.employeeNo }}</td>
<td>{{ employee.department }}</td>
<td>{{ employee.position }}</td>
<td><span class="level-pill">{{ employee.grade }}</span></td>
<td>
<div class="role-stack">
<span
v-for="role in employee.roles.slice(0, 2)"
:key="role"
class="role-pill"
>
{{ role }}
</span>
<span v-if="employee.roles.length > 2" class="more-pill">
+{{ employee.roles.length - 2 }}
</span>
</div>
</td>
<td>
<span class="status-pill" :class="employee.statusTone">{{ employee.status }}</span>
</td>
<td>{{ employee.updatedAt }}</td>
</tr>
</tbody>
</table>
</div>
<footer v-if="!loading && !errorMessage && totalCount" class="list-foot">
<span class="page-summary"> {{ totalCount }} 目前第 {{ currentPage }} </span>
<div class="pager" aria-label="分页">
<button
class="page-nav"
type="button"
:disabled="currentPage === 1"
aria-label="上一页"
@click="currentPage--"
>
<i class="mdi mdi-chevron-left"></i>
</button>
<button
v-for="page in totalPages"
:key="page"
class="page-number"
:class="{ active: currentPage === page }"
type="button"
:aria-current="currentPage === page ? 'page' : undefined"
@click="currentPage = page"
>
{{ page }}
</button>
<button
class="page-nav"
type="button"
:disabled="currentPage === totalPages"
aria-label="下一页"
@click="currentPage++"
>
<i class="mdi mdi-chevron-right"></i>
</button>
</div>
<div class="page-size-wrap">
<button class="page-size" type="button" @click="togglePageSizeOpen">
{{ pageSize }} / <i class="mdi mdi-chevron-down"></i>
</button>
<div v-if="pageSizeOpen" class="page-size-dropdown" role="listbox">
<button
v-for="size in pageSizes"
:key="size"
type="button"
role="option"
:aria-selected="pageSize === size"
:class="{ active: pageSize === size }"
@click="changePageSize(size)"
>
{{ size }} /
</button>
</div>
</div>
</footer>
</article>
</Transition>
<ConfirmDialog
:open="disableDialogOpen"
badge="停用账号"
badge-tone="warning"
:title="`确认停用 ${selectedEmployee?.name || '该员工'} 的账号吗?`"
description="停用后该员工将无法继续登录系统,相关个人操作入口也会立即失效。"
cancel-text="取消"
confirm-text="确认停用"
busy-text="停用中..."
confirm-tone="danger"
confirm-icon="mdi mdi-account-cancel-outline"
:busy="actionState === 'disable'"
@close="closeDisableDialog"
@confirm="confirmDisableEmployeeAccount"
/>
</section>
</template>
<script src="./scripts/EmployeeManagementView.js"></script>
<style scoped src="../assets/styles/views/employee-management-view.css"></style>