yangg 19 stundas atpakaļ
vecāks
revīzija
eb8f0d4914
1 mainītis faili ar 597 papildinājumiem un 109 dzēšanām
  1. 597 109
      src/views/JobApplication/list/index.vue

+ 597 - 109
src/views/JobApplication/list/index.vue

@@ -1,118 +1,277 @@
 <template>
 	<fs-page>
 		<div class="job-application-container">
-			<div class="sidebar">
-				<div class="tree-container">
-					<!-- 职位分类 -->
-					<div class="category-title" @click="togglePositionList">
-						<el-icon><Briefcase /></el-icon>
-						<span>职位</span>
-						<el-icon class="expand-icon" :class="{ 'is-expanded': showPositionList }">
-							<ArrowRight />
-						</el-icon>
-					</div>
-					
-					<!-- 职位列表 -->
-					<div v-if="showPositionList" class="position-list">
-						<!-- 全部职位 -->
-						<div class="tree-item" :class="{ active: activeNode === 'position-all' }" @click="handleNodeClick({ type: 'all', id: 'position-all' })">
-							<div class="item-content">
-								<span>全部</span>
+			<!-- 添加顶部标签页 -->
+			<div class="tabs-container">
+				<el-tabs v-model="activeTab" type="card" @tab-click="handleTabClick">
+					<el-tab-pane label="职位申请" name="application">
+						<!-- 第一个tab:保留现有功能 -->
+						<div class="tab-content">
+							<div class="sidebar">
+								<div class="tree-container">
+									<!-- 职位分类 -->
+									<div class="category-title" @click="togglePositionList">
+										<el-icon><Briefcase /></el-icon>
+										<span>职位</span>
+										<el-icon class="expand-icon" :class="{ 'is-expanded': showPositionList }">
+											<ArrowRight />
+										</el-icon>
+									</div>
+									
+									<!-- 职位列表 -->
+									<div v-if="showPositionList" class="position-list">
+										<!-- 全部职位 -->
+										<div class="tree-item" :class="{ active: activeNode === 'position-all' }" @click="handleNodeClick({ type: 'all', id: 'position-all' })">
+											<div class="item-content">
+												<span>全部</span>
+											</div>
+										</div>
+										
+										<!-- 职位列表项 -->
+										<div 
+											v-for="position in positions" 
+											:key="'position-' + position.id" 
+											class="tree-item" 
+											:class="{ active: activeNode === 'position-' + position.id }"
+											@click="handleNodeClick({ type: 'position', value: position.title, id: 'position-' + position.id })"
+										>
+											<div class="item-content">
+												<el-icon><ArrowRight /></el-icon>
+												<span>{{ position.title }}</span>
+											</div>
+											<div class="item-count" v-if="position.count">{{ position.count }}</div>
+										</div>
+									</div>
+									<!-- 全部 -->
+									<div class="tree-item" :class="{ active: activeNode === 'all' }" @click="handleNodeClick({ type: 'all' })">
+										<div class="item-content">
+											<el-icon><Grid /></el-icon>
+											<span>全部</span>
+										</div>
+										<div class="item-count">{{ totalCount }}人</div>
+									</div>
+									
+									<!-- 待安排 -->
+									<div class="tree-item" :class="{ active: activeNode === 'status-0' }" @click="handleNodeClick({ type: 'status', value: 0, id: 'status-0' })">
+										<div class="item-content">
+											<el-icon><Message /></el-icon>
+											<span>待通知</span>
+										</div>
+										<div class="item-count" v-if="statusCounts[1]">{{ statusCounts[1] }}</div>
+									</div>
+										<!-- 待安排 -->
+									<div class="tree-item" :class="{ active: activeNode === 'status-1' }" @click="handleNodeClick({ type: 'status', value: 1, id: 'status-1' })">
+										<div class="item-content">
+											<el-icon><Clock /></el-icon>
+											<span>已通知待面试</span>
+										</div>
+										<div class="item-count" v-if="statusCounts[1]">{{ statusCounts[5] }}</div>
+									</div>
+									<!-- 推进中 -->
+									<div class="tree-item" :class="{ active: activeNode === 'status-2' }" @click="handleNodeClick({ type: 'status', value: 2, id: 'status-2' })">
+										<div class="item-content">
+											<el-icon><ArrowRight /></el-icon>
+											<span>已面试待处理</span>
+										</div>
+										<div class="item-count" v-if="statusCounts[2]">{{ statusCounts[2] }}</div>
+									</div>
+									
+									<!-- 已入职 -->
+									<div class="tree-item" :class="{ active: activeNode === 'status-3' }" @click="handleNodeClick({ type: 'status', value: 3, id: 'status-3' })">
+										<div class="item-content">
+											<el-icon><Check /></el-icon>
+											<span>已录用</span>
+										</div>
+										<div class="item-count" v-if="statusCounts[3]">{{ statusCounts[3] }}</div>
+									</div>
+									
+									<!-- 待召回 -->
+									<div class="tree-item" :class="{ active: activeNode === 'status-4' }" @click="handleNodeClick({ type: 'status', value: 4, id: 'status-4' })">
+										<div class="item-content">
+											<el-icon><RefreshRight /></el-icon>
+											<span>拒不录用</span>
+										</div>
+										<div class="item-count" v-if="statusCounts[4]">{{ statusCounts[4] }}</div>
+									</div>
+									
+									
+								</div>
 							</div>
