shipOwnerDetail.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619
  1. <template>
  2. <div class="line-container-p24">
  3. <div
  4. class="df aic dib go-back ml8 pointer"
  5. @click="router.replace('/shipOwnerManage/shipOwnerList')"
  6. >
  7. <el-icon class="mr10"><ArrowLeftBold /></el-icon>
  8. <div>返回船员列表</div>
  9. </div>
  10. </div>
  11. <div class="container-title">基本信息</div>
  12. <div class="line-container-p24">
  13. <el-form
  14. :model="shipOwnerForm"
  15. :rules="rules"
  16. ref="shipOwnerFormRef"
  17. label-width="100px"
  18. label-position="left"
  19. >
  20. <el-row :gutter="20">
  21. <el-col :span="8">
  22. <el-form-item label="船员姓名" prop="userName" required>
  23. <el-input
  24. v-model="shipOwnerForm.userName"
  25. placeholder="请输入"
  26. :disabled="!!shipOwnerForm.shipOwnerId"
  27. ></el-input>
  28. </el-form-item>
  29. </el-col>
  30. <el-col :span="8">
  31. <el-form-item label="船员手机号" prop="userPhone" required>
  32. <el-input
  33. v-model="shipOwnerForm.userPhone"
  34. placeholder="请输入"
  35. :disabled="!!shipOwnerForm.shipOwnerId"
  36. @blur="handlePhoneBlur"
  37. ></el-input>
  38. </el-form-item>
  39. </el-col>
  40. </el-row>
  41. <el-row :gutter="20">
  42. <el-col :span="8">
  43. <el-form-item label="船员身份证号">
  44. <el-input
  45. v-model="shipOwnerForm.idcardNo"
  46. placeholder="请输入"
  47. :disabled="!!shipOwnerForm.shipOwnerId"
  48. ></el-input>
  49. </el-form-item>
  50. </el-col>
  51. </el-row>
  52. <el-row :gutter="20">
  53. <el-col :span="8">
  54. <el-form-item label="身份证人像面">
  55. <Uploader
  56. :action-url="uploadConfig.uploadUrl"
  57. :file-list="idCardFrontFileList"
  58. :params="{ type: 'idCardFront' }"
  59. @on-upload-file-list="handleIdCardFrontUpload"
  60. @on-remove-file-list="handleIdCardFrontRemove"
  61. :limit="1"
  62. upload-text="点击上传人像面"
  63. :disabled="!!shipOwnerForm.shipOwnerId"
  64. />
  65. </el-form-item>
  66. </el-col>
  67. <el-col :span="8">
  68. <el-form-item label="身份证国徽面">
  69. <Uploader
  70. :action-url="uploadConfig.uploadUrl"
  71. :file-list="idCardBackFileList"
  72. :params="{ type: 'idCardBack' }"
  73. @on-upload-file-list="handleIdCardBackUpload"
  74. @on-remove-file-list="handleIdCardBackRemove"
  75. :limit="1"
  76. upload-text="点击上传国徽面"
  77. :disabled="!!shipOwnerForm.shipOwnerId"
  78. />
  79. </el-form-item>
  80. </el-col>
  81. </el-row>
  82. </el-form>
  83. </div>
  84. <div class="container-title">船员证书信息</div>
  85. <div class="line-container-p24">
  86. <el-form
  87. :model="shipOwnerForm.cerificate"
  88. :rules="certificateRules"
  89. ref="certificateFormRef"
  90. label-width="100px"
  91. label-position="left"
  92. >
  93. <el-row :gutter="20">
  94. <el-col :span="8">
  95. <el-form-item label="性别">
  96. <el-select
  97. v-model="shipOwnerForm.cerificate.gender"
  98. placeholder="请选择"
  99. >
  100. <el-option label="男" :value="1"></el-option>
  101. <el-option label="女" :value="2"></el-option>
  102. </el-select>
  103. </el-form-item>
  104. </el-col>
  105. <el-col :span="8">
  106. <el-form-item label="证书编号" prop="certNo" required>
  107. <el-input
  108. v-model="shipOwnerForm.cerificate.certNo"
  109. placeholder="请输入"
  110. ></el-input>
  111. </el-form-item>
  112. </el-col>
  113. </el-row>
  114. <el-row :gutter="20">
  115. <el-col :span="8">
  116. <el-form-item label="证书类型">
  117. <el-input
  118. v-model="shipOwnerForm.cerificate.certType"
  119. placeholder="请输入"
  120. ></el-input>
  121. </el-form-item>
  122. </el-col>
  123. <el-col :span="8">
  124. <el-form-item label="职务资格" prop="postRole" required>
  125. <el-select
  126. v-model="shipOwnerForm.cerificate.postRole"
  127. placeholder="请选择"
  128. >
  129. <el-option label="大副" value="大副"></el-option>
  130. <el-option label="二副" value="二副"></el-option>
  131. <el-option label="三副" value="三副"></el-option>
  132. <el-option label="船长" value="船长"></el-option>
  133. </el-select>
  134. </el-form-item>
  135. </el-col>
  136. </el-row>
  137. <el-row :gutter="20">
  138. <el-col :span="8">
  139. <el-form-item label="发证日期" prop="issuerAt" required>
  140. <el-date-picker
  141. v-model="shipOwnerForm.cerificate.issuerAt"
  142. type="date"
  143. placeholder="选择日期"
  144. format="YYYY/MM/DD"
  145. />
  146. </el-form-item>
  147. </el-col>
  148. <el-col :span="8">
  149. <el-form-item label="截止日期" prop="expiryAt" required>
  150. <el-date-picker
  151. v-model="shipOwnerForm.cerificate.expiryAt"
  152. type="date"
  153. placeholder="选择日期"
  154. format="YYYY/MM/DD"
  155. />
  156. </el-form-item>
  157. </el-col>
  158. </el-row>
  159. <el-row :gutter="20">
  160. <el-col :span="8">
  161. <el-form-item label="签发机构" prop="issuerAuthority" required>
  162. <el-input
  163. v-model="shipOwnerForm.cerificate.issuerAuthority"
  164. placeholder="请输入"
  165. ></el-input>
  166. </el-form-item>
  167. </el-col>
  168. <el-col :span="16">
  169. <el-form-item label="适用限制">
  170. <el-input
  171. type="textarea"
  172. v-model="shipOwnerForm.cerificate.applicableRestrictions"
  173. :rows="4"
  174. placeholder="请输入"
  175. ></el-input>
  176. </el-form-item>
  177. </el-col>
  178. </el-row>
  179. <el-row>
  180. <el-col :span="8">
  181. <el-form-item label="证书照片" prop="certFile" required>
  182. <Uploader
  183. :action-url="uploadConfig.uploadUrl"
  184. :file-list="certFileList"
  185. :params="{ type: 'certFile', docType: 1 }"
  186. @on-upload-file-list="handleCertUpload"
  187. @on-remove-file-list="handleCertRemove"
  188. :limit="1"
  189. upload-text="点击上传证书"
  190. />
  191. </el-form-item>
  192. </el-col>
  193. </el-row>
  194. </el-form>
  195. </div>
  196. <div class="container-title">其他文件</div>
  197. <div class="line-container-p24">
  198. <div class="mb20">健康证明</div>
  199. <Uploader
  200. :action-url="uploadConfig.uploadUrl"
  201. :file-list="healthFileList"
  202. :params="{ type: 'healthFile', docType: 2 }"
  203. @on-upload-file-list="handleHealthUpload"
  204. @on-remove-file-list="handleHealthRemove"
  205. :limit="2"
  206. upload-text="点击上传"
  207. />
  208. <div class="mb20 mt30">服务簿</div>
  209. <Uploader
  210. :action-url="uploadConfig.uploadUrl"
  211. :file-list="serviceFileList"
  212. :params="{ type: 'serviceFile', docType: 3 }"
  213. @on-upload-file-list="handleServiceUpload"
  214. @on-remove-file-list="handleServiceRemove"
  215. :limit="2"
  216. upload-text="点击上传"
  217. />
  218. </div>
  219. <div class="df jcc mt30 mb30">
  220. <el-button @click="router.replace('/shipOwnerManage/shipOwnerList')">
  221. 取消
  222. </el-button>
  223. <el-button type="primary" @click="saveShipOwner">保存</el-button>
  224. </div>
  225. </template>
  226. <script setup>
  227. import { ref, h, reactive, toRefs, onMounted, computed } from "vue";
  228. import { ElNotification, ElMessageBox, ElMessage } from "element-plus";
  229. import store from "../../store";
  230. import router from "../../router";
  231. import md5 from "md5";
  232. import api from "../../apis/fetch";
  233. import { useRoute } from "vue-router";
  234. import _ from "lodash";
  235. const route = useRoute();
  236. const uploadConfig = ref({});
  237. const shipOwnerFormRef = ref(null);
  238. const certificateFormRef = ref(null);
  239. // 表单验证规则
  240. const rules = {
  241. userName: [{ required: true, message: "请输入船员姓名", trigger: "blur" }],
  242. userPhone: [
  243. { required: true, message: "请输入船员手机号", trigger: "blur" },
  244. {
  245. pattern: /^1[3-9]\d{9}$/,
  246. message: "请输入正确的手机号格式",
  247. trigger: "blur",
  248. },
  249. ],
  250. };
  251. // 验证证书照片
  252. const validateCertFile = (rule, value, callback) => {
  253. if (!certFileList.value || certFileList.value.length === 0) {
  254. callback(new Error("请上传证书照片"));
  255. } else {
  256. callback();
  257. }
  258. };
  259. // 验证截止日期
  260. const validateExpiryDate = (rule, value, callback) => {
  261. if (!value) {
  262. callback(new Error("请选择截止日期"));
  263. } else {
  264. if (shipOwnerForm.value.cerificate.issuerAt) {
  265. // 确保截止日期晚于发证日期
  266. if (
  267. new Date(value) <= new Date(shipOwnerForm.value.cerificate.issuerAt)
  268. ) {
  269. callback(new Error("截止日期必须晚于发证日期"));
  270. }
  271. }
  272. callback();
  273. }
  274. };
  275. // 验证发证日期
  276. const validateIssuerDate = (rule, value, callback) => {
  277. if (!value) {
  278. callback(new Error("请选择发证日期"));
  279. } else {
  280. if (shipOwnerForm.value.cerificate.expiryAt) {
  281. // 如果已选择截止日期,确保发证日期早于截止日期
  282. if (
  283. new Date(value) >= new Date(shipOwnerForm.value.cerificate.expiryAt)
  284. ) {
  285. callback(new Error("发证日期必须早于截止日期"));
  286. }
  287. }
  288. callback();
  289. }
  290. };
  291. // 证书表单验证规则
  292. const certificateRules = {
  293. certNo: [{ required: true, message: "请输入证书编号", trigger: "blur" }],
  294. postRole: [{ required: true, message: "请选择职务资格", trigger: "change" }],
  295. issuerAt: [
  296. { required: true, message: "请选择发证日期", trigger: "change" },
  297. { validator: validateIssuerDate, trigger: "change" },
  298. ],
  299. expiryAt: [
  300. { required: true, message: "请选择截止日期", trigger: "change" },
  301. { validator: validateExpiryDate, trigger: "change" },
  302. ],
  303. issuerAuthority: [
  304. { required: true, message: "请输入签发机构", trigger: "blur" },
  305. ],
  306. certFile: [{ validator: validateCertFile, trigger: "change" }],
  307. };
  308. // 初始化船员表单数据结构
  309. const shipOwnerForm = ref({
  310. shipOwnerId: 0, // 船员ID,匹配时返回,未匹配到为0,非0时下方内容无效
  311. userName: "", // 船员姓名(必填)
  312. userPhone: "", // 船员手机(必填)
  313. idcardNo: "", // 船员身份证号(非必填)
  314. idcardFrontFileKey: "", // 船员身份证正面文件key(非必填)
  315. idcardFrontViewUrl: "", // 船员身份证正面文件预览地址(非必填)
  316. idcardFrontDownloadUrl: "", // 船员身份证正面文件下载地址(非必填)
  317. idcardBackFileKey: "", // 船员身份证反面文件key(非必填)
  318. idcardBackViewUrl: "", // 船员身份证反面文件预览地址(非必填)
  319. idcardBackDownloadUrl: "", // 船员身份证反面文件下载地址(非必填)
  320. cerificate: {
  321. gender: "", // 性别(1-男;2-女)
  322. certNo: "", // 证书编号(必填)
  323. certType: "", // 证书类型(非必填)
  324. postRole: "", // 职务资格(必填)
  325. issuerAt: "", // 发证日期(必填)
  326. expiryAt: "", // 截止日期(必填)
  327. issuerAuthority: "", // 签发机构(必填)
  328. applicableRestrictions: "", // 适用限制(非必填)
  329. },
  330. documents: [], // 文件信息数组
  331. });
  332. // 计算属性 - 文件列表
  333. const idCardFrontFileList = computed(() => {
  334. if (
  335. shipOwnerForm.value.idcardFrontFileKey &&
  336. shipOwnerForm.value.idcardFrontViewUrl
  337. ) {
  338. return [
  339. {
  340. fileKey: shipOwnerForm.value.idcardFrontFileKey,
  341. viewUrl: shipOwnerForm.value.idcardFrontViewUrl,
  342. downloadUrl: shipOwnerForm.value.idcardFrontDownloadUrl,
  343. },
  344. ];
  345. }
  346. return [];
  347. });
  348. const idCardBackFileList = computed(() => {
  349. if (
  350. shipOwnerForm.value.idcardBackFileKey &&
  351. shipOwnerForm.value.idcardBackViewUrl
  352. ) {
  353. return [
  354. {
  355. fileKey: shipOwnerForm.value.idcardBackFileKey,
  356. viewUrl: shipOwnerForm.value.idcardBackViewUrl,
  357. downloadUrl: shipOwnerForm.value.idcardBackDownloadUrl,
  358. },
  359. ];
  360. }
  361. return [];
  362. });
  363. const certFileList = computed(() => {
  364. return (
  365. shipOwnerForm.value.documents?.filter((doc) => doc.docType === 1) || []
  366. );
  367. });
  368. const healthFileList = computed(() => {
  369. return (
  370. shipOwnerForm.value.documents?.filter((doc) => doc.docType === 2) || []
  371. );
  372. });
  373. const serviceFileList = computed(() => {
  374. return (
  375. shipOwnerForm.value.documents?.filter((doc) => doc.docType === 3) || []
  376. );
  377. });
  378. // 初始化数据
  379. onMounted(() => {
  380. // 如果有ID参数,获取船员详情
  381. if (route.query.id) {
  382. api.getShipOwnerDetail({ id: route.query.id }).then((res) => {
  383. if (res.data.code === 0) {
  384. const data = res.data.data;
  385. // 直接使用API返回的数据结构
  386. Object.assign(shipOwnerForm.value, data);
  387. }
  388. });
  389. }
  390. });
  391. // 手机号失去焦点时检查是否已存在船员信息
  392. const handlePhoneBlur = () => {
  393. if (!shipOwnerForm.value.userPhone) return;
  394. // 如果已经有shipOwnerId,说明已经匹配到了船员,不需要再查询
  395. if (shipOwnerForm.value.shipOwnerId) return;
  396. api
  397. .getShipOwnerDetail({ phone: shipOwnerForm.value.userPhone })
  398. .then((res) => {
  399. if (res.data.code === 0 && res.data.data && res.data.data.shipOwnerId) {
  400. // 匹配到船员信息,更新表单
  401. Object.assign(shipOwnerForm.value, res.data.data);
  402. ElMessage.info("已匹配到船员信息");
  403. }
  404. });
  405. };
  406. // 身份证人像面上传处理
  407. const handleIdCardFrontUpload = ({ response }) => {
  408. if (response.code === 0) {
  409. shipOwnerForm.value.idcardFrontFileKey = response.data.fileKey;
  410. shipOwnerForm.value.idcardFrontViewUrl = response.data.viewUrl;
  411. shipOwnerForm.value.idcardFrontDownloadUrl = response.data.downloadUrl;
  412. } else {
  413. ElMessage.error(response.msg || "上传失败");
  414. }
  415. };
  416. // 身份证人像面删除处理
  417. const handleIdCardFrontRemove = () => {
  418. shipOwnerForm.value.idcardFrontFileKey = "";
  419. shipOwnerForm.value.idcardFrontViewUrl = "";
  420. shipOwnerForm.value.idcardFrontDownloadUrl = "";
  421. };
  422. // 身份证国徽面上传处理
  423. const handleIdCardBackUpload = ({ response }) => {
  424. if (response.code === 0) {
  425. shipOwnerForm.value.idcardBackFileKey = response.data.fileKey;
  426. shipOwnerForm.value.idcardBackViewUrl = response.data.viewUrl;
  427. shipOwnerForm.value.idcardBackDownloadUrl = response.data.downloadUrl;
  428. } else {
  429. ElMessage.error(response.msg || "上传失败");
  430. }
  431. };
  432. // 身份证国徽面删除处理
  433. const handleIdCardBackRemove = () => {
  434. shipOwnerForm.value.idcardBackFileKey = "";
  435. shipOwnerForm.value.idcardBackViewUrl = "";
  436. shipOwnerForm.value.idcardBackDownloadUrl = "";
  437. };
  438. // 证书上传处理
  439. const handleCertUpload = ({ response }) => {
  440. if (response.code === 0) {
  441. if (!shipOwnerForm.value.documents) {
  442. shipOwnerForm.value.documents = [];
  443. }
  444. shipOwnerForm.value.documents.push({
  445. docType: 1, // 船员适任证书
  446. fileKey: response.data.fileKey,
  447. viewUrl: response.data.viewUrl,
  448. downloadUrl: response.data.downloadUrl,
  449. });
  450. } else {
  451. ElMessage.error(response.msg || "上传失败");
  452. }
  453. };
  454. // 证书删除处理
  455. const handleCertRemove = ({ fileIndex }) => {
  456. const certDocs = shipOwnerForm.value.documents.filter(
  457. (doc) => doc.docType === 1
  458. );
  459. const docToRemove = certDocs[fileIndex];
  460. shipOwnerForm.value.documents = shipOwnerForm.value.documents.filter(
  461. (doc) => doc !== docToRemove
  462. );
  463. };
  464. // 健康证明上传处理
  465. const handleHealthUpload = ({ response }) => {
  466. if (response.code === 0) {
  467. if (!shipOwnerForm.value.documents) {
  468. shipOwnerForm.value.documents = [];
  469. }
  470. shipOwnerForm.value.documents.push({
  471. docType: 2, // 体检表
  472. fileKey: response.data.fileKey,
  473. viewUrl: response.data.viewUrl,
  474. downloadUrl: response.data.downloadUrl,
  475. });
  476. } else {
  477. ElMessage.error(response.msg || "上传失败");
  478. }
  479. };
  480. // 健康证明删除处理
  481. const handleHealthRemove = ({ fileIndex }) => {
  482. const healthDocs = shipOwnerForm.value.documents.filter(
  483. (doc) => doc.docType === 2
  484. );
  485. const docToRemove = healthDocs[fileIndex];
  486. shipOwnerForm.value.documents = shipOwnerForm.value.documents.filter(
  487. (doc) => doc !== docToRemove
  488. );
  489. };
  490. // 服务簿上传处理
  491. const handleServiceUpload = ({ response }) => {
  492. if (response.code === 0) {
  493. if (!shipOwnerForm.value.documents) {
  494. shipOwnerForm.value.documents = [];
  495. }
  496. shipOwnerForm.value.documents.push({
  497. docType: 3, // 服务簿
  498. fileKey: response.data.fileKey,
  499. viewUrl: response.data.viewUrl,
  500. downloadUrl: response.data.downloadUrl,
  501. });
  502. } else {
  503. ElMessage.error(response.msg || "上传失败");
  504. }
  505. };
  506. // 服务簿删除处理
  507. const handleServiceRemove = ({ fileIndex }) => {
  508. const serviceDocs = shipOwnerForm.value.documents.filter(
  509. (doc) => doc.docType === 3
  510. );
  511. const docToRemove = serviceDocs[fileIndex];
  512. shipOwnerForm.value.documents = shipOwnerForm.value.documents.filter(
  513. (doc) => doc !== docToRemove
  514. );
  515. };
  516. // 保存船员信息
  517. const saveShipOwner = () => {
  518. // 使用Element Plus表单验证
  519. shipOwnerFormRef.value.validate((valid1) => {
  520. if (!valid1) return;
  521. certificateFormRef.value.validate((valid2) => {
  522. if (!valid2) return;
  523. // 验证证书照片是否上传
  524. if (!certFileList.value.length) {
  525. ElMessage.warning("请上传证书照片");
  526. return;
  527. }
  528. // 表单验证通过,继续保存操作
  529. });
  530. });
  531. // 如果有ID,则更新,否则新增
  532. const apiMethod = route.query.id ? api.updateShipOwner : api.addShipOwner;
  533. const successMsg = route.query.id ? "更新成功" : "添加成功";
  534. // 构建保存数据
  535. const saveData = { ...shipOwnerForm.value };
  536. if (route.query.id) {
  537. saveData.id = route.query.id;
  538. }
  539. apiMethod(saveData)
  540. .then((res) => {
  541. if (res.data.code === 0) {
  542. ElMessage.success(successMsg);
  543. router.replace("/shipOwnerManage/shipOwnerList");
  544. } else {
  545. ElMessage.error(res.data.msg || "保存失败");
  546. }
  547. })
  548. .catch((err) => {
  549. console.error(err);
  550. ElMessage.error("保存失败");
  551. });
  552. };
  553. </script>
  554. <style scoped>
  555. :deep().el-upload-list__item-thumbnail {
  556. object-fit: contain;
  557. }
  558. .upload-plus-icon {
  559. height: 15%;
  560. color: rgb(139, 147, 156);
  561. line-height: 100px;
  562. font-size: 40px;
  563. font-weight: 200;
  564. }
  565. .upload-text {
  566. height: 25%;
  567. color: rgb(139, 147, 156);
  568. }
  569. :deep().el-upload--picture-card {
  570. border: none;
  571. width: auto;
  572. }
  573. .unit {
  574. width: 40px;
  575. text-align: center;
  576. color: #555;
  577. }
  578. </style>