| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532 |
- <template>
- <view class="container">
- <!-- Tabs -->
- <view class="tab-header">
- <view
- v-for="tab in tabs"
- :key="tab.key"
- :class="['tab-item', activeTab === tab.key ? 'active' : '']"
- @click="activeTab = tab.key"
- >
- {{ tab.name }}
- </view>
- </view>
- <!-- Content List -->
- <scroll-view class="content-scroll" scroll-y>
- <!-- 岗位收藏列表 -->
- <view v-if="activeTab === 'job'" class="list-wrap">
- <view
- v-for="(item, index) in favoriteJobs"
- :key="item.id"
- class="job-card card-anim"
- @click="goToJobDetail(item)"
- >
- <view class="job-header">
- <view class="job-title-box">
- <text class="job-title">{{ item.title }}</text>
- <text class="urge-tag" v-if="item.isUrgent">急招</text>
- </view>
- <text class="job-salary">{{ item.salaryText }}</text>
- </view>
- <view class="tags-row">
- <text class="tag" v-for="(tag, tagIdx) in item.tags" :key="tagIdx">{{ tag }}</text>
- </view>
- <view class="info-row">
- <image src="/static/icons/user.svg" class="info-icon" mode="aspectFit"></image>
- <text class="info-text">招录 {{ item.count }} 人</text>
- </view>
- <view class="info-row">
- <image src="/static/icons/time.svg" class="info-icon" mode="aspectFit"></image>
- <text class="info-text">截止时间:{{ item.deadline }}</text>
- <text class="danger-text" v-if="item.isExpiring">即将截止</text>
- </view>
- <view class="company-row">
- <view class="company-info-wrap">
- <image :src="item.logo" class="company-logo" mode="aspectFill"></image>
- <view class="company-text-col">
- <view class="company-name-box">
- <text class="company-name">{{ item.company }}</text>
- <image src="/static/icons/verified.svg" class="verified-icon" mode="aspectFit"></image>
- </view>
- <text class="company-location">{{ item.location }}</text>
- </view>
- </view>
- <image src="/static/icons/close.svg" class="close-icon" mode="aspectFit" @click.stop="handleUnfavorite('job', index)"></image>
- </view>
- </view>
- <view v-if="favoriteJobs.length === 0" class="empty-status">
- <text>暂无收藏岗位</text>
- </view>
- </view>
-
- <!-- 测评收藏列表 -->
- <view v-else-if="activeTab === 'assessment'" class="list-wrap">
- <view
- v-for="(item, index) in favoriteAssessments"
- :key="item.id"
- class="assessment-card card-anim"
- @click="goToAssessmentDetail(item)"
- >
- <view class="card-left">
- <image :src="item.cover" class="cover-img" mode="aspectFill"></image>
- </view>
- <view class="card-right">
- <view class="top-line">
- <text class="job-title">{{ item.title }}</text>
- <text class="job-level-tag">{{ item.level }}</text>
- </view>
- <view class="tag-row">
- <text class="tag-badge type-tag">{{ item.type }}</text>
- <text class="tag-badge category-tag">{{ item.category }}</text>
- <text class="tag-badge" v-for="(tag, tIdx) in item.tags" :key="tIdx">{{ tag }}</text>
- </view>
- <view class="desc-text text-ellipsis-2">
- {{ item.desc }}
- </view>
- <view class="btn-wrap">
- <button class="consult-btn" @click.stop="handleConsult(item)">咨询</button>
- </view>
- </view>
- <view class="collect-icon-wrap" @click.stop="handleUnfavorite('assessment', index)">
- <image src="/static/icons/close.svg" class="collect-icon" mode="aspectFit"></image>
- </view>
- </view>
- <view v-if="favoriteAssessments.length === 0" class="empty-status">
- <text>暂无收藏测评</text>
- </view>
- </view>
- <view v-if="(activeTab === 'job' && favoriteJobs.length > 0) || (activeTab === 'assessment' && favoriteAssessments.length > 0)" class="no-more">—— 已到底啦~ ——</view>
- </scroll-view>
- </view>
- </template>
- <script setup>
- import { ref, computed, onMounted, watch } from 'vue';
- import { onShow, onPullDownRefresh } from '@dcloudio/uni-app';
- import { listCollection, delCollection } from '../../api/collection.js';
- import { createOrGetSession } from '../../api/message.js';
- const activeTab = ref('job');
- const tabs = [
- { name: '岗位', key: 'job' },
- { name: '测评', key: 'assessment' }
- ];
- const favoriteJobs = ref([]);
- const favoriteAssessments = ref([]);
- // 加载收藏列表
- const loadData = async () => {
- uni.showLoading({ title: '加载中...' });
- try {
- const userInfo = uni.getStorageSync('userInfo');
- if (!userInfo || !userInfo.studentId) return;
- // 加载岗位收藏
- if (activeTab.value === 'job') {
- const res = await listCollection({ studentId: userInfo.studentId, type: 'job' });
- if (res.code === 200) {
- // 假设后端返回的数据中包含 targetData 对象存储岗位详情
- favoriteJobs.value = (res.rows || res.data || []).map(item => {
- const data = item.targetData || {};
- return {
- collectionId: item.id,
- id: item.targetId,
- title: data.postName || data.positionName || data.name || '未知岗位',
- salaryText: data.salaryRange || '面议',
- tags: [data.workCity, data.educationRequirementLabel || data.educationRequirement, data.gradeRequirementLabel || data.gradeRequirement].filter(Boolean),
- isUrgent: data.isUrgent === 1,
- count: data.recruitNum || 1,
- deadline: data.registrationEndDate ? data.registrationEndDate.split(' ')[0] : '长期有效',
- isExpiring: false,
- company: data.companyName || '平台推荐',
- location: (data.workProvince || '') + (data.workCity ? '·' + data.workCity : ''),
- logo: data.companyAvatar || '/static/icons/default-company.png'
- };
- });
- }
- }
- // 加载测评收藏
- else {
- const res = await listCollection({ studentId: userInfo.studentId, type: 'assessment' });
- if (res.code === 200) {
- favoriteAssessments.value = (res.rows || res.data || []).map(item => {
- const data = item.targetData || {};
- return {
- collectionId: item.id,
- id: item.targetId,
- title: data.evaluationName || data.title || data.name || '未知测评',
- level: data.gradeLabel || data.grade || 'A1',
- type: data.positionTypeLabel || data.positionType || '',
- category: data.position || '',
- tags: data.tags ? data.tags.split(',') : [],
- desc: data.remark || data.detail || '暂无描述',
- cover: data.mainImageUrl || '/static/images/assess_cover.svg'
- };
- });
- }
- }
- } catch (err) {
- console.error('加载收藏失败', err);
- } finally {
- uni.hideLoading();
- }
- };
- // 监听Tab切换,重新加载数据
- watch(activeTab, () => {
- loadData();
- });
- onMounted(() => {
- loadData();
- });
- onPullDownRefresh(async () => {
- await loadData();
- uni.stopPullDownRefresh();
- });
- const handleUnfavorite = (type, index) => {
- uni.showModal({
- title: '提示',
- content: '确定要将该项从收藏夹移除吗?',
- success: async (res) => {
- if (res.confirm) {
- try {
- let collectionId;
- if (type === 'job') {
- collectionId = favoriteJobs.value[index].collectionId;
- } else {
- collectionId = favoriteAssessments.value[index].collectionId;
- }
-
- uni.showLoading({ title: '移除中...' });
- const delRes = await delCollection(collectionId);
- uni.hideLoading();
-
- if (delRes.code === 200) {
- if (type === 'job') {
- favoriteJobs.value.splice(index, 1);
- } else {
- favoriteAssessments.value.splice(index, 1);
- }
- uni.showToast({ title: '已移除收藏', icon: 'success' });
- }
- } catch (err) {
- uni.hideLoading();
- uni.showToast({ title: '移除失败', icon: 'none' });
- }
- }
- }
- });
- };
- const goToJobDetail = (item) => {
- uni.navigateTo({
- url: `/pages/jobdetail/index?id=${item.id}&title=${encodeURIComponent(item.title)}`
- });
- };
- const goToAssessmentDetail = (item) => {
- uni.navigateTo({
- url: `/pages/assessment/detail?id=${item.id}`
- });
- };
- const handleConsult = async (item) => {
- try {
- uni.showLoading({ title: '正在连接客服...' });
- const userInfo = uni.getStorageSync('userInfo') || {};
- const userId = userInfo.studentId || null;
- const userName = userInfo.name || '用户';
- const userAvatar = userInfo.avatarUrl || '/static/images/user_avatar.png';
- const res = await createOrGetSession({
- sessionType: 1,
- fromUserId: userId,
- fromUserName: userName,
- fromUserAvatar: userAvatar,
- sourceId: 'assessment_' + (item?.id || '')
- });
- uni.hideLoading();
- if (res.data) {
- const session = res.data;
- uni.navigateTo({
- url: `/pages/chat/chat?sessionId=${session.sessionId}&sessionNo=${session.sessionNo || ''}&fromUserId=${userId || ''}&userName=${encodeURIComponent(userName)}`
- });
- } else {
- uni.showToast({ title: '创建会话失败', icon: 'none' });
- }
- } catch (err) {
- uni.hideLoading();
- console.error('创建会话失败:', err);
- uni.showToast({ title: '连接失败,请重试', icon: 'none' });
- }
- };
- </script>
- <style lang="scss" scoped>
- .container {
- min-height: 100vh;
- background-color: #F8F9FB;
- display: flex;
- flex-direction: column;
- }
- .tab-header {
- display: flex;
- background-color: #FFF;
- padding: 0 80rpx;
- border-bottom: 2rpx solid #F0F2F5;
- .tab-item {
- flex: 1;
- height: 100rpx;
- line-height: 100rpx;
- text-align: center;
- font-size: 30rpx;
- color: #666;
- position: relative;
- &.active {
- color: #1F6CFF;
- font-weight: bold;
- &::after {
- content: '';
- position: absolute;
- bottom: 0;
- left: 50%;
- transform: translateX(-50%);
- width: 40rpx;
- height: 6rpx;
- background-color: #1F6CFF;
- border-radius: 3rpx;
- }
- }
- }
- }
- .content-scroll {
- flex: 1;
- .list-wrap {
- padding: 30rpx 40rpx;
- display: flex;
- flex-direction: column;
- gap: 30rpx;
- }
- }
- /* 岗位卡片 - 同步 jobs.vue 样式 */
- .job-card {
- background: #FFFFFF;
- border-radius: 24rpx;
- padding: 30rpx;
- position: relative;
- box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.03);
- .job-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 20rpx;
- .job-title-box {
- display: flex;
- align-items: center;
- .job-title {
- font-size: 32rpx;
- font-weight: bold;
- color: #1A1A1A;
- margin-right: 16rpx;
- }
- .urge-tag {
- font-size: 20rpx;
- color: #FF3B30;
- border: 1rpx solid #FF3B30;
- padding: 2rpx 10rpx;
- border-radius: 12rpx;
- }
- }
- .job-salary {
- font-size: 32rpx;
- font-weight: bold;
- color: #1F6CFF;
- }
- }
- .tags-row {
- display: flex;
- flex-wrap: wrap;
- gap: 16rpx;
- margin-bottom: 24rpx;
- .tag {
- background: #F4F5F7;
- color: #666666;
- font-size: 22rpx;
- padding: 6rpx 16rpx;
- border-radius: 8rpx;
- }
- }
- .info-row {
- display: flex;
- align-items: center;
- margin-bottom: 12rpx;
- .info-icon {
- width: 26rpx;
- height: 26rpx;
- margin-right: 12rpx;
- opacity: 0.5;
- }
- .info-text {
- font-size: 24rpx;
- color: #888888;
- margin-right: 16rpx;
- }
- .danger-text {
- font-size: 24rpx;
- color: #FF3B30;
- }
- }
- .company-row {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-top: 24rpx;
- padding-top: 24rpx;
- border-top: 1rpx dashed #EEEEEE;
- .company-info-wrap {
- display: flex;
- align-items: center;
- .company-logo {
- width: 80rpx;
- height: 80rpx;
- border-radius: 16rpx;
- margin-right: 20rpx;
- background-color: #f5f5f5;
- border: 1rpx solid #f0f0f0;
- }
- .company-text-col {
- display: flex;
- flex-direction: column;
- .company-name-box {
- display: flex;
- align-items: center;
- margin-bottom: 6rpx;
- .company-name {
- font-size: 28rpx;
- font-weight: 600;
- color: #1A1A1A;
- margin-right: 10rpx;
- }
- .verified-icon {
- width: 24rpx;
- height: 24rpx;
- }
- }
- .company-location {
- font-size: 24rpx;
- color: #888888;
- }
- }
- }
- .close-icon {
- width: 32rpx;
- height: 32rpx;
- opacity: 0.3;
- }
- }
- }
- /* 测评卡片 - 同步 assessment.vue 样式 */
- .assessment-card {
- background: #FFF; border-radius: 32rpx; padding: 24rpx; display: flex;
- box-shadow: 0 4rpx 24rpx rgba(0,0,0,0.03);
- position: relative;
-
- .card-left {
- width: 220rpx; height: 220rpx; flex-shrink: 0; margin-right: 24rpx;
- .cover-img { width: 100%; height: 100%; border-radius: 16rpx; }
- }
-
- .card-right {
- flex: 1; display: flex; flex-direction: column; justify-content: space-between;
- .top-line {
- display: flex; justify-content: space-between; align-items: center;
- .job-title { font-size: 34rpx; font-weight: bold; color: #1A1A1A; }
- .job-level-tag { font-size: 22rpx; color: #1F6CFF; background: rgba(31, 108, 255, 0.1); padding: 4rpx 12rpx; border-radius: 8rpx; }
- }
- .tag-row {
- display: flex; gap: 10rpx; margin: 12rpx 0;
- .tag-badge {
- font-size: 20rpx; color: #999; background: #F5F7FA; padding: 6rpx 14rpx; border-radius: 6rpx;
- &.type-tag { color: #1F6CFF; background: rgba(31, 108, 255, 0.08); }
- &.category-tag { color: #FF9500; background: rgba(255, 149, 0, 0.08); }
- }
- }
- .desc-text { font-size: 24rpx; color: #888; line-height: 1.4; margin-bottom: 16rpx; }
- .btn-wrap {
- display: flex; justify-content: flex-end;
- .consult-btn {
- margin: 0; background: #FFB700; color: #FFF; border-radius: 30rpx; height: 56rpx; line-height: 56rpx; padding: 0 36rpx; font-size: 26rpx; font-weight: bold;
- &::after { border:none; }
- }
- }
- }
-
- .collect-icon-wrap {
- position: absolute;
- top: 24rpx;
- right: 24rpx;
- width: 48rpx;
- height: 48rpx;
- display: flex;
- align-items: center;
- justify-content: center;
- background: rgba(255, 255, 255, 0.95);
- border-radius: 50%;
- box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.1);
- z-index: 10;
-
- .collect-icon {
- width: 32rpx;
- height: 32rpx;
- }
- }
- }
- .text-ellipsis-2 {
- display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden;
- }
- .empty-status { padding-top: 200rpx; text-align: center; color: #CCC; font-size: 28rpx; }
- .no-more { text-align: center; color: #CCC; font-size: 24rpx; padding: 40rpx 0; }
- .card-anim {
- animation: slideUp 0.4s ease-out backwards;
- @for $i from 1 through 10 { &:nth-child(#{$i}) { animation-delay: #{$i * 0.08s}; } }
- }
- @keyframes slideUp {
- from { transform: translateY(20rpx); opacity: 0; }
- to { transform: translateY(0); opacity: 1; }
- }
- </style>
|