-						</div>
-						
-						<!-- 职位列表项 -->
-						<div 
-							v-for="position in positions" 
-							:key="'position-' + position.id" 
-							class="tree-item" 
-							:class="{ active: activeNode === 'position-' + position.id }"
-							@click="handleNodeClick({ type: 'position', value: position.title, id: 'position-' + position.id })"
-						>
-							<div class="item-content">
-								<el-icon><ArrowRight /></el-icon>
-								<span>{{ position.title }}</span>
+							<div class="content">
+								<fs-crud ref="crudRef" v-bind="crudBinding">
+									<template #actionbar-right>
+										<div class="action-button-wrapper">
+											<el-upload
+												class="upload-demo"
+												:action="uploadUrl"
+												:headers="{
+													Authorization: `Bearer ${Session.get('token')}`
+												}"
+												:on-success="handleImportSuccess"
+												:on-error="handleImportError"
+												accept=".xlsx,.xls"
+											>
+												<el-button type="primary" size="small">
+													<el-icon><Download /></el-icon>
+													导入
+												</el-button>
+											</el-upload>
+										</div>
+									</template>
+								</fs-crud>
 							</div>
-							<div class="item-count" v-if="position.count">{{ position.count }}</div>
-						</div>
-					</div>
-					<!-- 全部 -->
-					<div class="tree-item" :class="{ active: activeNode === 'all' }" @click="handleNodeClick({ type: 'all' })">
-						<div class="item-content">
-							<el-icon><Grid /></el-icon>
-							<span>全部</span>
-						</div>
-						<div class="item-count">{{ totalCount }}人</div>
-					</div>
-					
-					<!-- 待安排 -->
-					<div class="tree-item" :class="{ active: activeNode === 'status-0' }" @click="handleNodeClick({ type: 'status', value: 0, id: 'status-0' })">
-						<div class="item-content">
-							<el-icon><Message /></el-icon>
-							<span>待通知</span>
-						</div>
-						<div class="item-count" v-if="statusCounts[1]">{{ statusCounts[1] }}</div>
-					</div>
-						<!-- 待安排 -->
-					<div class="tree-item" :class="{ active: activeNode === 'status-1' }" @click="handleNodeClick({ type: 'status', value: 1, id: 'status-1' })">
-						<div class="item-content">
-							<el-icon><Clock /></el-icon>
-							<span>已通知待面试</span>
-						</div>
-						<div class="item-count" v-if="statusCounts[1]">{{ statusCounts[5] }}</div>
-					</div>
-					<!-- 推进中 -->
-					<div class="tree-item" :class="{ active: activeNode === 'status-2' }" @click="handleNodeClick({ type: 'status', value: 2, id: 'status-2' })">
-						<div class="item-content">
-							<el-icon><ArrowRight /></el-icon>
-							<span>已面试待处理</span>
-						</div>
-						<div class="item-count" v-if="statusCounts[2]">{{ statusCounts[2] }}</div>
-					</div>
-					
-					<!-- 已入职 -->
-					<div class="tree-item" :class="{ active: activeNode === 'status-3' }" @click="handleNodeClick({ type: 'status', value: 3, id: 'status-3' })">
-						<div class="item-content">
-							<el-icon><Check /></el-icon>
-							<span>已录用</span>
-						</div>
-						<div class="item-count" v-if="statusCounts[3]">{{ statusCounts[3] }}</div>
-					</div>
-					
-					<!-- 待召回 -->
-					<div class="tree-item" :class="{ active: activeNode === 'status-4' }" @click="handleNodeClick({ type: 'status', value: 4, id: 'status-4' })">
-						<div class="item-content">
-							<el-icon><RefreshRight /></el-icon>
-							<span>拒不录用</span>
 						</div>
