Files
autoAiWorkSys/app/views/DeliveryPage.vue
张成 e17d5610f5 1
2025-12-22 16:26:59 +08:00

672 lines
15 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>
<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>