672 lines
15 KiB
Vue
672 lines
15 KiB
Vue
<template>
|
||
<div class="page-delivery">
|
||
<h2 class="page-title">投递管理</h2>
|
||
|
||
<!-- 统计信息 -->
|
||
<div class="stats-section" v-if="statistics">
|
||
<Card class="stat-card">
|
||
<template #content>
|
||
<div class="stat-value">{{ statistics.totalCount || 0 }}</div>
|
||
<div class="stat-label">总投递数</div>
|
||
</template>
|
||
</Card>
|
||
<Card class="stat-card">
|
||
<template #content>
|
||
<div class="stat-value">{{ statistics.successCount || 0 }}</div>
|
||
<div class="stat-label">成功数</div>
|
||
</template>
|
||
</Card>
|
||
<Card class="stat-card">
|
||
<template #content>
|
||
<div class="stat-value">{{ statistics.interviewCount || 0 }}</div>
|
||
<div class="stat-label">面试邀约</div>
|
||
</template>
|
||
</Card>
|
||
<Card class="stat-card">
|
||
<template #content>
|
||
<div class="stat-value">{{ statistics.successRate || 0 }}%</div>
|
||
<div class="stat-label">成功率</div>
|
||
</template>
|
||
</Card>
|
||
</div>
|
||
|
||
<!-- 筛选 -->
|
||
<div class="filter-section">
|
||
<div class="filter-box">
|
||
<Dropdown
|
||
v-model="searchOption.platform"
|
||
:options="platformOptions"
|
||
optionLabel="label"
|
||
optionValue="value"
|
||
placeholder="全部平台"
|
||
class="filter-select"
|
||
@change="handleSearch"
|
||
/>
|
||
<Dropdown
|
||
v-model="searchOption.applyStatus"
|
||
:options="applyStatusOptions"
|
||
optionLabel="label"
|
||
optionValue="value"
|
||
placeholder="全部状态"
|
||
class="filter-select"
|
||
@change="handleSearch"
|
||
/>
|
||
<Dropdown
|
||
v-model="searchOption.feedbackStatus"
|
||
:options="feedbackStatusOptions"
|
||
optionLabel="label"
|
||
optionValue="value"
|
||
placeholder="全部反馈"
|
||
class="filter-select"
|
||
@change="handleSearch"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 投递记录列表 -->
|
||
<div class="table-section">
|
||
<ProgressSpinner v-if="loading" />
|
||
<div v-else-if="records.length === 0" class="empty">暂无投递记录</div>
|
||
<DataTable v-else :value="records" tableStyle="min-width: 50rem">
|
||
<Column field="jobTitle" header="岗位名称">
|
||
<template #body="{ data }">
|
||
{{ data.jobTitle || '-' }}
|
||
</template>
|
||
</Column>
|
||
<Column field="companyName" header="公司名称">
|
||
<template #body="{ data }">
|
||
{{ data.companyName || '-' }}
|
||
</template>
|
||
</Column>
|
||
<Column field="platform" header="平台">
|
||
<template #body="{ data }">
|
||
<Tag
|
||
:value="data.platform === 'boss' ? 'Boss直聘' : data.platform === 'liepin' ? '猎聘' : data.platform"
|
||
severity="info"
|
||
/>
|
||
</template>
|
||
</Column>
|
||
<Column field="applyStatus" header="投递状态">
|
||
<template #body="{ data }">
|
||
<Tag
|
||
:value="getApplyStatusText(data.applyStatus)"
|
||
:severity="getApplyStatusSeverity(data.applyStatus)"
|
||
/>
|
||
</template>
|
||
</Column>
|
||
<Column field="feedbackStatus" header="反馈状态">
|
||
<template #body="{ data }">
|
||
<Tag
|
||
:value="getFeedbackStatusText(data.feedbackStatus)"
|
||
:severity="getFeedbackStatusSeverity(data.feedbackStatus)"
|
||
/>
|
||
</template>
|
||
</Column>
|
||
<Column field="applyTime" header="投递时间">
|
||
<template #body="{ data }">
|
||
{{ formatTime(data.applyTime) }}
|
||
</template>
|
||
</Column>
|
||
<Column header="操作">
|
||
<template #body="{ data }">
|
||
<Button label="查看详情" size="small" @click="handleViewDetail(data)" />
|
||
</template>
|
||
</Column>
|
||
</DataTable>
|
||
</div>
|
||
|
||
<!-- 分页 -->
|
||
<Paginator
|
||
v-if="total > 0"
|
||
:rows="pageOption.pageSize"
|
||
:totalRecords="total"
|
||
:first="(currentPage - 1) * pageOption.pageSize"
|
||
@page="onPageChange"
|
||
/>
|
||
|
||
<!-- 详情弹窗 -->
|
||
<Dialog
|
||
v-model:visible="showDetail"
|
||
modal
|
||
header="投递详情"
|
||
:style="{ width: '600px' }"
|
||
@hide="closeDetail"
|
||
>
|
||
<div v-if="currentRecord" class="detail-content">
|
||
<div class="detail-item">
|
||
<label>岗位名称:</label>
|
||
<span>{{ currentRecord.jobTitle || '-' }}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<label>公司名称:</label>
|
||
<span>{{ currentRecord.companyName || '-' }}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<label>薪资范围:</label>
|
||
<span>{{ currentRecord.salary || '-' }}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<label>工作地点:</label>
|
||
<span>{{ currentRecord.location || '-' }}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<label>投递状态:</label>
|
||
<span>{{ getApplyStatusText(currentRecord.applyStatus) }}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<label>反馈状态:</label>
|
||
<span>{{ getFeedbackStatusText(currentRecord.feedbackStatus) }}</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<label>投递时间:</label>
|
||
<span>{{ formatTime(currentRecord.applyTime) }}</span>
|
||
</div>
|
||
<div class="detail-item" v-if="currentRecord.feedbackContent">
|
||
<label>反馈内容:</label>
|
||
<span>{{ currentRecord.feedbackContent }}</span>
|
||
</div>
|
||
</div>
|
||
</Dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import applyRecordsAPI from '../api/apply_records.js';
|
||
import { mapState } from 'vuex';
|
||
import { Card, Dropdown, DataTable, Column, Tag, Button, Dialog, Paginator, ProgressSpinner } from '../components/PrimeVue';
|
||
import logMixin from '../mixins/logMixin.js';
|
||
|
||
export default {
|
||
name: 'DeliveryPage',
|
||
mixins: [logMixin],
|
||
components: {
|
||
Card,
|
||
Dropdown,
|
||
DataTable,
|
||
Column,
|
||
Tag,
|
||
Button,
|
||
Dialog,
|
||
Paginator,
|
||
ProgressSpinner
|
||
},
|
||
data() {
|
||
return {
|
||
loading: false,
|
||
records: [],
|
||
statistics: null,
|
||
searchOption: {
|
||
key: 'jobTitle',
|
||
value: '',
|
||
platform: '',
|
||
applyStatus: '',
|
||
feedbackStatus: ''
|
||
},
|
||
platformOptions: [
|
||
{ label: '全部平台', value: '' },
|
||
{ label: 'Boss直聘', value: 'boss' },
|
||
{ label: '猎聘', value: 'liepin' }
|
||
],
|
||
applyStatusOptions: [
|
||
{ label: '全部状态', value: '' },
|
||
{ label: '待投递', value: 'pending' },
|
||
{ label: '投递中', value: 'applying' },
|
||
{ label: '投递成功', value: 'success' },
|
||
{ label: '投递失败', value: 'failed' }
|
||
],
|
||
feedbackStatusOptions: [
|
||
{ label: '全部反馈', value: '' },
|
||
{ label: '无反馈', value: 'none' },
|
||
{ label: '已查看', value: 'viewed' },
|
||
{ label: '感兴趣', value: 'interested' },
|
||
{ label: '面试邀约', value: 'interview' }
|
||
],
|
||
pageOption: {
|
||
page: 1,
|
||
pageSize: 10
|
||
},
|
||
total: 0,
|
||
currentPage: 1,
|
||
showDetail: false,
|
||
currentRecord: null
|
||
};
|
||
},
|
||
computed: {
|
||
...mapState('auth', ['snCode']),
|
||
totalPages() {
|
||
return Math.ceil(this.total / this.pageOption.pageSize);
|
||
}
|
||
},
|
||
mounted() {
|
||
this.loadStatistics();
|
||
this.loadRecords();
|
||
},
|
||
methods: {
|
||
/**
|
||
* 加载统计数据
|
||
*/
|
||
async loadStatistics() {
|
||
try {
|
||
// 获取 snCode 用于请求
|
||
const snCode = this.$store?.state?.auth?.snCode;
|
||
if (!snCode) {
|
||
console.warn('未获取到设备SN码,无法加载统计数据');
|
||
return;
|
||
}
|
||
|
||
const result = await applyRecordsAPI.getStatistics(snCode);
|
||
if (result && result.code === 0) {
|
||
this.statistics = result.data;
|
||
}
|
||
} catch (error) {
|
||
console.error('加载统计数据失败:', error);
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 加载投递记录
|
||
*/
|
||
async loadRecords() {
|
||
this.loading = true;
|
||
try {
|
||
const params = {
|
||
sn_code: this.snCode,
|
||
seachOption: this.searchOption,
|
||
pageOption: this.pageOption
|
||
};
|
||
const result = await applyRecordsAPI.getList(params);
|
||
if (result && result.code === 0) {
|
||
this.records = result.data.rows || result.data.list || [];
|
||
this.total = result.data.count || result.data.total || 0;
|
||
this.currentPage = this.pageOption.page;
|
||
console.log('[投递管理] 加载记录成功:', {
|
||
recordsCount: this.records.length,
|
||
total: this.total,
|
||
currentPage: this.currentPage
|
||
});
|
||
} else {
|
||
console.warn('[投递管理] 响应格式异常:', result);
|
||
}
|
||
} catch (error) {
|
||
console.error('加载投递记录失败:', error);
|
||
if (this.addLog) {
|
||
this.addLog('error', '加载投递记录失败: ' + (error.message || '未知错误'));
|
||
}
|
||
} finally {
|
||
this.loading = false;
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 搜索
|
||
*/
|
||
handleSearch() {
|
||
this.pageOption.page = 1;
|
||
this.loadRecords();
|
||
},
|
||
|
||
|
||
/**
|
||
* 分页切换
|
||
*/
|
||
handlePageChange(page) {
|
||
this.pageOption.page = page;
|
||
this.currentPage = page;
|
||
this.loadRecords();
|
||
},
|
||
|
||
/**
|
||
* Paginator 分页事件
|
||
*/
|
||
onPageChange(event) {
|
||
this.currentPage = Math.floor(event.first / this.pageOption.pageSize) + 1;
|
||
this.pageOption.page = this.currentPage;
|
||
this.loadRecords();
|
||
},
|
||
|
||
/**
|
||
* 获取投递状态严重程度(用于 Tag 组件)
|
||
*/
|
||
getApplyStatusSeverity(status) {
|
||
const severityMap = {
|
||
'pending': 'warning',
|
||
'applying': 'info',
|
||
'success': 'success',
|
||
'failed': 'danger',
|
||
'duplicate': 'secondary'
|
||
};
|
||
return severityMap[status] || 'secondary';
|
||
},
|
||
|
||
/**
|
||
* 获取反馈状态严重程度(用于 Tag 组件)
|
||
*/
|
||
getFeedbackStatusSeverity(status) {
|
||
const severityMap = {
|
||
'none': 'secondary',
|
||
'viewed': 'info',
|
||
'interested': 'success',
|
||
'not_suitable': 'danger',
|
||
'interview': 'success'
|
||
};
|
||
return severityMap[status] || 'secondary';
|
||
},
|
||
|
||
/**
|
||
* 查看详情
|
||
*/
|
||
async handleViewDetail(record) {
|
||
try {
|
||
const result = await applyRecordsAPI.getDetail(record.id || record.applyId);
|
||
if (result && result.code === 0) {
|
||
this.currentRecord = result.data || record; // 如果没有返回详情,使用当前记录
|
||
this.showDetail = true;
|
||
}
|
||
} catch (error) {
|
||
console.error('获取详情失败:', error);
|
||
// 如果获取详情失败,直接使用当前记录显示
|
||
this.currentRecord = record;
|
||
this.showDetail = true;
|
||
if (this.addLog) {
|
||
this.addLog('error', '获取详情失败: ' + (error.message || '未知错误'));
|
||
}
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 关闭详情
|
||
*/
|
||
closeDetail() {
|
||
this.showDetail = false;
|
||
this.currentRecord = null;
|
||
},
|
||
|
||
/**
|
||
* 获取投递状态文本
|
||
*/
|
||
getApplyStatusText(status) {
|
||
const statusMap = {
|
||
'pending': '待投递',
|
||
'applying': '投递中',
|
||
'success': '投递成功',
|
||
'failed': '投递失败',
|
||
'duplicate': '重复投递'
|
||
};
|
||
return statusMap[status] || status || '-';
|
||
},
|
||
|
||
/**
|
||
* 获取反馈状态文本
|
||
*/
|
||
getFeedbackStatusText(status) {
|
||
const statusMap = {
|
||
'none': '无反馈',
|
||
'viewed': '已查看',
|
||
'interested': '感兴趣',
|
||
'not_suitable': '不合适',
|
||
'interview': '面试邀约'
|
||
};
|
||
return statusMap[status] || status || '-';
|
||
},
|
||
|
||
/**
|
||
* 格式化时间
|
||
*/
|
||
formatTime(time) {
|
||
if (!time) return '-';
|
||
const date = new Date(time);
|
||
return date.toLocaleString('zh-CN');
|
||
}
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
.page-delivery {
|
||
padding: 20px;
|
||
height: 100%;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.page-title {
|
||
margin: 0 0 20px 0;
|
||
font-size: 24px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
|
||
.stats-section {
|
||
display: flex;
|
||
gap: 15px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.stat-card {
|
||
flex: 1;
|
||
background: #fff;
|
||
padding: 20px;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||
text-align: center;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 32px;
|
||
font-weight: 600;
|
||
color: #4CAF50;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 14px;
|
||
color: #666;
|
||
}
|
||
|
||
.filter-section {
|
||
background: #fff;
|
||
padding: 15px;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.filter-box {
|
||
display: flex;
|
||
gap: 10px;
|
||
}
|
||
|
||
.filter-select {
|
||
flex: 1;
|
||
padding: 8px 12px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.table-section {
|
||
background: #fff;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.loading, .empty {
|
||
padding: 40px;
|
||
text-align: center;
|
||
color: #999;
|
||
}
|
||
|
||
.data-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
}
|
||
|
||
.data-table thead {
|
||
background: #f5f5f5;
|
||
}
|
||
|
||
.data-table th,
|
||
.data-table td {
|
||
padding: 12px;
|
||
text-align: left;
|
||
border-bottom: 1px solid #eee;
|
||
}
|
||
|
||
.data-table th {
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
|
||
.platform-tag {
|
||
display: inline-block;
|
||
padding: 4px 8px;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
|
||
&.boss {
|
||
background: #e3f2fd;
|
||
color: #1976d2;
|
||
}
|
||
|
||
&.liepin {
|
||
background: #e8f5e9;
|
||
color: #388e3c;
|
||
}
|
||
}
|
||
|
||
.status-tag {
|
||
display: inline-block;
|
||
padding: 4px 8px;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
background: #f5f5f5;
|
||
color: #666;
|
||
|
||
&.success {
|
||
background: #e8f5e9;
|
||
color: #388e3c;
|
||
}
|
||
|
||
&.failed {
|
||
background: #ffebee;
|
||
color: #d32f2f;
|
||
}
|
||
|
||
&.pending {
|
||
background: #fff3e0;
|
||
color: #f57c00;
|
||
}
|
||
|
||
&.interview {
|
||
background: #e3f2fd;
|
||
color: #1976d2;
|
||
}
|
||
}
|
||
|
||
.btn {
|
||
padding: 8px 16px;
|
||
border: none;
|
||
border-radius: 4px;
|
||
font-size: 14px;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
|
||
&.btn-primary {
|
||
background: #4CAF50;
|
||
color: #fff;
|
||
|
||
&:hover {
|
||
background: #45a049;
|
||
}
|
||
}
|
||
|
||
&:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
}
|
||
|
||
.btn-small {
|
||
padding: 4px 8px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.pagination-section {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
gap: 15px;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.page-info {
|
||
color: #666;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.modal-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
z-index: 1000;
|
||
}
|
||
|
||
.modal-content {
|
||
background: #fff;
|
||
border-radius: 8px;
|
||
width: 90%;
|
||
max-width: 600px;
|
||
max-height: 80vh;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.modal-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 20px;
|
||
border-bottom: 1px solid #eee;
|
||
}
|
||
|
||
.modal-header h3 {
|
||
margin: 0;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.btn-close {
|
||
background: none;
|
||
border: none;
|
||
font-size: 24px;
|
||
cursor: pointer;
|
||
color: #999;
|
||
|
||
&:hover {
|
||
color: #333;
|
||
}
|
||
}
|
||
|
||
.modal-body {
|
||
padding: 20px;
|
||
}
|
||
|
||
.detail-item {
|
||
margin-bottom: 15px;
|
||
|
||
label {
|
||
font-weight: 600;
|
||
color: #333;
|
||
margin-right: 8px;
|
||
}
|
||
|
||
span {
|
||
color: #666;
|
||
}
|
||
}
|
||
</style>
|