-						<div class="item-count" v-if="statusCounts[4]">{{ statusCounts[4] }}</div>
-					</div>
-					
+					</el-tab-pane>
 					
-				</div>
-			</div>
-			<div class="content">
-				<fs-crud ref="crudRef" v-bind="crudBinding">
-					<template #actionbar-right>
-						<div class="action-button-wrapper">
-							<el-upload
-								class="upload-demo"
-								:action="uploadUrl"
-								:headers="{
-									Authorization: `Bearer ${Session.get('token')}`
-								}"
-								:on-success="handleImportSuccess"
-								:on-error="handleImportError"D
-								accept=".xlsx,.xls"
-							>
-								<el-button type="primary" size="small">
-									<el-icon><Download /></el-icon>
-									导入
-								</el-button>
-							</el-upload>
+					<el-tab-pane label="待通知列表" name="resume">
+						<!-- 第二个tab:待通知列表 -->
+						<div class="tab-content">
+							<div class="notification-container">
+								<!-- 搜索表单 -->
+								<div class="search-form">
+									<el-form :model="notificationSearchForm" :inline="true" size="default">
+										<el-form-item label="姓名">
+											<el-input 
+												v-model="notificationSearchForm.name" 
+												placeholder="请输入姓名"
+												clearable
+												style="width: 200px"
+											/>
+										</el-form-item>
+										<el-form-item label="手机号">
+											<el-input 
+												v-model="notificationSearchForm.phone" 
+												placeholder="请输入手机号"
+												clearable
+												style="width: 200px"
+											/>
+										</el-form-item>
+										<el-form-item label="职位">
+											<el-select 
+												v-model="notificationSearchForm.position" 
+												placeholder="请选择职位"
+												clearable
+												style="width: 200px"
+											>
+												<el-option label="全部" value="" />
+												<el-option 
+													v-for="position in positions" 
+													:key="position.id" 
+													:label="position.title" 
+													:value="position.title" 
+												/>
+											</el-select>
+										</el-form-item>
+										<el-form-item label="申请时间">
+											<el-date-picker
+												v-model="notificationSearchForm.dateRange"
+												type="daterange"
+												range-separator="至"
+												start-placeholder="开始日期"
+												end-placeholder="结束日期"
+												format="YYYY-MM-DD"
+												value-format="YYYY-MM-DD"
+												style="width: 300px"
+											/>
+										</el-form-item>
+										<el-form-item>
+											<el-button type="primary" @click="handleNotificationSearch">
+												<el-icon><Search /></el-icon>
+												搜索
+											</el-button>
+											<el-button @click="handleNotificationReset">
+												<el-icon><Refresh /></el-icon>
+												重置
+											</el-button>
+										</el-form-item>
+									</el-form>
+								</div>
+
+								<!-- 操作按钮 -->
+								<div class="action-bar">
+									<div class="left-actions">
+										<el-button type="primary" @click="handleBatchNotify" :disabled="!selectedNotificationRows.length">
+											<el-icon><Message /></el-icon>
+											批量通知
+										</el-button>
+										<!-- <el-button type="success" @click="handleBatchInterview" :disabled="!selectedNotificationRows.length">
+											<el-icon><Clock /></el-icon>
+											安排面试
+										</el-button> -->
+									</div>
+									<!-- <div class="right-actions">
+										<el-button @click="handleNotificationRefresh">
+											<el-icon><Refresh /></el-icon>
+											刷新
+										</el-button>
+									</div> -->
+								</div>
+
+								<!-- 数据表格 -->
+								<div class="table-container">
+									<el-table
+										:data="notificationList"
+										@selection-change="handleNotificationSelectionChange"
+										:loading="notificationLoading"
+										style="width: 100%"
+										height="calc(100vh - 400px)"
+									>
+										<el-table-column type="selection" width="55" />
+										<el-table-column prop="name" label="姓名" width="120" />
+										<el-table-column prop="phone" label="手机号" width="130" />
+										<el-table-column prop="position_title" label="应聘职位" width="150" />
+										<el-table-column prop="apply_time" label="申请时间" width="120" />
+										<el-table-column prop="status" label="状态" width="100">
+											<template #default="scope">
+												<el-tag type="warning">待通知</el-tag>
+											</template>
+										</el-table-column>
+										<el-table-column prop="source" label="来源" width="100" />
+										<el-table-column prop="remark" label="备注" min-width="150" show-overflow-tooltip />
+										<el-table-column label="操作" width="200" fixed="right">
+											<template #default="scope">
+												<el-button 
+													type="primary" 
+													size="small" 
+													@click="handleNotify(scope.row)"
+												>
+													通知
+												</el-button>
+												<el-button 
+													type="success" 
+													size="small" 
+													@click="handleInterview(scope.row)"
+												>
+													安排面试
+												</el-button>
+												<el-button 
+													type="info" 
+													size="small" 
+													@click="handleViewDetail(scope.row)"
+												>
+													查看详情
+												</el-button>
+											</template>
+										</el-table-column>
+									</el-table>
+								</div>
+
+								<!-- 分页 -->
+								<div class="pagination-container">
+									<el-pagination
+										v-model:current-page="notificationPagination.current"
+										v-model:page-size="notificationPagination.size"
+										:page-sizes="[10, 20, 50, 100]"
+										:total="notificationPagination.total"
+										layout="total, sizes, prev, pager, next, jumper"
+										@size-change="handleNotificationSizeChange"
+										@current-change="handleNotificationCurrentChange"
+									/>
+								</div>
+							</div>
 						</div>
