| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257 |
- <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="goBack"
- ></uni-nav-bar>
- <!-- 详情态:统一申请视图(编号 / 状态 / 当前节点 / 提交时间) -->
- <view v-if="showDetailHeader" class="detail-head card section">
- <view class="detail-head-top">
- <text class="detail-title">{{ detailTitle || config.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 v-if="appAdmin && !isResignType && isReadonly" class="masked-card card section">
- <text class="masked-label">说明</text>
- <text class="masked-text">管理端仅展示统一申请信息(申请人 / 组织 / 状态 / 摘要),业务明细以来源单据为准。</text>
- </view>
- <!-- ============ 离职申请:字段与 PC 端「新建离职申请」一致 ============ -->
- <block v-if="isResignType">
- <!-- 员工信息 -->
- <view class="form-card card section">
- <view class="section-head">
- <view class="section-title"><text>员工信息</text></view>
- <text class="section-tip">按员工档案自动带出,可修改</text>
- </view>
- <view class="field">
- <text class="label required">申请员工</text>
- <view class="readonly">
- <view class="avatar small-avatar">{{ (detailApplicant || currentUser.name || '—').charAt(0) }}</view>
- <text>{{ detailApplicant || currentUser.name || '—' }}{{ applicantNo ? ' · ' + applicantNo : '' }}</text>
- </view>
- </view>
- <view class="divider"></view>
- <view class="field">
- <text class="label">入职日期</text>
- <text class="value readonly-text">{{ form.hireDate || "未填写" }}</text>
- </view>
- <view class="divider"></view>
- <view class="field">
- <text class="label required">手机号</text>
- <input
- class="input-flex"
- type="number"
- maxlength="11"
- :disabled="formDisabled"
- :value="form.phone"
- placeholder="请输入 11 位手机号码"
- @input="onPhoneInput"
- />
- </view>
- <view class="divider"></view>
- <hr-picker-row
- v-bind="pickerRowMetrics"
- label="所属机构"
- required
- :value="form.groupId"
- :display="form.company"
- :options="legalOptions.map((item) => ({ label: item.name, value: item.id }))"
- hint="机构数据加载中"
- @change="onCascadeLegalChange"
- />
- <view class="divider"></view>
- <hr-picker-row
- v-bind="pickerRowMetrics"
- mode="tree"
- label="所属部门"
- required
- :value="form.deptId"
- :display="form.department"
- :tree="cascadeDeptTree"
- :disabled="!form.groupId"
- :hint="deptPlaceholder"
- @change="onCascadeDeptPick"
- />
- <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="!form.deptId"
- :hint="sequencePlaceholder"
- @change="onCascadeSequenceChange"
- />
- <view class="divider"></view>
- <hr-picker-row
- v-bind="pickerRowMetrics"
- label="岗位名称"
- required
- :value="form.positionId"
- :display="form.position"
- :options="cascadePositionOptions.map((item) => ({ label: item.name, value: item.id }))"
- :disabled="!form.positionSequenceId"
- :hint="positionPlaceholder"
- @change="onCascadePositionChange"
- />
- <view class="divider"></view>
- <view class="field">
- <text class="label required">岗位性质</text>
- <text class="value readonly-text">{{ 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="!form.positionId"
- :hint="levelPlaceholder"
- @change="onCascadeLevelChange"
- />
- </view>
- <!-- 离职信息 -->
- <view class="form-card card section">
- <view class="section-head">
- <view class="section-title"><text>离职信息</text></view>
- <text class="section-tip">填写离职类型与关键日期,办理方式默认为线下附件</text>
- </view>
- <hr-picker-row
- v-bind="pickerRowMetrics"
- label="离职类型"
- required
- :value="form.resignationType"
- :display="resignTypeLabel"
- :options="resignTypeOptions"
- :disabled="isReadonly"
- @change="onResignTypeChange"
- />
- <view class="divider"></view>
- <view class="field">
- <text class="label required">申请日期</text>
- <picker class="field-picker" :disabled="formDisabled" mode="date" :value="form.applyDate" @change="onPick('applyDate', $event)">
- <text :class="form.applyDate ? 'value' : 'placeholder'">{{ form.applyDate || "请选择" }}</text>
- </picker>
- <text class="arrow">›</text>
- </view>
- <view class="divider"></view>
- <view class="field">
- <text class="label required">计划最后工作日</text>
- <picker class="field-picker"
- :disabled="formDisabled"
- mode="date"
- :value="form.plannedLastWorkDate"
- @change="onPick('plannedLastWorkDate', $event)"
- >
- <text :class="form.plannedLastWorkDate ? 'value' : 'placeholder'">
- {{ form.plannedLastWorkDate || "请选择" }}
- </text>
- </picker>
- <text class="arrow">›</text>
- </view>
- <view class="divider"></view>
- <view class="field">
- <text class="label">实际生效日期</text>
- <picker class="field-picker" :disabled="formDisabled" mode="date" :value="form.effectiveDate" @change="onPick('effectiveDate', $event)">
- <text :class="form.effectiveDate ? 'value' : 'placeholder'">
- {{ form.effectiveDate || "为空按最后工作日生效" }}
- </text>
- </picker>
- <text class="arrow">›</text>
- </view>
- <view class="divider"></view>
- <view class="field file-field">
- <text class="label required">离职申请附件</text>
- <view class="field-file">
- <fileMain v-model="form.attachmentIds" :type="isReadonly ? 'view' : ''" />
- </view>
- </view>
- </view>
- <!-- 离职原因 -->
- <view class="form-card card section">
- <view class="section-head">
- <view class="section-title"><text>离职原因</text></view>
- <text class="section-tip">请详细描述离职原因</text>
- </view>
- <view class="field textarea-field">
- <textarea v-model="form.reason" :disabled="formDisabled" maxlength="200" placeholder="请填写离职原因" />
- <text class="counter">{{ (form.reason || "").length }}/200</text>
- </view>
- </view>
- </block>
- <!-- ============ 其他考勤类申请(请假 / 加班 / 补卡 / 外勤 / 出差) ============ -->
- <block v-else-if="!appAdmin">
- <view class="form-card card section">
- <view class="field">
- <text class="label">申请人</text>
- <view class="readonly">
- <view class="avatar small-avatar">{{ (detailApplicant || currentUser.name || '—').charAt(0) }}</view>
- <text>{{ detailApplicant || currentUser.name || '—' }} · {{ currentUser.department || '—' }}</text>
- </view>
- </view>
- <view class="divider"></view>
- <hr-picker-row
- v-bind="pickerRowMetrics"
- v-if="type === 'leave'"
- label="请假类型"
- required
- :value="form.leaveType"
- :display="form.leaveType"
- :options="leaveTypeOptions"
- :disabled="isReadonly"
- @change="onLeaveTypeChange"
- />
- <block v-if="type === 'check'">
- <view class="field">
- <text class="label required">补卡日期</text>
- <picker class="field-picker" :disabled="formDisabled" mode="date" :value="form.startDate" @change="onPick('startDate', $event)">
- <text class="value">{{ form.startDate || "—" }}</text>
- </picker>
- <text class="arrow">›</text>
- </view>
- <view class="divider"></view>
- <hr-picker-row
- v-bind="pickerRowMetrics"
- label="补卡类型"
- required
- :value="form.repairType"
- :display="repairTypeLabel"
- :options="repairTypeOptions"
- :disabled="isReadonly"
- @change="onRepairTypeChange"
- />
- <view class="divider"></view>
- <view class="field">
- <text class="label required">补卡时间</text>
- <picker class="field-picker" :disabled="formDisabled" mode="time" :value="form.repairTime" @change="onPick('repairTime', $event)">
- <text class="value">{{ form.repairTime || "—" }}</text>
- </picker>
- <text class="arrow">›</text>
- </view>
- </block>
- <block v-if="!isCheckOrResign">
- <view class="field">
- <text class="label required">开始日期</text>
- <picker class="field-picker" :disabled="formDisabled" mode="date" :value="form.startDate" @change="onPick('startDate', $event)">
- <text class="value">{{ form.startDate || "—" }}</text>
- </picker>
- <text class="arrow">›</text>
- </view>
- <view class="divider"></view>
- <view class="field">
- <text class="label required">开始时间</text>
- <picker class="field-picker" :disabled="formDisabled" mode="time" :value="form.startTime" @change="onPick('startTime', $event)">
- <text class="value">{{ form.startTime || "—" }}</text>
- </picker>
- <text class="arrow">›</text>
- </view>
- <view class="divider"></view>
- <view class="field">
- <text class="label required">结束日期</text>
- <picker class="field-picker" :disabled="formDisabled" mode="date" :value="form.endDate" @change="onPick('endDate', $event)">
- <text class="value">{{ form.endDate || "—" }}</text>
- </picker>
- <text class="arrow">›</text>
- </view>
- <view class="divider"></view>
- <view class="field">
- <text class="label required">结束时间</text>
- <picker class="field-picker" :disabled="formDisabled" mode="time" :value="form.endTime" @change="onPick('endTime', $event)">
- <text class="value">{{ form.endTime || "—" }}</text>
- </picker>
- <text class="arrow">›</text>
- </view>
- <view class="divider"></view>
- <view class="field">
- <text class="label">{{ durationLabelName }}</text>
- <text class="value readonly-text">{{ durationLabel }}</text>
- </view>
- </block>
- <view class="field textarea-field">
- <text class="label required">申请事由</text>
- <textarea v-model="form.reason" :disabled="formDisabled" maxlength="200" :placeholder="reasonPlaceholder" />
- <text class="counter">{{ (form.reason || "").length }}/200</text>
- </view>
- </view>
- <view class="form-card card section">
- <view class="field">
- <text class="label">附件</text>
- <view class="field-file">
- <fileMain v-model="form.attachmentIds" :type="isReadonly ? 'view' : ''" />
- </view>
- </view>
- <view class="divider"></view>
- </view>
- </block>
- <view v-if="showBottomBar" class="bottom-action safe-bottom">
- <button class="submit-btn" @click="onBottomAction">{{ bottomButtonText }}</button>
- </view>
- </view>
- </template>
- <script>
- import { applicationTypes } from "@/pages/hr/data/apply-config";
- import { getDraft, removeDraft, addApplication, rememberResignId } from "@/utils/hr/storage";
- import {
- ATTENDANCE_BUSINESS_TYPE,
- REPAIR_TYPE_LABEL,
- REPAIR_TYPE_OPTIONS,
- buildAttendancePayload,
- computeDurationMinutes,
- hydrateAttendanceForm,
- loadAttendanceApplicationForEdit,
- pickApplicationId,
- startAttendanceApproval,
- submitAttendanceApplication,
- updateAttendanceApplication,
- } from "@/api/hr/attendanceApply";
- import {
- RESIGN_TYPE_OPTIONS,
- RESIGNATION_TYPE_LABELS,
- hydrateResignationForm,
- loadResignationDetail,
- submitResignationFlow,
- } from "@/api/hr/resign";
- import {
- formTypeOf,
- APPROVAL_STATUS_LABELS,
- ESS_STATUS_CLASS,
- canEditEssApplication,
- } from "@/api/hr/essApplication";
- import { buildProfileForm, fetchUserProfile } from "@/api/hr/user";
- import { applicationModeMixin, APPLICATION_MODE } from "@/utils/hr/application-mode";
- import { positionCascadeMixin } from "@/utils/hr/position-cascade";
- import HrPickerRow from "@/components/hr/HrPickerRow.vue";
- import fileMain from "@/pages/doc/index.vue";
- import { back } from "@/utils/hr/router";
- // 调岗 / 转正有专用的发起页(接口齐全),本页的通用表单不再承接,直接转发(详情参数一并带过去)
- const REDIRECT_ROUTES = {
- transfer: "/pages/hr/manage/transfer-create",
- regular: "/pages/hr/manage/regular-create",
- };
- // 转发时透传的参数
- const CARRIED_KEYS = ["mode", "id", "businessId", "businessType", "processInstanceId", "admin"];
- const LEAVE_TYPE_ITEMS = ["年假", "事假", "病假", "调休假", "婚假"];
- // 时长字段各类型的标签文案(请假 / 加班 / 出差 / 外勤,补卡与离职无时长)
- const DURATION_LABELS = {
- leave: "请假时长",
- overtime: "加班时长",
- trip: "出差时长",
- field: "外出时长",
- };
- function todayIso() {
- const d = new Date();
- const pad = (n) => String(n).padStart(2, "0");
- return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
- }
- function carriedQuery(options = {}) {
- return CARRIED_KEYS.filter((key) => options[key] !== undefined && options[key] !== null && options[key] !== "")
- .map((key) => `${key}=${encodeURIComponent(String(options[key]))}`)
- .join("&");
- }
- function emptyForm(today) {
- return {
- // —— 通用(考勤类)——
- leaveType: "",
- startDate: today,
- startTime: "08:30",
- endDate: today,
- endTime: "17:30",
- reason: "",
- attachmentIds: [],
- repairType: "",
- repairTime: "",
- // —— 离职申请(字段与 PC 端「新建离职申请」一致)——
- resignationId: null,
- userId: "",
- name: "",
- employeeNo: "",
- phone: "",
- hireDate: "",
- groupId: "",
- company: "",
- deptId: "",
- department: "",
- positionSequenceId: "",
- positionSequence: "",
- positionId: "",
- position: "",
- positionType: "",
- positionTypeName: "",
- positionLevelId: "",
- positionLevel: "",
- employmentPeriodId: "",
- resignationType: "VOLUNTARY",
- applyDate: today,
- plannedLastWorkDate: "",
- effectiveDate: "",
- // 办理方式固定线下附件(页面上不做切换)
- handlingMode: "OFFLINE",
- onlineDocumentTemplateId: "",
- };
- }
- export default {
- components: { fileMain, HrPickerRow },
- mixins: [applicationModeMixin, positionCascadeMixin],
- data() {
- const today = todayIso();
- return {
- todayStr: today,
- type: "leave",
- currentUser: { id: "", name: "", initials: "", department: "", managerName: "" },
- form: emptyForm(today),
- resignTypeOptions: RESIGN_TYPE_OPTIONS,
- submitting: false,
- approvalError: "",
- sourceLoaded: false,
- profileLoaded: false,
- };
- },
- computed: {
- /** 统一选择行(HrPickerRow)的行度量:与本页 .field/.label 口径一致,避免左侧对不齐 */
- pickerRowMetrics() {
- return {
- rowMinHeight: "108rpx",
- rowPadding: "28rpx 28rpx",
- labelWidth: "180rpx",
- };
- },
- config() {
- return applicationTypes[this.type] || applicationTypes.leave;
- },
- navTitle() {
- return this.applicationTitle(this.config.title);
- },
- pageTitle() {
- return this.config.title;
- },
- isCheckOrResign() {
- return this.type === "check" || this.type === "resign";
- },
- /** 时长标签文案(请假时长 / 加班时长 / 出差时长 / 外出时长) */
- durationLabelName() {
- return DURATION_LABELS[this.type] || "时长";
- },
- /** 时长(分钟):按开始/结束时间自动算,不可手填,最低 0.5 小时(与提交载荷同源) */
- durationMinutes() {
- if (this.isCheckOrResign) return 0;
- const f = this.form;
- if (!f.startDate || !f.endDate || !f.startTime || !f.endTime) return 0;
- return computeDurationMinutes(f.startDate, f.startTime, f.endDate, f.endTime);
- },
- /** 时长展示:整小时显示「X小时」,其余按小时小数显示(如 0.5小时 / 1.5小时) */
- durationLabel() {
- if (!this.durationMinutes) return "—";
- const hours = this.durationMinutes / 60;
- return `${Number.isInteger(hours) ? hours : +hours.toFixed(2)}小时`;
- },
- isResignType() {
- return this.type === "resign";
- },
- reasonPlaceholder() {
- return this.type === "check"
- ? "请说明补卡原因"
- : "请说明申请原因";
- },
- repairTypeLabel() {
- return this.form.repairType ? REPAIR_TYPE_LABEL[this.form.repairType] : "请选择";
- },
- applicantNo() {
- return this.form.employeeNo || this.currentUser.employeeNo || "";
- },
- /** 请假类型候选(统一选择器用) */
- leaveTypeOptions() {
- return LEAVE_TYPE_ITEMS.map((item) => ({ label: item, value: item }));
- },
- /** 补卡类型候选 */
- repairTypeOptions() {
- return REPAIR_TYPE_OPTIONS.map((key) => ({ label: REPAIR_TYPE_LABEL[key], value: key }));
- },
- resignTypeLabel() {
- const code = this.form.resignationType;
- if (!code) return "请选择";
- return RESIGNATION_TYPE_LABELS[code] || code;
- },
- deptPlaceholder() {
- return this.form.groupId ? "请选择" : "请先选择所属机构";
- },
- sequencePlaceholder() {
- return this.form.deptId ? "请选择" : "请先选择所属部门";
- },
- positionPlaceholder() {
- return this.form.positionSequenceId ? "请选择" : "请先选择岗位序列";
- },
- levelPlaceholder() {
- return this.form.positionId ? "请选择" : "请先选择岗位名称";
- },
- // ============ 详情头部:优先 ESS 统一详情,未加载到时回落业务详情字段 ============
- hasEssView() {
- return Boolean(this.essDetail && this.essDetail.id);
- },
- businessApprovalStatus() {
- return String(this.form.approvalStatus || "").toUpperCase();
- },
- detailNo() {
- return this.hasEssView ? this.essRow.applicationNo || "—" : this.form.documentCode || "—";
- },
- detailStatusLabel() {
- if (this.hasEssView) return this.essRow.statusLabel || "—";
- return APPROVAL_STATUS_LABELS[this.businessApprovalStatus] || "—";
- },
- detailStatusClass() {
- if (this.hasEssView) return this.essRow.statusClass || "pending";
- return ESS_STATUS_CLASS[this.businessApprovalStatus] || "pending";
- },
- detailApplicant() {
- return this.hasEssView ? this.essRow.applicantName || "" : this.form.userName || "";
- },
- detailTitle() {
- return this.hasEssView ? this.essRow.title || "" : this.config.title;
- },
- /** 编辑资格:有 ESS 视图按统一状态判断;否则按业务审批状态(仅已驳回可编辑) */
- canEditApplication() {
- if (!this.isDetailMode || this.appAdmin) return false;
- if (this.hasEssView) return canEditEssApplication(this.essDetail || {}, false);
- return this.businessApprovalStatus === "REJECTED";
- },
- /**
- * 办理方式规范化。页面上不再提供「线上模板 / 线下附件」切换,
- * 新建时固定为 OFFLINE(提交时传给接口的值),仅回显旧数据时兼容为只读展示。
- */
- normalizedHandlingMode() {
- const mode = String(this.form.handlingMode || "").trim().toUpperCase();
- return mode === "ONLINE" ? "ONLINE" : "OFFLINE";
- },
- },
- onLoad(options) {
- const opts = options || {};
- const query = this.initApplicationMode(opts);
- // 统一申请里挂到本页的调岗/转正,转发到专用发起页
- const incomingType = opts.type || formTypeOf(query.businessType);
- const redirectUrl = REDIRECT_ROUTES[incomingType];
- if (redirectUrl) {
- const qs = carriedQuery(opts);
- uni.redirectTo({ url: qs ? `${redirectUrl}?${qs}` : redirectUrl });
- return;
- }
- this.type = applicationTypes[incomingType] ? incomingType : "leave";
- // 本页不直接承接 mode=edit(编辑由详情页内「编辑」按钮进入)
- if (this.appMode === APPLICATION_MODE.EDIT) this.appMode = APPLICATION_MODE.DETAIL;
- this.loadCurrentUser();
- uni.setNavigationBarTitle({ title: this.navTitle });
- if (this.isCreateMode) {
- this.syncType(opts);
- return;
- }
- this.initDetailMode();
- },
- onShow() {
- if (!this.isCreateMode) return;
- const pages = getCurrentPages();
- const last = pages && pages.length ? pages[pages.length - 1] : null;
- this.syncType((last && last.options) || {});
- },
- methods: {
- goBack() {
- back("/pages/index/index");
- },
- /**
- * 详情取数按入口区分:
- * 申请记录(appAdmin)→ 只查 ESS 管理端统一详情 GET /hr/ess/admin/applications/{record.id}
- * 我的申请(本人) → ESS 本人详情(头部:编号/状态/提交时间)+ 考勤业务详情(完整表单)
- * 离职两类仍保留原链路(ESS 统一详情 + 离职明细)
- */
- async initDetailMode() {
- uni.showLoading({ title: "加载中..." });
- try {
- if (this.isResignType) {
- await this.loadEssDetail(false);
- await this.loadDetailBusiness();
- } else if (this.appAdmin) {
- await this.loadEssDetail(false);
- } else {
- await this.loadEssDetail(false);
- await this.loadDetailBusiness();
- }
- } catch (error) {
- uni.showToast({
- title: (error && error.message) || "申请明细加载失败",
- icon: "none",
- });
- } finally {
- uni.hideLoading();
- }
- },
- loadCurrentUser() {
- const vo = uni.getStorageSync("userInfo") || {};
- const name = vo.userName || vo.name || "";
- this.currentUser = {
- id: vo.userId || vo.id || "",
- name,
- initials: name ? String(name).charAt(0) : "",
- department: vo.deptName || vo.department || "",
- managerName: vo.managerName || "",
- employeeNo: vo.jobNumber || vo.employeeNo || "",
- };
- if (!this.form.userId) this.form.userId = this.currentUser.id || "";
- if (!this.form.name) this.form.name = name;
- if (!this.form.employeeNo) this.form.employeeNo = this.currentUser.employeeNo || "";
- },
- syncType(options) {
- const next = (options && options.type) || "leave";
- if (this.type !== next) this.resetForm();
- this.type = next;
- uni.setNavigationBarTitle({ title: this.navTitle });
- const draft = getDraft(next);
- if (draft) Object.assign(this.form, draft);
- // 草稿兼容:老草稿缺时间字段时兜底默认值,避免校验误报
- if (!this.form.startTime) this.form.startTime = "08:30";
- if (!this.form.endTime) this.form.endTime = "17:30";
- if (next === "check" && !this.form.startDate) this.form.startDate = this.todayStr;
- // 草稿兼容:老草稿的 attachments 状态列表已废弃,统一迁移为附件 id 数组
- if (!Array.isArray(this.form.attachmentIds)) this.form.attachmentIds = [];
- if (Array.isArray(this.form.attachments)) {
- const ids = this.form.attachments
- .map((a) => (a && a.status === "success" && a.id != null ? String(a.id) : ""))
- .filter(Boolean);
- if (ids.length && !this.form.attachmentIds.length) this.form.attachmentIds = ids;
- delete this.form.attachments;
- }
- if (next === "resign") {
- if (!this.form.applyDate) this.form.applyDate = this.todayStr;
- if (!this.form.handlingMode) this.form.handlingMode = "OFFLINE";
- if (!this.form.resignationType) this.form.resignationType = "VOLUNTARY";
- }
- },
- resetForm() {
- this.form = emptyForm(this.todayStr);
- this.profileLoaded = false;
- },
- // ============ 详情 / 编辑模式 ============
- /** 只读详情:拉取来源业务明细用于展示完整字段 */
- async loadDetailBusiness() {
- await this.fetchSourceDetail(this.appBusinessId);
- },
- /** 按来源接口取业务明细并回填表单(详情展示 + 编辑态共用) */
- async fetchSourceDetail(businessId) {
- const id = businessId || this.appBusinessId;
- this.sourceLoaded = false;
- if (this.isResignType) {
- if (id === undefined || id === null || id === "") return null;
- const row = await loadResignationDetail(id);
- const raw = (row && row.raw) || row;
- if (!raw) return null;
- this.form = Object.assign(emptyForm(this.todayStr), hydrateResignationForm(raw));
- this.sourceLoaded = true;
- // 回显后预热级联下拉,保证机构 / 部门 / 岗位名称都能显示
- await this.primeCascade();
- return raw;
- }
- const source = await loadAttendanceApplicationForEdit(id, this.appProcessInstanceId);
- if (!source) return null;
- Object.assign(this.form, hydrateAttendanceForm(source, this.type));
- this.sourceLoaded = true;
- return source;
- },
- async enterEditMode() {
- if (!this.canEditApplication) return;
- uni.showLoading({ title: "加载中..." });
- try {
- await this.fetchSourceDetail(this.appBusinessId);
- if (!this.sourceLoaded) throw new Error("未获取到申请明细,暂时无法编辑");
- this.appMode = APPLICATION_MODE.EDIT;
- uni.setNavigationBarTitle({ title: this.navTitle });
- } catch (error) {
- uni.showToast({
- title: (error && error.message) || "申请明细加载失败,无法编辑",
- icon: "none",
- });
- return;
- } finally {
- uni.hideLoading();
- }
- },
- // ============ 编辑交互 ============
- onPick(key, e) {
- if (this.isReadonly) return;
- this.form[key] = (e && e.detail && e.detail.value) || "";
- },
- onSwitch(key, e) {
- if (this.isReadonly) return;
- this.form[key] = !!(e && e.detail && e.detail.value);
- },
- onPhoneInput(e) {
- if (this.isReadonly) return;
- this.form.phone = String((e && e.detail && e.detail.value) || "").slice(0, 11);
- },
- onLeaveTypeChange(leaveType) {
- if (this.isReadonly) return;
- this.form.leaveType = leaveType;
- },
- onRepairTypeChange(repairType) {
- if (this.isReadonly) return;
- this.form.repairType = repairType;
- },
- onResignTypeChange(resignationType) {
- if (this.isReadonly) return;
- this.form.resignationType = resignationType;
- },
- // ============ 员工档案自动带出(对齐 PC「选择离职人员后自动带出档案」)============
- async loadResignProfile() {
- const userId = this.form.userId || this.currentUser.id;
- if (!userId || this.profileLoaded) return;
- this.profileLoaded = true;
- try {
- const raw = await fetchUserProfile(userId, false);
- const p = buildProfileForm(raw || {});
- this.form.userId = userId;
- this.form.employeeNo = this.form.employeeNo || p.employeeNo || "";
- if (!this.form.phone) this.form.phone = p.phone || "";
- this.form.hireDate = this.form.hireDate || p.hireDate || "";
- if (!this.form.groupId) {
- this.form.groupId = p.groupId || "";
- this.form.company = p.company || "";
- }
- if (!this.form.deptId) {
- this.form.deptId = p.deptId || "";
- this.form.department = p.deptName || "";
- }
- if (!this.form.positionSequenceId) {
- this.form.positionSequenceId = p.positionSequenceId || "";
- this.form.positionSequence = p.positionSequenceName || "";
- }
- if (!this.form.positionId) {
- this.form.positionId = p.positionId || "";
- this.form.position = p.positionName || "";
- }
- if (!this.form.positionTypeName) {
- this.form.positionTypeName = p.employmentTypeName || p.employmentType || "";
- this.form.positionType = p.employmentType || "";
- }
- if (!this.form.positionLevel) this.form.positionLevel = p.staffingLevel || "";
- } catch (error) {
- // 档案拉取失败时仍可手工选择机构 / 部门 / 岗位
- }
- // 机构 / 部门候选与档案接口是两条独立链路:档案失败也必须把级联预热出来,
- // 否则「所属机构」下拉是空的(用户点开看不到任何选项)
- try {
- await this.initCascadeOrg();
- await this.primeCascade();
- } catch (error) {
- // 级联预热失败不阻断表单填写
- }
- },
- // ============ 校验与提交 ============
- validate() {
- const form = this.form;
- if (this.type === "leave" && !form.leaveType) return "请选择请假类型";
- if (this.type === "check") {
- if (!form.startDate) return "请选择补卡日期";
- if (!form.repairType) return "请选择补卡类型";
- if (!form.repairTime) return "请选择打卡时间";
- if (!form.reason) return "请填写补卡原因";
- }
- if (this.type === "resign") {
- if (!form.userId) return "缺少申请人信息,请重新进入页面";
- if (!/^1[3-9]\d{9}$/.test(String(form.phone || "").trim())) return "请输入正确的 11 位手机号码";
- if (!form.groupId) return "请选择所属机构";
- if (!form.deptId) return "请选择所属部门";
- if (!form.positionSequenceId) return "请选择岗位序列";
- if (!form.positionId) return "请选择岗位名称";
- // 岗位性质:PC 的 cascadeRequiredRules 里为必填(选岗位后由接口自动带出)
- if (!form.positionType) return "请先选择岗位名称以自动带出岗位性质";
- if (!form.positionLevelId) return "请选择岗位层级";
- if (!form.resignationType) return "请选择离职类型";
- if (!form.applyDate) return "请选择申请日期";
- if (!form.plannedLastWorkDate) return "请选择计划最后工作日";
- if (form.plannedLastWorkDate < form.applyDate) return "计划最后工作日不能早于申请日期";
- if (form.effectiveDate && form.effectiveDate < form.plannedLastWorkDate) {
- return "实际生效日期不能早于计划最后工作日";
- }
- if (!form.reason) return "请填写离职原因";
- if (this.normalizedHandlingMode === "OFFLINE" && !attachmentsOf(form).length) {
- return "线下办理方式请上传离职申请附件";
- }
- return "";
- }
- if (!this.isCheckOrResign) {
- if (!form.startDate) return "请选择开始日期";
- if (!form.endDate) return "请选择结束日期";
- if (!form.startTime) return "请选择开始时间";
- if (!form.endTime) return "请选择结束时间";
- if (form.endDate < form.startDate) return "结束日期不能早于开始日期";
- if (form.endDate === form.startDate && form.endTime <= form.startTime)
- return "结束时间不能早于或等于开始时间";
- if (!form.reason) return "请填写申请事由";
- }
- return "";
- },
- buildAttendanceVariables(payload) {
- return Object.assign({}, payload, {
- businessType: ATTENDANCE_BUSINESS_TYPE[this.type] || this.type,
- businessName: `${this.config.title} - ${this.currentUser.name || ""}`,
- deptName: this.currentUser.department || "",
- });
- },
- /** 考勤类:创建或更新申请单 → 发起/重新发起审批 */
- async submitAttendanceFlow() {
- const attachmentFileIds = attachmentsOf(this.form).join(",");
- const payload = buildAttendancePayload(this.form, this.type, this.currentUser, attachmentFileIds);
- if (!payload) throw new Error("申请类型不支持");
- if (this.isEditMode && this.appBusinessId) {
- // 编辑被驳回的申请:更新原单后沿用同一 businessId 重新发起审批
- await updateAttendanceApplication(this.appBusinessId, payload, false);
- try {
- await startAttendanceApproval(this.appBusinessId, this.buildAttendanceVariables(payload), false, this.type);
- } catch (e) {
- this.approvalError = (e && e.message) || "审批流程发起失败";
- console.log("[考勤申请] 审批未发起(编辑重提):", this.approvalError);
- }
- return;
- }
- // 建单接口返回结构可能不同(标量主键 / { id } / { applicationId }),统一提取
- const created = await submitAttendanceApplication(payload, false);
- const applicationId = pickApplicationId(created);
- if (!applicationId) {
- throw new Error(`申请已提交但未返回申请主键(返回:${JSON.stringify(created)}),请联系管理员`);
- }
- console.log("[考勤申请] 建单成功:", this.type, applicationId);
- try {
- await startAttendanceApproval(applicationId, this.buildAttendanceVariables(payload), false, this.type);
- console.log("[考勤申请] 审批发起成功:", this.type, applicationId);
- } catch (e) {
- this.approvalError = (e && e.message) || "审批流程发起失败";
- console.log("[考勤申请] 审批未发起:", this.type, this.approvalError);
- }
- this.form.applicationId = applicationId;
- },
- /**
- * 离职:保存离职单后直接发起审批(对齐 PC saveAndSubmit → launchApproval)。
- * 后端 /main/resignations 无 /submit 端点,审批统一走 BPM main_resignation_approval。
- */
- async submitResignFlow() {
- const existingId = this.isEditMode
- ? this.appBusinessId
- : this.form.resignationId || null;
- const result = await submitResignationFlow({
- form: Object.assign({}, this.form, {
- name: this.form.name || this.currentUser.name,
- userId: this.form.userId || this.currentUser.id,
- // 页面已取消「线上模板 / 线下附件」切换,办理方式固定线下附件
- handlingMode: "OFFLINE",
- }),
- existingId,
- });
- if (result.id != null) {
- this.form.resignationId = result.id;
- this.form.applicationId = result.id;
- rememberResignId(String(result.id));
- }
- if (!result.approvalStarted) {
- this.approvalError = result.approvalError || "审批流程发起失败";
- }
- },
- async submitApplication() {
- if (this.submitting) return;
- const error = this.validate();
- if (error) {
- uni.showToast({ title: error, icon: "none" });
- return;
- }
- this.submitting = true;
- this.approvalError = "";
- uni.showLoading({ title: "提交中..." });
- try {
- if (this.isResignType) await this.submitResignFlow();
- else await this.submitAttendanceFlow();
- } catch (e) {
- uni.hideLoading();
- this.submitting = false;
- uni.showToast({ title: (e && e.message) || "提交失败", icon: "none" });
- return;
- }
- uni.hideLoading();
- this.submitting = false;
- this.afterSubmitted();
- },
- afterSubmitted() {
- const isEdit = this.isEditMode;
- if (!isEdit) {
- removeDraft(this.type);
- this.recordLocalApplication();
- // 补卡:返回考勤页,由 onShow 重新拉月度 + 当日详情
- if (this.type === "check") {
- const pages = getCurrentPages();
- if (pages.length > 1) {
- uni.showToast({ title: "补卡申请已提交", icon: "success" });
- setTimeout(() => uni.navigateBack({ delta: 1 }), 600);
- return;
- }
- }
- }
- const title = isEdit ? "修改成功" : this.approvalError ? "申请已提交" : "提交成功";
- let content;
- if (this.approvalError) {
- content = isEdit
- ? `申请内容已更新,但审批流程未重新发起:${this.approvalError}。请联系管理员处理。`
- : `申请已保存,但审批流程未发起:${this.approvalError}。请稍后在“我的申请”中确认,或联系管理员。`;
- } else if (isEdit) {
- content = "申请已修改并重新提交审批,可在“我的申请”中查看进度。";
- } else if (this.currentUser.managerName) {
- content = `申请已提交给${this.currentUser.managerName}审批,可在“我的申请”中查看进度。`;
- } else {
- content = "申请已提交,可在“我的申请”中查看审批进度。";
- }
- uni.showModal({
- title,
- content,
- showCancel: false,
- success: () => this.backToApplicationList("/pages/hr/apply/list"),
- });
- },
- /** 本地留痕(仅创建态;列表已改用服务端接口,此处保留兼容) */
- recordLocalApplication() {
- if (this.type === "resign") {
- addApplication({
- id: this.form.applicationId || undefined,
- type: this.config.title,
- summary: this.summary(),
- reason: this.form.reason || "",
- color: this.config.color,
- metadata: {
- employeeId: this.form.userId || this.currentUser.id,
- resignationType: this.form.resignationType,
- plannedLastWorkDate: this.form.plannedLastWorkDate,
- handlingMode: this.normalizedHandlingMode,
- source: "server",
- serverId: this.form.applicationId || undefined,
- },
- });
- return;
- }
- addApplication({
- type: this.config.title,
- summary: this.summary(),
- reason: this.form.reason || "用于个人材料办理",
- color: this.config.color,
- });
- },
- summary() {
- if (this.type === "check") {
- const typeLabel = this.form.repairType ? REPAIR_TYPE_LABEL[this.form.repairType] : "补卡";
- return `${this.form.startDate || ""} · ${typeLabel} · ${this.form.repairTime || ""}`;
- }
- if (this.type === "resign") {
- return `${this.resignTypeLabel} · ${this.form.plannedLastWorkDate || this.form.applyDate || ""}`;
- }
- return `${this.form.startDate} 至 ${this.form.endDate}`;
- },
- },
- watch: {
- /** 进入离职申请创建态时带出本人档案,减少手工填写 */
- isResignType: {
- immediate: true,
- handler(val) {
- if (!val || !this.isCreateMode) return;
- this.$nextTick(() => this.loadResignProfile());
- },
- },
- },
- };
- /** 附件数组 → 非空 id 列表 */
- function attachmentsOf(form = {}) {
- return (Array.isArray(form.attachmentIds) ? form.attachmentIds : []).filter(Boolean).map((id) => String(id));
- }
- </script>
- <style scoped lang="scss">
- @import "@/pages/hr/common-style.scss";
- .page-no-tab {
- padding-bottom: 140rpx;
- }
- /* 只读态:隐藏所有可点击指示箭头 */
- .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;
- }
- .form-card {
- overflow: hidden;
- margin-top: 20rpx;
- }
- /* .section-head / .section-title / .section-tip 统一取自
- @/pages/hr/common-style.scss,本页不再单独覆盖。 */
- .field {
- position: relative;
- min-height: 108rpx;
- padding: 28rpx 28rpx;
- display: flex;
- align-items: center;
- }
- .label {
- /* 「计划最后工作日」7 个字在 180rpx 下会折行 → 加宽并禁止换行 */
- 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 {
- color: #1f2329;
- font-size: 27rpx;
- }
- .readonly-text {
- margin-left: auto;
- color: #8b939f;
- }
- .placeholder {
- color: #b2b7be;
- }
- .arrow {
- margin-left: 13rpx;
- color: #c4c8ce;
- font-size: 38rpx;
- }
- .textarea-field {
- display: block;
- min-height: 260rpx;
- }
- .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;
- }
- .field-file {
- flex: 1;
- display: flex;
- justify-content: flex-end;
- }
- /* 办理方式固定为线下附件(不提供线上模板切换),相关切换样式已移除 */
- .bottom-action {
- position: fixed;
- z-index: 10;
- left: 0;
- right: 0;
- bottom: 0;
- height: 120rpx;
- padding: 16rpx 24rpx;
- display: flex;
- 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%);
- }
- }
- .input-flex {
- flex: 1;
- text-align: right;
- font-size: 27rpx;
- color: #1f2329;
- padding: 0 6rpx;
- }
- .small-textarea {
- min-height: 180rpx;
- }
- .small-textarea textarea {
- height: 100rpx;
- }
- .check-field {
- display: block;
- align-items: flex-start;
- }
- .check-field .label {
- display: block;
- width: auto;
- margin-bottom: 16rpx;
- }
- .check-list {
- display: flex;
- flex-wrap: wrap;
- padding-top: 4rpx;
- }
- .check-row {
- display: flex;
- align-items: center;
- margin: 0 24rpx 18rpx 0;
- font-size: 26rpx;
- color: #4b5563;
- }
- .checkbox {
- width: 32rpx;
- height: 32rpx;
- margin-right: 10rpx;
- border: 1rpx solid #c4c8ce;
- border-radius: 6rpx;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 22rpx;
- color: #fff;
- background: #fff;
- }
- .checkbox.is-checked {
- background: #157a2c;
- border-color: #157a2c;
- }
- .switch-field {
- justify-content: space-between;
- }
- </style>
|