1
This commit is contained in:
671
app/views/DeliveryPage.vue
Normal file
671
app/views/DeliveryPage.vue
Normal file
@@ -0,0 +1,671 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user