form.vue 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257
  1. <template>
  2. <view class="page-no-tab" :class="{ 'readonly-mode': isReadonly }">
  3. <uni-nav-bar
  4. fixed="true"
  5. statusBar="true"
  6. left-icon="back"
  7. :title="navTitle"
  8. background-color="#157A2C"
  9. color="#fff"
  10. @clickLeft="goBack"
  11. ></uni-nav-bar>
  12. <!-- 详情态:统一申请视图(编号 / 状态 / 当前节点 / 提交时间) -->
  13. <view v-if="showDetailHeader" class="detail-head card section">
  14. <view class="detail-head-top">
  15. <text class="detail-title">{{ detailTitle || config.title }}</text>
  16. <text class="status-tag" :class="'status-' + detailStatusClass">{{ detailStatusLabel }}</text>
  17. </view>
  18. <view class="detail-row">
  19. <text class="detail-label">申请编号</text>
  20. <text class="detail-value">{{ detailNo }}</text>
  21. </view>
  22. <view class="detail-row">
  23. <text class="detail-label">申请人</text>
  24. <text class="detail-value">{{ detailApplicant || currentUser.name || '—' }}</text>
  25. </view>
  26. <view class="detail-row">
  27. <text class="detail-label">提交时间</text>
  28. <text class="detail-value">{{ detailCreatedAt || '—' }}</text>
  29. </view>
  30. <view v-if="detailNode" class="detail-row">
  31. <text class="detail-label">当前节点</text>
  32. <text class="detail-value">{{ detailNode }}</text>
  33. </view>
  34. </view>
  35. <view v-if="essRow.maskedSummary && isReadonly" class="masked-card card section">
  36. <text class="masked-label">申请摘要</text>
  37. <text class="masked-text">{{ essRow.maskedSummary }}</text>
  38. </view>
  39. <view v-if="appAdmin && !isResignType && isReadonly" class="masked-card card section">
  40. <text class="masked-label">说明</text>
  41. <text class="masked-text">管理端仅展示统一申请信息(申请人 / 组织 / 状态 / 摘要),业务明细以来源单据为准。</text>
  42. </view>
  43. <!-- ============ 离职申请:字段与 PC 端「新建离职申请」一致 ============ -->
  44. <block v-if="isResignType">
  45. <!-- 员工信息 -->
  46. <view class="form-card card section">
  47. <view class="section-head">
  48. <view class="section-title"><text>员工信息</text></view>
  49. <text class="section-tip">按员工档案自动带出,可修改</text>
  50. </view>
  51. <view class="field">
  52. <text class="label required">申请员工</text>
  53. <view class="readonly">
  54. <view class="avatar small-avatar">{{ (detailApplicant || currentUser.name || '—').charAt(0) }}</view>
  55. <text>{{ detailApplicant || currentUser.name || '—' }}{{ applicantNo ? ' · ' + applicantNo : '' }}</text>
  56. </view>
  57. </view>
  58. <view class="divider"></view>
  59. <view class="field">
  60. <text class="label">入职日期</text>
  61. <text class="value readonly-text">{{ form.hireDate || "未填写" }}</text>
  62. </view>
  63. <view class="divider"></view>
  64. <view class="field">
  65. <text class="label required">手机号</text>
  66. <input
  67. class="input-flex"
  68. type="number"
  69. maxlength="11"
  70. :disabled="formDisabled"
  71. :value="form.phone"
  72. placeholder="请输入 11 位手机号码"
  73. @input="onPhoneInput"
  74. />
  75. </view>
  76. <view class="divider"></view>
  77. <hr-picker-row
  78. v-bind="pickerRowMetrics"
  79. label="所属机构"
  80. required
  81. :value="form.groupId"
  82. :display="form.company"
  83. :options="legalOptions.map((item) => ({ label: item.name, value: item.id }))"
  84. hint="机构数据加载中"
  85. @change="onCascadeLegalChange"
  86. />
  87. <view class="divider"></view>
  88. <hr-picker-row
  89. v-bind="pickerRowMetrics"
  90. mode="tree"
  91. label="所属部门"
  92. required
  93. :value="form.deptId"
  94. :display="form.department"
  95. :tree="cascadeDeptTree"
  96. :disabled="!form.groupId"
  97. :hint="deptPlaceholder"
  98. @change="onCascadeDeptPick"
  99. />
  100. <view class="divider"></view>
  101. <hr-picker-row
  102. v-bind="pickerRowMetrics"
  103. label="岗位序列"
  104. required
  105. :value="form.positionSequenceId"
  106. :display="form.positionSequence"
  107. :options="sequenceOptions.map((item) => ({ label: item.name, value: item.id }))"
  108. :disabled="!form.deptId"
  109. :hint="sequencePlaceholder"
  110. @change="onCascadeSequenceChange"
  111. />
  112. <view class="divider"></view>
  113. <hr-picker-row
  114. v-bind="pickerRowMetrics"
  115. label="岗位名称"
  116. required
  117. :value="form.positionId"
  118. :display="form.position"
  119. :options="cascadePositionOptions.map((item) => ({ label: item.name, value: item.id }))"
  120. :disabled="!form.positionSequenceId"
  121. :hint="positionPlaceholder"
  122. @change="onCascadePositionChange"
  123. />
  124. <view class="divider"></view>
  125. <view class="field">
  126. <text class="label required">岗位性质</text>
  127. <text class="value readonly-text">{{ form.positionTypeName || "选择岗位名称后自动带出" }}</text>
  128. </view>
  129. <view class="divider"></view>
  130. <hr-picker-row
  131. v-bind="pickerRowMetrics"
  132. label="岗位层级"
  133. required
  134. :value="form.positionLevelId"
  135. :display="form.positionLevel"
  136. :options="levelOptions.map((item) => ({ label: item.name, value: item.id }))"
  137. :disabled="!form.positionId"
  138. :hint="levelPlaceholder"
  139. @change="onCascadeLevelChange"
  140. />
  141. </view>
  142. <!-- 离职信息 -->
  143. <view class="form-card card section">
  144. <view class="section-head">
  145. <view class="section-title"><text>离职信息</text></view>
  146. <text class="section-tip">填写离职类型与关键日期,办理方式默认为线下附件</text>
  147. </view>
  148. <hr-picker-row
  149. v-bind="pickerRowMetrics"
  150. label="离职类型"
  151. required
  152. :value="form.resignationType"
  153. :display="resignTypeLabel"
  154. :options="resignTypeOptions"
  155. :disabled="isReadonly"
  156. @change="onResignTypeChange"
  157. />
  158. <view class="divider"></view>
  159. <view class="field">
  160. <text class="label required">申请日期</text>
  161. <picker class="field-picker" :disabled="formDisabled" mode="date" :value="form.applyDate" @change="onPick('applyDate', $event)">
  162. <text :class="form.applyDate ? 'value' : 'placeholder'">{{ form.applyDate || "请选择" }}</text>
  163. </picker>
  164. <text class="arrow">›</text>
  165. </view>
  166. <view class="divider"></view>
  167. <view class="field">
  168. <text class="label required">计划最后工作日</text>
  169. <picker class="field-picker"
  170. :disabled="formDisabled"
  171. mode="date"
  172. :value="form.plannedLastWorkDate"
  173. @change="onPick('plannedLastWorkDate', $event)"
  174. >
  175. <text :class="form.plannedLastWorkDate ? 'value' : 'placeholder'">
  176. {{ form.plannedLastWorkDate || "请选择" }}
  177. </text>
  178. </picker>
  179. <text class="arrow">›</text>
  180. </view>
  181. <view class="divider"></view>
  182. <view class="field">
  183. <text class="label">实际生效日期</text>
  184. <picker class="field-picker" :disabled="formDisabled" mode="date" :value="form.effectiveDate" @change="onPick('effectiveDate', $event)">
  185. <text :class="form.effectiveDate ? 'value' : 'placeholder'">
  186. {{ form.effectiveDate || "为空按最后工作日生效" }}
  187. </text>
  188. </picker>
  189. <text class="arrow">›</text>
  190. </view>
  191. <view class="divider"></view>
  192. <view class="field file-field">
  193. <text class="label required">离职申请附件</text>
  194. <view class="field-file">
  195. <fileMain v-model="form.attachmentIds" :type="isReadonly ? 'view' : ''" />
  196. </view>
  197. </view>
  198. </view>
  199. <!-- 离职原因 -->
  200. <view class="form-card card section">
  201. <view class="section-head">
  202. <view class="section-title"><text>离职原因</text></view>
  203. <text class="section-tip">请详细描述离职原因</text>
  204. </view>
  205. <view class="field textarea-field">
  206. <textarea v-model="form.reason" :disabled="formDisabled" maxlength="200" placeholder="请填写离职原因" />
  207. <text class="counter">{{ (form.reason || "").length }}/200</text>
  208. </view>
  209. </view>
  210. </block>
  211. <!-- ============ 其他考勤类申请(请假 / 加班 / 补卡 / 外勤 / 出差) ============ -->
  212. <block v-else-if="!appAdmin">
  213. <view class="form-card card section">
  214. <view class="field">
  215. <text class="label">申请人</text>
  216. <view class="readonly">
  217. <view class="avatar small-avatar">{{ (detailApplicant || currentUser.name || '—').charAt(0) }}</view>
  218. <text>{{ detailApplicant || currentUser.name || '—' }} · {{ currentUser.department || '—' }}</text>
  219. </view>
  220. </view>
  221. <view class="divider"></view>
  222. <hr-picker-row
  223. v-bind="pickerRowMetrics"
  224. v-if="type === 'leave'"
  225. label="请假类型"
  226. required
  227. :value="form.leaveType"
  228. :display="form.leaveType"
  229. :options="leaveTypeOptions"
  230. :disabled="isReadonly"
  231. @change="onLeaveTypeChange"
  232. />
  233. <block v-if="type === 'check'">
  234. <view class="field">
  235. <text class="label required">补卡日期</text>
  236. <picker class="field-picker" :disabled="formDisabled" mode="date" :value="form.startDate" @change="onPick('startDate', $event)">
  237. <text class="value">{{ form.startDate || "—" }}</text>
  238. </picker>
  239. <text class="arrow">›</text>
  240. </view>
  241. <view class="divider"></view>
  242. <hr-picker-row
  243. v-bind="pickerRowMetrics"
  244. label="补卡类型"
  245. required
  246. :value="form.repairType"
  247. :display="repairTypeLabel"
  248. :options="repairTypeOptions"
  249. :disabled="isReadonly"
  250. @change="onRepairTypeChange"
  251. />
  252. <view class="divider"></view>
  253. <view class="field">
  254. <text class="label required">补卡时间</text>
  255. <picker class="field-picker" :disabled="formDisabled" mode="time" :value="form.repairTime" @change="onPick('repairTime', $event)">
  256. <text class="value">{{ form.repairTime || "—" }}</text>
  257. </picker>
  258. <text class="arrow">›</text>
  259. </view>
  260. </block>
  261. <block v-if="!isCheckOrResign">
  262. <view class="field">
  263. <text class="label required">开始日期</text>
  264. <picker class="field-picker" :disabled="formDisabled" mode="date" :value="form.startDate" @change="onPick('startDate', $event)">
  265. <text class="value">{{ form.startDate || "—" }}</text>
  266. </picker>
  267. <text class="arrow">›</text>
  268. </view>
  269. <view class="divider"></view>
  270. <view class="field">
  271. <text class="label required">开始时间</text>
  272. <picker class="field-picker" :disabled="formDisabled" mode="time" :value="form.startTime" @change="onPick('startTime', $event)">
  273. <text class="value">{{ form.startTime || "—" }}</text>
  274. </picker>
  275. <text class="arrow">›</text>
  276. </view>
  277. <view class="divider"></view>
  278. <view class="field">
  279. <text class="label required">结束日期</text>
  280. <picker class="field-picker" :disabled="formDisabled" mode="date" :value="form.endDate" @change="onPick('endDate', $event)">
  281. <text class="value">{{ form.endDate || "—" }}</text>
  282. </picker>
  283. <text class="arrow">›</text>
  284. </view>
  285. <view class="divider"></view>
  286. <view class="field">
  287. <text class="label required">结束时间</text>
  288. <picker class="field-picker" :disabled="formDisabled" mode="time" :value="form.endTime" @change="onPick('endTime', $event)">
  289. <text class="value">{{ form.endTime || "—" }}</text>
  290. </picker>
  291. <text class="arrow">›</text>
  292. </view>
  293. <view class="divider"></view>
  294. <view class="field">
  295. <text class="label">{{ durationLabelName }}</text>
  296. <text class="value readonly-text">{{ durationLabel }}</text>
  297. </view>
  298. </block>
  299. <view class="field textarea-field">
  300. <text class="label required">申请事由</text>
  301. <textarea v-model="form.reason" :disabled="formDisabled" maxlength="200" :placeholder="reasonPlaceholder" />
  302. <text class="counter">{{ (form.reason || "").length }}/200</text>
  303. </view>
  304. </view>
  305. <view class="form-card card section">
  306. <view class="field">
  307. <text class="label">附件</text>
  308. <view class="field-file">
  309. <fileMain v-model="form.attachmentIds" :type="isReadonly ? 'view' : ''" />
  310. </view>
  311. </view>
  312. <view class="divider"></view>
  313. </view>
  314. </block>
  315. <view v-if="showBottomBar" class="bottom-action safe-bottom">
  316. <button class="submit-btn" @click="onBottomAction">{{ bottomButtonText }}</button>
  317. </view>
  318. </view>
  319. </template>
  320. <script>
  321. import { applicationTypes } from "@/pages/hr/data/apply-config";
  322. import { getDraft, removeDraft, addApplication, rememberResignId } from "@/utils/hr/storage";
  323. import {
  324. ATTENDANCE_BUSINESS_TYPE,
  325. REPAIR_TYPE_LABEL,
  326. REPAIR_TYPE_OPTIONS,
  327. buildAttendancePayload,
  328. computeDurationMinutes,
  329. hydrateAttendanceForm,
  330. loadAttendanceApplicationForEdit,
  331. pickApplicationId,
  332. startAttendanceApproval,
  333. submitAttendanceApplication,
  334. updateAttendanceApplication,
  335. } from "@/api/hr/attendanceApply";
  336. import {
  337. RESIGN_TYPE_OPTIONS,
  338. RESIGNATION_TYPE_LABELS,
  339. hydrateResignationForm,
  340. loadResignationDetail,
  341. submitResignationFlow,
  342. } from "@/api/hr/resign";
  343. import {
  344. formTypeOf,
  345. APPROVAL_STATUS_LABELS,
  346. ESS_STATUS_CLASS,
  347. canEditEssApplication,
  348. } from "@/api/hr/essApplication";
  349. import { buildProfileForm, fetchUserProfile } from "@/api/hr/user";
  350. import { applicationModeMixin, APPLICATION_MODE } from "@/utils/hr/application-mode";
  351. import { positionCascadeMixin } from "@/utils/hr/position-cascade";
  352. import HrPickerRow from "@/components/hr/HrPickerRow.vue";
  353. import fileMain from "@/pages/doc/index.vue";
  354. import { back } from "@/utils/hr/router";
  355. // 调岗 / 转正有专用的发起页(接口齐全),本页的通用表单不再承接,直接转发(详情参数一并带过去)
  356. const REDIRECT_ROUTES = {
  357. transfer: "/pages/hr/manage/transfer-create",
  358. regular: "/pages/hr/manage/regular-create",
  359. };
  360. // 转发时透传的参数
  361. const CARRIED_KEYS = ["mode", "id", "businessId", "businessType", "processInstanceId", "admin"];
  362. const LEAVE_TYPE_ITEMS = ["年假", "事假", "病假", "调休假", "婚假"];
  363. // 时长字段各类型的标签文案(请假 / 加班 / 出差 / 外勤,补卡与离职无时长)
  364. const DURATION_LABELS = {
  365. leave: "请假时长",
  366. overtime: "加班时长",
  367. trip: "出差时长",
  368. field: "外出时长",
  369. };
  370. function todayIso() {
  371. const d = new Date();
  372. const pad = (n) => String(n).padStart(2, "0");
  373. return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
  374. }
  375. function carriedQuery(options = {}) {
  376. return CARRIED_KEYS.filter((key) => options[key] !== undefined && options[key] !== null && options[key] !== "")
  377. .map((key) => `${key}=${encodeURIComponent(String(options[key]))}`)
  378. .join("&");
  379. }
  380. function emptyForm(today) {
  381. return {
  382. // —— 通用(考勤类)——
  383. leaveType: "",
  384. startDate: today,
  385. startTime: "08:30",
  386. endDate: today,
  387. endTime: "17:30",
  388. reason: "",
  389. attachmentIds: [],
  390. repairType: "",
  391. repairTime: "",
  392. // —— 离职申请(字段与 PC 端「新建离职申请」一致)——
  393. resignationId: null,
  394. userId: "",
  395. name: "",
  396. employeeNo: "",
  397. phone: "",
  398. hireDate: "",
  399. groupId: "",
  400. company: "",
  401. deptId: "",
  402. department: "",
  403. positionSequenceId: "",
  404. positionSequence: "",
  405. positionId: "",
  406. position: "",
  407. positionType: "",
  408. positionTypeName: "",
  409. positionLevelId: "",
  410. positionLevel: "",
  411. employmentPeriodId: "",
  412. resignationType: "VOLUNTARY",
  413. applyDate: today,
  414. plannedLastWorkDate: "",
  415. effectiveDate: "",
  416. // 办理方式固定线下附件(页面上不做切换)
  417. handlingMode: "OFFLINE",
  418. onlineDocumentTemplateId: "",
  419. };
  420. }
  421. export default {
  422. components: { fileMain, HrPickerRow },
  423. mixins: [applicationModeMixin, positionCascadeMixin],
  424. data() {
  425. const today = todayIso();
  426. return {
  427. todayStr: today,
  428. type: "leave",
  429. currentUser: { id: "", name: "", initials: "", department: "", managerName: "" },
  430. form: emptyForm(today),
  431. resignTypeOptions: RESIGN_TYPE_OPTIONS,
  432. submitting: false,
  433. approvalError: "",
  434. sourceLoaded: false,
  435. profileLoaded: false,
  436. };
  437. },
  438. computed: {
  439. /** 统一选择行(HrPickerRow)的行度量:与本页 .field/.label 口径一致,避免左侧对不齐 */
  440. pickerRowMetrics() {
  441. return {
  442. rowMinHeight: "108rpx",
  443. rowPadding: "28rpx 28rpx",
  444. labelWidth: "180rpx",
  445. };
  446. },
  447. config() {
  448. return applicationTypes[this.type] || applicationTypes.leave;
  449. },
  450. navTitle() {
  451. return this.applicationTitle(this.config.title);
  452. },
  453. pageTitle() {
  454. return this.config.title;
  455. },
  456. isCheckOrResign() {
  457. return this.type === "check" || this.type === "resign";
  458. },
  459. /** 时长标签文案(请假时长 / 加班时长 / 出差时长 / 外出时长) */
  460. durationLabelName() {
  461. return DURATION_LABELS[this.type] || "时长";
  462. },
  463. /** 时长(分钟):按开始/结束时间自动算,不可手填,最低 0.5 小时(与提交载荷同源) */
  464. durationMinutes() {
  465. if (this.isCheckOrResign) return 0;
  466. const f = this.form;
  467. if (!f.startDate || !f.endDate || !f.startTime || !f.endTime) return 0;
  468. return computeDurationMinutes(f.startDate, f.startTime, f.endDate, f.endTime);
  469. },
  470. /** 时长展示:整小时显示「X小时」,其余按小时小数显示(如 0.5小时 / 1.5小时) */
  471. durationLabel() {
  472. if (!this.durationMinutes) return "—";
  473. const hours = this.durationMinutes / 60;
  474. return `${Number.isInteger(hours) ? hours : +hours.toFixed(2)}小时`;
  475. },
  476. isResignType() {
  477. return this.type === "resign";
  478. },
  479. reasonPlaceholder() {
  480. return this.type === "check"
  481. ? "请说明补卡原因"
  482. : "请说明申请原因";
  483. },
  484. repairTypeLabel() {
  485. return this.form.repairType ? REPAIR_TYPE_LABEL[this.form.repairType] : "请选择";
  486. },
  487. applicantNo() {
  488. return this.form.employeeNo || this.currentUser.employeeNo || "";
  489. },
  490. /** 请假类型候选(统一选择器用) */
  491. leaveTypeOptions() {
  492. return LEAVE_TYPE_ITEMS.map((item) => ({ label: item, value: item }));
  493. },
  494. /** 补卡类型候选 */
  495. repairTypeOptions() {
  496. return REPAIR_TYPE_OPTIONS.map((key) => ({ label: REPAIR_TYPE_LABEL[key], value: key }));
  497. },
  498. resignTypeLabel() {
  499. const code = this.form.resignationType;
  500. if (!code) return "请选择";
  501. return RESIGNATION_TYPE_LABELS[code] || code;
  502. },
  503. deptPlaceholder() {
  504. return this.form.groupId ? "请选择" : "请先选择所属机构";
  505. },
  506. sequencePlaceholder() {
  507. return this.form.deptId ? "请选择" : "请先选择所属部门";
  508. },
  509. positionPlaceholder() {
  510. return this.form.positionSequenceId ? "请选择" : "请先选择岗位序列";
  511. },
  512. levelPlaceholder() {
  513. return this.form.positionId ? "请选择" : "请先选择岗位名称";
  514. },
  515. // ============ 详情头部:优先 ESS 统一详情,未加载到时回落业务详情字段 ============
  516. hasEssView() {
  517. return Boolean(this.essDetail && this.essDetail.id);
  518. },
  519. businessApprovalStatus() {
  520. return String(this.form.approvalStatus || "").toUpperCase();
  521. },
  522. detailNo() {
  523. return this.hasEssView ? this.essRow.applicationNo || "—" : this.form.documentCode || "—";
  524. },
  525. detailStatusLabel() {
  526. if (this.hasEssView) return this.essRow.statusLabel || "—";
  527. return APPROVAL_STATUS_LABELS[this.businessApprovalStatus] || "—";
  528. },
  529. detailStatusClass() {
  530. if (this.hasEssView) return this.essRow.statusClass || "pending";
  531. return ESS_STATUS_CLASS[this.businessApprovalStatus] || "pending";
  532. },
  533. detailApplicant() {
  534. return this.hasEssView ? this.essRow.applicantName || "" : this.form.userName || "";
  535. },
  536. detailTitle() {
  537. return this.hasEssView ? this.essRow.title || "" : this.config.title;
  538. },
  539. /** 编辑资格:有 ESS 视图按统一状态判断;否则按业务审批状态(仅已驳回可编辑) */
  540. canEditApplication() {
  541. if (!this.isDetailMode || this.appAdmin) return false;
  542. if (this.hasEssView) return canEditEssApplication(this.essDetail || {}, false);
  543. return this.businessApprovalStatus === "REJECTED";
  544. },
  545. /**
  546. * 办理方式规范化。页面上不再提供「线上模板 / 线下附件」切换,
  547. * 新建时固定为 OFFLINE(提交时传给接口的值),仅回显旧数据时兼容为只读展示。
  548. */
  549. normalizedHandlingMode() {
  550. const mode = String(this.form.handlingMode || "").trim().toUpperCase();
  551. return mode === "ONLINE" ? "ONLINE" : "OFFLINE";
  552. },
  553. },
  554. onLoad(options) {
  555. const opts = options || {};
  556. const query = this.initApplicationMode(opts);
  557. // 统一申请里挂到本页的调岗/转正,转发到专用发起页
  558. const incomingType = opts.type || formTypeOf(query.businessType);
  559. const redirectUrl = REDIRECT_ROUTES[incomingType];
  560. if (redirectUrl) {
  561. const qs = carriedQuery(opts);
  562. uni.redirectTo({ url: qs ? `${redirectUrl}?${qs}` : redirectUrl });
  563. return;
  564. }
  565. this.type = applicationTypes[incomingType] ? incomingType : "leave";
  566. // 本页不直接承接 mode=edit(编辑由详情页内「编辑」按钮进入)
  567. if (this.appMode === APPLICATION_MODE.EDIT) this.appMode = APPLICATION_MODE.DETAIL;
  568. this.loadCurrentUser();
  569. uni.setNavigationBarTitle({ title: this.navTitle });
  570. if (this.isCreateMode) {
  571. this.syncType(opts);
  572. return;
  573. }
  574. this.initDetailMode();
  575. },
  576. onShow() {
  577. if (!this.isCreateMode) return;
  578. const pages = getCurrentPages();
  579. const last = pages && pages.length ? pages[pages.length - 1] : null;
  580. this.syncType((last && last.options) || {});
  581. },
  582. methods: {
  583. goBack() {
  584. back("/pages/index/index");
  585. },
  586. /**
  587. * 详情取数按入口区分:
  588. * 申请记录(appAdmin)→ 只查 ESS 管理端统一详情 GET /hr/ess/admin/applications/{record.id}
  589. * 我的申请(本人) → ESS 本人详情(头部:编号/状态/提交时间)+ 考勤业务详情(完整表单)
  590. * 离职两类仍保留原链路(ESS 统一详情 + 离职明细)
  591. */
  592. async initDetailMode() {
  593. uni.showLoading({ title: "加载中..." });
  594. try {
  595. if (this.isResignType) {
  596. await this.loadEssDetail(false);
  597. await this.loadDetailBusiness();
  598. } else if (this.appAdmin) {
  599. await this.loadEssDetail(false);
  600. } else {
  601. await this.loadEssDetail(false);
  602. await this.loadDetailBusiness();
  603. }
  604. } catch (error) {
  605. uni.showToast({
  606. title: (error && error.message) || "申请明细加载失败",
  607. icon: "none",
  608. });
  609. } finally {
  610. uni.hideLoading();
  611. }
  612. },
  613. loadCurrentUser() {
  614. const vo = uni.getStorageSync("userInfo") || {};
  615. const name = vo.userName || vo.name || "";
  616. this.currentUser = {
  617. id: vo.userId || vo.id || "",
  618. name,
  619. initials: name ? String(name).charAt(0) : "",
  620. department: vo.deptName || vo.department || "",
  621. managerName: vo.managerName || "",
  622. employeeNo: vo.jobNumber || vo.employeeNo || "",
  623. };
  624. if (!this.form.userId) this.form.userId = this.currentUser.id || "";
  625. if (!this.form.name) this.form.name = name;
  626. if (!this.form.employeeNo) this.form.employeeNo = this.currentUser.employeeNo || "";
  627. },
  628. syncType(options) {
  629. const next = (options && options.type) || "leave";
  630. if (this.type !== next) this.resetForm();
  631. this.type = next;
  632. uni.setNavigationBarTitle({ title: this.navTitle });
  633. const draft = getDraft(next);
  634. if (draft) Object.assign(this.form, draft);
  635. // 草稿兼容:老草稿缺时间字段时兜底默认值,避免校验误报
  636. if (!this.form.startTime) this.form.startTime = "08:30";
  637. if (!this.form.endTime) this.form.endTime = "17:30";
  638. if (next === "check" && !this.form.startDate) this.form.startDate = this.todayStr;
  639. // 草稿兼容:老草稿的 attachments 状态列表已废弃,统一迁移为附件 id 数组
  640. if (!Array.isArray(this.form.attachmentIds)) this.form.attachmentIds = [];
  641. if (Array.isArray(this.form.attachments)) {
  642. const ids = this.form.attachments
  643. .map((a) => (a && a.status === "success" && a.id != null ? String(a.id) : ""))
  644. .filter(Boolean);
  645. if (ids.length && !this.form.attachmentIds.length) this.form.attachmentIds = ids;
  646. delete this.form.attachments;
  647. }
  648. if (next === "resign") {
  649. if (!this.form.applyDate) this.form.applyDate = this.todayStr;
  650. if (!this.form.handlingMode) this.form.handlingMode = "OFFLINE";
  651. if (!this.form.resignationType) this.form.resignationType = "VOLUNTARY";
  652. }
  653. },
  654. resetForm() {
  655. this.form = emptyForm(this.todayStr);
  656. this.profileLoaded = false;
  657. },
  658. // ============ 详情 / 编辑模式 ============
  659. /** 只读详情:拉取来源业务明细用于展示完整字段 */
  660. async loadDetailBusiness() {
  661. await this.fetchSourceDetail(this.appBusinessId);
  662. },
  663. /** 按来源接口取业务明细并回填表单(详情展示 + 编辑态共用) */
  664. async fetchSourceDetail(businessId) {
  665. const id = businessId || this.appBusinessId;
  666. this.sourceLoaded = false;
  667. if (this.isResignType) {
  668. if (id === undefined || id === null || id === "") return null;
  669. const row = await loadResignationDetail(id);
  670. const raw = (row && row.raw) || row;
  671. if (!raw) return null;
  672. this.form = Object.assign(emptyForm(this.todayStr), hydrateResignationForm(raw));
  673. this.sourceLoaded = true;
  674. // 回显后预热级联下拉,保证机构 / 部门 / 岗位名称都能显示
  675. await this.primeCascade();
  676. return raw;
  677. }
  678. const source = await loadAttendanceApplicationForEdit(id, this.appProcessInstanceId);
  679. if (!source) return null;
  680. Object.assign(this.form, hydrateAttendanceForm(source, this.type));
  681. this.sourceLoaded = true;
  682. return source;
  683. },
  684. async enterEditMode() {
  685. if (!this.canEditApplication) return;
  686. uni.showLoading({ title: "加载中..." });
  687. try {
  688. await this.fetchSourceDetail(this.appBusinessId);
  689. if (!this.sourceLoaded) throw new Error("未获取到申请明细,暂时无法编辑");
  690. this.appMode = APPLICATION_MODE.EDIT;
  691. uni.setNavigationBarTitle({ title: this.navTitle });
  692. } catch (error) {
  693. uni.showToast({
  694. title: (error && error.message) || "申请明细加载失败,无法编辑",
  695. icon: "none",
  696. });
  697. return;
  698. } finally {
  699. uni.hideLoading();
  700. }
  701. },
  702. // ============ 编辑交互 ============
  703. onPick(key, e) {
  704. if (this.isReadonly) return;
  705. this.form[key] = (e && e.detail && e.detail.value) || "";
  706. },
  707. onSwitch(key, e) {
  708. if (this.isReadonly) return;
  709. this.form[key] = !!(e && e.detail && e.detail.value);
  710. },
  711. onPhoneInput(e) {
  712. if (this.isReadonly) return;
  713. this.form.phone = String((e && e.detail && e.detail.value) || "").slice(0, 11);
  714. },
  715. onLeaveTypeChange(leaveType) {
  716. if (this.isReadonly) return;
  717. this.form.leaveType = leaveType;
  718. },
  719. onRepairTypeChange(repairType) {
  720. if (this.isReadonly) return;
  721. this.form.repairType = repairType;
  722. },
  723. onResignTypeChange(resignationType) {
  724. if (this.isReadonly) return;
  725. this.form.resignationType = resignationType;
  726. },
  727. // ============ 员工档案自动带出(对齐 PC「选择离职人员后自动带出档案」)============
  728. async loadResignProfile() {
  729. const userId = this.form.userId || this.currentUser.id;
  730. if (!userId || this.profileLoaded) return;
  731. this.profileLoaded = true;
  732. try {
  733. const raw = await fetchUserProfile(userId, false);
  734. const p = buildProfileForm(raw || {});
  735. this.form.userId = userId;
  736. this.form.employeeNo = this.form.employeeNo || p.employeeNo || "";
  737. if (!this.form.phone) this.form.phone = p.phone || "";
  738. this.form.hireDate = this.form.hireDate || p.hireDate || "";
  739. if (!this.form.groupId) {
  740. this.form.groupId = p.groupId || "";
  741. this.form.company = p.company || "";
  742. }
  743. if (!this.form.deptId) {
  744. this.form.deptId = p.deptId || "";
  745. this.form.department = p.deptName || "";
  746. }
  747. if (!this.form.positionSequenceId) {
  748. this.form.positionSequenceId = p.positionSequenceId || "";
  749. this.form.positionSequence = p.positionSequenceName || "";
  750. }
  751. if (!this.form.positionId) {
  752. this.form.positionId = p.positionId || "";
  753. this.form.position = p.positionName || "";
  754. }
  755. if (!this.form.positionTypeName) {
  756. this.form.positionTypeName = p.employmentTypeName || p.employmentType || "";
  757. this.form.positionType = p.employmentType || "";
  758. }
  759. if (!this.form.positionLevel) this.form.positionLevel = p.staffingLevel || "";
  760. } catch (error) {
  761. // 档案拉取失败时仍可手工选择机构 / 部门 / 岗位
  762. }
  763. // 机构 / 部门候选与档案接口是两条独立链路:档案失败也必须把级联预热出来,
  764. // 否则「所属机构」下拉是空的(用户点开看不到任何选项)
  765. try {
  766. await this.initCascadeOrg();
  767. await this.primeCascade();
  768. } catch (error) {
  769. // 级联预热失败不阻断表单填写
  770. }
  771. },
  772. // ============ 校验与提交 ============
  773. validate() {
  774. const form = this.form;
  775. if (this.type === "leave" && !form.leaveType) return "请选择请假类型";
  776. if (this.type === "check") {
  777. if (!form.startDate) return "请选择补卡日期";
  778. if (!form.repairType) return "请选择补卡类型";
  779. if (!form.repairTime) return "请选择打卡时间";
  780. if (!form.reason) return "请填写补卡原因";
  781. }
  782. if (this.type === "resign") {
  783. if (!form.userId) return "缺少申请人信息,请重新进入页面";
  784. if (!/^1[3-9]\d{9}$/.test(String(form.phone || "").trim())) return "请输入正确的 11 位手机号码";
  785. if (!form.groupId) return "请选择所属机构";
  786. if (!form.deptId) return "请选择所属部门";
  787. if (!form.positionSequenceId) return "请选择岗位序列";
  788. if (!form.positionId) return "请选择岗位名称";
  789. // 岗位性质:PC 的 cascadeRequiredRules 里为必填(选岗位后由接口自动带出)
  790. if (!form.positionType) return "请先选择岗位名称以自动带出岗位性质";
  791. if (!form.positionLevelId) return "请选择岗位层级";
  792. if (!form.resignationType) return "请选择离职类型";
  793. if (!form.applyDate) return "请选择申请日期";
  794. if (!form.plannedLastWorkDate) return "请选择计划最后工作日";
  795. if (form.plannedLastWorkDate < form.applyDate) return "计划最后工作日不能早于申请日期";
  796. if (form.effectiveDate && form.effectiveDate < form.plannedLastWorkDate) {
  797. return "实际生效日期不能早于计划最后工作日";
  798. }
  799. if (!form.reason) return "请填写离职原因";
  800. if (this.normalizedHandlingMode === "OFFLINE" && !attachmentsOf(form).length) {
  801. return "线下办理方式请上传离职申请附件";
  802. }
  803. return "";
  804. }
  805. if (!this.isCheckOrResign) {
  806. if (!form.startDate) return "请选择开始日期";
  807. if (!form.endDate) return "请选择结束日期";
  808. if (!form.startTime) return "请选择开始时间";
  809. if (!form.endTime) return "请选择结束时间";
  810. if (form.endDate < form.startDate) return "结束日期不能早于开始日期";
  811. if (form.endDate === form.startDate && form.endTime <= form.startTime)
  812. return "结束时间不能早于或等于开始时间";
  813. if (!form.reason) return "请填写申请事由";
  814. }
  815. return "";
  816. },
  817. buildAttendanceVariables(payload) {
  818. return Object.assign({}, payload, {
  819. businessType: ATTENDANCE_BUSINESS_TYPE[this.type] || this.type,
  820. businessName: `${this.config.title} - ${this.currentUser.name || ""}`,
  821. deptName: this.currentUser.department || "",
  822. });
  823. },
  824. /** 考勤类:创建或更新申请单 → 发起/重新发起审批 */
  825. async submitAttendanceFlow() {
  826. const attachmentFileIds = attachmentsOf(this.form).join(",");
  827. const payload = buildAttendancePayload(this.form, this.type, this.currentUser, attachmentFileIds);
  828. if (!payload) throw new Error("申请类型不支持");
  829. if (this.isEditMode && this.appBusinessId) {
  830. // 编辑被驳回的申请:更新原单后沿用同一 businessId 重新发起审批
  831. await updateAttendanceApplication(this.appBusinessId, payload, false);
  832. try {
  833. await startAttendanceApproval(this.appBusinessId, this.buildAttendanceVariables(payload), false, this.type);
  834. } catch (e) {
  835. this.approvalError = (e && e.message) || "审批流程发起失败";
  836. console.log("[考勤申请] 审批未发起(编辑重提):", this.approvalError);
  837. }
  838. return;
  839. }
  840. // 建单接口返回结构可能不同(标量主键 / { id } / { applicationId }),统一提取
  841. const created = await submitAttendanceApplication(payload, false);
  842. const applicationId = pickApplicationId(created);
  843. if (!applicationId) {
  844. throw new Error(`申请已提交但未返回申请主键(返回:${JSON.stringify(created)}),请联系管理员`);
  845. }
  846. console.log("[考勤申请] 建单成功:", this.type, applicationId);
  847. try {
  848. await startAttendanceApproval(applicationId, this.buildAttendanceVariables(payload), false, this.type);
  849. console.log("[考勤申请] 审批发起成功:", this.type, applicationId);
  850. } catch (e) {
  851. this.approvalError = (e && e.message) || "审批流程发起失败";
  852. console.log("[考勤申请] 审批未发起:", this.type, this.approvalError);
  853. }
  854. this.form.applicationId = applicationId;
  855. },
  856. /**
  857. * 离职:保存离职单后直接发起审批(对齐 PC saveAndSubmit → launchApproval)。
  858. * 后端 /main/resignations 无 /submit 端点,审批统一走 BPM main_resignation_approval。
  859. */
  860. async submitResignFlow() {
  861. const existingId = this.isEditMode
  862. ? this.appBusinessId
  863. : this.form.resignationId || null;
  864. const result = await submitResignationFlow({
  865. form: Object.assign({}, this.form, {
  866. name: this.form.name || this.currentUser.name,
  867. userId: this.form.userId || this.currentUser.id,
  868. // 页面已取消「线上模板 / 线下附件」切换,办理方式固定线下附件
  869. handlingMode: "OFFLINE",
  870. }),
  871. existingId,
  872. });
  873. if (result.id != null) {
  874. this.form.resignationId = result.id;
  875. this.form.applicationId = result.id;
  876. rememberResignId(String(result.id));
  877. }
  878. if (!result.approvalStarted) {
  879. this.approvalError = result.approvalError || "审批流程发起失败";
  880. }
  881. },
  882. async submitApplication() {
  883. if (this.submitting) return;
  884. const error = this.validate();
  885. if (error) {
  886. uni.showToast({ title: error, icon: "none" });
  887. return;
  888. }
  889. this.submitting = true;
  890. this.approvalError = "";
  891. uni.showLoading({ title: "提交中..." });
  892. try {
  893. if (this.isResignType) await this.submitResignFlow();
  894. else await this.submitAttendanceFlow();
  895. } catch (e) {
  896. uni.hideLoading();
  897. this.submitting = false;
  898. uni.showToast({ title: (e && e.message) || "提交失败", icon: "none" });
  899. return;
  900. }
  901. uni.hideLoading();
  902. this.submitting = false;
  903. this.afterSubmitted();
  904. },
  905. afterSubmitted() {
  906. const isEdit = this.isEditMode;
  907. if (!isEdit) {
  908. removeDraft(this.type);
  909. this.recordLocalApplication();
  910. // 补卡:返回考勤页,由 onShow 重新拉月度 + 当日详情
  911. if (this.type === "check") {
  912. const pages = getCurrentPages();
  913. if (pages.length > 1) {
  914. uni.showToast({ title: "补卡申请已提交", icon: "success" });
  915. setTimeout(() => uni.navigateBack({ delta: 1 }), 600);
  916. return;
  917. }
  918. }
  919. }
  920. const title = isEdit ? "修改成功" : this.approvalError ? "申请已提交" : "提交成功";
  921. let content;
  922. if (this.approvalError) {
  923. content = isEdit
  924. ? `申请内容已更新,但审批流程未重新发起:${this.approvalError}。请联系管理员处理。`
  925. : `申请已保存,但审批流程未发起:${this.approvalError}。请稍后在“我的申请”中确认,或联系管理员。`;
  926. } else if (isEdit) {
  927. content = "申请已修改并重新提交审批,可在“我的申请”中查看进度。";
  928. } else if (this.currentUser.managerName) {
  929. content = `申请已提交给${this.currentUser.managerName}审批,可在“我的申请”中查看进度。`;
  930. } else {
  931. content = "申请已提交,可在“我的申请”中查看审批进度。";
  932. }
  933. uni.showModal({
  934. title,
  935. content,
  936. showCancel: false,
  937. success: () => this.backToApplicationList("/pages/hr/apply/list"),
  938. });
  939. },
  940. /** 本地留痕(仅创建态;列表已改用服务端接口,此处保留兼容) */
  941. recordLocalApplication() {
  942. if (this.type === "resign") {
  943. addApplication({
  944. id: this.form.applicationId || undefined,
  945. type: this.config.title,
  946. summary: this.summary(),
  947. reason: this.form.reason || "",
  948. color: this.config.color,
  949. metadata: {
  950. employeeId: this.form.userId || this.currentUser.id,
  951. resignationType: this.form.resignationType,
  952. plannedLastWorkDate: this.form.plannedLastWorkDate,
  953. handlingMode: this.normalizedHandlingMode,
  954. source: "server",
  955. serverId: this.form.applicationId || undefined,
  956. },
  957. });
  958. return;
  959. }
  960. addApplication({
  961. type: this.config.title,
  962. summary: this.summary(),
  963. reason: this.form.reason || "用于个人材料办理",
  964. color: this.config.color,
  965. });
  966. },
  967. summary() {
  968. if (this.type === "check") {
  969. const typeLabel = this.form.repairType ? REPAIR_TYPE_LABEL[this.form.repairType] : "补卡";
  970. return `${this.form.startDate || ""} · ${typeLabel} · ${this.form.repairTime || ""}`;
  971. }
  972. if (this.type === "resign") {
  973. return `${this.resignTypeLabel} · ${this.form.plannedLastWorkDate || this.form.applyDate || ""}`;
  974. }
  975. return `${this.form.startDate} 至 ${this.form.endDate}`;
  976. },
  977. },
  978. watch: {
  979. /** 进入离职申请创建态时带出本人档案,减少手工填写 */
  980. isResignType: {
  981. immediate: true,
  982. handler(val) {
  983. if (!val || !this.isCreateMode) return;
  984. this.$nextTick(() => this.loadResignProfile());
  985. },
  986. },
  987. },
  988. };
  989. /** 附件数组 → 非空 id 列表 */
  990. function attachmentsOf(form = {}) {
  991. return (Array.isArray(form.attachmentIds) ? form.attachmentIds : []).filter(Boolean).map((id) => String(id));
  992. }
  993. </script>
  994. <style scoped lang="scss">
  995. @import "@/pages/hr/common-style.scss";
  996. .page-no-tab {
  997. padding-bottom: 140rpx;
  998. }
  999. /* 只读态:隐藏所有可点击指示箭头 */
  1000. .readonly-mode .arrow {
  1001. display: none;
  1002. }
  1003. .detail-head {
  1004. margin: 24rpx 24rpx 0;
  1005. padding: 28rpx;
  1006. }
  1007. .detail-head-top {
  1008. display: flex;
  1009. align-items: center;
  1010. justify-content: space-between;
  1011. margin-bottom: 20rpx;
  1012. }
  1013. .detail-title {
  1014. font-size: 32rpx;
  1015. font-weight: 600;
  1016. color: #1f2329;
  1017. }
  1018. .detail-row {
  1019. display: flex;
  1020. align-items: center;
  1021. justify-content: space-between;
  1022. padding: 12rpx 0;
  1023. font-size: 26rpx;
  1024. }
  1025. .detail-label {
  1026. color: #8b939f;
  1027. }
  1028. .detail-value {
  1029. color: #1f2329;
  1030. }
  1031. .masked-card {
  1032. margin: 20rpx 24rpx 0;
  1033. padding: 26rpx 28rpx;
  1034. }
  1035. .masked-label {
  1036. display: block;
  1037. margin-bottom: 12rpx;
  1038. color: #8b939f;
  1039. font-size: 23rpx;
  1040. }
  1041. .masked-text {
  1042. color: #4b5563;
  1043. font-size: 26rpx;
  1044. line-height: 1.6;
  1045. }
  1046. .form-card {
  1047. overflow: hidden;
  1048. margin-top: 20rpx;
  1049. }
  1050. /* .section-head / .section-title / .section-tip 统一取自
  1051. @/pages/hr/common-style.scss,本页不再单独覆盖。 */
  1052. .field {
  1053. position: relative;
  1054. min-height: 108rpx;
  1055. padding: 28rpx 28rpx;
  1056. display: flex;
  1057. align-items: center;
  1058. }
  1059. .label {
  1060. /* 「计划最后工作日」7 个字在 180rpx 下会折行 → 加宽并禁止换行 */
  1061. width: 210rpx;
  1062. flex-shrink: 0;
  1063. white-space: nowrap;
  1064. font-size: 28rpx;
  1065. }
  1066. .required::before {
  1067. content: "*";
  1068. color: #ee4d4d;
  1069. margin-right: 6rpx;
  1070. }
  1071. .readonly {
  1072. flex: 1;
  1073. display: flex;
  1074. align-items: center;
  1075. justify-content: flex-end;
  1076. font-size: 26rpx;
  1077. }
  1078. .small-avatar {
  1079. width: 52rpx;
  1080. height: 52rpx;
  1081. margin-right: 14rpx;
  1082. font-size: 22rpx;
  1083. }
  1084. .value {
  1085. color: #1f2329;
  1086. font-size: 27rpx;
  1087. }
  1088. .readonly-text {
  1089. margin-left: auto;
  1090. color: #8b939f;
  1091. }
  1092. .placeholder {
  1093. color: #b2b7be;
  1094. }
  1095. .arrow {
  1096. margin-left: 13rpx;
  1097. color: #c4c8ce;
  1098. font-size: 38rpx;
  1099. }
  1100. .textarea-field {
  1101. display: block;
  1102. min-height: 260rpx;
  1103. }
  1104. .textarea-field .label {
  1105. display: block;
  1106. width: auto;
  1107. margin-bottom: 20rpx;
  1108. }
  1109. .textarea-field textarea {
  1110. width: 100%;
  1111. height: 130rpx;
  1112. padding: 18rpx;
  1113. border-radius: 12rpx;
  1114. background: #f6f7f8;
  1115. font-size: 27rpx;
  1116. }
  1117. .counter {
  1118. position: absolute;
  1119. right: 45rpx;
  1120. bottom: 38rpx;
  1121. color: #b2b7be;
  1122. font-size: 21rpx;
  1123. }
  1124. .field-file {
  1125. flex: 1;
  1126. display: flex;
  1127. justify-content: flex-end;
  1128. }
  1129. /* 办理方式固定为线下附件(不提供线上模板切换),相关切换样式已移除 */
  1130. .bottom-action {
  1131. position: fixed;
  1132. z-index: 10;
  1133. left: 0;
  1134. right: 0;
  1135. bottom: 0;
  1136. height: 120rpx;
  1137. padding: 16rpx 24rpx;
  1138. display: flex;
  1139. background: #fff;
  1140. border-top: 1rpx solid #e8e9eb;
  1141. }
  1142. .submit-btn {
  1143. flex: 1;
  1144. height: 84rpx;
  1145. border-radius: 12rpx;
  1146. display: flex;
  1147. align-items: center;
  1148. justify-content: center;
  1149. font-size: 30rpx;
  1150. background: #157a2c;
  1151. color: #fff;
  1152. }
  1153. @media (min-width: 800px) {
  1154. .bottom-action {
  1155. max-width: 520px;
  1156. left: 50%;
  1157. transform: translateX(-50%);
  1158. }
  1159. }
  1160. .input-flex {
  1161. flex: 1;
  1162. text-align: right;
  1163. font-size: 27rpx;
  1164. color: #1f2329;
  1165. padding: 0 6rpx;
  1166. }
  1167. .small-textarea {
  1168. min-height: 180rpx;
  1169. }
  1170. .small-textarea textarea {
  1171. height: 100rpx;
  1172. }
  1173. .check-field {
  1174. display: block;
  1175. align-items: flex-start;
  1176. }
  1177. .check-field .label {
  1178. display: block;
  1179. width: auto;
  1180. margin-bottom: 16rpx;
  1181. }
  1182. .check-list {
  1183. display: flex;
  1184. flex-wrap: wrap;
  1185. padding-top: 4rpx;
  1186. }
  1187. .check-row {
  1188. display: flex;
  1189. align-items: center;
  1190. margin: 0 24rpx 18rpx 0;
  1191. font-size: 26rpx;
  1192. color: #4b5563;
  1193. }
  1194. .checkbox {
  1195. width: 32rpx;
  1196. height: 32rpx;
  1197. margin-right: 10rpx;
  1198. border: 1rpx solid #c4c8ce;
  1199. border-radius: 6rpx;
  1200. display: flex;
  1201. align-items: center;
  1202. justify-content: center;
  1203. font-size: 22rpx;
  1204. color: #fff;
  1205. background: #fff;
  1206. }
  1207. .checkbox.is-checked {
  1208. background: #157a2c;
  1209. border-color: #157a2c;
  1210. }
  1211. .switch-field {
  1212. justify-content: space-between;
  1213. }
  1214. </style>