-					</template>
-				</fs-crud>
+					</el-tab-pane>
+				</el-tabs>
 			</div>
 		</div>
 		<BatchTagsDialog ref="batchTagsDialogRef" :crudExpose="crudExpose" />
@@ -127,7 +286,7 @@ import { useFs } from '@fast-crud/fast-crud';
 import { createCrudOptions } from './crud';
 import { GetPermission } from './api';
 import { handleColumnPermission } from '/@/utils/columnPermission';
-import { Grid, Clock, ArrowRight, Check, RefreshRight, Briefcase, Message, Upload } from '@element-plus/icons-vue';
+import { Grid, Clock, ArrowRight, Check, RefreshRight, Briefcase, Message, Upload, Download, Search, Refresh } from '@element-plus/icons-vue';
 import BatchTagsDialog from './components/index.vue';
 import BatchStatusDialog from './components/BatchStatusDialog.vue';
 import smSnotification from './components/smSnotification.vue';
@@ -138,6 +297,36 @@ import { Session } from '/@/utils/storage';
 
 const route = useRoute();
 
+// 添加标签页相关变量
+const activeTab = ref('application');
+
+// 待通知列表相关数据
+interface NotificationItem {
+	id: number;
+	name: string;
+	phone: string;
+	position_title: string;
+	apply_time: string;
+	status: string;
+	source: string;
+	remark: string;
+}
+
+const notificationList = ref<NotificationItem[]>([]);
+const notificationLoading = ref(false);
+const selectedNotificationRows = ref<NotificationItem[]>([]);
+const notificationSearchForm = reactive({
+	name: '',
+	phone: '',
+	position: '',
+	dateRange: [] as string[]
+});
+const notificationPagination = reactive({
+	current: 1,
+	size: 20,
+	total: 0
+});
+
 // 定义上传地址
 const uploadUrl = `${import.meta.env.VITE_API_URL}/api/job/application/import`;
 
@@ -335,6 +524,185 @@ const handleImportError = (error: any) => {
 	ElMessage.error('导入失败,请检查文件格式或网络连接');
 };
 
