select-resume.vue 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. <template>
  2. <view class="select-resume-container">
  3. <!-- 页面标题 -->
  4. <view class="page-header">
  5. <text class="header-title">选择投递简历</text>
  6. <text class="header-subtitle">请选择一份附件简历进行投递</text>
  7. </view>
  8. <!-- 加载状态 -->
  9. <view class="loading-box" v-if="isLoading">
  10. <view class="loading-spinner"></view>
  11. <text class="loading-text">加载中...</text>
  12. </view>
  13. <!-- 简历列表 -->
  14. <view class="resume-list" v-else-if="resumeList.length > 0">
  15. <view
  16. class="resume-card"
  17. :class="{ 'selected': selectedOssId === item.ossId }"
  18. v-for="(item, idx) in resumeList"
  19. :key="item.id"
  20. @click="selectResume(item)"
  21. >
  22. <view class="card-left">
  23. <view class="file-icon-box">
  24. <image src="/static/icons/pdf.svg" mode="aspectFit" class="pdf-icon"></image>
  25. </view>
  26. <view class="file-info">
  27. <text class="file-name text-ellipsis">{{ item.name }}</text>
  28. <text class="file-size" v-if="item.fileSize">{{ formatFileSize(item.fileSize) }}</text>
  29. </view>
  30. </view>
  31. <view class="radio-box">
  32. <view class="radio-outer">
  33. <view class="radio-inner" v-if="selectedOssId === item.ossId"></view>
  34. </view>
  35. </view>
  36. </view>
  37. </view>
  38. <!-- 空状态 -->
  39. <view class="empty-state" v-else>
  40. <image src="/static/icons/empty_resume.svg" mode="aspectFit" class="empty-icon" v-if="false"></image>
  41. <view class="empty-icon-text">
  42. <text>暂无附件简历</text>
  43. </view>
  44. <text class="empty-desc">请先在"我的"页面上传附件简历</text>
  45. <view class="empty-action" @click="goToUpload">
  46. <text>去上传简历</text>
  47. </view>
  48. </view>
  49. <!-- 底部操作栏 -->
  50. <view class="bottom-bar" v-if="resumeList.length > 0">
  51. <button class="submit-btn" :class="{ 'disabled': !selectedOssId }" @click="handleDeliver">
  52. <text>{{ delivering ? '投递中...' : '确认投递' }}</text>
  53. </button>
  54. </view>
  55. </view>
  56. </template>
  57. <script setup lang="js">
  58. import { ref } from 'vue';
  59. import { onLoad } from '@dcloudio/uni-app';
  60. import { getAppendixList } from '../../api/student.js';
  61. import { applyPosition } from '../../api/assessment.js';
  62. const isLoading = ref(true);
  63. const delivering = ref(false);
  64. const resumeList = ref([]);
  65. const selectedOssId = ref(null);
  66. const postId = ref(null);
  67. onLoad((options) => {
  68. if (options.postId) {
  69. postId.value = options.postId;
  70. }
  71. fetchResumeList();
  72. });
  73. const fetchResumeList = async () => {
  74. try {
  75. isLoading.value = true;
  76. const userInfo = uni.getStorageSync('userInfo');
  77. if (!userInfo || !userInfo.studentId) {
  78. uni.showToast({ title: '请先登录', icon: 'none' });
  79. setTimeout(() => {
  80. uni.navigateTo({ url: '/pages/login/login' });
  81. }, 1000);
  82. return;
  83. }
  84. const res = await getAppendixList(userInfo.studentId);
  85. if (res && res.code === 200) {
  86. resumeList.value = res.data.map(item => ({
  87. id: item.id,
  88. name: item.fileName,
  89. url: item.url,
  90. ossId: item.ossId,
  91. fileSize: item.fileSize
  92. }));
  93. // 默认选中第一个
  94. if (resumeList.value.length > 0) {
  95. selectedOssId.value = resumeList.value[0].ossId;
  96. }
  97. }
  98. } catch (err) {
  99. console.error('获取简历列表失败:', err);
  100. uni.showToast({ title: '获取简历列表失败', icon: 'none' });
  101. } finally {
  102. isLoading.value = false;
  103. }
  104. };
  105. const selectResume = (item) => {
  106. selectedOssId.value = item.ossId;
  107. };
  108. const formatFileSize = (size) => {
  109. if (!size) return '';
  110. if (size < 1024) return size + 'B';
  111. if (size < 1024 * 1024) return (size / 1024).toFixed(1) + 'KB';
  112. return (size / (1024 * 1024)).toFixed(1) + 'MB';
  113. };
  114. const handleDeliver = async () => {
  115. if (!selectedOssId.value || delivering.value) return;
  116. const userInfo = uni.getStorageSync('userInfo');
  117. if (!userInfo || !userInfo.studentId) {
  118. uni.showToast({ title: '请先登录', icon: 'none' });
  119. return;
  120. }
  121. try {
  122. delivering.value = true;
  123. uni.showLoading({ title: '投递中...' });
  124. const res = await applyPosition({
  125. postId: postId.value,
  126. resumeOssId: selectedOssId.value
  127. });
  128. uni.hideLoading();
  129. if (res.code === 200) {
  130. // 标记为已投递
  131. uni.setStorageSync(`candidate_applied_${postId.value}`, true);
  132. uni.showToast({ title: '投递成功', icon: 'success' });
  133. setTimeout(() => {
  134. // 返回岗位详情页并刷新状态
  135. const pages = getCurrentPages();
  136. if (pages.length > 1) {
  137. uni.$emit('resume_delivered', { postId: postId.value });
  138. uni.navigateBack();
  139. }
  140. }, 1500);
  141. } else if (res.msg && res.msg.includes('已投递')) {
  142. uni.setStorageSync(`candidate_applied_${postId.value}`, true);
  143. uni.showToast({ title: '您已投递过该岗位', icon: 'none' });
  144. setTimeout(() => {
  145. uni.$emit('resume_delivered', { postId: postId.value });
  146. uni.navigateBack();
  147. }, 1500);
  148. } else {
  149. uni.showToast({ title: res.msg || '投递失败', icon: 'none' });
  150. }
  151. } catch (err) {
  152. uni.hideLoading();
  153. console.error('投递失败:', err);
  154. const errMsg = String(err?.msg || err?.message || '');
  155. if (errMsg.includes('已投递')) {
  156. uni.setStorageSync(`candidate_applied_${postId.value}`, true);
  157. uni.showToast({ title: '您已投递过该岗位', icon: 'none' });
  158. setTimeout(() => {
  159. uni.$emit('resume_delivered', { postId: postId.value });
  160. uni.navigateBack();
  161. }, 1500);
  162. } else {
  163. uni.showToast({ title: '网络错误,投递失败', icon: 'none' });
  164. }
  165. } finally {
  166. delivering.value = false;
  167. }
  168. };
  169. const goToUpload = () => {
  170. uni.navigateBack();
  171. };
  172. </script>
  173. <style lang="scss" scoped>
  174. .select-resume-container {
  175. min-height: 100vh;
  176. background-color: #F6F8FB;
  177. padding-bottom: 160rpx;
  178. }
  179. .page-header {
  180. padding: 40rpx 40rpx 20rpx;
  181. .header-title {
  182. display: block;
  183. font-size: 40rpx;
  184. font-weight: bold;
  185. color: #1A1A1A;
  186. margin-bottom: 12rpx;
  187. }
  188. .header-subtitle {
  189. font-size: 26rpx;
  190. color: #999999;
  191. }
  192. }
  193. .loading-box {
  194. display: flex;
  195. flex-direction: column;
  196. align-items: center;
  197. justify-content: center;
  198. height: 50vh;
  199. .loading-spinner {
  200. width: 60rpx;
  201. height: 60rpx;
  202. border: 6rpx solid #f3f3f3;
  203. border-top: 6rpx solid #1F6CFF;
  204. border-radius: 50%;
  205. animation: spin 1s linear infinite;
  206. }
  207. .loading-text {
  208. margin-top: 20rpx;
  209. font-size: 28rpx;
  210. color: #999;
  211. }
  212. }
  213. @keyframes spin {
  214. 0% { transform: rotate(0deg); }
  215. 100% { transform: rotate(360deg); }
  216. }
  217. .resume-list {
  218. padding: 20rpx 30rpx;
  219. }
  220. .resume-card {
  221. display: flex;
  222. align-items: center;
  223. justify-content: space-between;
  224. background: #FFFFFF;
  225. border-radius: 20rpx;
  226. padding: 30rpx;
  227. margin-bottom: 20rpx;
  228. border: 2rpx solid #F0F2F5;
  229. transition: all 0.2s ease;
  230. &.selected {
  231. border-color: #1F6CFF;
  232. background: #F0F6FF;
  233. box-shadow: 0 4rpx 16rpx rgba(31, 108, 255, 0.1);
  234. }
  235. &:active {
  236. opacity: 0.9;
  237. }
  238. .card-left {
  239. display: flex;
  240. align-items: center;
  241. flex: 1;
  242. overflow: hidden;
  243. .file-icon-box {
  244. width: 80rpx;
  245. height: 80rpx;
  246. background: #FFF5F0;
  247. border-radius: 16rpx;
  248. display: flex;
  249. align-items: center;
  250. justify-content: center;
  251. margin-right: 24rpx;
  252. flex-shrink: 0;
  253. .pdf-icon {
  254. width: 48rpx;
  255. height: 48rpx;
  256. }
  257. }
  258. .file-info {
  259. flex: 1;
  260. overflow: hidden;
  261. .file-name {
  262. display: block;
  263. font-size: 28rpx;
  264. font-weight: 500;
  265. color: #1A1A1A;
  266. margin-bottom: 8rpx;
  267. overflow: hidden;
  268. white-space: nowrap;
  269. text-overflow: ellipsis;
  270. }
  271. .file-size {
  272. font-size: 24rpx;
  273. color: #999999;
  274. }
  275. }
  276. }
  277. .radio-box {
  278. flex-shrink: 0;
  279. margin-left: 20rpx;
  280. .radio-outer {
  281. width: 44rpx;
  282. height: 44rpx;
  283. border-radius: 50%;
  284. border: 3rpx solid #D0D0D0;
  285. display: flex;
  286. align-items: center;
  287. justify-content: center;
  288. transition: all 0.2s ease;
  289. }
  290. .radio-inner {
  291. width: 24rpx;
  292. height: 24rpx;
  293. border-radius: 50%;
  294. background: #1F6CFF;
  295. }
  296. }
  297. &.selected .radio-outer {
  298. border-color: #1F6CFF;
  299. }
  300. }
  301. .text-ellipsis {
  302. overflow: hidden;
  303. white-space: nowrap;
  304. text-overflow: ellipsis;
  305. }
  306. .empty-state {
  307. display: flex;
  308. flex-direction: column;
  309. align-items: center;
  310. justify-content: center;
  311. padding-top: 200rpx;
  312. .empty-icon-text {
  313. width: 120rpx;
  314. height: 120rpx;
  315. background: #F0F2F5;
  316. border-radius: 50%;
  317. display: flex;
  318. align-items: center;
  319. justify-content: center;
  320. margin-bottom: 30rpx;
  321. text {
  322. font-size: 24rpx;
  323. color: #CCCCCC;
  324. }
  325. }
  326. .empty-desc {
  327. font-size: 28rpx;
  328. color: #999999;
  329. margin-bottom: 40rpx;
  330. }
  331. .empty-action {
  332. padding: 16rpx 48rpx;
  333. background: #1F6CFF;
  334. border-radius: 36rpx;
  335. text {
  336. font-size: 28rpx;
  337. color: #FFFFFF;
  338. font-weight: 500;
  339. }
  340. }
  341. }
  342. .bottom-bar {
  343. position: fixed;
  344. left: 0;
  345. right: 0;
  346. bottom: 0;
  347. background: rgba(255, 255, 255, 0.95);
  348. backdrop-filter: blur(10px);
  349. padding: 24rpx 40rpx calc(24rpx + env(safe-area-inset-bottom));
  350. box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.06);
  351. z-index: 999;
  352. .submit-btn {
  353. width: 100%;
  354. height: 88rpx;
  355. line-height: 88rpx;
  356. background: #1F6CFF;
  357. color: #FFFFFF;
  358. font-size: 32rpx;
  359. font-weight: bold;
  360. border-radius: 44rpx;
  361. text-align: center;
  362. border: none;
  363. box-shadow: 0 8rpx 16rpx rgba(31, 108, 255, 0.2);
  364. &::after {
  365. border: none;
  366. }
  367. &:active {
  368. opacity: 0.9;
  369. }
  370. &.disabled {
  371. background: #E0E0E0;
  372. box-shadow: none;
  373. color: #999;
  374. pointer-events: none;
  375. }
  376. }
  377. }
  378. </style>