my.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609
  1. <template>
  2. <view class="container">
  3. <!-- 状态栏与胶囊位占位:确保内容不下压至标题栏重合 -->
  4. <view class="nav-placeholder" :style="{ height: navFullHeight + 'px' }"></view>
  5. <!-- 顶部用户信息区 -->
  6. <view class="user-card-wrap">
  7. <view class="user-main">
  8. <view class="avatar-wrap">
  9. <image class="avatar" :src="studentInfo.avatarUrl || '/static/images/hr_avatar.svg'" mode="aspectFill"></image>
  10. </view>
  11. <view class="info-content">
  12. <view class="name-box">
  13. <text class="user-name">{{ studentInfo.name || '加载中...' }}</text>
  14. <image class="diamond-icon" src="/static/icons/diamond.svg" mode="aspectFit"></image>
  15. <view class="flex-spacer"></view>
  16. <view class="resume-link" @click="handleOnlineResume">
  17. <text>在线简历</text>
  18. <image class="link-arrow" src="/static/icons/chevron-right-blue.svg" mode="aspectFit"></image>
  19. </view>
  20. </view>
  21. <view class="badge-row">
  22. <text class="profile-tag" v-if="schoolNameLabel">{{ schoolNameLabel }}</text>
  23. <text class="profile-tag" v-if="educationLabel">{{ educationLabel }}</text>
  24. <text class="profile-tag" v-if="jobTypeLabel">{{ jobTypeLabel }}</text>
  25. </view>
  26. </view>
  27. </view>
  28. </view>
  29. <!-- 磁贴功能区 -->
  30. <view class="feature-grid">
  31. <view class="grid-cell" @click="navigateTo('collection')">
  32. <view class="icon-wrap-premium">
  33. <image src="/static/my/fav_premium.svg" mode="aspectFit"></image>
  34. </view>
  35. <text class="cell-label">我的收藏</text>
  36. </view>
  37. <view class="grid-cell" @click="navigateTo('intention')">
  38. <view class="icon-wrap-premium">
  39. <image src="/static/my/intent_premium.svg" mode="aspectFit"></image>
  40. </view>
  41. <text class="cell-label">意向岗位</text>
  42. </view>
  43. <view class="grid-cell" @click="navigateTo('assessment')">
  44. <view class="icon-wrap-premium">
  45. <image src="/static/my/test_premium.svg" mode="aspectFit"></image>
  46. </view>
  47. <text class="cell-label">我的测评</text>
  48. </view>
  49. <view class="grid-cell" @click="navigateTo('order')">
  50. <view class="icon-wrap-premium">
  51. <image src="/static/my/order_premium.svg" mode="aspectFit"></image>
  52. </view>
  53. <text class="cell-label">我的订单</text>
  54. </view>
  55. </view>
  56. <!-- 服务列表区 -->
  57. <view class="service-panel">
  58. <view class="panel-header">
  59. <text class="panel-title">我的服务</text>
  60. </view>
  61. <!-- 附件简历区块 -->
  62. <view class="service-item resume-section">
  63. <view class="res-head">
  64. <text class="res-title">附件简历 ({{ resumeList.length }}/3)</text>
  65. <view class="upload-links">
  66. <text class="link-btn active" @click="uploadFromWechat">从微信聊天文件上传</text>
  67. <text class="link-btn" @click="uploadFromLocal">从手机本地上传</text>
  68. </view>
  69. </view>
  70. <!-- 动态简历列表 -->
  71. <view class="resume-list-container" v-if="resumeList.length > 0">
  72. <view class="file-card card-anim" v-for="(file, idx) in resumeList" :key="file.id">
  73. <view class="file-info-main">
  74. <view class="file-icon-box">
  75. <image src="/static/icons/pdf.svg" mode="aspectFit" class="pdf-thumb"></image>
  76. </view>
  77. <text class="file-display-name text-ellipsis">{{ file.name }}</text>
  78. </view>
  79. <view class="remove-action" @click.stop="removeResume(idx)">
  80. <image class="remove-icon" src="/static/icons/close_gray.svg" mode="aspectFit"></image>
  81. </view>
  82. </view>
  83. </view>
  84. <!-- 空状态提示 -->
  85. <view class="empty-resume" v-else>
  86. <text>暂无附件简历,最多可上传3份</text>
  87. </view>
  88. </view>
  89. <!-- 普通功能条 -->
  90. <view class="list-item" @click="handleOffer">
  91. <text class="list-label">我的offer/签约</text>
  92. <image class="list-arrow" src="/static/icons/chevron-right.svg" mode="aspectFit"></image>
  93. </view>
  94. <view class="list-item" @click="handlePrivacy">
  95. <text class="list-label">隐私政策</text>
  96. <image class="list-arrow" src="/static/icons/chevron-right.svg" mode="aspectFit"></image>
  97. </view>
  98. </view>
  99. <!-- 底部登出与信息 -->
  100. <view class="bottom-area">
  101. <view class="logout-action" @click="handleLogout">
  102. <text>退出登录</text>
  103. </view>
  104. <view class="contact-info">
  105. <text>电话:400-XXXX-XX9 工作时间:8:00-22:00</text>
  106. </view>
  107. </view>
  108. <!-- 页面底部垫高 -->
  109. <view class="bottom-pad"></view>
  110. <!-- 自定义提示弹窗 -->
  111. <view class="modal-mask" v-if="showResumeModal" @touchmove.stop.prevent>
  112. <view class="modal-content confirm-modal">
  113. <view class="confirm-title">提示</view>
  114. <view class="confirm-desc">您当前还未填写在线简历,是否去填写?</view>
  115. <view class="confirm-footer">
  116. <button class="btn-cancel" @click="showResumeModal = false">取消</button>
  117. <button class="btn-primary" @click="confirmToFill">去填写</button>
  118. </view>
  119. </view>
  120. </view>
  121. <!-- 自定义导航栏 -->
  122. <custom-tabbar :activeIndex="3"></custom-tabbar>
  123. </view>
  124. </template>
  125. <script setup lang="js">
  126. import { ref, computed, onMounted } from 'vue';
  127. import { onShow, onPullDownRefresh } from '@dcloudio/uni-app';
  128. import CustomTabbar from '../../components/custom-tabbar/custom-tabbar.vue';
  129. import { getStudent, getAppendixList, addAppendix, removeAppendix } from '../../api/student.js';
  130. import { UPLOAD_URL } from '../../utils/request';
  131. const navFullHeight = ref(80); // 默认高度
  132. const showResumeModal = ref(false);
  133. // 学生详细信息(从后端获取)
  134. const studentInfo = ref({
  135. id: null,
  136. name: '',
  137. avatarUrl: '',
  138. schoolName: '',
  139. education: '',
  140. educationLabel: '',
  141. jobType: '',
  142. jobTypeLabel: '',
  143. resumeFile: null,
  144. educationList: [], // 教育经历列表
  145. });
  146. // 计算学历展示标签(从教育经历列表中获取最高学历)
  147. const educationLabel = computed(() => {
  148. if (studentInfo.value.educationList && studentInfo.value.educationList.length > 0) {
  149. const educationLevel = { '初中及以下': 1, '高中': 2, '中专': 2, '大专': 3, '本科': 4, '硕士': 5, '博士': 6 };
  150. let highestEdu = studentInfo.value.educationList[0];
  151. let maxLevel = educationLevel[highestEdu.education] || 0;
  152. for (const edu of studentInfo.value.educationList) {
  153. const level = educationLevel[edu.education] || 0;
  154. if (level > maxLevel) { maxLevel = level; highestEdu = edu; }
  155. }
  156. return highestEdu.education || '';
  157. }
  158. if (studentInfo.value.educationLabel) return studentInfo.value.educationLabel;
  159. const val = studentInfo.value.education;
  160. if (!val || val === '0' || val === 0) return '';
  161. return /^\d+$/.test(String(val)) ? '' : val;
  162. });
  163. // 计算求职类型展示标签(通过字典值转换)
  164. const jobTypeMap = { '1': '全职', '2': '实习', '3': '兼职', '全职': '全职', '实习': '实习', '兼职': '兼职' };
  165. const jobTypeLabel = computed(() => {
  166. if (studentInfo.value.jobTypeLabel) return studentInfo.value.jobTypeLabel;
  167. const val = studentInfo.value.jobType;
  168. return jobTypeMap[val] || val || '';
  169. });
  170. // 计算院校名称展示标签(从教育经历中获取最高学历的学校)
  171. const schoolNameLabel = computed(() => {
  172. if (studentInfo.value.educationList && studentInfo.value.educationList.length > 0) {
  173. const educationLevel = { '初中及以下': 1, '高中': 2, '中专': 2, '大专': 3, '本科': 4, '硕士': 5, '博士': 6 };
  174. let highestEdu = studentInfo.value.educationList[0];
  175. let maxLevel = educationLevel[highestEdu.education] || 0;
  176. for (const edu of studentInfo.value.educationList) {
  177. const level = educationLevel[edu.education] || 0;
  178. if (level > maxLevel) { maxLevel = level; highestEdu = edu; }
  179. }
  180. return highestEdu.school || '';
  181. }
  182. const val = studentInfo.value.schoolName;
  183. if (!val || /^\d+$/.test(val)) return '';
  184. return val;
  185. });
  186. // 是否有在线简历(有 resumeFile 字段值则判断为有)
  187. const hasResume = computed(() => !!studentInfo.value.resumeFile);
  188. // 加载用户信息
  189. const loadUserInfo = async () => {
  190. const userInfo = uni.getStorageSync('userInfo');
  191. if (!userInfo || !userInfo.studentId) return;
  192. try {
  193. const res = await getStudent(userInfo.studentId);
  194. if (res && res.data) {
  195. const data = res.data;
  196. studentInfo.value = {
  197. id: data.id,
  198. name: data.name || userInfo.name || '',
  199. avatarUrl: data.avatarUrl || userInfo.avatarUrl || '',
  200. schoolName: data.schoolName || '',
  201. education: data.education || '',
  202. educationLabel: data.educationLabel || '',
  203. jobType: data.jobType || '',
  204. jobTypeLabel: data.jobTypeLabel || '',
  205. resumeFile: data.resumeFile || null,
  206. educationList: data.educationList || [],
  207. };
  208. }
  209. } catch (err) {
  210. console.error('获取用户详情失败', err);
  211. }
  212. fetchAppendixList();
  213. };
  214. onMounted(async () => {
  215. // #ifdef MP-WEIXIN
  216. try {
  217. const menuButton = uni.getMenuButtonBoundingClientRect();
  218. if (menuButton) {
  219. navFullHeight.value = menuButton.bottom + 10;
  220. }
  221. } catch (e) {
  222. console.error('导航栏高度计算失败', e);
  223. }
  224. // #endif
  225. // 从本地缓存读取登录信息
  226. const userInfo = uni.getStorageSync('userInfo');
  227. if (!userInfo || !userInfo.studentId) {
  228. // 未登录,跳转登录页
  229. uni.reLaunch({ url: '/pages/login/login' });
  230. return;
  231. }
  232. // 先用登录时缓存的基本信息快速展示
  233. studentInfo.value.id = userInfo.studentId;
  234. studentInfo.value.name = userInfo.name || '';
  235. studentInfo.value.avatarUrl = userInfo.avatarUrl || '';
  236. // 再从后端拉取完整学员信息
  237. await loadUserInfo();
  238. });
  239. onShow(async () => {
  240. const userInfo = uni.getStorageSync('userInfo');
  241. if (userInfo && userInfo.studentId) {
  242. await loadUserInfo();
  243. }
  244. });
  245. onPullDownRefresh(async () => {
  246. await loadUserInfo();
  247. uni.stopPullDownRefresh();
  248. });
  249. // 获取附件简历列表
  250. const fetchAppendixList = async () => {
  251. try {
  252. const userInfo = uni.getStorageSync('userInfo');
  253. if (userInfo && userInfo.studentId) {
  254. const res = await getAppendixList(userInfo.studentId);
  255. if (res && res.code === 200) {
  256. resumeList.value = res.data.map(item => ({
  257. id: item.id,
  258. name: item.fileName,
  259. url: item.url,
  260. ossId: item.ossId
  261. }));
  262. }
  263. }
  264. } catch (err) {
  265. console.error('获取附件列表失败', err);
  266. }
  267. };
  268. const handleOnlineResume = () => {
  269. if (!hasResume.value) {
  270. showResumeModal.value = true;
  271. } else {
  272. uni.navigateTo({ url: '/pages/my/resume_view' });
  273. }
  274. };
  275. const confirmToFill = () => {
  276. showResumeModal.value = false;
  277. uni.navigateTo({ url: '/pages/profile/profile?editMode=1' });
  278. };
  279. const navigateTo = (type) => {
  280. const routes = {
  281. 'collection': '/pages/my/favorites',
  282. 'intention': '/pages/intention/intention?editMode=1',
  283. 'assessment': '/pages/my/assessment-records',
  284. 'order': '/pages/my/orders'
  285. };
  286. if (routes[type]) {
  287. uni.navigateTo({ url: routes[type] });
  288. }
  289. };
  290. const handleOffer = () => {
  291. uni.navigateTo({ url: '/pages/my/offer' });
  292. };
  293. const handlePrivacy = () => {
  294. uni.navigateTo({ url: '/pages/my/privacy_policy' });
  295. };
  296. // 附件简历列表(本地维护,最多3份)
  297. const resumeList = ref([]);
  298. const uploadFromWechat = () => {
  299. if (resumeList.value.length >= 3) {
  300. uni.showToast({ title: '最多上传3份简历', icon: 'none' });
  301. return;
  302. }
  303. // #ifdef MP-WEIXIN
  304. // 微信小程序中,从聊天记录选择文件是唯一支持PDF等文档的官方接口
  305. uni.chooseMessageFile({
  306. count: 1,
  307. type: 'file',
  308. extension: ['pdf'],
  309. success: (res) => {
  310. const file = res.tempFiles[0];
  311. processUpload(file.name, file.path, file.size);
  312. }
  313. });
  314. // #endif
  315. };
  316. const uploadFromLocal = () => {
  317. // 在微信小程序中,uni.chooseFile 兼容性极差且非官方推荐
  318. // 实际上的“本地上传”在小程序内也往往需要通过 chooseMessageFile 调起(微信提供了从相册/文件选择的入口)
  319. // 为了用户体验一致,这里我们统一调用 uploadFromWechat
  320. uploadFromWechat();
  321. };
  322. const processUpload = (name, path, size) => {
  323. if (!name.toLowerCase().endsWith('.pdf')) {
  324. uni.showToast({ title: '仅支持 PDF 格式文件', icon: 'none' });
  325. return;
  326. }
  327. uni.showLoading({ title: '上传中...' });
  328. // 1. 先调用通用的 OSS 上传接口
  329. // 注意:如果是生产环境,请确保 baseUrl 正确
  330. const baseUrl = UPLOAD_URL;
  331. uni.uploadFile({
  332. url: baseUrl + '/portal/oss/upload',
  333. filePath: path,
  334. name: 'file',
  335. header: {
  336. 'Authorization': uni.getStorageSync('token') ? `Bearer ${uni.getStorageSync('token')}` : '',
  337. 'PLATFORM_CODE': 'PINGTAIDUAN'
  338. },
  339. success: async (uploadRes) => {
  340. const uploadData = JSON.parse(uploadRes.data);
  341. if (uploadData.code === 200) {
  342. const ossId = uploadData.data.ossId;
  343. // 2. 将 OSS 关联到学员附件表
  344. try {
  345. const userInfo = uni.getStorageSync('userInfo');
  346. const saveRes = await addAppendix({
  347. studentId: userInfo.studentId,
  348. ossId: ossId,
  349. fileName: name,
  350. fileSize: size
  351. });
  352. uni.hideLoading();
  353. if (saveRes.code === 200) {
  354. uni.showToast({ title: '上传成功', icon: 'success' });
  355. fetchAppendixList(); // 刷新列表
  356. } else {
  357. uni.showToast({ title: saveRes.msg || '保存失败', icon: 'none' });
  358. }
  359. } catch (e) {
  360. uni.hideLoading();
  361. uni.showToast({ title: '服务器异常', icon: 'none' });
  362. }
  363. } else {
  364. uni.hideLoading();
  365. uni.showToast({ title: uploadData.msg || '上传失败', icon: 'none' });
  366. }
  367. },
  368. fail: (err) => {
  369. uni.hideLoading();
  370. uni.showToast({ title: '网络上传失败', icon: 'none' });
  371. }
  372. });
  373. };
  374. const removeResume = (index) => {
  375. const target = resumeList.value[index];
  376. uni.showModal({
  377. title: '提示',
  378. content: '确定要删除这份简历吗?',
  379. success: async (res) => {
  380. if (res.confirm) {
  381. try {
  382. uni.showLoading({ title: '删除中...' });
  383. const delRes = await removeAppendix(target.id);
  384. uni.hideLoading();
  385. if (delRes.code === 200) {
  386. resumeList.value.splice(index, 1);
  387. uni.showToast({ title: '已删除', icon: 'none' });
  388. } else {
  389. uni.showToast({ title: delRes.msg || '删除失败', icon: 'none' });
  390. }
  391. } catch (e) {
  392. uni.hideLoading();
  393. uni.showToast({ title: '网络异常', icon: 'none' });
  394. }
  395. }
  396. }
  397. });
  398. };
  399. const handleLogout = () => {
  400. uni.showModal({
  401. title: '提示',
  402. content: '确定要退出登录吗?',
  403. success: (res) => {
  404. if (res.confirm) {
  405. // 清除本地登录信息
  406. uni.removeStorageSync('token');
  407. uni.removeStorageSync('userInfo');
  408. uni.reLaunch({ url: '/pages/login/login' });
  409. }
  410. }
  411. });
  412. };
  413. </script>
  414. <style lang="scss" scoped>
  415. .container {
  416. min-height: 100vh;
  417. background-color: #F8F9FB;
  418. }
  419. .nav-placeholder {
  420. width: 100%;
  421. background-color: #FFF;
  422. }
  423. .user-card-wrap {
  424. padding: 20rpx 40rpx 10rpx;
  425. background-color: #FFF;
  426. .user-main {
  427. display: flex;
  428. align-items: center;
  429. .avatar-wrap {
  430. width: 140rpx; height: 140rpx; border-radius: 50%; overflow: hidden;
  431. margin-right: 24rpx; background: #F0F0F0; border: 4rpx solid #F8F9FB;
  432. image { width: 100%; height: 100%; }
  433. }
  434. .info-content {
  435. flex: 1;
  436. .name-box {
  437. display: flex; align-items: center; margin-bottom: 24rpx;
  438. .user-name { font-size: 42rpx; font-weight: bold; color: #1A1A1A; margin-right: 12rpx; }
  439. .diamond-icon { width: 36rpx; height: 36rpx; }
  440. .flex-spacer { flex: 1; }
  441. .resume-link {
  442. display: flex; align-items: center; font-size: 26rpx; color: #1F6CFF;
  443. .link-arrow { width: 24rpx; height: 24rpx; margin-left: 4rpx; }
  444. }
  445. }
  446. .badge-row {
  447. display: flex; flex-wrap: wrap; gap: 12rpx;
  448. .profile-tag {
  449. font-size: 22rpx; color: #666; background: #F4F6F9;
  450. padding: 8rpx 22rpx; border-radius: 20rpx;
  451. }
  452. }
  453. }
  454. }
  455. }
  456. .feature-grid {
  457. display: flex; justify-content: space-around;
  458. padding: 40rpx 20rpx; background: #FFF;
  459. margin-top: 20rpx;
  460. .grid-cell {
  461. display: flex; flex-direction: column; align-items: center;
  462. .icon-wrap-premium {
  463. width: 80rpx; height: 80rpx;
  464. display: flex; align-items: center; justify-content: center;
  465. margin-bottom: 24rpx;
  466. position: relative;
  467. &::after {
  468. content: ''; position: absolute; left: 10%; right: 10%; bottom: -4rpx; height: 8rpx;
  469. background: rgba(0,0,0,0.06); filter: blur(6rpx); border-radius: 50%; z-index: -1;
  470. }
  471. image { width: 100%; height: 100%; }
  472. &:active { transform: translateY(2rpx) scale(0.96); opacity: 0.9; }
  473. }
  474. .cell-label { font-size: 26rpx; color: #333; font-weight: 500; }
  475. }
  476. }
  477. .service-panel {
  478. background: #FFF; margin-top: 20rpx; padding: 40rpx 40rpx 20rpx;
  479. .panel-header {
  480. margin-bottom: 40rpx;
  481. .panel-title { font-size: 32rpx; font-weight: bold; color: #1A1A1A; }
  482. }
  483. .resume-section {
  484. display: flex; flex-direction: column; align-items: stretch; margin-bottom: 20rpx;
  485. .res-head {
  486. display: flex; align-items: center; justify-content: space-between; margin-bottom: 30rpx;
  487. .res-title { font-size: 28rpx; color: #333; font-weight: 500; }
  488. .upload-links {
  489. display: flex; gap: 24rpx;
  490. .link-btn { font-size: 24rpx; color: #999; &.active { color: #1F6CFF; } }
  491. }
  492. }
  493. .file-card {
  494. background: #F8F9FA; display: flex; align-items: center; justify-content: space-between;
  495. padding: 14rpx 30rpx; border-radius: 20rpx; margin-bottom: 20rpx; border: 1rpx solid #F0F2F5;
  496. .file-info-main {
  497. display: flex; align-items: center; flex: 1; overflow: hidden;
  498. .file-icon-box {
  499. width: 44rpx; height: 44rpx; margin-right: 16rpx; display: flex; align-items: center; flex-shrink: 0;
  500. image { width: 100%; height: 100%; }
  501. }
  502. .file-display-name {
  503. font-size: 24rpx; color: #1F6CFF; font-weight: 500;
  504. overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
  505. }
  506. }
  507. .remove-action {
  508. padding: 10rpx; margin-right: -10rpx; flex-shrink: 0;
  509. image { width: 32rpx; height: 32rpx; opacity: 0.2; }
  510. }
  511. }
  512. }
  513. .empty-resume {
  514. padding: 40rpx 0; text-align: center; border: 2rpx dashed #EAECEF; border-radius: 20rpx;
  515. text { font-size: 24rpx; color: #BBB; }
  516. }
  517. .list-item {
  518. display: flex; align-items: center; justify-content: space-between;
  519. height: 110rpx; border-top: 1rpx solid #F7F8FA;
  520. .list-label { font-size: 28rpx; color: #333; }
  521. .list-arrow { width: 32rpx; height: 32rpx; opacity: 0.3; }
  522. }
  523. }
  524. .bottom-area {
  525. padding: 80rpx 0 100rpx; text-align: center;
  526. .logout-action { margin-bottom: 30rpx; text { font-size: 30rpx; color: #AAA; } }
  527. .contact-info { text { font-size: 22rpx; color: #CCC; } }
  528. }
  529. .modal-mask {
  530. position: fixed; top: 0; left: 0; right: 0; bottom: 0;
  531. background: rgba(0,0,0,0.6); z-index: 2000;
  532. display: flex; align-items: center; justify-content: center;
  533. }
  534. .confirm-modal {
  535. background: #FFF; border-radius: 32rpx; width: 540rpx; padding: 60rpx 40rpx; text-align: center;
  536. .confirm-title { font-size: 36rpx; font-weight: bold; margin-bottom: 20rpx; color: #1A1A1A; }
  537. .confirm-desc { font-size: 28rpx; color: #888; margin-bottom: 60rpx; }
  538. .confirm-footer {
  539. display: flex; gap: 20rpx;
  540. button {
  541. flex: 1; height: 68rpx; line-height: 68rpx; border-radius: 34rpx; font-size: 26rpx;
  542. &::after { border: none; }
  543. &.btn-cancel { background: #F5F5F7; color: #666; }
  544. &.btn-primary { background: #1F6CFF; color: #FFF; }
  545. }
  546. }
  547. }
  548. .bottom-pad { height: 160rpx; }
  549. .card-anim {
  550. animation: slideIn 0.4s ease-out;
  551. }
  552. @keyframes slideIn { from { opacity: 0; transform: translateY(10rpx); } to { opacity: 1; transform: translateY(0); } }
  553. </style>