Files
hzdyjs/mobile/src/pages/workStudy/index.vue
T
2025-10-25 17:19:54 +08:00

1027 lines
33 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="work-study-container">
<!-- 顶部导航 -->
<u-navbar title="勤工俭学" :border-bottom="false" bg-color="#0051d9" title-color="#fff">
<view slot="left" @click="goBack">
<u-icon name="arrow-left" color="#fff" size="22"></u-icon>
</view>
</u-navbar>
<!-- 搜索栏 -->
<view class="search-container">
<u-search
v-model="searchKeyword"
placeholder="搜索岗位名称或联系老师"
:show-action="false"
@search="handleSearch"
@custom="handleSearch"
bg-color="#f5f6fa"
shape="round"
/>
</view>
<!-- 标签页 -->
<view class="tabs-container">
<u-tabs
:list="tabsList"
:current="currentTab"
@change="onTabChange"
active-color="#1890ff"
inactive-color="#999"
bar-width="40"
bar-height="4"
font-size="30"
/>
</view>
<!-- 岗位列表 -->
<view class="list-container" v-if="currentTab === 0">
<!-- 筛选条件 -->
<view class="filter-bar" v-if="showFilter">
<view class="filter-item">
<view @click="showScopePicker = true">
<u-input
v-model="scopeText"
placeholder="选择发布范围"
readonly
border="surround"
suffix-icon="arrow-down"
/>
</view>
</view>
<view class="filter-item">
<view @click="showStatusPicker = true">
<u-input
v-model="statusText"
placeholder="选择岗位状态"
readonly
border="surround"
suffix-icon="arrow-down"
/>
</view>
</view>
<view class="filter-actions">
<u-button type="primary" size="small" @click="handleFilter" :loading="loading">
筛选
</u-button>
<u-button
type="default"
size="small"
@click="resetFilter"
style="margin-left: 10rpx"
>
重置
</u-button>
</view>
</view>
<!-- 筛选按钮 -->
<view class="action-bar">
<u-button
type="default"
size="small"
:icon="showFilter ? 'arrow-up' : 'arrow-down'"
@click="showFilter = !showFilter"
>
{{ showFilter ? '收起筛选' : '展开筛选' }}
</u-button>
<u-button
type="primary"
size="small"
icon="reload"
@click="refreshData"
:loading="refreshing"
>
刷新
</u-button>
</view>
<!-- 岗位卡片列表 -->
<view class="list-content" v-if="positionList.length > 0">
<view
class="position-card"
v-for="item in positionList"
:key="item.id"
@click="handleViewDetail(item)"
>
<!-- 卡片头部 -->
<view class="card-header">
<view class="position-info">
<view class="position-name">{{ item.positionName }}</view>
<view class="position-type">
{{ item.positionScopeName || '未设置' }}
</view>
</view>
<view class="status-badge">
<u-tag
:text="item.statusName || '未知'"
:type="getStatusType(item.status)"
size="mini"
shape="circle"
/>
</view>
</view>
<!-- 岗位信息 -->
<view class="card-content">
<view class="info-row">
<u-icon name="account" size="18" color="#666" />
<text class="info-text">需求人数:{{ item.requiredCount }}人</text>
<text class="info-text current-count">
已招录:{{ item.currentCount || 0 }}人
</text>
</view>
<view class="info-row">
<u-icon name="clock" size="18" color="#666" />
<text class="info-text">工作时间:{{ item.workTime }}</text>
</view>
<view class="info-row">
<u-icon name="map" size="18" color="#666" />
<text class="info-text">联系老师:{{ item.contactTeacher }}</text>
</view>
<view class="info-row">
<u-icon name="phone" size="18" color="#666" />
<text class="info-text">联系电话:{{ item.contactPhone }}</text>
</view>
<view class="info-row">
<u-icon name="calendar" size="18" color="#666" />
<text class="info-text">
有效期:{{ item.startDate }} 至 {{ item.endDate }}
</text>
</view>
<!-- 申请状态信息 -->
<view class="info-row" v-if="getPositionApplicationStatus(item)">
<u-icon name="checkmark-circle" size="18" color="#1890ff" />
<text class="info-text" style="color: #1890ff">
我的申请:{{ getPositionApplicationStatus(item) }}
</text>
</view>
</view>
<!-- 操作按钮 -->
<view class="card-actions">
<u-button
type="default"
size="small"
@click.stop="handleViewDetail($event, item)"
>
查看详情
</u-button>
<u-button
type="primary"
size="small"
@click.stop="handleApply($event, item)"
:disabled="!canApply(item)"
style="margin-left: 20rpx"
>
{{ getApplyButtonText(item) }}
</u-button>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-else-if="!loading">
<u-empty text="暂无岗位信息" icon="search" :show="true" />
</view>
<!-- 加载更多 -->
<view class="load-more" v-if="hasMore && positionList.length > 0">
<u-loadmore :status="loadMoreStatus" :load-text="loadText" @loadmore="loadMore" />
</view>
</view>
<!-- 我的申请列表 -->
<view class="list-container" v-if="currentTab === 1">
<view class="list-content" v-if="applicationList.length > 0">
<view
class="application-card"
v-for="item in applicationList"
:key="item.id"
@click="handleViewApplication(item)"
>
<!-- 申请信息 -->
<view class="card-header">
<view class="application-info">
<view class="position-name">{{ item.positionName }}</view>
<view class="apply-time">申请时间:{{ item.createTime }}</view>
</view>
<view class="status-badge">
<u-tag
:text="getApplicationStatusText(item)"
:type="getApplicationStatusType(item)"
size="mini"
shape="circle"
/>
</view>
</view>
<!-- 申请状态 -->
<view class="card-content">
<view class="info-row">
<u-icon name="file-text" size="18" color="#666" />
<text class="info-text">
申请状态:{{ getApplicationStatusText(item) }}
</text>
</view>
<view class="info-row" v-if="item.collegeReviewStatus">
<u-icon name="checkmark-circle" size="18" color="#666" />
<text class="info-text">
学院审核:{{ getReviewStatusText(item.collegeReviewStatus) }}
</text>
</view>
<view class="info-row" v-if="item.finalReviewStatus">
<u-icon name="checkmark-circle" size="18" color="#666" />
<text class="info-text">
学生处审核:{{ getReviewStatusText(item.finalReviewStatus) }}
</text>
</view>
</view>
<!-- 操作按钮 -->
<view class="card-actions">
<u-button
type="default"
size="small"
@click.stop="handleViewApplication($event, item)"
>
查看详情
</u-button>
<u-button
type="warning"
size="small"
@click.stop="handleCancelApplication($event, item)"
v-if="canCancelApplication(item)"
style="margin-left: 20rpx"
>
撤销申请
</u-button>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-else-if="!loading">
<u-empty text="暂无申请记录" icon="file-text" :show="true" />
</view>
</view>
<!-- 发布范围选择器 -->
<u-picker
:show="showScopePicker"
:columns="scopeOptions"
keyName="label"
@confirm="onScopeConfirm"
@cancel="showScopePicker = false"
/>
<!-- 状态选择器 -->
<u-picker
:show="showStatusPicker"
:columns="statusOptions"
keyName="label"
@confirm="onStatusConfirm"
@cancel="showStatusPicker = false"
/>
<!-- 加载状态 -->
<u-loading-page :loading="loading" loading-text="加载中..." />
</view>
</template>
<script>
import {
getAvailablePositions,
getMyApplications,
cancelApplication,
getCurrentStudentInfo,
getStudentApplicationStatusList,
} from '@/api/workStudy.js';
export default {
name: 'WorkStudyIndex',
data() {
return {
// 搜索关键词
searchKeyword: '',
// 标签页
currentTab: 0, // 确保是数字类型
tabsList: [{ name: '可申请岗位' }, { name: '我的申请' }],
// 筛选条件
showFilter: false,
showScopePicker: false,
showStatusPicker: false,
scopeText: '',
statusText: '',
// 选择器选项
scopeOptions: [
[
{ label: '全部', value: '' },
{ label: '本学院', value: 'COLLEGE' },
{ label: '全校', value: 'UNIVERSITY' },
],
],
statusOptions: [
[
{ label: '全部', value: '' },
{ label: '已发布', value: 'PUBLISHED' },
{ label: '招录中', value: 'RECRUITING' },
{ label: '已暂停', value: 'SUSPENDED' },
],
],
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
positionName: '',
positionScope: '',
status: '',
},
// 列表数据
positionList: [],
applicationList: [],
// 分页状态
hasMore: true,
loadMoreStatus: 'loadmore',
loadText: {
loadmore: '点击或上拉加载更多',
loading: '正在加载...',
nomore: '没有更多了',
},
// 加载状态
loading: false,
refreshing: false,
// 用户信息
studentInfo: null,
// 学生申请状态列表(用于判断申请状态)
studentApplications: [],
};
},
onLoad() {
// 确保currentTab是数字类型
this.currentTab = Number(this.currentTab);
this.initData();
},
onReady() {
// 页面渲染完成后再次确保数据类型正确
this.$nextTick(() => {
this.currentTab = Number(this.currentTab);
});
},
onShow() {
// 如果是从申请页面返回,刷新数据
if (this.currentTab === 1) {
this.loadMyApplications(true);
}
},
// 下拉刷新
onPullDownRefresh() {
this.refreshData();
},
// 上拉加载更多
onReachBottom() {
if (this.currentTab === 0 && this.hasMore && !this.loading) {
this.loadMore();
}
},
methods: {
// 返回上一页
goBack() {
uni.navigateBack();
},
// 初始化数据
async initData() {
try {
// 获取学生信息
await this.loadStudentInfo();
// 获取学生申请状态列表
await this.loadStudentApplications();
// 加载岗位列表
await this.loadPositionList(true);
} catch (error) {
console.error('初始化数据失败:', error);
uni.showToast({
title: '加载数据失败',
icon: 'none',
});
}
},
// 获取学生信息
async loadStudentInfo() {
try {
const response = await getCurrentStudentInfo();
if (response.code === 200) {
this.studentInfo = response.data;
}
} catch (error) {
console.error('获取学生信息失败:', error);
}
},
// 加载学生申请状态列表
async loadStudentApplications() {
try {
const response = await getStudentApplicationStatusList();
if (response.code === 200) {
this.studentApplications = response.data || [];
}
} catch (error) {
console.error('获取申请状态失败:', error);
}
},
// 加载岗位列表
async loadPositionList(reset = false) {
if (this.loading) return;
if (reset) {
this.queryParams.pageNum = 1;
this.positionList = [];
this.hasMore = true;
}
this.loading = true;
this.loadMoreStatus = 'loading';
try {
const response = await getAvailablePositions(this.queryParams);
if (response.code === 200) {
const newList = response.rows || [];
if (reset) {
this.positionList = newList;
} else {
this.positionList = [...this.positionList, ...newList];
}
// 检查是否还有更多数据
if (newList.length < this.queryParams.pageSize) {
this.hasMore = false;
this.loadMoreStatus = 'nomore';
} else {
this.loadMoreStatus = 'loadmore';
}
} else {
throw new Error(response.msg || '获取岗位列表失败');
}
} catch (error) {
console.error('加载岗位列表失败:', error);
uni.showToast({
title: error.message || '加载失败',
icon: 'none',
});
this.loadMoreStatus = 'loadmore';
} finally {
this.loading = false;
uni.stopPullDownRefresh();
}
},
// 加载我的申请列表
async loadMyApplications(reset = false) {
if (this.loading) return;
this.loading = true;
try {
const response = await getMyApplications();
if (response.code === 200) {
this.applicationList = response.data || [];
} else {
throw new Error(response.msg || '获取申请列表失败');
}
} catch (error) {
console.error('加载申请列表失败:', error);
uni.showToast({
title: error.message || '加载失败',
icon: 'none',
});
} finally {
this.loading = false;
uni.stopPullDownRefresh();
}
},
// 标签页切换
onTabChange(index) {
// 确保index是数字类型
const tabIndex = typeof index === 'object' ? index.index : index;
this.currentTab = Number(tabIndex);
// 延迟执行,确保DOM渲染完成
this.$nextTick(() => {
if (this.currentTab === 0) {
this.loadPositionList(true);
} else if (this.currentTab === 1) {
this.loadMyApplications(true);
}
});
},
// 搜索
handleSearch() {
this.queryParams.positionName = this.searchKeyword;
this.loadPositionList(true);
},
// 筛选
handleFilter() {
this.loadPositionList(true);
},
// 重置筛选
resetFilter() {
this.queryParams.positionScope = '';
this.queryParams.status = '';
this.scopeText = '';
this.statusText = '';
this.searchKeyword = '';
this.queryParams.positionName = '';
this.loadPositionList(true);
},
// 刷新数据
async refreshData() {
this.refreshing = true;
try {
// 同时刷新学生申请状态
await this.loadStudentApplications();
if (this.currentTab === 0) {
await this.loadPositionList(true);
} else {
await this.loadMyApplications(true);
}
} catch (error) {
console.error('刷新数据失败:', error);
} finally {
this.refreshing = false;
}
},
// 加载更多
loadMore() {
if (!this.hasMore || this.loading) return;
this.queryParams.pageNum++;
this.loadPositionList(false);
},
// 查看岗位详情
handleViewDetail(event, item) {
// 如果只传了一个参数,说明是从卡片点击调用的,第一个参数就是item
if (!item && typeof event === 'object' && event.id) {
item = event;
}
uni.navigateTo({
url: `/pages/workStudy/detail?id=${item.id}`,
});
},
// 申请岗位(参考web端实现)
handleApply(event, position) {
// 如果只传了一个参数,说明是从卡片点击调用的,第一个参数就是position
if (!position && typeof event === 'object' && event.id) {
position = event;
}
const application = this.studentApplications.find(
(app) => app.positionId === position.id,
);
// 如果已有申请且状态为已提交或审核中,跳转到申请管理页面
if (
application &&
['submitted', 'college_reviewing', 'final_reviewing'].includes(application.status)
) {
uni.switchTab({
url: '/pages/workStudy/index',
});
// 切换到"我的申请"标签页
this.currentTab = 1;
this.onTabChange(1);
return;
}
// 如果已录用,不允许操作
if (application && application.status === 'approved') {
uni.showToast({
title: '您已被该岗位录用',
icon: 'none',
});
return;
}
if (!this.canApply(position)) {
uni.showToast({
title: '该岗位当前不可申请',
icon: 'none',
});
return;
}
uni.navigateTo({
url: `/pages/workStudy/apply?positionId=${position.id}`,
});
},
// 查看申请详情
handleViewApplication(event, item) {
// 如果只传了一个参数,说明是从卡片点击调用的,第一个参数就是item
if (!item && typeof event === 'object' && event.id) {
item = event;
}
uni.navigateTo({
url: `/pages/workStudy/applicationDetail?id=${item.id}`,
});
},
// 撤销申请
async handleCancelApplication(event, item) {
// 如果只传了一个参数,说明是从卡片点击调用的,第一个参数就是item
if (!item && typeof event === 'object' && event.id) {
item = event;
}
try {
const { confirm } = await uni.showModal({
title: '确认撤销',
content: '确定要撤销此申请吗?',
showCancel: true,
});
if (!confirm) return; // 用户取消
const response = await cancelApplication(item.id);
if (response.code === 200) {
uni.showToast({
title: '撤销成功',
icon: 'success',
});
this.loadMyApplications(true);
} else {
throw new Error(response.msg || '撤销失败');
}
} catch (error) {
console.error('撤销申请失败:', error);
uni.showToast({
title: error.message || '撤销失败',
icon: 'none',
});
}
},
// 发布范围选择确认
onScopeConfirm(e) {
const index = e.indexs[0];
const selected = this.scopeOptions[0][index];
this.queryParams.positionScope = selected.value;
this.scopeText = selected.label;
this.showScopePicker = false;
},
// 状态选择确认
onStatusConfirm(e) {
const index = e.indexs[0];
const selected = this.statusOptions[0][index];
this.queryParams.status = selected.value;
this.statusText = selected.label;
this.showStatusPicker = false;
},
// 获取状态类型
getStatusType(status) {
const typeMap = {
DRAFT: 'info',
PUBLISHED: 'warning',
RECRUITING: 'success',
FULL: 'primary',
EXPIRED: 'error',
SUSPENDED: 'warning',
CANCELLED: 'error',
};
return typeMap[status] || 'default';
},
// 判断是否可以申请(完全参考web端实现)
canApply(position) {
const application = this.studentApplications.find(
(app) => app.positionId === position.id,
);
// 如果已录用,禁用按钮
if (application && application.status === 'approved') {
return false;
}
// 检查岗位状态
if (!['PUBLISHED', 'RECRUITING'].includes(position.status || '')) {
return false;
}
// 检查是否还有空缺
const current = position.currentCount || 0;
const required = position.requiredCount || 0;
if (current >= required) {
return false;
}
// 检查有效期
if (position.endDate) {
const endDate = new Date(position.endDate);
if (endDate < new Date()) {
return false;
}
}
return true;
},
// 获取申请按钮文字(参考web端实现8种状态)
getApplyButtonText(position) {
const application = this.studentApplications.find(
(app) => app.positionId === position.id,
);
// 如果已录用
if (application && application.status === 'approved') {
return '申请已通过';
}
// 如果有申请且状态为审核中
if (
application &&
['submitted', 'college_reviewing', 'final_reviewing'].includes(application.status)
) {
return '申请审核中';
}
// 如果被拒绝
if (application && application.status === 'rejected') {
return '重新申请';
}
// 检查岗位状态
if (!['PUBLISHED', 'RECRUITING'].includes(position.status || '')) {
if (position.status === 'EXPIRED') {
return '岗位已过期';
} else if (position.status === 'SUSPENDED') {
return '岗位已暂停';
} else if (position.status === 'CANCELLED') {
return '岗位已取消';
} else {
return '暂不可申请';
}
}
// 检查是否还有空缺
const current = position.currentCount || 0;
const required = position.requiredCount || 0;
if (current >= required) {
return '岗位已满员';
}
// 检查有效期
if (position.endDate) {
const endDate = new Date(position.endDate);
if (endDate < new Date()) {
return '岗位已过期';
}
}
// 默认可申请
return '申请此岗位';
},
// 获取申请状态文字(与web端保持一致)
getApplicationStatusText(item) {
if (!item) return '未申请';
// 支持两种格式:web端的小写格式和移动端的大写格式
const status = item.status ? item.status.toLowerCase() : '';
switch (status) {
case 'submitted':
case 'SUBMITTED':
return '已提交';
case 'college_reviewing':
case 'COLLEGE_REVIEWING':
return '学院审核中';
case 'final_reviewing':
case 'FINAL_REVIEWING':
return '学生处审核中';
case 'approved':
case 'FINAL_APPROVED':
return '审核通过';
case 'rejected':
case 'FINAL_REJECTED':
return '审核不通过';
case 'cancelled':
case 'CANCELLED':
return '已撤销';
case 'draft':
case 'DRAFT':
return '草稿';
default:
return '未知状态';
}
},
// 获取申请状态类型(与web端保持一致)
getApplicationStatusType(item) {
if (!item) return 'default';
// 支持两种格式:web端的小写格式和移动端的大写格式
const status = item.status ? item.status.toLowerCase() : '';
const typeMap = {
draft: 'info',
submitted: 'warning',
college_reviewing: 'warning',
final_reviewing: 'warning',
approved: 'success',
rejected: 'error',
cancelled: 'info',
};
return typeMap[status] || 'default';
},
// 获取审核状态文字
getReviewStatusText(status) {
const statusMap = {
PENDING: '待审核',
APPROVED: '通过',
REJECTED: '未通过',
};
return statusMap[status] || '未知';
},
// 判断是否可以撤销申请(与web端保持一致)
canCancelApplication(item) {
if (!item) return false;
// 支持两种格式:web端的小写格式和移动端的大写格式
const status = item.status ? item.status.toLowerCase() : '';
return (
['draft', 'submitted', 'college_reviewing'].includes(status) ||
['DRAFT', 'SUBMITTED', 'COLLEGE_REVIEWING'].includes(item.status)
);
},
// 获取岗位的申请状态(用于在岗位卡片中显示)
getPositionApplicationStatus(position) {
const application = this.studentApplications.find(
(app) => app.positionId === position.id,
);
if (!application) return null;
return this.getApplicationStatusText(application);
},
},
};
</script>
<style scoped>
.work-study-container {
min-height: 100vh;
background-color: #f5f6fa;
}
.search-container {
padding: 20rpx;
background-color: #fff;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.tabs-container {
background-color: #fff;
border-bottom: 1rpx solid #eee;
}
.filter-bar {
background-color: #fff;
padding: 20rpx;
border-bottom: 1rpx solid #eee;
}
.filter-item {
margin-bottom: 20rpx;
}
.filter-actions {
display: flex;
justify-content: flex-end;
margin-top: 20rpx;
}
.action-bar {
display: flex;
justify-content: space-between;
padding: 20rpx;
background-color: #fff;
border-bottom: 1rpx solid #eee;
}
.list-container {
flex: 1;
}
.list-content {
padding: 20rpx;
}
.position-card,
.application-card {
background-color: #fff;
border-radius: 16rpx;
margin-bottom: 20rpx;
padding: 24rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 20rpx;
}
.position-info,
.application-info {
flex: 1;
}
.position-name {
font-size: 32rpx;
font-weight: bold;
color: #303133;
margin-bottom: 8rpx;
}
.position-type,
.apply-time {
font-size: 24rpx;
color: #999;
}
.status-badge {
margin-left: 20rpx;
}
.card-content {
margin-bottom: 20rpx;
}
.info-row {
display: flex;
align-items: center;
margin-bottom: 12rpx;
}
.info-text {
font-size: 28rpx;
color: #606266;
margin-left: 12rpx;
flex: 1;
}
.current-count {
margin-left: 20rpx;
color: #0051d9;
}
.card-actions {
display: flex;
justify-content: flex-end;
border-top: 1rpx solid #eee;
padding-top: 20rpx;
}
.empty-state {
padding: 80rpx 20rpx;
text-align: center;
}
.load-more {
padding: 20rpx;
}
</style>