+// 处理标签页切换
+const handleTabClick = (tab: any) => {
+	console.log('切换到标签页:', tab.name);
+	// 这里可以根据需要添加标签页切换时的逻辑
+	if (tab.name === 'application') {
+		// 切换到职位申请标签页时的处理
+		console.log('显示职位申请功能');
+	} else if (tab.name === 'resume') {
+		// 切换到待通知列表标签页时的处理
+		console.log('显示待通知列表功能');
+		fetchNotificationList();
+	}
+};
+
+// 获取待通知列表数据
+const fetchNotificationList = async () => {
+	notificationLoading.value = true;
+	try {
+		// 构建查询参数
+		const params: Record<string, string | number> = {
+			page: notificationPagination.current,
+			limit: notificationPagination.size,
+			status: 0, // 待通知状态
+		};
+		
+		// 添加搜索条件
+		if (notificationSearchForm.name) params.name = notificationSearchForm.name;
+		if (notificationSearchForm.phone) params.phone = notificationSearchForm.phone;
+		if (notificationSearchForm.position) params.position = notificationSearchForm.position;
+		
+		// 处理日期范围
+		if (notificationSearchForm.dateRange && notificationSearchForm.dateRange.length === 2) {
+			params.start_date = notificationSearchForm.dateRange[0];
+			params.end_date = notificationSearchForm.dateRange[1];
+		}
+		
+		// 调用API获取待通知列表
+		const response = await fetch(`${import.meta.env.VITE_API_URL}/api/job/application/list?${new URLSearchParams(params as Record<string, string>)}`, {
+			headers: {
+				'Authorization': `Bearer ${Session.get('token')}`,
+				'Content-Type': 'application/json'
+			}
+		});
+		
+		const data = await response.json();
+		
+		if (data.code === 2000 && data.data) {
+			notificationList.value = data.data.records || [];
+			notificationPagination.total = data.data.total || 0;
+		} else {
+			ElMessage.error(data.message || '获取待通知列表失败');
+			// 如果API调用失败,显示测试数据
+			showTestData();
+		}
+	} catch (error) {
+		console.error('获取待通知列表失败:', error);
+		ElMessage.error('获取待通知列表失败');
+		// 如果API调用失败,显示测试数据
+		showTestData();
+	} finally {
+		notificationLoading.value = false;
+	}
+};
+
+// 显示测试数据
+const showTestData = () => {
+	notificationList.value = [
+		{
+			id: 1,
+			name: '张三',
+			phone: '13800138001',
+			position_title: '前端开发工程师',
+			apply_time: '2024-01-15',
+			status: '待通知',
+			source: '智联招聘',
+			remark: '有3年前端开发经验,熟悉Vue.js'
+		},
+		{
+			id: 2,
+			name: '李四',
+			phone: '13800138002',
+			position_title: 'Java开发工程师',
+			apply_time: '2024-01-14',
+			status: '待通知',
+			source: 'BOSS直聘',
+			remark: '5年Java开发经验,熟悉Spring框架'
+		},
+		{
+			id: 3,
+			name: '王五',
+			phone: '13800138003',
+			position_title: 'UI设计师',
+			apply_time: '2024-01-13',
+			status: '待通知',
+			source: '拉勾网',
+			remark: '有设计作品集,擅长移动端设计'
+		}
+	];
+	notificationPagination.total = 3;
+};
+
+// 搜索待通知列表
+const handleNotificationSearch = () => {
+	notificationPagination.current = 1;
+	fetchNotificationList();
+};
+
+// 重置搜索条件
+const handleNotificationReset = () => {
+	Object.assign(notificationSearchForm, {
+		name: '',
+		phone: '',
+		position: '',
+		dateRange: []
+	});
+	notificationPagination.current = 1;
+	fetchNotificationList();
+};
+
+// 刷新待通知列表
+const handleNotificationRefresh = () => {
+	fetchNotificationList();
+};
+
+// 处理表格选择变化
+const handleNotificationSelectionChange = (selection: NotificationItem[]) => {
+	selectedNotificationRows.value = selection;
+};
+
+// 处理分页大小变化
+const handleNotificationSizeChange = (size: number) => {
+	notificationPagination.size = size;
+	notificationPagination.current = 1;
+	fetchNotificationList();
+};
+
+// 处理当前页变化
+const handleNotificationCurrentChange = (current: number) => {
+	notificationPagination.current = current;
+	fetchNotificationList();
+};
+
+// 单个通知
+const handleNotify = (row: NotificationItem) => {
+	// 这里可以调用短信通知组件
+	smSnotificationRef.value.open([row]);
+};
+
+// 安排面试
+const handleInterview = (row: NotificationItem) => {
+	// 这里可以调用批量状态修改组件
+	batchStatusDialogRef.value.open([row]);
+};
+
+// 查看详情
+const handleViewDetail = (row: NotificationItem) => {
+	// 这里可以跳转到详情页面或打开详情弹窗
+	console.log('查看详情:', row);
+	ElMessage.info('查看详情功能开发中...');
+};
+
+// 批量通知
+const handleBatchNotify = () => {
+	if (selectedNotificationRows.value.length === 0) {
+		ElMessage.warning('请选择要通知的申请');
+		return;
+	}
+	smSnotificationRef.value.open(selectedNotificationRows.value);
+};
+
+// 批量安排面试
+const handleBatchInterview = () => {
+	if (selectedNotificationRows.value.length === 0) {
+		ElMessage.warning('请选择要安排面试的申请');
+		return;
+	}
+	batchStatusDialogRef.value.open(selectedNotificationRows.value);
+};
+
 // 页面打开后获取列表数据
 onMounted(async () => {
 	// 获取职位列表
@@ -426,12 +794,132 @@ const getPositionList = async () => {
 <style scoped>
 .job-application-container {
 	display: flex;
+	flex-direction: column;
 	height: 100%;
+	margin-top: 15px;
+	overflow: hidden;
+}
+
+/* 标签页容器样式 */
+.tabs-container {
+	background-color: #fff;
+	border-radius: 8px;
+	margin-bottom: 10px;
+	box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.tabs-container :deep(.el-tabs__header) {
+	margin: 0;
+	padding: 0 20px;
+	height: auto;
+}
+
+.tabs-container :deep(.el-tabs__nav-wrap) {
+	padding: 0;
+}
+
+.tabs-container :deep(.el-tabs__item) {
+	height: 50px;
+	line-height: 50px;
+	font-size: 14px;
+	font-weight: 500;
+}
+
+.tabs-container :deep(.el-tabs__content) {
+	padding: 20px;
+}
+
+/* 标签页内容样式 */
+.tab-content {
+	display: flex;
+	height: calc(100vh - 200px);
+}
+
+/* 简历库样式 */
+.resume-container {
+	width: 100%;
+	padding: 20px;
+}
+
+.resume-header {
+	text-align: center;
+	margin-bottom: 40px;
+}
+
+.resume-header h3 {
+	font-size: 24px;
+	color: #303133;
+	margin-bottom: 10px;
+}
+
+.resume-header p {
+	color: #909399;
+	font-size: 14px;
+}
+
+.resume-content {
+	display: flex;
+	justify-content: center;
+	align-items: center;
+	min-height: 300px;
+}
+
+/* 待通知列表样式 */
+.notification-container {
+	width: 100%;
+	padding: 0;
+}
+
+.search-form {
+	background-color: #fff;
+	padding: 20px;
+	border-radius: 8px;
+	margin-bottom: 16px;
+	box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.action-bar {
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+	background-color: #fff;
+	padding: 16px 20px;
+	border-radius: 8px;
+	margin-bottom: 16px;
+	box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.left-actions {
+	display: flex;
+	gap: 12px;
+}
+
+.right-actions {
+	display: flex;
+	gap: 12px;
+}
+
+.table-container {
+	background-color: #fff;
+	border-radius: 8px;
+	margin-bottom: 16px;
+	box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+	overflow: hidden;
+}
+
+.pagination-container {
+	background-color: #fff;
+	padding: 16px 20px;
+	border-radius: 8px;
+	display: flex;
+	justify-content: center;
+	box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
 }
 
+/* 原有的侧边栏和内容样式 */
 .sidebar {
 	overflow: auto;
-	margin-top: 10px;
+	margin-top: 0;
 	width: 200px;
 	margin-right: 5px;
 	flex-shrink: 0;