| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173 |
- <template>
- <view class="page-no-tab" :class="{ 'readonly-mode': isReadonly }">
- <uni-nav-bar fixed="true" statusBar="true" left-icon="back" :title="navTitle" background-color="#157A2C" color="#fff"
- @clickLeft="back"></uni-nav-bar>
- <!-- 详情态:统一申请视图(编号 / 状态 / 当前节点 / 提交时间) -->
- <view v-if="showDetailHeader" class="detail-head card section">
- <view class="detail-head-top">
- <text class="detail-title">{{ detailTitle || meta.title }}</text>
- <text class="status-tag" :class="'status-' + detailStatusClass">{{ detailStatusLabel }}</text>
- </view>
- <view class="detail-row">
- <text class="detail-label">申请编号</text>
- <text class="detail-value">{{ detailNo }}</text>
- </view>
- <view class="detail-row">
- <text class="detail-label">申请人</text>
- <text class="detail-value">{{ detailApplicant || currentUser.name || '—' }}</text>
- </view>
- <view class="detail-row">
- <text class="detail-label">提交时间</text>
- <text class="detail-value">{{ detailCreatedAt || '—' }}</text>
- </view>
- <view v-if="detailNode" class="detail-row">
- <text class="detail-label">当前节点</text>
- <text class="detail-value">{{ detailNode }}</text>
- </view>
- </view>
- <view v-if="essRow.maskedSummary && isReadonly" class="masked-card card section">
- <text class="masked-label">申请摘要</text>
- <text class="masked-text">{{ essRow.maskedSummary }}</text>
- </view>
- <view class="section-title"><text>原合同信息</text></view>
- <view class="form-card card section">
- <view class="field">
- <text class="label">申请人</text>
- <view class="readonly">
- <view class="avatar small-avatar">{{ currentUser.initials || '—' }}</view>
- <text>{{ currentUser.name || '—' }}</text>
- </view>
- </view>
- <view class="divider"></view>
- <view class="field">
- <text class="label">所属机构</text>
- <text class="value">{{ profile.legalName || '—' }}</text>
- </view>
- <view class="divider"></view>
- <view class="field">
- <text class="label">所属部门</text>
- <text class="value">{{ profile.dept || '—' }}</text>
- </view>
- <view class="divider"></view>
- <view class="field">
- <text class="label">岗位</text>
- <text class="value">{{ profile.position || '—' }}</text>
- </view>
- <view class="divider"></view>
- <view class="field">
- <text class="label">原合同签订年限</text>
- <text class="value">{{ profile.contractTerm || '—' }}</text>
- </view>
- <view class="divider"></view>
- <view class="field">
- <text class="label">原合同期限</text>
- <text class="value">{{ profile.contractPeriod || '—' }}</text>
- </view>
- <view class="divider"></view>
- <view class="field">
- <text class="label">岗位层级</text>
- <text class="value">{{ profile.level || '—' }}</text>
- </view>
- <view class="divider"></view>
- <view class="field">
- <text class="label required">手机号</text>
- <input
- v-model="form.phone"
- class="field-input"
- type="number"
- maxlength="11"
- :disabled="formDisabled"
- placeholder="请输入手机号"
- />
- </view>
- </view>
- <view class="section-title"><text>变更信息</text></view>
- <view class="form-card card section">
- <hr-picker-row
- v-bind="pickerRowMetrics"
- label="所属机构"
- required
- :value="form.targetLegalId"
- :display="form.targetLegalName"
- :options="legalOptions.map((item) => ({ label: item.name, value: item.id }))"
- :disabled="formDisabled"
- @change="onLegalChange"
- />
- <view class="divider"></view>
- <hr-picker-row
- v-bind="pickerRowMetrics"
- mode="tree"
- label="调动部门"
- required
- :value="form.targetDeptId"
- :display="form.targetDeptName"
- :tree="deptTree"
- :disabled="formDisabled || !form.targetLegalId"
- :hint="deptPlaceholder"
- @change="onTargetDeptPick"
- />
- <view class="divider"></view>
- <hr-picker-row
- v-bind="pickerRowMetrics"
- label="岗位序列"
- required
- :value="form.positionSequenceId"
- :display="form.positionSequence"
- :options="sequenceOptions.map((item) => ({ label: item.name, value: item.id }))"
- :disabled="formDisabled || !form.targetDeptId"
- :hint="sequencePlaceholder"
- @change="onSequenceChange"
- />
- <view class="divider"></view>
- <hr-picker-row
- v-bind="pickerRowMetrics"
- label="调动岗位"
- required
- :value="form.targetPositionId"
- :display="form.targetPositionName"
- :options="positionOptions.map((item) => ({ label: item.name, value: item.id }))"
- :disabled="formDisabled || !form.positionSequenceId"
- :hint="positionPlaceholder"
- @change="onTargetPositionChange"
- />
- <view class="divider"></view>
- <view class="field">
- <text class="label required">岗位性质</text>
- <text :class="form.positionTypeName ? 'value' : 'placeholder'">{{ form.positionTypeName || '选择岗位后自动带出' }}</text>
- </view>
- <view class="divider"></view>
- <hr-picker-row
- v-bind="pickerRowMetrics"
- label="岗位层级"
- required
- :value="form.positionLevelId"
- :display="form.positionLevel"
- :options="levelOptions.map((item) => ({ label: item.name, value: item.id }))"
- :disabled="formDisabled || !form.targetPositionId"
- :hint="levelPlaceholder"
- @change="onLevelChange"
- />
- <view class="divider"></view>
- <view class="field">
- <text class="label">工作地址</text>
- <input class="field-input" v-model="form.workLocation" :disabled="formDisabled" maxlength="60" placeholder="调动后工作地点" />
- </view>
- <view class="divider"></view>
- <view class="field press">
- <text class="label required">生效日期</text>
- <picker class="field-picker" :disabled="formDisabled" mode="date" :value="form.effectiveDate" :start="todayIso"
- @change="onEffectiveDateChange">
- <text :class="form.effectiveDate ? 'value' : 'placeholder'">{{ form.effectiveDate || '请选择' }}</text>
- </picker>
- <text class="arrow">›</text>
- </view>
- <view class="divider"></view>
- <hr-picker-row
- v-bind="pickerRowMetrics"
- label="异动类型"
- required
- :value="form.changeType"
- :display="changeTypeText"
- :options="changeTypeOptions"
- :disabled="formDisabled"
- @change="onChangeType"
- />
- <view class="divider"></view>
- <view class="field column">
- <text class="label">签章附件</text>
- <fileMain v-model="form.attachmentIds" :type="isReadonly ? 'view' : ''" />
- </view>
- </view>
- <view class="section-title"><text>申请说明</text></view>
- <view class="form-card card section">
- <view class="field textarea-field">
- <text class="label required">变更原因</text>
- <textarea v-model="form.reason" :disabled="formDisabled" maxlength="300" placeholder="请说明岗位调动原因:晋升、轮岗、组织调整等" />
- <text class="counter">{{ (form.reason || '').length }} / 300</text>
- </view>
- </view>
- <view v-if="showBottomBar" class="bottom-action safe-bottom">
- <view class="submit-btn press" @click="onBottomAction">{{ bottomButtonText }}</view>
- </view>
- </view>
- </template>
- <script>
- import fileMain from "@/pages/doc/index.vue";
- import {
- applicationTypes
- } from "@/pages/hr/data/apply-config";
- import {
- listOrganizations,
- getUserById
- } from "@/api/hr/organization";
- import {
- loadEmployeeContractInfo
- } from "@/api/hr/contract";
- import {
- fetchUserProfile,
- pickPrimaryPosition,
- dictToPositionNatureOptions
- } from "@/api/hr/user";
- import {
- loadEmploymentPeriod
- } from "@/api/hr/regularization";
- import {
- getDeptPositionSequences,
- getDeptPositions,
- getDeptPositionType,
- getDeptPositionLevels,
- } from "@/api/hr/position";
- import { getDictByCode } from "@/api/common";
- import HrPickerRow from "@/components/hr/HrPickerRow.vue";
- import {
- buildDeptTreeUnderLegal,
- LEGAL_TYPES
- } from "@/utils/hr/organization-tree";
- import { getOrgOptionsWithFallback } from "@/utils/hr/current-org";
- import {
- addApplication
- } from "@/utils/hr/storage";
- import {
- ESS_CHANGE_TYPE_OPTIONS,
- canEditUserChange,
- changeTypeLabel,
- loadTransferDetail,
- submitTransferApplication,
- } from "@/api/hr/userChange";
- import { applicationModeMixin, APPLICATION_MODE } from "@/utils/hr/application-mode";
- /** 生效日期不得早于今天(员工自助不允许回溯改历史档案) */
- function todayIso() {
- const d = new Date();
- return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
- }
- export default {
- components: {
- fileMain,
- HrPickerRow
- },
- mixins: [applicationModeMixin],
- data() {
- return {
- /** 生效日期最小可选值(picker start):不早于今天 */
- todayIso: todayIso(),
- currentUser: {
- id: null,
- name: "",
- initials: "",
- department: "",
- position: "",
- level: ""
- },
- // 原合同信息(只读,来自员工档案 + 劳动合同接口)
- profile: {
- employeeNo: "",
- legalName: "",
- dept: "",
- position: "",
- level: "",
- contractTerm: "",
- contractPeriod: "",
- phone: "",
- },
- form: {
- id: null,
- userId: null,
- employmentPeriodId: null,
- name: "",
- employeeNo: "",
- dept: "",
- position: "",
- level: "",
- // 手机号:PC 端「原合同信息」里是可编辑 + 必填(rules 543-550),提交载荷也要传 phone
- phone: "",
- changeType: "POSITION_TRANSFER",
- targetLegalId: null,
- targetLegalName: "",
- targetDeptId: null,
- targetDeptName: "",
- positionSequenceId: null,
- positionSequence: "",
- targetPositionId: null,
- targetPositionName: "",
- positionType: "",
- positionTypeName: "",
- positionLevelId: null,
- positionLevel: "",
- workLocation: "",
- effectiveDate: "",
- attachmentIds: [],
- reason: "",
- remark: "",
- },
- submitting: false,
- orgList: [],
- // 调动部门树(树选择器用;公司层已在 rebuildDeptOptions 里去掉)
- deptTree: [],
- orgPromise: null,
- legalOptions: [],
- sequenceOptions: [],
- positionOptions: [],
- levelOptions: [],
- sequenceLoading: false,
- positionLoading: false,
- levelLoading: false,
- /** 岗位性质字典(编码 → 名称,接口不返 positionTypeName 时反查) */
- positionNatureOptions: [],
- };
- },
- computed: {
- /** 统一选择行(HrPickerRow)的行度量:与本页 .field/.label 口径一致,避免左侧对不齐 */
- pickerRowMetrics() {
- return {
- rowMinHeight: "108rpx",
- rowPadding: "28rpx 36rpx",
- labelWidth: "180rpx",
- };
- },
- meta() {
- return applicationTypes.transfer || {
- title: "调岗申请",
- subtitle: "",
- icon: "岗",
- color: "#9270ca"
- };
- },
- navTitle() {
- return this.applicationTitle(this.meta.title);
- },
- pageTitle() {
- return this.meta.title;
- },
- changeTypeText() {
- return this.form.changeType ? changeTypeLabel(this.form.changeType) : "请选择";
- },
- /** 异动类型候选(统一选择器用) */
- changeTypeOptions() {
- return ESS_CHANGE_TYPE_OPTIONS.map((item) => ({ label: item.label, value: item.value }));
- },
- deptPlaceholder() {
- return this.form.targetLegalId ? "请选择" : "请先选择所属机构";
- },
- sequencePlaceholder() {
- return this.form.targetDeptId ? "请选择" : "请先选择调动部门";
- },
- positionPlaceholder() {
- return this.form.positionSequenceId ? "请选择" : "请先选择岗位序列";
- },
- levelPlaceholder() {
- return this.form.targetPositionId ? "请选择" : "请先选择调动岗位";
- },
- summaryText() {
- const parts = [changeTypeLabel(this.form.changeType)];
- if (this.form.targetDeptName) parts.push(this.form.targetDeptName);
- if (this.form.targetPositionName) parts.push(this.form.targetPositionName);
- if (this.form.effectiveDate) parts.push(`生效 ${this.form.effectiveDate}`);
- return parts.join(" · ");
- },
- },
- onLoad(options) {
- const opts = options || {};
- this.initApplicationMode(opts);
- // 本页不直接承接 mode=edit(编辑由详情页内「编辑」按钮进入)
- if (this.appMode === APPLICATION_MODE.EDIT) this.appMode = APPLICATION_MODE.DETAIL;
- this.loadCurrentUser();
- this.orgPromise = this.loadOrganizations();
- uni.setNavigationBarTitle({
- title: this.navTitle
- });
- if (!this.isCreateMode) {
- // 详情态:只读展示,业务明细按异动单主键拉取
- this.initDetailMode();
- return;
- }
- // 兼容旧入口:/pages/hr/manage/transfer-create?id=<异动单主键>
- const legacyId = opts.id || "";
- if (legacyId) {
- this.hydrateForm(legacyId);
- return;
- }
- this.syncOrigin();
- this.loadProfile();
- },
- methods: {
- back() {
- uni.navigateBack();
- },
- loadCurrentUser() {
- const vo = uni.getStorageSync("userInfo") || {};
- const name = vo.userName || vo.name || "";
- this.currentUser = {
- id: vo.userId || vo.id || null,
- name,
- initials: name ? String(name).charAt(0) : "",
- department: vo.deptName || vo.department || "",
- position: vo.postName || vo.position || "",
- level: vo.positionLevelName || vo.level || "",
- };
- },
- async loadOrganizations() {
- try {
- // 组织树取全量:部门级联要按当前机构从中过滤出部门
- const list = await listOrganizations();
- this.orgList = Array.isArray(list) ? list : [];
- } catch (error) {
- this.orgList = [];
- }
- // 所属机构候选与 PC 一致:只给当前账号所属公司(登录缓存 loginChangeGroupVOList),
- // 不再列全量机构树;缺名称时回落组织树,编辑老数据时补当前项
- this.legalOptions = getOrgOptionsWithFallback(
- this.orgList,
- this.form.targetLegalId,
- this.form.targetLegalName
- );
- // 新建态默认带出当前登录机构(对齐 PC openCreate 的 getCurrentOrgId),
- // 否则「所属机构」空着,下面的部门 / 序列 / 岗位整条级联都是灰的
- if (!this.form.targetLegalId) {
- const first = this.legalOptions[0];
- if (first) {
- this.form.targetLegalId = first.id;
- this.form.targetLegalName = first.name || "";
- }
- }
- this.rebuildDeptOptions();
- },
- rebuildDeptOptions() {
- if (!this.form.targetLegalId) {
- this.deptTree = [];
- return;
- }
- const nodes = buildDeptTreeUnderLegal(this.orgList, this.form.targetLegalId);
- // 树选择器用:公司层不再入列,直接从部门层开始(与个人档案页一致)
- const legal = nodes.find((node) => String(node.id) === String(this.form.targetLegalId)) || nodes[0];
- this.deptTree = legal && Array.isArray(legal.children) ? legal.children : [];
- },
- // ============ 原合同信息:员工档案 + 劳动合同接口 ============
- async loadProfile() {
- const userId = this.form.userId || this.currentUser.id;
- this.profile = Object.assign({}, this.profile, {
- dept: this.currentUser.department,
- position: this.currentUser.position,
- level: this.currentUser.level,
- });
- if (userId == null || userId === "") return;
- // 机构名称兜底需要组织树就绪(首屏可能与 loadOrganizations 并发)
- if (!this.orgList.length && this.orgPromise) {
- try {
- await this.orgPromise;
- } catch (error) {
- // 组织树加载失败时跳过机构上溯
- }
- }
- // 员工档案:工号 / 机构 / 部门 / 岗位 / 层级 / 手机号
- try {
- const vo = (await getUserById(userId, false)) || {};
- const name = vo.name || vo.userName || this.currentUser.name;
- if (name) {
- this.currentUser.name = name;
- this.currentUser.initials = String(name).charAt(0);
- }
- const deptId = vo.deptId != null ? vo.deptId : vo.groupId;
- this.profile.employeeNo = vo.jobNumber || vo.employeeNo || this.profile.employeeNo;
- if (!this.form.employeeNo) this.form.employeeNo = this.profile.employeeNo;
- this.profile.dept = vo.deptName || vo.department || this.profile.dept;
- this.profile.position = vo.postName || vo.position || this.profile.position;
- this.profile.level =
- vo.positionLevelName || vo.positionLevel || vo.grade || vo.level || this.profile.level;
- this.profile.phone = vo.phone || vo.mobile || this.profile.phone;
- // 手机号是可编辑项:档案带出后写入表单(已有值不覆盖)
- if (!this.form.phone) this.form.phone = vo.phone || vo.mobile || "";
- this.profile.legalName =
- vo.company ||
- vo.legalEntity ||
- vo.legalEntityName ||
- this.findLegalNameByDept(deptId) ||
- this.profile.legalName;
- } catch (error) {
- // 档案带出失败不阻断,保留本地缓存值
- }
- // 岗位层级:只有档案聚合接口的「主岗位 positionLevelName」才可靠 ——
- // /main/user/getById 多数情况不返回 positionList,导致层级一直显示「—」
- try {
- const raw = await fetchUserProfile(userId, false);
- const primary = pickPrimaryPosition(raw || {});
- const levelName =
- (primary && (primary.positionLevelName || primary.positionLevel)) || "";
- if (levelName) this.profile.level = levelName;
- } catch (error) {
- // 档案聚合接口不可用时保留 getById 的结果
- }
- // 劳动合同:签订年限 / 合同期限(岗位层级缺失时用合同快照兜底)
- const contract = await loadEmployeeContractInfo(userId, false);
- const levelMissing = !this.profile.level;
- this.profile.contractTerm = contract.contractTerm || "";
- this.profile.contractPeriod = contract.periodText || "";
- if (levelMissing && contract.positionLevel) this.profile.level = contract.positionLevel;
- // 任职周期:随单保存 employmentPeriodId,为空时后端取当前有效周期
- const period = await loadEmploymentPeriod(userId, false);
- if (period && period.id != null) this.form.employmentPeriodId = period.id;
- },
- /** 按部门沿 parentId 上溯到法人节点,取机构名称 */
- findLegalNameByDept(deptId) {
- if (deptId == null || deptId === "" || !this.orgList.length) return "";
- const byId = new Map(this.orgList.map((node) => [String(node.id), node]));
- let node = byId.get(String(deptId));
- let guard = 0;
- while (node && guard < 40) {
- if (LEGAL_TYPES.has(String(node.type))) return node.name || "";
- const parentId = node.parentId == null ? "0" : String(node.parentId);
- if (parentId === "0") break;
- node = byId.get(parentId);
- guard++;
- }
- return "";
- },
- syncOrigin() {
- const user = this.currentUser || {};
- this.form.userId = this.form.userId || user.id || null;
- this.form.name = this.form.name || user.name || "";
- this.form.employeeNo = this.form.employeeNo || "";
- this.form.dept = this.form.dept || user.department || "";
- this.form.position = this.form.position || user.position || "";
- this.form.level = this.form.level || user.level || "";
- },
- // ============ 级联:机构 → 部门 → 序列 → 岗位 → 性质 + 层级 ============
- clearBelowDept() {
- this.form.targetDeptId = null;
- this.form.targetDeptName = "";
- this.sequenceOptions = [];
- this.clearBelowSequence();
- },
- clearBelowSequence() {
- this.form.positionSequenceId = null;
- this.form.positionSequence = "";
- this.positionOptions = [];
- this.clearBelowPosition();
- },
- clearBelowPosition() {
- this.form.targetPositionId = null;
- this.form.targetPositionName = "";
- this.form.positionType = "";
- this.form.positionTypeName = "";
- this.form.positionLevelId = null;
- this.form.positionLevel = "";
- this.levelOptions = [];
- },
- onLegalChange(legalId) {
- if (this.isReadonly) return;
- const legal = this.legalOptions.find((item) => String(item.id) === String(legalId));
- if (!legal) return;
- this.form.targetLegalId = legal.id;
- this.form.targetLegalName = legal.name;
- this.clearBelowDept();
- this.rebuildDeptOptions();
- },
- /** 调动部门:树选择器回传节点 id,这里从部门树里补出节点名 */
- onTargetDeptPick(deptId) {
- if (this.isReadonly) return;
- const hit = this.findDeptNode(this.deptTree, deptId);
- this.form.targetDeptId = deptId;
- this.form.targetDeptName = (hit && hit.name) || "";
- this.clearBelowSequence();
- this.loadSequences();
- },
- /** 在部门树里按 id 找节点 */
- findDeptNode(tree, id) {
- for (const node of tree || []) {
- if (String(node.id) === String(id)) return node;
- const hit = this.findDeptNode(node.children || [], id);
- if (hit) return hit;
- }
- return null;
- },
- async loadSequences() {
- if (!this.form.targetDeptId) {
- this.sequenceOptions = [];
- return;
- }
- this.sequenceLoading = true;
- try {
- const list = await getDeptPositionSequences(this.form.targetDeptId, false);
- this.sequenceOptions = (Array.isArray(list) ? list : [])
- .filter((item) => item && item.positionSequenceId != null)
- .map((item) => ({
- id: String(item.positionSequenceId),
- // 名称字段后端有 positionSequence / positionSequenceName 两种口径,
- // 只取前者会出现「选项存在但文字空白」(与 utils/hr/position-cascade.js 对齐)
- name: item.positionSequenceName || item.positionSequence || "",
- }));
- } catch (error) {
- this.sequenceOptions = [];
- } finally {
- this.sequenceLoading = false;
- }
- },
- async onSequenceChange(sequenceId) {
- if (this.isReadonly) return;
- const sequence = this.sequenceOptions.find(
- (item) => String(item.id) === String(sequenceId)
- );
- if (!sequence) return;
- this.form.positionSequenceId = sequence.id;
- this.form.positionSequence = sequence.name;
- this.clearBelowPosition();
- await this.loadPositions();
- },
- async loadPositions() {
- const deptId = this.form.targetDeptId;
- const positionSequenceId = this.form.positionSequenceId;
- if (!deptId || !positionSequenceId) {
- this.positionOptions = [];
- return;
- }
- this.positionLoading = true;
- try {
- const list = await getDeptPositions({
- deptId,
- positionSequenceId
- }, false);
- this.positionOptions = (Array.isArray(list) ? list : [])
- .filter((item) => item && item.positionId != null)
- .map((item) => ({
- id: String(item.positionId),
- name: item.positionName || "",
- }));
- } catch (error) {
- this.positionOptions = [];
- } finally {
- this.positionLoading = false;
- }
- },
- async onTargetPositionChange(positionId) {
- if (this.isReadonly) return;
- const position = this.positionOptions.find(
- (item) => String(item.id) === String(positionId)
- );
- if (!position) return;
- this.form.targetPositionId = position.id;
- this.form.targetPositionName = position.name;
- this.clearBelowPositionKeepName();
- await this.loadTypeAndLevels();
- },
- // 选择岗位后清空性质/层级但保留岗位本身
- clearBelowPositionKeepName() {
- this.form.positionType = "";
- this.form.positionTypeName = "";
- this.form.positionLevelId = null;
- this.form.positionLevel = "";
- this.levelOptions = [];
- },
- /**
- * 部门 + 序列 + 岗位 → 岗位性质(取首条)+ 岗位层级下拉。
- * @param {{keepWhenEmpty?:boolean}} options
- * keepWhenEmpty:详情/编辑回显时不要把单据里已有的岗位性质清空
- * (接口没返回 ≠ 单据没值,PC 的详情回显同样保留原值)
- */
- async loadTypeAndLevels(options = {}) {
- const keepWhenEmpty = !!options.keepWhenEmpty;
- const deptId = this.form.targetDeptId;
- const positionId = this.form.targetPositionId;
- const positionSequenceId = this.form.positionSequenceId;
- if (!deptId || !positionId || !positionSequenceId) return;
- this.levelLoading = true;
- try {
- const typeList = await getDeptPositionType({
- deptId,
- positionId,
- positionSequenceId,
- }, false);
- const first = (Array.isArray(typeList) ? typeList : [])[0];
- // 编码只存编码(提交值就是它);名称:接口 positionTypeName 优先,
- // 取不到时按岗位性质字典 position_nature 反查(对齐 PC 与 positionCascade 混入),
- // 不拿编码当名称,否则只读框会显示成 "1"
- const rawType = (first && first.positionType) || "";
- let typeName = String((first && first.positionTypeName) || "").trim();
- if (!typeName && rawType) {
- await this.loadPositionNatureDict();
- typeName = this.positionNatureLabelOf(rawType);
- }
- if (rawType) {
- this.form.positionType = rawType;
- if (typeName) this.form.positionTypeName = typeName;
- } else if (!keepWhenEmpty) {
- this.form.positionType = "";
- this.form.positionTypeName = "";
- }
- } catch (error) {
- if (!keepWhenEmpty) {
- this.form.positionType = "";
- this.form.positionTypeName = "";
- }
- }
- if (!this.form.positionType) {
- this.levelOptions = [];
- this.levelLoading = false;
- return;
- }
- try {
- const levelList = await getDeptPositionLevels({
- deptId,
- positionId,
- positionSequenceId,
- positionType: this.form.positionType,
- }, false);
- this.levelOptions = (Array.isArray(levelList) ? levelList : [])
- .filter((item) => item && item.positionLevelId != null)
- .map((item) => ({
- id: String(item.positionLevelId),
- name: item.positionLevel || "",
- }));
- } catch (error) {
- this.levelOptions = [];
- } finally {
- this.levelLoading = false;
- }
- },
- /* ===== 岗位性质字典(接口只回编码时反查显示名,口径对齐 PC) ===== */
- async loadPositionNatureDict() {
- if ((this.positionNatureOptions || []).length) return this.positionNatureOptions;
- try {
- const dict = await getDictByCode("position_nature");
- this.positionNatureOptions = dictToPositionNatureOptions(dict);
- } catch (error) {
- this.positionNatureOptions = [];
- }
- return this.positionNatureOptions;
- },
- /** 岗位性质编码 → 字典文案;查不到返回空串(不把编码当名称) */
- positionNatureLabelOf(code) {
- const key = code == null ? "" : String(code).trim();
- if (!key) return "";
- const hit = (this.positionNatureOptions || []).find(
- (item) => String(item.value).trim() === key
- );
- return hit && hit.label ? String(hit.label).trim() : "";
- },
- onLevelChange(levelId) {
- if (this.isReadonly) return;
- const level = this.levelOptions.find((item) => String(item.id) === String(levelId));
- if (!level) return;
- this.form.positionLevelId = level.id;
- this.form.positionLevel = level.name;
- },
- onChangeType(changeType) {
- if (this.isReadonly) return;
- this.form.changeType = changeType;
- },
- onEffectiveDateChange(e) {
- if (this.isReadonly) return;
- this.form.effectiveDate = (e && e.detail && e.detail.value) || "";
- },
- validate() {
- const f = this.form;
- if (!f.changeType) return "请选择异动类型";
- if (!f.targetLegalId) return "请选择所属机构";
- if (!f.targetDeptId) return "请选择调动部门";
- if (!f.positionSequenceId) return "请选择岗位序列";
- if (!f.targetPositionId) return "请选择调动岗位";
- if (!f.positionType) return "请先选择调动岗位以自动带出岗位性质";
- if (!f.positionLevelId) return "请选择岗位层级";
- if (!f.effectiveDate) return "请选择生效日期";
- // 手机号口径与 PC 一致:必填 + 11 位手机号格式
- if (!String(f.phone || "").trim()) return "请输入手机号";
- if (!/^1[3-9]\d{9}$/.test(String(f.phone).trim())) return "手机号格式不正确";
- if (!String(f.reason || "").trim()) return "请填写变更原因";
- return null;
- },
- async submitApplication() {
- if (this.submitting) return;
- const error = this.validate();
- if (error) {
- uni.showToast({
- title: error,
- icon: "none"
- });
- return;
- }
- const isEdit = this.isEditMode;
- this.submitting = true;
- uni.showLoading({
- title: "提交中..."
- });
- try {
- this.syncOrigin();
- this.form.approvalDocumentFileId = this.form.attachmentIds[0] || null;
- // 编辑态:existingId 为原异动单主键,submitTransferApplication 会先更新再重新发起审批
- const result = await submitTransferApplication({
- form: Object.assign({}, this.form, {
- name: this.form.name || this.currentUser.name
- }),
- existingId: this.form.id,
- });
- this.form.id = result.id;
- if (!isEdit) {
- addApplication({
- type: this.meta.title,
- summary: this.summaryText,
- reason: this.form.reason,
- color: this.meta.color,
- metadata: {
- changeId: result.id,
- changeType: this.form.changeType,
- targetPositionName: this.form.targetPositionName,
- effectiveDate: this.form.effectiveDate,
- source: "server",
- },
- });
- }
- uni.hideLoading();
- const modal = this.resultModal(result, isEdit);
- uni.showModal({
- title: modal.title,
- content: modal.content,
- showCancel: false,
- success: () => this.leaveAfterSubmit(),
- });
- } catch (e) {
- uni.hideLoading();
- uni.showToast({
- title: (e && e.message) || "提交失败",
- icon: "none"
- });
- } finally {
- this.submitting = false;
- }
- },
- /** 提交结果文案(创建态与编辑态区分) */
- resultModal(result, isEdit) {
- if (result && result.approvalStarted) {
- return {
- title: isEdit ? "修改成功" : "提交成功",
- content: isEdit
- ? "调岗申请已修改并重新提交审批,可在「我的申请」中查看进度。"
- : "调岗申请已提交,审批流程已发起,可在「我的申请」中查看进度。",
- };
- }
- const reason = (result && result.approvalError) || "未知原因";
- return {
- title: "申请已提交",
- content: isEdit
- ? `异动单已更新,但审批流程未重新发起:${reason}。请联系管理员处理。`
- : `异动单已创建,但审批流程未发起:${reason}。请联系管理员处理。`,
- };
- },
- leaveAfterSubmit() {
- // 异动列表页已下线:提交后统一回「我的申请」(从菜单直达发起页,没有列表页可退)
- this.backToApplicationList("/pages/hr/apply/list");
- },
- // ============ 详情 / 编辑模式 ============
- async loadDetailBusiness() {
- await this.fetchSourceDetail();
- },
- /** 只读详情与编辑态共用:按异动单主键拉取业务明细并回填 */
- async fetchSourceDetail(businessId) {
- const id = businessId || this.appBusinessId;
- if (!id) throw new Error("缺少异动单主键,无法加载申请明细");
- await this.hydrateForm(id, {
- silent: true
- });
- },
- async hydrateForm(id, options = {}) {
- const silent = !!options.silent;
- if (!id) return null;
- if (!silent) {
- uni.showLoading({
- title: "加载中..."
- });
- }
- try {
- const row = await loadTransferDetail(id);
- if (!row) throw new Error("未找到该调岗单");
- // 静默回填(详情/编辑)不做状态校验:只读详情需要展示任意状态的数据
- if (!silent && !canEditUserChange(row)) throw new Error("当前状态不可编辑");
- const raw = row.raw || {};
- Object.assign(this.form, {
- id: row.id,
- userId: row.userId,
- name: row.name,
- employeeNo: row.employeeNo,
- dept: row.dept,
- position: row.position,
- level: row.level,
- changeType: row.changeType || "POSITION_TRANSFER",
- targetLegalId: raw.targetLegalId != null ? raw.targetLegalId : null,
- targetLegalName: raw.targetLegalName || "",
- targetDeptId: row.targetDeptId == null ? null : row.targetDeptId,
- targetDeptName: row.targetDept,
- positionSequenceId: raw.positionSequenceId != null ? raw.positionSequenceId : null,
- positionSequence: raw.positionSequence || "",
- targetPositionId: row.targetPositionId == null ? null : row.targetPositionId,
- targetPositionName: row.targetPosition,
- positionType: raw.positionType || "",
- positionTypeName: raw.positionTypeName || "",
- positionLevelId: raw.positionLevelId != null ? raw.positionLevelId : null,
- positionLevel: raw.positionLevel || row.targetLevel || "",
- workLocation: row.workLocation || raw.workLocation || "",
- effectiveDate: row.effectiveDate,
- employmentPeriodId: raw.employmentPeriodId != null ? raw.employmentPeriodId : null,
- attachmentIds: raw.approvalDocumentFileId != null ? [raw.approvalDocumentFileId] : [],
- reason: row.reason,
- remark: row.remark,
- });
- this.form.approvalDocumentFileId =
- raw.approvalDocumentFileId != null ? raw.approvalDocumentFileId : null;
- // 详情/编辑回显:单据只存了岗位性质编码时,按字典补显示名
- if (this.form.positionType && !this.form.positionTypeName) {
- await this.loadPositionNatureDict();
- this.form.positionTypeName = this.positionNatureLabelOf(this.form.positionType);
- }
- // 原合同信息(编辑态同样带出档案 + 合同)
- this.loadProfile();
- // 还原级联下拉选项(不回填表单值)
- this.rebuildDeptOptions();
- if (this.form.targetDeptId) await this.loadSequences();
- if (this.form.positionSequenceId) await this.loadPositions();
- if (this.form.targetPositionId) await this.loadTypeAndLevels({ keepWhenEmpty: true });
- return row;
- } catch (e) {
- if (silent) throw e;
- uni.hideLoading();
- uni.showToast({
- title: (e && e.message) || "加载失败",
- icon: "none"
- });
- // 加载失败兜底:去「我的申请」(异动列表页已下线,不能再跳它)
- setTimeout(() => this.backToApplicationList("/pages/hr/apply/list"), 900);
- return null;
- } finally {
- if (!silent) uni.hideLoading();
- }
- },
- },
- };
- </script>
- <style scoped lang="scss">
- @import "@/pages/hr/common-style.scss";
- .page-no-tab {
- padding-bottom: 150rpx;
- }
- .form-card {
- overflow: hidden;
- }
- /* ============ 详情态(只读) ============ */
- /* 只读态隐藏可点击箭头 */
- .readonly-mode .arrow {
- display: none;
- }
- .detail-head {
- margin: 24rpx 24rpx 0;
- padding: 28rpx;
- }
- .detail-head-top {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 20rpx;
- }
- .detail-title {
- font-size: 32rpx;
- font-weight: 600;
- color: #1f2329;
- }
- .detail-row {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 12rpx 0;
- font-size: 26rpx;
- }
- .detail-label {
- color: #8b939f;
- }
- .detail-value {
- color: #1f2329;
- }
- .masked-card {
- margin: 20rpx 24rpx 0;
- padding: 26rpx 28rpx;
- }
- .masked-label {
- display: block;
- margin-bottom: 12rpx;
- color: #8b939f;
- font-size: 23rpx;
- }
- .masked-text {
- color: #4b5563;
- font-size: 26rpx;
- line-height: 1.6;
- }
- .status-tag.status-draft,
- .status-tag.status-cancelled {
- color: #6f7782;
- background: #eef1f3;
- }
- /* 小节标题(主题色竖条 + 加粗深色文字)已统一到
- @/pages/hr/common-style.scss,本页不再重复定义。 */
- .field {
- position: relative;
- min-height: 108rpx;
- /* 右侧多留 8rpx:值 / 箭头离卡片右边缘太贴边 */
- padding: 28rpx 36rpx;
- display: flex;
- align-items: center;
- }
- .field.column {
- display: block;
- padding-bottom: 22rpx;
- }
- .field.column .label {
- display: block;
- width: auto;
- margin-bottom: 16rpx;
- }
- .label {
- /* 「原合同签订年限」7 个字在 180rpx 下会折行 → 加宽到 210rpx 并禁止换行 */
- width: 210rpx;
- flex-shrink: 0;
- white-space: nowrap;
- font-size: 28rpx;
- }
- .required::before {
- content: "*";
- color: #ee4d4d;
- margin-right: 6rpx;
- }
- .readonly {
- flex: 1;
- display: flex;
- align-items: center;
- justify-content: flex-end;
- font-size: 26rpx;
- }
- .small-avatar {
- width: 52rpx;
- height: 52rpx;
- margin-right: 14rpx;
- font-size: 22rpx;
- }
- .value {
- flex: 1;
- text-align: right;
- color: #1f2329;
- font-size: 27rpx;
- }
- .placeholder {
- flex: 1;
- text-align: right;
- color: #b2b7be;
- font-size: 27rpx;
- }
- .field-input {
- flex: 1;
- min-width: 0;
- text-align: right;
- color: #1f2329;
- font-size: 27rpx;
- }
- .arrow {
- margin-left: 13rpx;
- color: #c4c8ce;
- font-size: 38rpx;
- }
- .textarea-field {
- display: block;
- min-height: 240rpx;
- }
- .textarea-field .label {
- display: block;
- width: auto;
- margin-bottom: 20rpx;
- }
- .textarea-field textarea {
- width: 100%;
- height: 130rpx;
- padding: 18rpx;
- border-radius: 12rpx;
- background: #f6f7f8;
- font-size: 27rpx;
- }
- .counter {
- position: absolute;
- right: 45rpx;
- bottom: 38rpx;
- color: #b2b7be;
- font-size: 21rpx;
- }
- .bottom-action {
- position: fixed;
- z-index: 10;
- left: 0;
- right: 0;
- bottom: 0;
- height: 120rpx;
- padding: 16rpx 24rpx;
- display: flex;
- gap: 18rpx;
- background: #fff;
- border-top: 1rpx solid #e8e9eb;
- }
- .submit-btn {
- flex: 1;
- height: 84rpx;
- border-radius: 12rpx;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 30rpx;
- background: #157a2c;
- color: #fff;
- }
- @media (min-width: 800px) {
- .bottom-action {
- max-width: 520px;
- left: 50%;
- transform: translateX(-50%);
- }
- }
- </style>
|