feat: 增强员工管理与报销单全流程功能
- 新增员工Excel导入服务(employee_spreadsheet)及导入/导出API端点 - 员工服务增加批量创建、邮箱唯一校验、组织架构关联等能力 - 报销单提交补充身份回填、部门信息透传及预审结果展示优化 - 认证流程增加部门信息(departmentName)并在schema中同步扩展 - 用户Agent服务增加部门关联与报销单回填逻辑 - 前端员工管理页面全面重构,新增导入导出、搜索过滤、分页等功能 - 前端审批中心、审计、差旅报销等视图交互与样式优化 - 新增TableLoadingState共享组件及员工导入测试用例
This commit is contained in:
@@ -61,11 +61,23 @@
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>年龄</span>
|
||||
<input :value="detailAge" readonly />
|
||||
<input
|
||||
v-model="employeeForm.age"
|
||||
type="number"
|
||||
min="0"
|
||||
max="120"
|
||||
inputmode="numeric"
|
||||
placeholder="请输入年龄"
|
||||
@input="syncBirthDateFromAge"
|
||||
/>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>出生日期</span>
|
||||
<input v-model="employeeForm.birthDate" type="date" />
|
||||
<input
|
||||
v-model="employeeForm.birthDate"
|
||||
type="date"
|
||||
@change="syncAgeFromBirthDate"
|
||||
/>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>手机号</span>
|
||||
@@ -104,9 +116,50 @@
|
||||
</div>
|
||||
|
||||
<div class="form-grid">
|
||||
<label class="field">
|
||||
<label
|
||||
class="field manager-picker department-picker"
|
||||
:class="{ open: departmentPickerOpen }"
|
||||
>
|
||||
<span>所属部门</span>
|
||||
<input v-model="employeeForm.department" readonly />
|
||||
<button
|
||||
class="manager-picker-trigger"
|
||||
type="button"
|
||||
@click.stop="toggleDepartmentPicker"
|
||||
>
|
||||
<span class="manager-picker-label">{{ departmentDisplayLabel }}</span>
|
||||
<i class="mdi mdi-chevron-down"></i>
|
||||
</button>
|
||||
<div
|
||||
v-if="departmentPickerOpen"
|
||||
class="manager-picker-panel"
|
||||
@click.stop
|
||||
>
|
||||
<input
|
||||
v-model="departmentSearchKeyword"
|
||||
type="search"
|
||||
placeholder="输入部门名称或编码搜索"
|
||||
@keydown.enter.prevent="resolveDepartmentSelectionFromKeyword"
|
||||
/>
|
||||
<div class="manager-picker-options">
|
||||
<button
|
||||
v-for="option in filteredDepartmentOptions"
|
||||
:key="option.id"
|
||||
type="button"
|
||||
class="manager-picker-option"
|
||||
:class="{ active: employeeForm.organizationUnitCode === option.code }"
|
||||
@click="selectDepartment(option)"
|
||||
>
|
||||
<strong>{{ option.name }}({{ option.code }})</strong>
|
||||
<span v-if="option.unitType">{{ option.unitType }}</span>
|
||||
</button>
|
||||
<p
|
||||
v-if="!filteredDepartmentOptions.length"
|
||||
class="manager-picker-empty"
|
||||
>
|
||||
没有匹配的部门,请调整搜索关键词。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>岗位</span>
|
||||
@@ -116,9 +169,56 @@
|
||||
<span>职级</span>
|
||||
<input v-model="employeeForm.grade" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<label class="field manager-picker" :class="{ open: managerPickerOpen }">
|
||||
<span>直属上级</span>
|
||||
<input v-model="employeeForm.manager" readonly />
|
||||
<button
|
||||
class="manager-picker-trigger"
|
||||
type="button"
|
||||
@click.stop="toggleManagerPicker"
|
||||
>
|
||||
<span class="manager-picker-label">{{ managerDisplayLabel }}</span>
|
||||
<i class="mdi mdi-chevron-down"></i>
|
||||
</button>
|
||||
<div
|
||||
v-if="managerPickerOpen"
|
||||
class="manager-picker-panel"
|
||||
@click.stop
|
||||
>
|
||||
<input
|
||||
v-model="managerSearchKeyword"
|
||||
type="search"
|
||||
placeholder="输入姓名、工号或部门搜索"
|
||||
@keydown.enter.prevent="resolveManagerSelectionFromKeyword"
|
||||
/>
|
||||
<div class="manager-picker-options">
|
||||
<button
|
||||
type="button"
|
||||
class="manager-picker-option"
|
||||
:class="{ active: !hasManagerAssignment }"
|
||||
@click="selectManager(null)"
|
||||
>
|
||||
<strong>无直属上级</strong>
|
||||
<span>清空当前设置</span>
|
||||
</button>
|
||||
<button
|
||||
v-for="option in filteredManagerOptions"
|
||||
:key="option.id"
|
||||
type="button"
|
||||
class="manager-picker-option"
|
||||
:class="{ active: employeeForm.managerEmployeeNo === option.employeeNo }"
|
||||
@click="selectManager(option)"
|
||||
>
|
||||
<strong>{{ option.name }}({{ option.employeeNo }})</strong>
|
||||
<span>{{ option.department }} / {{ option.position }}</span>
|
||||
</button>
|
||||
<p
|
||||
v-if="!filteredManagerOptions.length"
|
||||
class="manager-picker-empty"
|
||||
>
|
||||
没有匹配的员工,请调整搜索关键词。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>财务归口</span>
|
||||
@@ -177,15 +277,26 @@
|
||||
<div class="card-head">
|
||||
<div>
|
||||
<h3>最近变更</h3>
|
||||
<p>查看角色与档案调整记录</p>
|
||||
<p>仅保留最近 5 次角色与档案调整记录</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="history-list">
|
||||
<div v-for="item in selectedEmployee.history" :key="item.time" class="history-row">
|
||||
<div
|
||||
v-for="item in recentEmployeeHistory"
|
||||
:key="`${item.occurredAt || item.time}-${item.action}`"
|
||||
class="history-row"
|
||||
>
|
||||
<strong>{{ item.action }}</strong>
|
||||
<span>{{ item.owner }}</span>
|
||||
<small>{{ item.time }}</small>
|
||||
<div class="history-row-meta">
|
||||
<span class="history-row-owner">{{ item.owner }}</span>
|
||||
<small class="history-row-time">{{
|
||||
formatEmployeeHistoryTime(item.time || item.occurredAt)
|
||||
}}</small>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="!recentEmployeeHistory.length" class="manager-picker-empty">
|
||||
暂无变更记录
|
||||
</p>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
@@ -392,16 +503,34 @@
|
||||
<span>清空筛选</span>
|
||||
</button>
|
||||
|
||||
<button class="create-btn" type="button">
|
||||
<i class="mdi mdi-plus"></i>
|
||||
<span>新增员工</span>
|
||||
<button class="template-btn" type="button" :disabled="importExportBusy" @click="handleDownloadTemplate">
|
||||
<i class="mdi mdi-file-download-outline"></i>
|
||||
<span>下载模板</span>
|
||||
</button>
|
||||
|
||||
<button class="export-btn" type="button" :disabled="importExportBusy" @click="handleExportEmployees">
|
||||
<i class="mdi mdi-export"></i>
|
||||
<span>{{ actionState === 'export' ? '导出中...' : '导出员工' }}</span>
|
||||
</button>
|
||||
|
||||
<button class="create-btn" type="button" :disabled="importExportBusy" @click="openImportFilePicker">
|
||||
<i class="mdi mdi-file-upload-outline"></i>
|
||||
<span>{{ actionState === 'import' ? '导入中...' : '导入员工' }}</span>
|
||||
</button>
|
||||
|
||||
<input
|
||||
ref="importFileInput"
|
||||
class="import-file-input"
|
||||
type="file"
|
||||
accept=".xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
@change="handleImportFileChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="hint">
|
||||
<i class="mdi mdi-information-outline"></i>
|
||||
点击任意员工行可进入基础信息与角色权限编辑界面
|
||||
点击任意员工行可进入基础信息与角色权限编辑界面;导入将按员工编号覆盖已有档案,任一行校验失败则整批不写入。
|
||||
</p>
|
||||
|
||||
<div v-if="activeFilterTokens.length" class="active-filter-strip">
|
||||
@@ -412,8 +541,11 @@
|
||||
|
||||
<div class="table-wrap">
|
||||
<div v-if="loading" class="table-state">
|
||||
<i class="mdi mdi-loading mdi-spin"></i>
|
||||
<p>正在加载员工数据...</p>
|
||||
<TableLoadingState
|
||||
title="员工数据同步中"
|
||||
message="正在加载员工档案与角色权限"
|
||||
icon="mdi mdi-account-group-outline"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else-if="errorMessage" class="table-state error">
|
||||
@@ -496,7 +628,7 @@
|
||||
<td>
|
||||
<span class="status-pill" :class="employee.statusTone">{{ employee.status }}</span>
|
||||
</td>
|
||||
<td>{{ employee.updatedAt }}</td>
|
||||
<td class="cell-updated">{{ employee.updatedAt }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -572,9 +704,62 @@
|
||||
@close="closeDisableDialog"
|
||||
@confirm="confirmDisableEmployeeAccount"
|
||||
/>
|
||||
|
||||
<ConfirmDialog
|
||||
:open="importConfirmDialogOpen"
|
||||
badge="导入确认"
|
||||
badge-tone="warning"
|
||||
title="确认导入员工 Excel?"
|
||||
description="系统将先校验全部数据,全部通过后才写入数据库。若存在错误,将不会修改任何现有员工信息。"
|
||||
cancel-text="取消"
|
||||
confirm-text="开始导入"
|
||||
busy-text="导入中..."
|
||||
confirm-tone="primary"
|
||||
confirm-icon="mdi mdi-file-upload-outline"
|
||||
:busy="actionState === 'import'"
|
||||
@close="closeImportConfirmDialog"
|
||||
@confirm="confirmImportEmployees"
|
||||
/>
|
||||
|
||||
<ConfirmDialog
|
||||
:open="importErrorDialogOpen"
|
||||
badge="导入失败"
|
||||
badge-tone="danger"
|
||||
title="导入未执行"
|
||||
:description="importResultMessage"
|
||||
cancel-text="关闭"
|
||||
confirm-text="下载模板"
|
||||
confirm-tone="primary"
|
||||
confirm-icon="mdi mdi-file-download-outline"
|
||||
@close="closeImportErrorDialog"
|
||||
@confirm="handleDownloadTemplate"
|
||||
>
|
||||
<div class="import-error-table-wrap">
|
||||
<table class="import-error-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>行号</th>
|
||||
<th>字段</th>
|
||||
<th>工号</th>
|
||||
<th>原因</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(item, index) in importErrors" :key="`${item.row}-${item.column}-${index}`">
|
||||
<td>{{ item.row || '-' }}</td>
|
||||
<td>{{ item.column }}</td>
|
||||
<td>{{ item.employeeNo || '-' }}</td>
|
||||
<td>{{ item.message }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ConfirmDialog>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script src="./scripts/EmployeeManagementView.js"></script>
|
||||
|
||||
<style scoped src="../assets/styles/views/employee-management-view.css"></style>
|
||||
<style scoped>
|
||||
@import "../assets/styles/views/employee-management-view.css";
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user