|
|
@@ -0,0 +1,884 @@
|
|
|
+<template>
|
|
|
+ <main class="transfer-page" v-loading="loading">
|
|
|
+ <section class="metric-grid" aria-label="岗位调动概览">
|
|
|
+ <article
|
|
|
+ v-for="item in metrics"
|
|
|
+ :key="item.label"
|
|
|
+ class="metric-card"
|
|
|
+ :class="'metric-' + item.tone"
|
|
|
+ >
|
|
|
+ <span class="metric-icon"><i :class="item.icon"></i></span>
|
|
|
+ <div>
|
|
|
+ <small>{{ item.label }}</small
|
|
|
+ ><strong>{{ item.value }}<em>{{ item.unit }}</em></strong>
|
|
|
+ <p>{{ item.note }}</p>
|
|
|
+ </div>
|
|
|
+ </article>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <section class="workspace-panel">
|
|
|
+ <div class="filter-bar">
|
|
|
+ <div class="filter-field">
|
|
|
+ <label>合同编号</label>
|
|
|
+ <el-input
|
|
|
+ v-model.trim="draftFilters.contractNo"
|
|
|
+ clearable
|
|
|
+ placeholder="输入合同编号"
|
|
|
+ @keyup.enter.native="applyFilters"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div class="filter-field">
|
|
|
+ <label>员工姓名</label>
|
|
|
+ <el-input
|
|
|
+ v-model.trim="draftFilters.employeeName"
|
|
|
+ clearable
|
|
|
+ prefix-icon="el-icon-search"
|
|
|
+ placeholder="输入员工姓名"
|
|
|
+ @keyup.enter.native="applyFilters"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div class="filter-field">
|
|
|
+ <label>单据状态</label>
|
|
|
+ <el-select v-model="draftFilters.docStatus" clearable placeholder="全部状态">
|
|
|
+ <el-option
|
|
|
+ v-for="item in docStatusOptions"
|
|
|
+ :key="item.value"
|
|
|
+ :label="item.label"
|
|
|
+ :value="item.value"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ </div>
|
|
|
+ <div class="filter-field">
|
|
|
+ <label>岗位</label>
|
|
|
+ <el-select
|
|
|
+ v-model="draftFilters.position"
|
|
|
+ clearable
|
|
|
+ filterable
|
|
|
+ placeholder="全部岗位"
|
|
|
+ >
|
|
|
+ <el-option
|
|
|
+ v-for="item in positionOptions"
|
|
|
+ :key="item.id"
|
|
|
+ :label="item.name"
|
|
|
+ :value="item.name"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ </div>
|
|
|
+ <div class="filter-actions">
|
|
|
+ <el-button type="primary" icon="el-icon-search" @click="applyFilters"
|
|
|
+ >查询</el-button
|
|
|
+ >
|
|
|
+ <el-button icon="el-icon-refresh-left" @click="resetFilters"
|
|
|
+ >重置</el-button
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="hasFilters" class="active-filters">
|
|
|
+ <span><i class="el-icon-filter"></i> 筛选条件</span>
|
|
|
+ <el-tag
|
|
|
+ v-if="filters.contractNo"
|
|
|
+ closable
|
|
|
+ size="small"
|
|
|
+ @close="clearFilter('contractNo')"
|
|
|
+ >合同编号:{{ filters.contractNo }}</el-tag
|
|
|
+ >
|
|
|
+ <el-tag
|
|
|
+ v-if="filters.employeeName"
|
|
|
+ closable
|
|
|
+ size="small"
|
|
|
+ type="success"
|
|
|
+ @close="clearFilter('employeeName')"
|
|
|
+ >员工:{{ filters.employeeName }}</el-tag
|
|
|
+ >
|
|
|
+ <el-tag
|
|
|
+ v-if="filters.docStatus"
|
|
|
+ closable
|
|
|
+ size="small"
|
|
|
+ :type="docStatusTagType(filters.docStatus)"
|
|
|
+ @close="clearFilter('docStatus')"
|
|
|
+ >单据:{{ docStatusName(filters.docStatus) }}</el-tag
|
|
|
+ >
|
|
|
+ <el-tag
|
|
|
+ v-if="filters.position"
|
|
|
+ closable
|
|
|
+ size="small"
|
|
|
+ type="warning"
|
|
|
+ @close="clearFilter('position')"
|
|
|
+ >岗位:{{ filters.position }}</el-tag
|
|
|
+ >
|
|
|
+ <button type="button" @click="resetFilters">清空全部</button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <ele-pro-table
|
|
|
+ ref="table"
|
|
|
+ row-key="id"
|
|
|
+ :columns="columns"
|
|
|
+ :datasource="filteredRecords"
|
|
|
+ :need-page="true"
|
|
|
+ :page-size="8"
|
|
|
+ :cache-key="cacheKeyUrl"
|
|
|
+ title="岗位调动台账"
|
|
|
+ :sub-title="
|
|
|
+ '共 ' +
|
|
|
+ filteredRecords.length +
|
|
|
+ ' 条调动记录,可按合同编号、员工、单据状态与岗位快速定位'
|
|
|
+ "
|
|
|
+ :toolkit="['size', 'columns', 'fullscreen']"
|
|
|
+ stripe
|
|
|
+ class="transfer-table"
|
|
|
+ >
|
|
|
+ <template slot="toolbar">
|
|
|
+ <div class="table-toolbar-actions">
|
|
|
+ <el-button size="small" icon="el-icon-refresh" @click="refreshData"
|
|
|
+ >刷新</el-button
|
|
|
+ >
|
|
|
+ <el-button size="small" icon="el-icon-download" @click="exportLedger"
|
|
|
+ >导出台账</el-button
|
|
|
+ >
|
|
|
+ <el-button
|
|
|
+ size="small"
|
|
|
+ type="primary"
|
|
|
+ icon="el-icon-plus"
|
|
|
+ @click="openCreate"
|
|
|
+ >新增岗位变更</el-button
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <template v-slot:contractNo="{ row }">
|
|
|
+ <button type="button" class="contract-cell" @click.stop="openView(row)">
|
|
|
+ <strong>{{ row.contractNo }}</strong>
|
|
|
+ </button>
|
|
|
+ </template>
|
|
|
+ <template v-slot:employee="{ row }">
|
|
|
+ <button type="button" class="employee-cell" @click.stop="openView(row)">
|
|
|
+ <span class="employee-avatar">{{ initials(row.employeeName) }}</span>
|
|
|
+ <span><strong>{{ row.employeeName }}</strong></span>
|
|
|
+ </button>
|
|
|
+ </template>
|
|
|
+ <template v-slot:duties="{ row }">
|
|
|
+ <span class="duties-cell">{{ row.duties }}</span>
|
|
|
+ </template>
|
|
|
+ <template v-slot:move="{ row }">
|
|
|
+ <div class="move-cell">
|
|
|
+ <strong>{{ row.targetPosition }}</strong>
|
|
|
+ <small>{{ row.targetDept }}</small>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <template v-slot:audit="{ row }">
|
|
|
+ <span class="status-pill" :class="auditClass(row.auditStatus)"
|
|
|
+ ><i></i>{{ row.auditStatus }}</span
|
|
|
+ >
|
|
|
+ </template>
|
|
|
+ <template v-slot:docStatus="{ row }">
|
|
|
+ <span class="status-pill" :class="docStatusClass(row.docStatus)"
|
|
|
+ ><i></i>{{ docStatusName(row.docStatus) }}</span
|
|
|
+ >
|
|
|
+ </template>
|
|
|
+ <template v-slot:action="{ row }">
|
|
|
+ <div class="action-cell">
|
|
|
+ <el-button type="text" icon="el-icon-view" @click.stop="openView(row)"
|
|
|
+ >查看</el-button
|
|
|
+ >
|
|
|
+ <el-button
|
|
|
+ v-if="row.docStatus === 'draft'"
|
|
|
+ type="text"
|
|
|
+ icon="el-icon-edit"
|
|
|
+ @click.stop="openEdit(row)"
|
|
|
+ >编辑</el-button
|
|
|
+ >
|
|
|
+ <el-button
|
|
|
+ v-if="row.docStatus === 'draft'"
|
|
|
+ type="text"
|
|
|
+ class="danger-text"
|
|
|
+ icon="el-icon-delete"
|
|
|
+ @click.stop="removeRecord(row)"
|
|
|
+ >删除</el-button
|
|
|
+ >
|
|
|
+ <el-button
|
|
|
+ type="text"
|
|
|
+ icon="el-icon-s-check"
|
|
|
+ @click.stop="openFlow(row)"
|
|
|
+ >审批流程</el-button
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <template slot="empty"
|
|
|
+ ><div class="table-empty">
|
|
|
+ <span><i class="el-icon-switch-button"></i></span
|
|
|
+ ><strong>没有找到匹配的岗位调动记录</strong>
|
|
|
+ <p>请调整筛选条件后再试,或新增一条岗位变更。</p>
|
|
|
+ </div></template
|
|
|
+ >
|
|
|
+ </ele-pro-table>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <el-drawer
|
|
|
+ :visible.sync="formVisible"
|
|
|
+ direction="rtl"
|
|
|
+ :size="drawerSize"
|
|
|
+ custom-class="transfer-drawer"
|
|
|
+ append-to-body
|
|
|
+ :before-close="closeForm"
|
|
|
+ @closed="resetForm"
|
|
|
+ >
|
|
|
+ <template slot="title">
|
|
|
+ <div class="drawer-title">
|
|
|
+ <span><i :class="formReadOnly ? 'el-icon-view' : 'el-icon-switch-button'"></i></span>
|
|
|
+ <div>
|
|
|
+ <h2>{{ formTitle }}</h2>
|
|
|
+ <p>{{ form.contractNo || "保存后自动生成合同编码" }}</p>
|
|
|
+ </div>
|
|
|
+ <em v-if="form.id" :class="docStatusTone(form.docStatus)"
|
|
|
+ ><i></i>{{ docStatusName(form.docStatus) }}</em
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <div class="drawer-body">
|
|
|
+ <template v-if="formReadOnly">
|
|
|
+ <section class="detail-overview">
|
|
|
+ <div>
|
|
|
+ <h3>{{ form.employeeName }} · 岗位调动</h3>
|
|
|
+ <p>
|
|
|
+ {{ form.originPosition }} → {{ form.targetPosition }} ·
|
|
|
+ {{ form.targetDept }}
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ <strong>{{ auditStatusName(form.auditStatus) }}</strong>
|
|
|
+ </section>
|
|
|
+ <section class="detail-section">
|
|
|
+ <div class="detail-heading">
|
|
|
+ <span><i class="el-icon-document"></i></span>
|
|
|
+ <div><h3>变更信息</h3><p>调动目标与签署方式</p></div>
|
|
|
+ </div>
|
|
|
+ <div class="detail-grid">
|
|
|
+ <div><span>调动岗位</span><strong>{{ form.targetPosition }}</strong></div>
|
|
|
+ <div><span>调动部门</span><strong>{{ form.targetDept }}</strong></div>
|
|
|
+ <div><span>岗位序列</span><strong>{{ form.sequence || "-" }}</strong></div>
|
|
|
+ <div><span>工作地点</span><strong>{{ form.workLocation || "-" }}</strong></div>
|
|
|
+ <div><span>岗位层级</span><strong>{{ form.targetLevel || "-" }}</strong></div>
|
|
|
+ <div><span>签署方式</span><strong>{{ signTypeName(form.signType) }}</strong></div>
|
|
|
+ </div>
|
|
|
+ <div class="detail-block">
|
|
|
+ <span>变更原因</span>
|
|
|
+ <p class="detail-text">{{ form.reason || "暂无" }}</p>
|
|
|
+ </div>
|
|
|
+ <div class="detail-block">
|
|
|
+ <span>附件</span>
|
|
|
+ <div class="detail-tags">
|
|
|
+ <em v-for="file in form.attachments" :key="file.name">{{ file.name }}</em>
|
|
|
+ <span v-if="!form.attachments.length" class="detail-muted">未上传附件</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+ <section class="detail-section">
|
|
|
+ <div class="detail-heading">
|
|
|
+ <span><i class="el-icon-user"></i></span>
|
|
|
+ <div><h3>原合同信息</h3><p>员工当前档案信息</p></div>
|
|
|
+ </div>
|
|
|
+ <div class="detail-grid">
|
|
|
+ <div><span>所属部门</span><strong>{{ form.dept }}</strong></div>
|
|
|
+ <div><span>原岗位</span><strong>{{ form.originPosition }}</strong></div>
|
|
|
+ <div><span>原合同签订年限</span><strong>{{ form.contractYears || "-" }}</strong></div>
|
|
|
+ <div><span>原合同期限</span><strong>{{ form.contractPeriod || "-" }}</strong></div>
|
|
|
+ <div><span>岗位层级</span><strong>{{ form.level || "-" }}</strong></div>
|
|
|
+ <div><span>手机号</span><strong>{{ form.phone || "-" }}</strong></div>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <el-form
|
|
|
+ v-else
|
|
|
+ ref="transferForm"
|
|
|
+ :model="form"
|
|
|
+ :rules="rules"
|
|
|
+ label-position="top"
|
|
|
+ class="transfer-form"
|
|
|
+ >
|
|
|
+ <div class="form-guide">
|
|
|
+ <i class="el-icon-info"></i>
|
|
|
+ <span>
|
|
|
+ <strong>{{ formMode === "create" ? "发起岗位变更" : "调整岗位变更单据" }}</strong>
|
|
|
+ <small>选择员工后自动带出原合同信息,填写调动目标与签署方式。</small>
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ <section class="form-section">
|
|
|
+ <div class="section-title">
|
|
|
+ <span><i class="el-icon-user"></i></span>
|
|
|
+ <div><h3>原合同信息</h3><p>选择员工后自动带出,只读</p></div>
|
|
|
+ </div>
|
|
|
+ <div class="form-grid">
|
|
|
+ <el-form-item label="合同编码">
|
|
|
+ <el-input v-model="form.contractNo" disabled placeholder="系统自动生成" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="姓名" prop="employeeId">
|
|
|
+ <el-select v-model="form.employeeId" filterable placeholder="请选择员工" @change="fillEmployee">
|
|
|
+ <el-option
|
|
|
+ v-for="item in employees"
|
|
|
+ :key="item.id"
|
|
|
+ :label="item.name"
|
|
|
+ :value="item.id"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="所属部门">
|
|
|
+ <el-input v-model="form.dept" disabled />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="岗位">
|
|
|
+ <el-input v-model="form.originPosition" disabled />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="原合同签订年限">
|
|
|
+ <el-input v-model="form.contractYears" disabled />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="原合同期限">
|
|
|
+ <el-input v-model="form.contractPeriod" disabled />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="岗位层级">
|
|
|
+ <el-input v-model="form.level" disabled />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="手机号">
|
|
|
+ <el-input v-model="form.phone" disabled />
|
|
|
+ </el-form-item>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+ <section class="form-section">
|
|
|
+ <div class="section-title">
|
|
|
+ <span><i class="el-icon-switch-button"></i></span>
|
|
|
+ <div><h3>变更信息</h3><p>填写调动目标岗位、部门与签署方式</p></div>
|
|
|
+ </div>
|
|
|
+ <div class="form-grid">
|
|
|
+ <el-form-item label="调动岗位" prop="targetPositionId">
|
|
|
+ <el-select v-model="form.targetPositionId" filterable placeholder="请选择调动岗位" @change="syncTargetPosition">
|
|
|
+ <el-option
|
|
|
+ v-for="item in positionOptions"
|
|
|
+ :key="item.id"
|
|
|
+ :label="item.name"
|
|
|
+ :value="item.id"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="调动部门" prop="targetDeptId">
|
|
|
+ <el-select v-model="form.targetDeptId" filterable placeholder="请选择调动部门" @change="syncTargetDept">
|
|
|
+ <el-option
|
|
|
+ v-for="item in organizationOptions"
|
|
|
+ :key="item.id"
|
|
|
+ :label="item.name"
|
|
|
+ :value="item.id"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="岗位序列">
|
|
|
+ <el-select v-model="form.sequence" clearable placeholder="请选择岗位序列">
|
|
|
+ <el-option
|
|
|
+ v-for="item in sequenceOptions"
|
|
|
+ :key="item"
|
|
|
+ :label="item"
|
|
|
+ :value="item"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="岗位层级">
|
|
|
+ <el-select v-model="form.targetLevel" clearable placeholder="请选择岗位层级">
|
|
|
+ <el-option
|
|
|
+ v-for="item in levelOptions"
|
|
|
+ :key="item"
|
|
|
+ :label="item"
|
|
|
+ :value="item"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="工作地址">
|
|
|
+ <el-input v-model.trim="form.workLocation" placeholder="调动后工作地点" />
|
|
|
+ </el-form-item>
|
|
|
+ </div>
|
|
|
+ <el-form-item label="附件">
|
|
|
+ <el-upload
|
|
|
+ :auto-upload="false"
|
|
|
+ multiple
|
|
|
+ :file-list="form.attachments"
|
|
|
+ :on-change="handleFileChange"
|
|
|
+ :on-remove="handleFileRemove"
|
|
|
+ class="transfer-upload"
|
|
|
+ >
|
|
|
+ <el-button size="small" icon="el-icon-upload2">上传证明材料</el-button>
|
|
|
+ <div slot="tip" class="el-upload__tip">支持多文件上传,如调岗协议、岗位变动证明</div>
|
|
|
+ </el-upload>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="签署方式" prop="signType">
|
|
|
+ <el-checkbox
|
|
|
+ :value="form.signType === 'online'"
|
|
|
+ @change="(v) => (form.signType = v ? 'online' : '')"
|
|
|
+ >线上签署</el-checkbox
|
|
|
+ >
|
|
|
+ <el-checkbox
|
|
|
+ :value="form.signType === 'offline'"
|
|
|
+ @change="(v) => (form.signType = v ? 'offline' : '')"
|
|
|
+ >线下签署</el-checkbox
|
|
|
+ >
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="变更原因" prop="reason">
|
|
|
+ <el-input
|
|
|
+ v-model.trim="form.reason"
|
|
|
+ type="textarea"
|
|
|
+ :rows="4"
|
|
|
+ maxlength="300"
|
|
|
+ show-word-limit
|
|
|
+ placeholder="填写岗位调动原因:晋升、轮岗、组织调整等"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ </section>
|
|
|
+ </el-form>
|
|
|
+ </div>
|
|
|
+ <div class="drawer-footer">
|
|
|
+ <span><i class="el-icon-lock"></i> 合同编码由系统按编码规则自动生成</span>
|
|
|
+ <div v-if="formReadOnly">
|
|
|
+ <el-button @click="formVisible = false">关闭</el-button>
|
|
|
+ <el-button
|
|
|
+ v-if="form.docStatus === 'draft'"
|
|
|
+ type="primary"
|
|
|
+ icon="el-icon-edit"
|
|
|
+ @click="formMode = 'edit'"
|
|
|
+ >编辑</el-button
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+ <div v-else>
|
|
|
+ <el-button @click="closeForm()">取消</el-button>
|
|
|
+ <el-button @click="saveForm(false)">保存草稿</el-button>
|
|
|
+ <el-button type="primary" icon="el-icon-position" @click="saveForm(true)"
|
|
|
+ >提交审批</el-button
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-drawer>
|
|
|
+
|
|
|
+ <el-drawer
|
|
|
+ :visible.sync="flowVisible"
|
|
|
+ direction="rtl"
|
|
|
+ :size="drawerSize"
|
|
|
+ custom-class="flow-drawer"
|
|
|
+ append-to-body
|
|
|
+ >
|
|
|
+ <template slot="title">
|
|
|
+ <div class="drawer-title">
|
|
|
+ <span><i class="el-icon-s-check"></i></span>
|
|
|
+ <div>
|
|
|
+ <h2>审批流程</h2>
|
|
|
+ <p>{{ flowRecord.contractNo }} · {{ flowRecord.employeeName }}</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <div class="drawer-body">
|
|
|
+ <section class="detail-section">
|
|
|
+ <div class="detail-heading">
|
|
|
+ <span><i class="el-icon-s-opportunity"></i></span>
|
|
|
+ <div><h3>岗位调动审批进度</h3><p>部门负责人 → HR 专员 → HR 经理</p></div>
|
|
|
+ </div>
|
|
|
+ <el-steps
|
|
|
+ :active="flowActive"
|
|
|
+ finish-status="success"
|
|
|
+ process-status="process"
|
|
|
+ align-center
|
|
|
+ >
|
|
|
+ <el-step title="提交申请" />
|
|
|
+ <el-step title="部门负责人" />
|
|
|
+ <el-step title="HR 专员" />
|
|
|
+ <el-step title="HR 经理" />
|
|
|
+ <el-step title="审批通过" />
|
|
|
+ </el-steps>
|
|
|
+ </section>
|
|
|
+ <section class="detail-section">
|
|
|
+ <div class="detail-heading">
|
|
|
+ <span><i class="el-icon-document"></i></span>
|
|
|
+ <div><h3>单据信息</h3><p>岗位调动内容摘要</p></div>
|
|
|
+ </div>
|
|
|
+ <div class="detail-grid">
|
|
|
+ <div><span>员工姓名</span><strong>{{ flowRecord.employeeName }}</strong></div>
|
|
|
+ <div><span>原岗位</span><strong>{{ flowRecord.originPosition }}</strong></div>
|
|
|
+ <div><span>调动岗位</span><strong>{{ flowRecord.targetPosition }}</strong></div>
|
|
|
+ <div><span>调动部门</span><strong>{{ flowRecord.targetDept }}</strong></div>
|
|
|
+ <div><span>变更原因</span><strong>{{ flowRecord.reason }}</strong></div>
|
|
|
+ <div><span>审核状态</span><strong>{{ flowRecord.auditStatus }}</strong></div>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+ </div>
|
|
|
+ <div class="drawer-footer">
|
|
|
+ <span><i class="el-icon-time"></i> 最近更新 {{ flowRecord.updatedAt }}</span>
|
|
|
+ <div><el-button type="primary" @click="flowVisible = false">关闭</el-button></div>
|
|
|
+ </div>
|
|
|
+ </el-drawer>
|
|
|
+ </main>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import {
|
|
|
+ employees,
|
|
|
+ positions,
|
|
|
+ organizations,
|
|
|
+ sequenceOptions,
|
|
|
+ levelOptions,
|
|
|
+ transferRecords,
|
|
|
+} from "./mock";
|
|
|
+
|
|
|
+const emptyFilters = () => ({
|
|
|
+ contractNo: "",
|
|
|
+ employeeName: "",
|
|
|
+ docStatus: "",
|
|
|
+ position: "",
|
|
|
+});
|
|
|
+
|
|
|
+const emptyForm = () => ({
|
|
|
+ id: null,
|
|
|
+ contractNo: "",
|
|
|
+ employeeId: null,
|
|
|
+ employeeName: "",
|
|
|
+ dept: "",
|
|
|
+ originPosition: "",
|
|
|
+ contractYears: "",
|
|
|
+ contractPeriod: "",
|
|
|
+ level: "",
|
|
|
+ phone: "",
|
|
|
+ targetPositionId: null,
|
|
|
+ targetPosition: "",
|
|
|
+ targetDeptId: null,
|
|
|
+ targetDept: "",
|
|
|
+ sequence: "",
|
|
|
+ targetLevel: "",
|
|
|
+ workLocation: "",
|
|
|
+ attachments: [],
|
|
|
+ signType: "",
|
|
|
+ reason: "",
|
|
|
+ auditStatus: "待审核",
|
|
|
+ docStatus: "draft",
|
|
|
+ updatedAt: "",
|
|
|
+});
|
|
|
+
|
|
|
+const DOC_STATUS_LABELS = { draft: "草稿", pending: "待审核", approved: "已审核" };
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: "PositionTransfer",
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ loading: false,
|
|
|
+ records: JSON.parse(JSON.stringify(transferRecords)),
|
|
|
+ employees,
|
|
|
+ positionOptions: positions,
|
|
|
+ organizationOptions: organizations,
|
|
|
+ sequenceOptions,
|
|
|
+ levelOptions,
|
|
|
+ docStatusOptions: [
|
|
|
+ { value: "draft", label: "草稿" },
|
|
|
+ { value: "pending", label: "待审核" },
|
|
|
+ { value: "approved", label: "已审核" },
|
|
|
+ ],
|
|
|
+ draftFilters: emptyFilters(),
|
|
|
+ filters: emptyFilters(),
|
|
|
+ cacheKeyUrl: "oa-pc-position-transfer-v1",
|
|
|
+ formVisible: false,
|
|
|
+ formMode: "view",
|
|
|
+ form: emptyForm(),
|
|
|
+ flowVisible: false,
|
|
|
+ flowRecord: {},
|
|
|
+ rules: {
|
|
|
+ employeeId: [
|
|
|
+ { required: true, message: "请选择员工", trigger: "change" },
|
|
|
+ ],
|
|
|
+ targetPositionId: [
|
|
|
+ { required: true, message: "请选择调动岗位", trigger: "change" },
|
|
|
+ ],
|
|
|
+ targetDeptId: [
|
|
|
+ { required: true, message: "请选择调动部门", trigger: "change" },
|
|
|
+ ],
|
|
|
+ signType: [
|
|
|
+ {
|
|
|
+ validator: (_rule, _value, callback) => {
|
|
|
+ if (!this.form.signType) {
|
|
|
+ callback(new Error("请选择签署方式(线上或线下)"));
|
|
|
+ } else {
|
|
|
+ callback();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ trigger: "change",
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ reason: [
|
|
|
+ { required: true, message: "请填写变更原因", trigger: "blur" },
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ };
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ columns() {
|
|
|
+ return [
|
|
|
+ { type: "index", label: "序号", width: 58, align: "center" },
|
|
|
+ { columnKey: "contractNo", label: "合同编号", width: 140, slot: "contractNo" },
|
|
|
+ { columnKey: "employee", label: "员工姓名", minWidth: 130, slot: "employee" },
|
|
|
+ { prop: "dept", label: "所属部门", minWidth: 150 },
|
|
|
+ { prop: "originPosition", label: "原岗位", minWidth: 130 },
|
|
|
+ { columnKey: "duties", label: "岗位职责", minWidth: 180, slot: "duties" },
|
|
|
+ { columnKey: "move", label: "调动岗位 / 部门", minWidth: 170, slot: "move" },
|
|
|
+ { prop: "reason", label: "调动原因", minWidth: 100 },
|
|
|
+ { prop: "workLocation", label: "调动工作地点", minWidth: 120 },
|
|
|
+ { columnKey: "audit", label: "审核状态", width: 100, slot: "audit" },
|
|
|
+ { columnKey: "docStatus", label: "单据状态", width: 100, slot: "docStatus" },
|
|
|
+ { columnKey: "action", label: "操作", width: 210, fixed: "right", align: "center", resizable: false, slot: "action" },
|
|
|
+ ];
|
|
|
+ },
|
|
|
+ metrics() {
|
|
|
+ const total = this.records.length;
|
|
|
+ const pending = this.records.filter((item) => item.docStatus === "pending").length;
|
|
|
+ const draft = this.records.filter((item) => item.docStatus === "draft").length;
|
|
|
+ const approved = this.records.filter((item) => item.docStatus === "approved").length;
|
|
|
+ return [
|
|
|
+ { label: "变更总数", value: total, unit: "单", note: "岗位调动台账合计", icon: "el-icon-switch-button", tone: "blue" },
|
|
|
+ { label: "待审核", value: pending, unit: "单", note: "需部门负责人 / HR 处理", icon: "el-icon-time", tone: "orange" },
|
|
|
+ { label: "草稿", value: draft, unit: "单", note: "未提交可编辑删除", icon: "el-icon-edit-outline", tone: "cyan" },
|
|
|
+ { label: "已通过", value: approved, unit: "单", note: "已生效岗位调动", icon: "el-icon-circle-check", tone: "green" },
|
|
|
+ ];
|
|
|
+ },
|
|
|
+ filteredRecords() {
|
|
|
+ const contractNo = this.filters.contractNo.trim().toLowerCase();
|
|
|
+ const employeeName = this.filters.employeeName.trim().toLowerCase();
|
|
|
+ const position = this.filters.position;
|
|
|
+ return this.records.filter((item) => {
|
|
|
+ const text = [item.originPosition, item.targetPosition].join(" ");
|
|
|
+ return (
|
|
|
+ (!contractNo || String(item.contractNo || "").toLowerCase().includes(contractNo)) &&
|
|
|
+ (!employeeName || item.employeeName.toLowerCase().includes(employeeName)) &&
|
|
|
+ (!this.filters.docStatus || item.docStatus === this.filters.docStatus) &&
|
|
|
+ (!position || text.includes(position))
|
|
|
+ );
|
|
|
+ });
|
|
|
+ },
|
|
|
+ hasFilters() {
|
|
|
+ return Boolean(
|
|
|
+ this.filters.contractNo ||
|
|
|
+ this.filters.employeeName ||
|
|
|
+ this.filters.docStatus ||
|
|
|
+ this.filters.position,
|
|
|
+ );
|
|
|
+ },
|
|
|
+ formReadOnly() {
|
|
|
+ return this.formMode === "view";
|
|
|
+ },
|
|
|
+ formTitle() {
|
|
|
+ return this.formMode === "create"
|
|
|
+ ? "新增岗位变更"
|
|
|
+ : this.formMode === "edit"
|
|
|
+ ? "编辑岗位变更"
|
|
|
+ : "岗位变更详情";
|
|
|
+ },
|
|
|
+ drawerSize() {
|
|
|
+ return window.innerWidth < 760 ? "96%" : "620px";
|
|
|
+ },
|
|
|
+ flowActive() {
|
|
|
+ return (
|
|
|
+ {
|
|
|
+ draft: 0,
|
|
|
+ pending: 2,
|
|
|
+ approved: 4,
|
|
|
+ }[this.flowRecord.docStatus] || 0
|
|
|
+ );
|
|
|
+ },
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ // ===== 筛选 =====
|
|
|
+ applyFilters() {
|
|
|
+ this.filters = { ...this.draftFilters };
|
|
|
+ this.reloadTable();
|
|
|
+ },
|
|
|
+ resetFilters() {
|
|
|
+ this.draftFilters = emptyFilters();
|
|
|
+ this.filters = emptyFilters();
|
|
|
+ this.reloadTable();
|
|
|
+ },
|
|
|
+ clearFilter(key) {
|
|
|
+ this.draftFilters[key] = "";
|
|
|
+ this.filters[key] = "";
|
|
|
+ this.reloadTable();
|
|
|
+ },
|
|
|
+ reloadTable() {
|
|
|
+ this.$nextTick(() => this.$refs.table?.reload({ page: 1 }));
|
|
|
+ },
|
|
|
+ refreshData() {
|
|
|
+ this.$message.success("岗位调动数据已刷新(mock)");
|
|
|
+ },
|
|
|
+ exportLedger() {
|
|
|
+ this.$message.success("岗位调动台账已导出(mock)");
|
|
|
+ },
|
|
|
+ // ===== 表单 =====
|
|
|
+ openCreate() {
|
|
|
+ this.formMode = "create";
|
|
|
+ this.form = {
|
|
|
+ ...emptyForm(),
|
|
|
+ contractNo: this.generateContractNo(),
|
|
|
+ };
|
|
|
+ this.formVisible = true;
|
|
|
+ this.$nextTick(() => this.$refs.transferForm?.clearValidate());
|
|
|
+ },
|
|
|
+ openEdit(row) {
|
|
|
+ if (row.docStatus !== "draft") return;
|
|
|
+ this.formMode = "edit";
|
|
|
+ this.form = this.rowToForm(row);
|
|
|
+ this.formVisible = true;
|
|
|
+ this.$nextTick(() => this.$refs.transferForm?.clearValidate());
|
|
|
+ },
|
|
|
+ openView(row) {
|
|
|
+ this.formMode = "view";
|
|
|
+ this.form = this.rowToForm(row);
|
|
|
+ this.formVisible = true;
|
|
|
+ },
|
|
|
+ rowToForm(row) {
|
|
|
+ const employee = this.employees.find((item) => item.name === row.employeeName);
|
|
|
+ const target = this.positionOptions.find((item) => item.name === row.targetPosition);
|
|
|
+ const dept = this.organizationOptions.find((item) => item.name === row.targetDept);
|
|
|
+ return {
|
|
|
+ ...emptyForm(),
|
|
|
+ ...row,
|
|
|
+ employeeId: employee?.id || null,
|
|
|
+ dept: row.dept,
|
|
|
+ originPosition: row.originPosition,
|
|
|
+ contractYears: employee?.contractYears || "",
|
|
|
+ contractPeriod: employee?.contractPeriod || "",
|
|
|
+ level: employee?.level || "",
|
|
|
+ phone: employee?.phone || "",
|
|
|
+ targetPositionId: target?.id || null,
|
|
|
+ targetDeptId: dept?.id || null,
|
|
|
+ attachments: row.attachments || [],
|
|
|
+ signType: row.signType || "",
|
|
|
+ auditStatus: row.auditStatus || "待审核",
|
|
|
+ docStatus: row.docStatus || "draft",
|
|
|
+ };
|
|
|
+ },
|
|
|
+ // 选择员工后自动带出原合同信息
|
|
|
+ fillEmployee() {
|
|
|
+ const employee = this.employees.find(
|
|
|
+ (item) => item.id === this.form.employeeId,
|
|
|
+ );
|
|
|
+ if (!employee) return;
|
|
|
+ this.form.employeeName = employee.name;
|
|
|
+ this.form.dept = employee.dept;
|
|
|
+ this.form.originPosition = employee.position;
|
|
|
+ this.form.contractYears = employee.contractYears;
|
|
|
+ this.form.contractPeriod = employee.contractPeriod;
|
|
|
+ this.form.level = employee.level;
|
|
|
+ this.form.phone = employee.phone;
|
|
|
+ },
|
|
|
+ syncTargetPosition() {
|
|
|
+ const item = this.positionOptions.find(
|
|
|
+ (p) => p.id === this.form.targetPositionId,
|
|
|
+ );
|
|
|
+ if (item) {
|
|
|
+ this.form.targetPosition = item.name;
|
|
|
+ this.form.sequence = item.sequence || this.form.sequence;
|
|
|
+ this.form.targetLevel = item.level || this.form.targetLevel;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ syncTargetDept() {
|
|
|
+ const item = this.organizationOptions.find(
|
|
|
+ (p) => p.id === this.form.targetDeptId,
|
|
|
+ );
|
|
|
+ if (item) this.form.targetDept = item.name;
|
|
|
+ },
|
|
|
+ handleFileChange(file) {
|
|
|
+ this.form.attachments = this.form.attachments || [];
|
|
|
+ if (!this.form.attachments.some((f) => f.name === file.name)) {
|
|
|
+ this.form.attachments.push({ name: file.name, url: file.url, status: "ready" });
|
|
|
+ }
|
|
|
+ },
|
|
|
+ handleFileRemove(file) {
|
|
|
+ this.form.attachments = (this.form.attachments || []).filter(
|
|
|
+ (f) => f.name !== file.name,
|
|
|
+ );
|
|
|
+ },
|
|
|
+ generateContractNo() {
|
|
|
+ const now = new Date();
|
|
|
+ const ymd = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, "0")}${String(
|
|
|
+ now.getDate(),
|
|
|
+ ).padStart(2, "0")}`;
|
|
|
+ const serial = String(this.records.length + 1).padStart(3, "0");
|
|
|
+ return `HT${ymd}${serial}`;
|
|
|
+ },
|
|
|
+ closeForm(done) {
|
|
|
+ this.formVisible = false;
|
|
|
+ if (typeof done === "function") done();
|
|
|
+ },
|
|
|
+ resetForm() {
|
|
|
+ this.form = emptyForm();
|
|
|
+ this.$refs.transferForm?.clearValidate();
|
|
|
+ },
|
|
|
+ saveForm(submit) {
|
|
|
+ const persist = () => {
|
|
|
+ const record = {
|
|
|
+ ...this.form,
|
|
|
+ employeeName: this.form.employeeName,
|
|
|
+ docStatus: submit ? "pending" : "draft",
|
|
|
+ auditStatus: submit ? "待审核" : this.form.auditStatus || "待审核",
|
|
|
+ updatedAt: new Date().toLocaleString("zh-CN", { hour12: false }),
|
|
|
+ };
|
|
|
+ if (record.id) {
|
|
|
+ const index = this.records.findIndex((item) => item.id === record.id);
|
|
|
+ if (index >= 0) this.$set(this.records, index, record);
|
|
|
+ } else {
|
|
|
+ record.id = Date.now();
|
|
|
+ this.records.unshift(record);
|
|
|
+ }
|
|
|
+ this.formVisible = false;
|
|
|
+ this.$message.success(submit ? "岗位变更已提交审批" : "岗位变更草稿已保存");
|
|
|
+ this.reloadTable();
|
|
|
+ };
|
|
|
+ if (submit) this.$refs.transferForm.validate((valid) => valid && persist());
|
|
|
+ else persist();
|
|
|
+ },
|
|
|
+ removeRecord(row) {
|
|
|
+ this.$confirm(`确定删除合同编号【${row.contractNo}】的岗位变更吗?`, "删除确认", {
|
|
|
+ type: "warning",
|
|
|
+ })
|
|
|
+ .then(() => {
|
|
|
+ this.records = this.records.filter((item) => item.id !== row.id);
|
|
|
+ this.$message.success("岗位变更已删除");
|
|
|
+ this.reloadTable();
|
|
|
+ })
|
|
|
+ .catch(() => {});
|
|
|
+ },
|
|
|
+ openFlow(row) {
|
|
|
+ this.flowRecord = row;
|
|
|
+ this.flowVisible = true;
|
|
|
+ },
|
|
|
+ // ===== 展示辅助 =====
|
|
|
+ initials(name = "") {
|
|
|
+ return name.length > 2 ? name.slice(-2) : name;
|
|
|
+ },
|
|
|
+ docStatusName(value) {
|
|
|
+ return DOC_STATUS_LABELS[value] || value || "-";
|
|
|
+ },
|
|
|
+ docStatusClass(value) {
|
|
|
+ return { draft: "is-inactive", pending: "is-warning", approved: "is-active" }[value];
|
|
|
+ },
|
|
|
+ docStatusTagType(value) {
|
|
|
+ return { draft: "info", pending: "warning", approved: "success" }[value];
|
|
|
+ },
|
|
|
+ docStatusTone(value) {
|
|
|
+ return { draft: "muted", pending: "warning", approved: "success" }[value];
|
|
|
+ },
|
|
|
+ auditClass(status) {
|
|
|
+ return status === "已通过"
|
|
|
+ ? "is-active"
|
|
|
+ : status === "已驳回"
|
|
|
+ ? "is-danger"
|
|
|
+ : "is-warning";
|
|
|
+ },
|
|
|
+ auditStatusName(status) {
|
|
|
+ return status || "-";
|
|
|
+ },
|
|
|
+ signTypeName(value) {
|
|
|
+ return value === "online" ? "线上签署" : value === "offline" ? "线下签署" : "-";
|
|
|
+ },
|
|
|
+ },
|
|
|
+};
|
|
|
+</script>
|
|
|
+
|
|
|
+<style
|
|
|
+ lang="scss"
|
|
|
+ scoped
|
|
|
+ src="../../styles/views/positionTransfer/index.scss"
|
|
|
+></style>
|