index.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464
  1. <template>
  2. <view class="auth-container">
  3. <!-- 顶部提示 -->
  4. <view class="top-tip">请确保身份信息的准确,以免影响后续履约费用结算。</view>
  5. <!-- 表单信息 -->
  6. <view class="form-card">
  7. <!-- 证件类型 -->
  8. <view class="form-item">
  9. <text class="label">证件类型</text>
  10. <view class="read-only-text">居民身份证</view>
  11. </view>
  12. <!-- 真实姓名 -->
  13. <view class="form-item">
  14. <text class="label"><text class="required">*</text>真实姓名</text>
  15. <view class="gray-input-box">
  16. <input class="input-area" type="text" v-model="formData.name" placeholder="证件姓名"
  17. placeholder-class="input-placeholder" />
  18. </view>
  19. </view>
  20. <!-- 证件号码 -->
  21. <view class="form-item">
  22. <text class="label"><text class="required">*</text>证件号码</text>
  23. <view class="gray-input-box">
  24. <input class="input-area" type="idcard" v-model="formData.idNumber" placeholder="身份证号"
  25. placeholder-class="input-placeholder" />
  26. </view>
  27. </view>
  28. <!-- 有效日期 -->
  29. <view class="form-item">
  30. <text class="label"><text class="required">*</text>有效日期</text>
  31. <view class="gray-input-box" @click="openPicker">
  32. <text class="input-area" :class="{ 'input-placeholder': !formData.expiryDate }">
  33. {{ formData.expiryDate || '选择有效结束期限' }}
  34. </text>
  35. <!-- 箭头SVG -->
  36. <svg class="arrow-right" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg">
  37. <path
  38. d="M340.864 149.312a30.592 30.592 0 0 0 0 42.752L652.736 512 340.864 831.872a30.592 30.592 0 0 0 0 42.752 29.12 29.12 0 0 0 41.728 0L714.24 534.336a32 32 0 0 0 0-45.056L382.592 149.312a29.12 29.12 0 0 0-41.728 0z"
  39. fill="#CCCCCC"></path>
  40. </svg>
  41. </view>
  42. </view>
  43. </view>
  44. <!-- 身份证正面 -->
  45. <view class="upload-card">
  46. <view class="upload-box" @click="chooseImage('front')">
  47. <image v-if="idCardFront" :src="idCardFront" class="preview-img" mode="aspectFill"></image>
  48. <template v-else>
  49. <!-- 相机图标SVG -->
  50. <svg class="camera-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
  51. <path
  52. d="M12 12C14.7614 12 17 9.76142 17 7C17 4.23858 14.7614 2 12 2C9.23858 2 7 4.23858 7 7C7 9.76142 9.23858 12 12 12Z"
  53. fill="#E0E0E0" />
  54. <circle cx="12" cy="12" r="3" stroke="#CCCCCC" stroke-width="2" />
  55. <path
  56. d="M20 6H17.82L16.4 4.47C15.96 4 15.34 3.73 14.68 3.73H9.32C8.66 3.73 8.04 4 7.6 4.47L6.18 6H4C2.9 6 2 6.9 2 8V18C2 19.1 2.9 20 4 20H20C21.1 20 22 19.1 22 18V8C22 6.9 21.1 6 20 6ZM12 17C9.24 17 7 14.76 7 12C7 9.24 9.24 7 12 7C14.76 7 17 9.24 17 12C17 14.76 14.76 17 12 17Z"
  57. fill="#CCCCCC" />
  58. </svg>
  59. <text class="upload-text">点击上传</text>
  60. </template>
  61. </view>
  62. <text class="card-label"><text class="required">*</text>证件带照片面</text>
  63. </view>
  64. <!-- 身份证反面 -->
  65. <view class="upload-card">
  66. <view class="upload-box" @click="chooseImage('back')">
  67. <image v-if="idCardBack" :src="idCardBack" class="preview-img" mode="aspectFill"></image>
  68. <template v-else>
  69. <!-- 相机图标SVG -->
  70. <svg class="camera-icon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
  71. <path
  72. d="M20 6H17.82L16.4 4.47C15.96 4 15.34 3.73 14.68 3.73H9.32C8.66 3.73 8.04 4 7.6 4.47L6.18 6H4C2.9 6 2 6.9 2 8V18C2 19.1 2.9 20 4 20H20C21.1 20 22 19.1 22 18V8C22 6.9 21.1 6 20 6ZM12 17C9.24 17 7 14.76 7 12C7 9.24 9.24 7 12 7C14.76 7 17 9.24 17 12C17 14.76 14.76 17 12 17Z"
  73. fill="#CCCCCC" />
  74. </svg>
  75. <text class="upload-text">点击上传</text>
  76. </template>
  77. </view>
  78. <text class="card-label"><text class="required">*</text>证件国徽面</text>
  79. </view>
  80. <!-- 底部按钮 -->
  81. <view class="footer-btn-area">
  82. <button class="next-btn" @click="goToQualifications">下一步,完善资质</button>
  83. </view>
  84. <!-- 自定义日期选择器 -->
  85. <view class="picker-mask" :class="{ show: showPicker }" @click="closePicker">
  86. <view class="picker-content" @click.stop>
  87. <view class="picker-header">
  88. <text class="picker-btn-cancel" @click="closePicker">取消</text>
  89. <text class="picker-title">选择有效结束期限</text>
  90. <text class="picker-btn-confirm" @click="confirmPicker">确定</text>
  91. </view>
  92. <picker-view class="picker-view" indicator-style="height: 50px;" :value="pickerValue" @change="onPickerChange">
  93. <picker-view-column>
  94. <view class="picker-item" v-for="(item, index) in years" :key="index">{{ item }}年</view>
  95. </picker-view-column>
  96. <picker-view-column>
  97. <view class="picker-item" v-for="(item, index) in months" :key="index">{{ item }}月</view>
  98. </picker-view-column>
  99. <picker-view-column>
  100. <view class="picker-item" v-for="(item, index) in days" :key="index">{{ item }}日</view>
  101. </picker-view-column>
  102. </picker-view>
  103. </view>
  104. </view>
  105. </view>
  106. </template>
  107. <script>
  108. import { uploadFile } from '@/api/fulfiller/app'
  109. export default {
  110. data() {
  111. return {
  112. formData: { idType: '居民身份证', name: '', idNumber: '', expiryDate: '' },
  113. idCardFront: '', idCardBack: '',
  114. idCardFrontOssId: '', idCardBackOssId: '',
  115. showPicker: false, pickerValue: [0, 0, 0],
  116. years: [], months: [], days: [],
  117. tempYear: 0, tempMonth: 0, tempDay: 0,
  118. serviceType: [], isChoosingImage: false
  119. }
  120. },
  121. onLoad(options) {
  122. if (options.services) {
  123. try { this.serviceType = JSON.parse(decodeURIComponent(options.services)); }
  124. catch (e) { console.error('Parse services failed', e); }
  125. }
  126. this.initDateData(); this.restoreAuthData();
  127. },
  128. onShow() { if (this.isChoosingImage) this.isChoosingImage = false; },
  129. methods: {
  130. initDateData() {
  131. const year = new Date().getFullYear();
  132. for (let i = year; i <= year + 50; i++) this.years.push(i);
  133. for (let i = 1; i <= 12; i++) this.months.push(i);
  134. for (let i = 1; i <= 31; i++) this.days.push(i);
  135. },
  136. openPicker() {
  137. const date = new Date();
  138. const dateStr = this.formData.expiryDate || `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
  139. const [y, m, d] = dateStr.split('-').map(Number);
  140. let yIndex = this.years.indexOf(y); let mIndex = this.months.indexOf(m); let dIndex = this.days.indexOf(d);
  141. this.pickerValue = [yIndex > -1 ? yIndex : 0, mIndex > -1 ? mIndex : 0, dIndex > -1 ? dIndex : 0];
  142. this.tempYear = this.years[this.pickerValue[0]]; this.tempMonth = this.months[this.pickerValue[1]]; this.tempDay = this.days[this.pickerValue[2]];
  143. this.showPicker = true;
  144. },
  145. closePicker() { this.showPicker = false; },
  146. onPickerChange(e) {
  147. const val = e.detail.value;
  148. this.tempYear = this.years[val[0]]; this.tempMonth = this.months[val[1]]; this.tempDay = this.days[val[2]];
  149. },
  150. confirmPicker() {
  151. const mStr = this.tempMonth < 10 ? '0' + this.tempMonth : this.tempMonth;
  152. const dStr = this.tempDay < 10 ? '0' + this.tempDay : this.tempDay;
  153. this.formData.expiryDate = `${this.tempYear}-${mStr}-${dStr}`; this.closePicker();
  154. },
  155. restoreAuthData() {
  156. try {
  157. const saved = uni.getStorageSync('recruit_auth_data');
  158. if (saved) {
  159. const d = JSON.parse(saved);
  160. this.formData.name = d.name || ''; this.formData.idNumber = d.idNumber || '';
  161. this.formData.expiryDate = d.expiryDate || '';
  162. this.idCardFront = d.idCardFront || ''; this.idCardBack = d.idCardBack || '';
  163. this.idCardFrontOssId = d.idCardFrontOssId || ''; this.idCardBackOssId = d.idCardBackOssId || '';
  164. }
  165. } catch (e) { console.error('恢复认证数据失败', e); }
  166. },
  167. saveAuthData() {
  168. try {
  169. uni.setStorageSync('recruit_auth_data', JSON.stringify({
  170. name: this.formData.name, idNumber: this.formData.idNumber, expiryDate: this.formData.expiryDate,
  171. idCardFront: this.idCardFront, idCardBack: this.idCardBack,
  172. idCardFrontOssId: this.idCardFrontOssId, idCardBackOssId: this.idCardBackOssId
  173. }));
  174. } catch (e) { console.error('保存认证数据失败', e); }
  175. },
  176. resetFormData() {
  177. this.formData.name = ''; this.formData.idNumber = ''; this.formData.expiryDate = '';
  178. this.idCardFront = ''; this.idCardBack = '';
  179. this.idCardFrontOssId = ''; this.idCardBackOssId = '';
  180. try { uni.removeStorageSync('recruit_auth_data'); } catch (e) { console.error('清除缓存失败', e); }
  181. },
  182. chooseImage(side) {
  183. this.isChoosingImage = true;
  184. uni.chooseImage({
  185. count: 1, sizeType: ['compressed'], sourceType: ['album', 'camera'],
  186. success: async (res) => {
  187. const tempPath = res.tempFilePaths[0];
  188. if (side === 'front') this.idCardFront = tempPath; else this.idCardBack = tempPath;
  189. try {
  190. uni.showLoading({ title: '上传中...' });
  191. const uploadRes = await uploadFile(tempPath);
  192. if (side === 'front') this.idCardFrontOssId = uploadRes.data.ossId;
  193. else this.idCardBackOssId = uploadRes.data.ossId;
  194. uni.hideLoading(); this.saveAuthData();
  195. } catch (err) { uni.hideLoading(); console.error('上传身份证图片失败:', err); uni.showToast({ title: err.message || err.msg || '上传失败', icon: 'none' }); }
  196. }
  197. });
  198. },
  199. goToQualifications() {
  200. // 必填项校验
  201. if (!this.formData.name.trim()) {
  202. uni.showToast({ title: '请输入真实姓名', icon: 'none' });
  203. return;
  204. }
  205. if (!this.formData.idNumber.trim()) {
  206. uni.showToast({ title: '请输入证件号码', icon: 'none' });
  207. return;
  208. }
  209. // 简单的身份证格式校验建议(此处仅做非空,如果需要正则可以添加)
  210. if (this.formData.idNumber.length < 15) {
  211. uni.showToast({ title: '请输入正确的证件号码', icon: 'none' });
  212. return;
  213. }
  214. if (!this.formData.expiryDate) {
  215. uni.showToast({ title: '请选择有效日期', icon: 'none' });
  216. return;
  217. }
  218. if (!this.idCardFrontOssId) {
  219. uni.showToast({ title: '请上传身份证照片面', icon: 'none' });
  220. return;
  221. }
  222. if (!this.idCardBackOssId) {
  223. uni.showToast({ title: '请上传身份证国徽面', icon: 'none' });
  224. return;
  225. }
  226. this.saveAuthData();
  227. try {
  228. const stored = uni.getStorageSync('recruit_form_data');
  229. if (stored) {
  230. const data = JSON.parse(stored);
  231. data.realName = this.formData.name; data.idNumber = this.formData.idNumber;
  232. data.expiryDate = this.formData.expiryDate;
  233. data.idCardFrontOssId = this.idCardFrontOssId; data.idCardBackOssId = this.idCardBackOssId;
  234. uni.setStorageSync('recruit_form_data', JSON.stringify(data));
  235. }
  236. } catch (e) { console.error('保存认证数据失败', e); }
  237. const services = JSON.stringify(this.serviceType);
  238. uni.navigateTo({ url: `/pages/recruit/qualifications/index?services=${encodeURIComponent(services)}` });
  239. }
  240. }
  241. }
  242. </script>
  243. <style>
  244. page {
  245. background-color: #F8F8F8;
  246. font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica, Segoe UI, Arial, Roboto, 'PingFang SC', 'miui', 'Hiragino Sans GB', 'Microsoft Yahei', sans-serif;
  247. }
  248. .auth-container {
  249. padding: 20rpx;
  250. padding-bottom: 200rpx;
  251. }
  252. .top-tip {
  253. font-size: 24rpx;
  254. color: #666;
  255. margin-bottom: 20rpx;
  256. padding: 0 10rpx;
  257. }
  258. .form-card {
  259. background-color: #fff;
  260. border-radius: 20rpx;
  261. padding: 0 30rpx;
  262. margin-bottom: 20rpx;
  263. }
  264. .form-item {
  265. display: flex;
  266. align-items: center;
  267. height: 100rpx;
  268. border-bottom: none;
  269. }
  270. .label {
  271. width: 170rpx;
  272. font-size: 26rpx;
  273. font-weight: bold;
  274. color: #333;
  275. }
  276. .required {
  277. color: #FF5252;
  278. margin-right: 4rpx;
  279. font-weight: bold;
  280. }
  281. .input-area {
  282. flex: 1;
  283. font-size: 26rpx;
  284. color: #333;
  285. }
  286. .input-placeholder {
  287. color: #ccc;
  288. font-size: 26rpx;
  289. }
  290. .read-only-text {
  291. color: #333;
  292. font-size: 26rpx;
  293. }
  294. .card-label {
  295. font-size: 26rpx;
  296. color: #333;
  297. font-weight: bold;
  298. }
  299. .next-btn {
  300. background: linear-gradient(90deg, #FF6F00 0%, #FF5722 100%);
  301. color: #fff;
  302. font-size: 28rpx;
  303. font-weight: bold;
  304. height: 90rpx;
  305. line-height: 90rpx;
  306. border-radius: 45rpx;
  307. box-shadow: 0 10rpx 20rpx rgba(255, 87, 34, 0.2);
  308. }
  309. .gray-input-box {
  310. flex: 1;
  311. height: 70rpx;
  312. background-color: #F8F8F8;
  313. border-radius: 8rpx;
  314. display: flex;
  315. align-items: center;
  316. padding: 0 20rpx;
  317. }
  318. .arrow-right {
  319. width: 24rpx;
  320. height: 24rpx;
  321. margin-left: auto;
  322. }
  323. .upload-card {
  324. background-color: #fff;
  325. border-radius: 20rpx;
  326. padding: 40rpx 0;
  327. margin-bottom: 20rpx;
  328. display: flex;
  329. flex-direction: column;
  330. align-items: center;
  331. justify-content: center;
  332. }
  333. .upload-box {
  334. width: 600rpx;
  335. height: 360rpx;
  336. background-color: #F8F8F8;
  337. border: 2rpx dashed #eee;
  338. border-radius: 12rpx;
  339. display: flex;
  340. flex-direction: column;
  341. align-items: center;
  342. justify-content: center;
  343. margin-bottom: 20rpx;
  344. position: relative;
  345. overflow: hidden;
  346. }
  347. .camera-icon {
  348. width: 64rpx;
  349. height: 64rpx;
  350. margin-bottom: 15rpx;
  351. }
  352. .upload-text {
  353. font-size: 26rpx;
  354. color: #ccc;
  355. }
  356. .preview-img {
  357. width: 100%;
  358. height: 100%;
  359. }
  360. .footer-btn-area {
  361. margin-top: 60rpx;
  362. padding: 0 20rpx;
  363. }
  364. .picker-mask {
  365. position: fixed;
  366. top: 0;
  367. left: 0;
  368. right: 0;
  369. bottom: 0;
  370. background-color: rgba(0, 0, 0, 0.5);
  371. z-index: 999;
  372. visibility: hidden;
  373. opacity: 0;
  374. transition: all 0.3s;
  375. }
  376. .picker-mask.show {
  377. visibility: visible;
  378. opacity: 1;
  379. }
  380. .picker-content {
  381. position: absolute;
  382. bottom: 0;
  383. left: 0;
  384. width: 100%;
  385. background-color: #fff;
  386. border-radius: 20rpx 20rpx 0 0;
  387. transform: translateY(100%);
  388. transition: all 0.3s;
  389. }
  390. .picker-mask.show .picker-content {
  391. transform: translateY(0);
  392. }
  393. .picker-header {
  394. display: flex;
  395. justify-content: space-between;
  396. align-items: center;
  397. padding: 30rpx;
  398. border-bottom: 1px solid #eee;
  399. font-size: 32rpx;
  400. }
  401. .picker-btn-cancel {
  402. color: #999;
  403. }
  404. .picker-title {
  405. font-weight: bold;
  406. color: #333;
  407. }
  408. .picker-btn-confirm {
  409. color: #FF5722;
  410. font-weight: bold;
  411. }
  412. .picker-view {
  413. width: 100%;
  414. height: 400rpx;
  415. }
  416. .picker-item {
  417. line-height: 50px;
  418. text-align: center;
  419. font-size: 32rpx;
  420. color: #333;
  421. }
  422. </style>