This commit is contained in:
张成
2025-12-22 16:26:59 +08:00
parent aa2d03ee30
commit e17d5610f5
54 changed files with 11735 additions and 3 deletions

671
app/views/DeliveryPage.vue Normal file
View 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>